knife-cloudformation 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## v0.1.14
2
+ * Extract template building tools
3
+ * Add support for custom CF locations and prompting
4
+ * Updates in fetching and caching behavior
5
+
1
6
  ## v0.1.12
2
7
  * Use the split value when re-joining parameters
3
8
 
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -10,8 +10,10 @@ Gem::Specification.new do |s|
10
10
  s.description = 'Knife tooling for Cloud Formation'
11
11
  s.require_path = 'lib'
12
12
  s.add_dependency 'chef'
13
- s.add_dependency 'fog', '~> 1.15'
13
+ s.add_dependency 'fog', '~> 1.17'
14
14
  s.add_dependency 'net-sftp'
15
- s.add_dependency 'attribute_struct', '~> 0.1.6'
15
+ s.add_dependency 'sparkle_formation', '~> 0.1.2'
16
+ s.add_dependency 'redis-objects'
17
+ s.add_dependency 'attribute_struct', '~> 0.1.8'
16
18
  s.files = Dir['**/*']
17
19
  end
@@ -1,4 +1,5 @@
1
- require 'knife-cloudformation/sparkle_formation'
1
+ require 'sparkle_formation'
2
+ require 'pathname'
2
3
  require 'chef/knife/cloudformation_base'
3
4
 
4
5
  class Chef
@@ -94,6 +95,16 @@ class Chef
94
95
  :long => '--print-only',
95
96
  :description => 'Print template and exit'
96
97
  )
98
+ option(:base_directory,
99
+ :long => '--cloudformation-directory PATH',
100
+ :description => 'Path to cloudformation directory',
101
+ :proc => lambda {|val| Chef::Config[:knife][:cloudformation][:base_directory] = val}
102
+ )
103
+ option(:no_base_directory,
104
+ :long => '--no-cloudformation-directory',
105
+ :description => 'Unset any value used for cloudformation path',
106
+ :proc => lambda {|*val| Chef::Config[:knife][:cloudformation][:base_directory] = nil}
107
+ )
97
108
 
98
109
  %w(rollback polling interactive_parameters).each do |key|
99
110
  if(Chef::Config[:knife][:cloudformation][key].nil?)
@@ -109,13 +120,20 @@ class Chef
109
120
 
110
121
  def run
111
122
  @action_type = self.class.name.split('::').last.sub('Cloudformation', '').upcase
123
+ name = name_args.first
124
+ unless(name)
125
+ ui.fatal "Formation name must be specified!"
126
+ exit 1
127
+ end
128
+
129
+ set_paths_and_discover_file!
112
130
  unless(File.exists?(Chef::Config[:knife][:cloudformation][:file].to_s))
113
131
  ui.fatal "Invalid formation file path provided: #{Chef::Config[:knife][:cloudformation][:file]}"
114
132
  exit 1
115
133
  end
116
- name = name_args.first
134
+
117
135
  if(Chef::Config[:knife][:cloudformation][:processing])
118
- file = KnifeCloudformation::SparkleFormation.compile(Chef::Config[:knife][:cloudformation][:file])
136
+ file = SparkleFormation.compile(Chef::Config[:knife][:cloudformation][:file])
119
137
  else
120
138
  file = _from_json(File.read(Chef::Config[:knife][:cloudformation][:file]))
121
139
  end
@@ -176,6 +194,94 @@ class Chef
176
194
  end
177
195
  end
178
196
 
197
+ private
198
+
199
+ def set_paths_and_discover_file!
200
+ if(Chef::Config[:knife][:cloudformation][:base_directory])
201
+ SparkleFormation.components_path = File.join(
202
+ Chef::Config[:knife][:cloudformation][:base_directory], 'components'
203
+ )
204
+ SparkleFormation.dynamics_path = File.join(
205
+ Chef::Config[:knife][:cloudformation][:base_directory], 'dynamics'
206
+ )
207
+ end
208
+ unless(Chef::Config[:knife][:cloudformation][:file])
209
+ Chef::Config[:knife][:cloudformation][:file] = prompt_for_file(
210
+ Chef::Config[:knife][:cloudformation][:base_directory] || File.join(Dir.pwd, 'cloudformation')
211
+ )
212
+ else
213
+ unless(Pathname(Chef::Config[:knife][:cloudformation][:file]).absolute?)
214
+ Chef::Config[:knife][:cloudformation][:file] = File.join(
215
+ Chef::Config[:knife][:cloudformation][:base_directory] || File.join(Dir.pwd, 'cloudformation'),
216
+ Chef::Config[:knife][:cloudformation][:file]
217
+ )
218
+ end
219
+ end
220
+ end
221
+
222
+ IGNORE_CF_DIRS = %w(dynamics components)
223
+
224
+ def prompt_for_file(dir)
225
+ directory = Dir.new(dir)
226
+ directories = directory.map do |d|
227
+ if(!d.start_with?('.') && !IGNORE_CF_DIRS.include?(d) && File.directory?(path = File.join(dir, d)))
228
+ path
229
+ end
230
+ end.compact.sort
231
+ files = directory.map do |f|
232
+ if(!f.start_with?('.') && File.file?(path = File.join(dir, f)))
233
+ path
234
+ end
235
+ end.compact.sort
236
+ if(directories.empty? && files.empty?)
237
+ ui.fatal 'No formation paths discoverable!'
238
+ else
239
+ output = ['Please select the formation to create']
240
+ output << '(or directory to list):' unless directories.empty?
241
+ ui.info output.join(' ')
242
+ output.clear
243
+ idx = 1
244
+ valid = {}
245
+ unless(directories.empty?)
246
+ output << ui.color('Directories:', :bold)
247
+ directories.each do |path|
248
+ valid[idx] = {:path => path, :type => :directory}
249
+ output << [idx, "#{File.basename(path).sub('.rb', '').split(/[-_]/).map(&:capitalize).join(' ')}"]
250
+ idx += 1
251
+ end
252
+ end
253
+ unless(files.empty?)
254
+ output << ui.color('Templates:', :bold)
255
+ files.each do |path|
256
+ valid[idx] = {:path => path, :type => :file}
257
+ output << [idx, "#{File.basename(path).sub('.rb', '').split(/[-_]/).map(&:capitalize).join(' ')}"]
258
+ idx += 1
259
+ end
260
+ end
261
+ max = idx.to_s.length
262
+ output.map! do |o|
263
+ if(o.is_a?(Array))
264
+ " #{o.first}.#{' ' * (max - o.first.to_s.length)} #{o.last}"
265
+ else
266
+ o
267
+ end
268
+ end
269
+ ui.info "#{output.join("\n")}\n"
270
+ response = ask_question('Enter selection: ').to_i
271
+ unless(valid[response])
272
+ ui.fatal 'How about using a real value'
273
+ exit 1
274
+ else
275
+ entry = valid[response.to_i]
276
+ if(entry[:type] == :directory)
277
+ prompt_for_file(entry[:path])
278
+ else
279
+ Chef::Config[:knife][:cloudformation][:file] = entry[:path]
280
+ end
281
+ end
282
+ end
283
+ end
284
+
179
285
  end
180
286
  end
181
287
  end
@@ -26,8 +26,12 @@ class Chef
26
26
  ui.info "Destroy request sent for stack: #{ui.color(stack_name, :bold)}"
27
27
  end
28
28
  if(config[:polling])
29
- stacks.each do |stack_name|
30
- poll_stack(stack_name)
29
+ begin
30
+ stacks.each do |stack_name|
31
+ poll_stack(stack_name)
32
+ end
33
+ rescue Fog::AWS::CloudFormation::NotFound
34
+ # ignore this error since this is the end result we want!
31
35
  end
32
36
  ui.info " -> Destroyed Cloud Formation#{plural}: #{ui.color(stacks.join(', '), :bold, :red)}"
33
37
  end
@@ -38,10 +38,35 @@ class Chef
38
38
 
39
39
  def get_list
40
40
  get_things do
41
- aws.stacks
41
+ aws.aws(:cloud_formation).list_stacks(list_options).body['StackSummaries'].sort do |x,y|
42
+ if(y['CreationTime'].to_s.empty?)
43
+ -1
44
+ elsif(x['CreationTime'].to_s.empty?)
45
+ 1
46
+ else
47
+ Time.parse(y['CreationTime'].to_s) <=> Time.parse(x['CreationTime'].to_s)
48
+ end
49
+ end
42
50
  end
43
51
  end
44
52
 
53
+ def list_options
54
+ status = Chef::Config[:knife][:cloudformation][:status] ||
55
+ KnifeCloudformation::AwsCommons::DEFAULT_STACK_STATUS
56
+ if(status.map(&:downcase).include?('none'))
57
+ filter = {}
58
+ else
59
+ count = 0
60
+ filter = Hash[*(
61
+ status.map do |n|
62
+ count += 1
63
+ ["StackStatusFilter.member.#{count}", n]
64
+ end.flatten
65
+ )]
66
+ end
67
+ filter
68
+ end
69
+
45
70
  def default_attributes
46
71
  %w(StackName CreationTime StackStatus TemplateDescription)
47
72
  end
@@ -1,9 +1,12 @@
1
- require 'knife-cloudformation/aws_commons.rb'
1
+ require 'knife-cloudformation/cache'
2
+ require 'knife-cloudformation/aws_commons'
3
+ require 'digest/sha2'
2
4
 
3
5
  module KnifeCloudformation
4
6
  class AwsCommons
5
7
  class Stack
6
8
 
9
+ include KnifeCloudformation::Utils::Debug
7
10
  include KnifeCloudformation::Utils::JSON
8
11
  include KnifeCloudformation::Utils::AnimalStrings
9
12
 
@@ -27,7 +30,7 @@ module KnifeCloudformation
27
30
  def build_stack_definition(template, options={})
28
31
  stack = Mash.new
29
32
  options.each do |key, value|
30
- format_key = key.split('_').map do |k|
33
+ format_key = key.to_s.split('_').map do |k|
31
34
  "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"
32
35
  end.join
33
36
  stack[format_key] = value
@@ -61,12 +64,19 @@ module KnifeCloudformation
61
64
  def initialize(name, common, raw_stack=nil)
62
65
  @name = name
63
66
  @common = common
64
- @memo = {}
67
+ @memo = Cache.new(common.credentials.merge(:stack => name))
68
+ reset_local
69
+ @memo.init(:raw_stack, :stamped)
65
70
  if(raw_stack)
66
- @raw_stack = raw_stack
67
- else
68
- load_stack
71
+ if(@memo[:stacks])
72
+ if(@memo[:stacks].stamp > @memo[:raw_stack].stamp)
73
+ @memo[:raw_stack].value = raw_stack
74
+ end
75
+ else
76
+ @memo[:raw_stack].value = raw_stack
77
+ end
69
78
  end
79
+ load_stack
70
80
  @force_refresh = false
71
81
  @force_refresh = in_progress?
72
82
  end
@@ -99,27 +109,63 @@ module KnifeCloudformation
99
109
  res
100
110
  end
101
111
 
102
- def load_stack
103
- @raw_stack = common.aws(:cloud_formation)
104
- .describe_stacks('StackName' => name)
105
- .body['Stacks'].first
112
+ def load_stack(*args)
113
+ @memo.init(:raw_stack, :stamped)
114
+ begin
115
+ @memo.init(:raw_stack_lock, :lock)
116
+ @memo[:raw_stack_lock].lock do
117
+ if(args.include?(:force) || @memo[:raw_stack].update_allowed?)
118
+ @memo[:raw_stack].value = common.aws(:cloud_formation)
119
+ .describe_stacks('StackName' => name)
120
+ .body['Stacks'].first
121
+ end
122
+ end
123
+ rescue => e
124
+ if(defined?(Redis) && e.is_a?(Redis::Lock::LockTimeout))
125
+ # someone else is updating
126
+ debug 'Got lock timeout on stack load'
127
+ else
128
+ raise
129
+ end
130
+ end
106
131
  end
107
132
 
108
133
  def load_resources
109
- @raw_resources = common.aws(:cloud_formation)
110
- .describe_stack_resources('StackName' => name)
111
- .body['StackResources']
134
+ @memo.init(:raw_resources, :stamped)
135
+ begin
136
+ @memo.init(:raw_resources_lock, :lock)
137
+ @memo[:raw_resources_lock].lock do
138
+ if(@memo[:raw_resources].update_allowed?)
139
+ @memo[:raw_resources].value = common.aws(:cloud_formation)
140
+ .describe_stack_resources('StackName' => name)
141
+ .body['StackResources']
142
+ end
143
+ end
144
+ rescue => e
145
+ if(defined?(Redis) && e.is_a?(Redis::Lock::LockTimeout))
146
+ debug 'Got lock timeout on resource load'
147
+ else
148
+ raise e
149
+ end
150
+ end
112
151
  end
113
152
 
114
153
  def refresh?(bool=nil)
115
154
  bool || (bool.nil? && @force_refresh)
116
155
  end
117
156
 
157
+ def reset_local
158
+ @local = {
159
+ :nodes => []
160
+ }
161
+ end
162
+
118
163
  def reload!
119
- load_stack
120
- load_resources
121
- @force_refresh = in_progress?
122
- @memo = {}
164
+ @memo.clear! do
165
+ load_stack(:force)
166
+ load_resources
167
+ @force_refresh = in_progress?
168
+ end
123
169
  true
124
170
  end
125
171
 
@@ -141,99 +187,142 @@ module KnifeCloudformation
141
187
  end
142
188
 
143
189
  def template
144
- unless(@memo[:template])
145
- @memo[:template] = _from_json(
190
+ @memo.init(:template, :value)
191
+ unless(@memo[:template].value)
192
+ @memo[:template].value = _from_json(
146
193
  common.aws(:cloud_formation)
147
194
  .get_template(name).body['TemplateBody']
148
195
  )
149
196
  end
150
- @memo[:template]
197
+ @memo[:template].value
151
198
  end
152
199
 
153
200
  ## Stack metadata ##
154
201
  def parameters(raw=false)
155
202
  if(raw)
156
- @raw_stack['Parameters']
203
+ @memo[:raw_stack].value['Parameters']
157
204
  else
158
- unless(@memo[:parameters])
159
- @memo[:parameters] = Hash[*(
160
- @raw_stack['Parameters'].map do |ary|
205
+ @memo.init(:parameters, :value)
206
+ unless(@memo[:parameters].value)
207
+ @memo[:parameters].value = Hash[*(
208
+ @memo[:raw_stack].value['Parameters'].map do |ary|
161
209
  [ary['ParameterKey'], ary['ParameterValue']]
162
210
  end.flatten
163
211
  )]
164
212
  end
165
- @memo[:parameters]
213
+ @memo[:parameters].value
166
214
  end
167
215
  end
168
216
 
169
217
  def capabilities
170
- @raw_stack['Capabilities']
218
+ @memo[:raw_stack].value['Capabilities']
171
219
  end
172
220
 
173
221
  def disable_rollback
174
- @raw_stack['DisableRollback']
222
+ @memo[:raw_stack].value['DisableRollback']
175
223
  end
176
224
 
177
225
  def notification_arns
178
- @raw_stack['NotificationARNs']
226
+ @memo[:raw_stack].value['NotificationARNs']
179
227
  end
180
228
 
181
229
  def timeout_in_minutes
182
- @raw_stack['TimeoutInMinutes']
230
+ @memo[:raw_stack].value['TimeoutInMinutes']
183
231
  end
184
232
  alias_method :timeout_in_minutes, :timeout
185
233
 
186
234
  def stack_id
187
- @raw_stack['StackId']
235
+ @memo[:raw_stack].value['StackId']
188
236
  end
189
237
  alias_method :id, :stack_id
190
238
 
191
239
  def creation_time
192
- @raw_stack['CreationTime']
240
+ @memo[:raw_stack].value['CreationTime']
193
241
  end
194
242
  alias_method :created_at, :creation_time
195
243
 
196
244
  def status(force_refresh=nil)
197
245
  load_stack if refresh?(force_refresh)
198
- @raw_stack['StackStatus']
246
+ @memo[:raw_stack].value['StackStatus']
199
247
  end
200
248
 
201
249
  def resources(force_refresh=nil)
202
- load_resources if @raw_resources.nil? || refresh?(force_refresh)
203
- @raw_resources
250
+ load_resources if @memo[:raw_resources].nil? || refresh?(force_refresh)
251
+ @memo[:raw_resources].value
204
252
  end
205
253
 
206
254
  def events(all=false)
207
- if(@memo[:events].nil? || refresh?)
208
- res = common.aws(:cloud_formation).describe_stack_events(name).body['StackEvents']
209
- @memo[:events] ||= []
210
- current = @memo[:events].map{|e| e['EventId']}
211
- res.delete_if{|e| current.include?(e['EventId'])}
212
- @memo[:events] += res
213
- @memo[:events].uniq!
214
- @memo[:events].sort!{|x,y| x['Timestamp'] <=> y['Timestamp']}
215
- else
216
- res = []
255
+ @memo.init(:events, :stamped)
256
+ res = []
257
+ if(@memo[:events].value.nil? || refresh?)
258
+ begin
259
+ @memo.init(:events_lock, :lock)
260
+ @memo[:events_lock].lock do
261
+ if(@memo[:events].update_allowed?)
262
+ res = common.aws(:cloud_formation).describe_stack_events(name).body['StackEvents']
263
+ current = @memo[:events].value || []
264
+ current_events = current.map{|e| e['EventId']}
265
+ res.delete_if{|e| current_events.include?(e['EventId'])}
266
+ current += res
267
+ current.uniq!
268
+ current.sort!{|x,y| x['Timestamp'] <=> y['Timestamp']}
269
+ @memo[:events].value = current
270
+ end
271
+ end
272
+ rescue => e
273
+ if(defined?(Redis) && e.is_a?(Redis::Lock::LockTimeout))
274
+ debug 'Got lock timeout on events'
275
+ else
276
+ raise
277
+ end
278
+ end
217
279
  end
218
- all ? @memo[:events] : res
280
+ all ? @memo[:events].value : res
219
281
  end
220
282
 
221
283
  def outputs(style=:unformatted)
222
284
  case style
223
285
  when :formatted
224
286
  Hash[*(
225
- @raw_stack['Outputs'].map do |item|
287
+ @memo[:raw_stack].value['Outputs'].map do |item|
226
288
  [item['OutputKey'].gsub(/(?<![A-Z])([A-Z])/, '_\1').sub(/^_/, '').downcase.to_sym, item['OutputValue']]
227
289
  end.flatten
228
290
  )]
229
291
  when :unformatted
230
292
  Hash[*(
231
- @raw_stack['Outputs'].map do |item|
293
+ @memo[:raw_stack].value['Outputs'].map do |item|
232
294
  [item['OutputKey'], item['OutputValue']]
233
295
  end.flatten
234
296
  )]
235
297
  else
236
- @raw_stack['Outputs']
298
+ @memo[:raw_stack].value['Outputs']
299
+ end
300
+ end
301
+
302
+ def event_start_index(given_events, status)
303
+ Array(given_events).flatten.compact.rindex do |e|
304
+ e['ResourceType'] == 'AWS::CloudFormation::Stack' &&
305
+ e['ResourceStatus'] == status.to_s.upcase
306
+ end.to_i
307
+ end
308
+
309
+ # min:: do not return value lower than this (defaults to 5)
310
+ # Returns Numeric < 100 to represent completed resources
311
+ # percentage (never returns less than 5)
312
+ def percent_complete(min=5)
313
+ if(complete?)
314
+ 100
315
+ else
316
+ all_events = events(:all)
317
+ total_expected = template['Resources'].size
318
+ action = performing
319
+ start = event_start_index(all_events, "#{action}_in_progress".to_sym)
320
+ finished = all_events.find_all do |e|
321
+ e['ResourceStatus'] == "#{action}_complete".upcase ||
322
+ e['ResourceStatus'] == "#{action}_failed".upcase
323
+ end.size
324
+ calculated = ((finished / total_expected.to_f) * 100).to_i
325
+ calculated < min ? min : calculated
237
326
  end
238
327
  end
239
328
 
@@ -254,7 +343,42 @@ module KnifeCloudformation
254
343
  end
255
344
 
256
345
  def success?
257
- !failed?
346
+ !failed? && complete?
347
+ end
348
+
349
+ def creating?
350
+ in_progress? && status.to_s.downcase.start_with?('create')
351
+ end
352
+
353
+ def deleting?
354
+ in_progress? && status.to_s.downcase.start_with?('delete')
355
+ end
356
+
357
+ def updating?
358
+ in_progress? && status.to_s.downcase.start_with?('update')
359
+ end
360
+
361
+ def rollbacking?
362
+ in_progress? && status.to_s.downcase.start_with?('rollback')
363
+ end
364
+
365
+ def performing
366
+ if(in_progress?)
367
+ status.to_s.downcase.split('_').first.to_sym
368
+ end
369
+ end
370
+
371
+ # Lets build in some color coding!
372
+ def red?
373
+ failed? || deleting?
374
+ end
375
+
376
+ def yellow?
377
+ !red? && !green?
378
+ end
379
+
380
+ def green?
381
+ success? || creating? || updating?
258
382
  end
259
383
 
260
384
  ## Fog instance helpers ##
@@ -271,18 +395,34 @@ module KnifeCloudformation
271
395
  end
272
396
 
273
397
  def nodes
274
- reload! if refresh?
275
- unless(@memo[:nodes])
276
- as_resources = resources.find_all{|r|r['ResourceType'] == 'AWS::AutoScaling::AutoScalingGroup'}
277
- @memo[:nodes] =
278
- as_resources.map do |as_resource|
398
+ if(@local[:nodes].empty?)
399
+ as_resources = resources.find_all do |r|
400
+ r['ResourceType'] == 'AWS::AutoScaling::AutoScalingGroup'
401
+ end
402
+ @local[:nodes] = as_resources.map do |as_resource|
279
403
  as_group = expand_resource(as_resource)
280
404
  as_group.instances.map do |inst|
281
405
  common.aws(:ec2).servers.get(inst.id)
282
406
  end
283
407
  end.flatten
284
408
  end
285
- @memo[:nodes]
409
+ @local[:nodes]
410
+ end
411
+
412
+ def nodes_data(*args)
413
+ cache_key = ['nd', name, Digest::SHA256.hexdigest(args.map(&:to_s).join)].join('_')
414
+ @memo.init(cache_key, :value)
415
+ unless(@memo[cache_key].value)
416
+ data = nodes.map do |n|
417
+ [:id, args].flatten.compact.map do |k|
418
+ n.send(k)
419
+ end
420
+ end
421
+ end
422
+ unless(data.empty?)
423
+ @memo[cache_key].value = data
424
+ end
425
+ @memo[cache_key].value || data
286
426
  end
287
427
 
288
428
  end
@@ -1,5 +1,6 @@
1
1
  require 'fog'
2
2
  require 'knife-cloudformation/utils'
3
+ require 'knife-cloudformation/cache'
3
4
 
4
5
  Dir.glob(File.join(File.dirname(__FILE__), 'aws_commons/*.rb')).each do |item|
5
6
  require "knife-cloudformation/aws_commons/#{File.basename(item).sub('.rb', '')}"
@@ -8,26 +9,29 @@ end
8
9
  module KnifeCloudformation
9
10
  class AwsCommons
10
11
 
12
+ include KnifeCloudformation::Utils::AnimalStrings
13
+ include KnifeCloudformation::Utils::Debug
14
+
11
15
  FOG_MAP = {
12
16
  :ec2 => :compute
13
17
  }
14
18
 
19
+ attr_reader :credentials
20
+
15
21
  def initialize(args={})
16
22
  @ui = args[:ui]
17
- @creds = args[:fog]
23
+ @credentials = @creds = args[:fog]
18
24
  @connections = {}
19
- @memo = {
20
- :stacks => {},
21
- :event_ids => [],
22
- :stack_list => {}
23
- }
25
+ @memo = Cache.new(credentials)
26
+ @local = {:stacks => {}}
27
+ end
28
+
29
+ def cache
30
+ @memo
24
31
  end
25
32
 
26
33
  def clear_cache(*types)
27
- keys = types.empty? ? @memo.keys : types.map(&:to_sym)
28
- keys.each do |key|
29
- @memo[key].clear if @memo[key]
30
- end
34
+ @memo.clear!(*types)
31
35
  true
32
36
  end
33
37
 
@@ -43,9 +47,20 @@ module KnifeCloudformation
43
47
  dns_creds.delete(:region) || dns_creds.delete('region')
44
48
  @connections[:dns] = Fog::DNS::AWS.new(dns_creds)
45
49
  else
46
- Fog.credentials = Fog.symbolize_credentials(@creds)
47
- @connections[type] = Fog::AWS[type]
48
- Fog.credentials = {}
50
+ begin
51
+ Fog.credentials = Fog.symbolize_credentials(@creds)
52
+ @connections[type] = Fog::AWS[type]
53
+ Fog.credentials = {}
54
+ rescue NameError
55
+ klass = [camel(type.to_s), 'AWS'].inject(Fog) do |memo, item|
56
+ memo.const_defined?(item) ? memo.const_get(item) : break
57
+ end
58
+ if(klass)
59
+ @connections[type] = klass.new(Fog.symbolize_credentials(@creds))
60
+ else
61
+ raise
62
+ end
63
+ end
49
64
  end
50
65
  end
51
66
  @connections[type]
@@ -62,64 +77,55 @@ module KnifeCloudformation
62
77
  )
63
78
 
64
79
  def stacks(args={})
65
- status = args[:status] || DEFAULT_STACK_STATUS
66
- key = status.hash
67
- @memo[:stack_list].delete(key) if args[:force_refresh]
68
- count = 0
69
- if(status.map(&:downcase).include?('none'))
70
- filter = {}
80
+ status = Array(args[:status] || DEFAULT_STACK_STATUS).flatten.compact.map do |stat|
81
+ stat.to_s.upcase
82
+ end
83
+ @memo.init(:stacks_lock, :lock)
84
+ @memo.init(:stacks, :stamped)
85
+ if(args[:cache_time])
86
+ @memo[:stacks].stamp
71
87
  else
72
- filter = Hash[*(
73
- status.map do |n|
74
- count += 1
75
- ["StackStatusFilter.member.#{count}", n]
76
- end.flatten
77
- )]
88
+ if(args[:refresh_every])
89
+ cache.apply_limit(:stacks, args[:refresh_every].to_i)
90
+ end
91
+ if(@memo[:stacks].update_allowed? || args[:force_refresh])
92
+ @memo[:stacks_lock].lock do
93
+ @memo[:stacks].value = aws(:cloud_formation).describe_stacks.body['Stacks']
94
+ end
95
+ end
78
96
  end
79
- unless(@memo[:stack_list][key])
80
- @memo[:stack_list][key] = aws(:cloud_formation).list_stacks(filter).body['StackSummaries']
97
+ @memo[:stacks].value.find_all do |s|
98
+ status.include?(s['StackStatus'])
81
99
  end
82
- @memo[:stack_list][key]
83
100
  end
84
101
 
85
- def name_from_stack_id(name)
102
+ def name_from_stack_id(s_id)
86
103
  found = stacks.detect do |s|
87
- s['StackId'] == name
104
+ s['StackId'] == s_id
88
105
  end
89
- if(found)
90
- s['StackName']
91
- else
92
- raise "Failed to locate stack with ID: #{name}"
106
+ found ? found['StackName'] : raise(IndexError.new("Failed to locate stack with ID: #{s_id}"))
107
+ end
108
+
109
+ def id_from_stack_name(name)
110
+ found = stacks.detect do |s|
111
+ s['StackName'] == name
93
112
  end
113
+ found ? found['StackId'] : raise(IndexError.new("Failed to locate stack with name: #{name}"))
94
114
  end
95
115
 
96
116
  def stack(*names)
97
- names = names.map do |name|
98
- if(name.start_with?('arn:'))
99
- name_from_stack_id(name)
100
- else
101
- name
102
- end
103
- end
104
- if(names.size == 1)
105
- name = names.first
106
- unless(@memo[:stacks][name])
107
- @memo[:stacks][name] = Stack.new(name, self)
108
- end
109
- @memo[:stacks][name]
110
- else
111
- to_fetch = names - @memo[:stacks].keys
112
- slim_stacks = {}
113
- unless(to_fetch.empty?)
114
- to_fetch.each do |name|
115
- slim_stacks[name] = Stack.new(name, self, stacks.detect{|s| s['StackName'] == name})
117
+ result = names.map do |name|
118
+ [name, name.start_with?('arn:') ? name : id_from_stack_name(name)]
119
+ end.map do |name, s_id|
120
+ unless(@local[:stacks][s_id])
121
+ seed = stacks.detect do |stk|
122
+ stk['StackId'] == s_id
116
123
  end
124
+ @local[:stacks][s_id] = Stack.new(name, self, seed)
117
125
  end
118
- result = names.map do |n|
119
- @memo[:stacks][n] || slim_stacks[n]
120
- end
121
- result
126
+ @local[:stacks][s_id]
122
127
  end
128
+ result.size == 1 ? result.first : result
123
129
  end
124
130
 
125
131
  def create_stack(name, definition)
@@ -131,7 +137,7 @@ module KnifeCloudformation
131
137
  def process(things, args={})
132
138
  @event_ids ||= []
133
139
  processed = things.reverse.map do |thing|
134
- next if @memo[:event_ids].include?(thing['EventId'])
140
+ next if @event_ids.include?(thing['EventId'])
135
141
  @event_ids.push(thing['EventId']).compact!
136
142
  if(args[:attributes])
137
143
  args[:attributes].map do |key|
@@ -0,0 +1,204 @@
1
+ require 'digest/sha2'
2
+
3
+ module KnifeCloudformation
4
+ class Cache
5
+
6
+ class << self
7
+
8
+ def configure(type, args={})
9
+ type = type.to_sym
10
+ case type
11
+ when :redis
12
+ require 'redis-objects'
13
+ Redis::Objects.redis = Redis.new(args)
14
+ when :local
15
+ else
16
+ raise TypeError.new("Unsupported caching type: #{type}")
17
+ end
18
+ enable(type)
19
+ end
20
+
21
+ def enable(type)
22
+ @type = type.to_sym
23
+ end
24
+
25
+ def type
26
+ @type || :local
27
+ end
28
+
29
+ def apply_limit(kind, seconds=nil)
30
+ @apply_limit ||= {}
31
+ if(seconds)
32
+ @apply_limit[kind.to_sym] = seconds.to_i
33
+ end
34
+ @apply_limit[kind.to_sym].to_i
35
+ end
36
+
37
+ def default_limits
38
+ (@apply_limit || {}).dup
39
+ end
40
+
41
+ end
42
+
43
+ attr_reader :key
44
+ attr_reader :direct_store
45
+
46
+ def initialize(key)
47
+ if(key.respond_to?(:sort))
48
+ key = key.flatten if key.respond_to?(:flatten)
49
+ key = key.map(&:to_s).sort
50
+ end
51
+ @key = Digest::SHA256.hexdigest(key.to_s)
52
+ @direct_store = {}
53
+ @apply_limit = self.class.default_limits
54
+ end
55
+
56
+ def init(name, kind, args={})
57
+ name = name.to_sym
58
+ unless(@direct_store[name])
59
+ full_name = [key, name.to_s].join('_')
60
+ @direct_store[name] = get_storage(self.class.type, kind, full_name, args)
61
+ end
62
+ true
63
+ end
64
+
65
+ def clear!(*args)
66
+ internal_lock do
67
+ args = @direct_store.keys if args.empty?
68
+ args.each do |key|
69
+ value = @direct_store[key]
70
+ if(value.respond_to?(:clear))
71
+ value.clear
72
+ elsif(value.respond_to?(:value))
73
+ value.value = nil
74
+ end
75
+ end
76
+ yield if block_given?
77
+ end
78
+ true
79
+ end
80
+
81
+ def get_storage(store_type, data_type, full_name, args={})
82
+ case store_type.to_sym
83
+ when :redis
84
+ get_redis_storage(data_type, full_name, args)
85
+ when :local
86
+ get_local_storage(data_type, full_name, args)
87
+ else
88
+ raise TypeError.new("Unsupported caching storage type encountered: #{store_type}")
89
+ end
90
+ end
91
+
92
+ def get_redis_storage(data_type, full_name, args={})
93
+ case data_type.to_sym
94
+ when :array
95
+ Redis::List.new(full_name, {:marshal => true}.merge(args))
96
+ when :hash
97
+ Redis::HashKey.new(full_name)
98
+ when :value
99
+ Redis::Value.new(full_name, {:marshal => true}.merge(args))
100
+ when :lock
101
+ Redis::Lock.new(full_name, {:expiration => 3, :timeout => 0.1}.merge(args))
102
+ when :stamped
103
+ Stamped.new(full_name.sub("#{key}_", '').to_sym, get_redis_storage(:value, full_name), self)
104
+ else
105
+ raise TypeError.new("Unsupported caching data type encountered: #{data_type}")
106
+ end
107
+ end
108
+
109
+ def get_local_storage(data_type, full_name, args={})
110
+ case data_type.to_sym
111
+ when :array
112
+ []
113
+ when :hash
114
+ {}
115
+ when :value
116
+ LocalValue.new
117
+ when :lock
118
+ LocalLock.new
119
+ when :stamped
120
+ Stamped.new(full_name.sub("#{key}_", '').to_sym, get_local_storage(:value, full_name), self)
121
+ else
122
+ raise TypeError.new("Unsupported caching data type encountered: #{data_type}")
123
+ end
124
+ end
125
+
126
+ def internal_lock
127
+ get_storage(self.class.type, :lock, :internal_access, :timeout => 20).lock do
128
+ yield
129
+ end
130
+ end
131
+
132
+ def [](name)
133
+ internal_lock do
134
+ @direct_store[name.to_sym]
135
+ end
136
+ end
137
+
138
+ def []=(key, val)
139
+ raise 'Setting backend data is not allowed'
140
+ end
141
+
142
+ def time_check_allow?(key, stamp)
143
+ Time.now.to_i - stamp.to_i > apply_limit(key)
144
+ end
145
+
146
+ def apply_limit(kind, seconds=nil)
147
+ @apply_limit ||= {}
148
+ if(seconds)
149
+ @apply_limit[kind.to_sym] = seconds.to_i
150
+ end
151
+ @apply_limit[kind.to_sym].to_i
152
+ end
153
+
154
+
155
+ class LocalValue
156
+ attr_accessor :value
157
+ def initialize(*args)
158
+ @value = nil
159
+ end
160
+ end
161
+
162
+ class LocalLock
163
+ def initialize(*args)
164
+ end
165
+
166
+ def lock
167
+ yield
168
+ end
169
+
170
+ def clear
171
+ end
172
+ end
173
+
174
+ class Stamped
175
+
176
+ def initialize(name, base, cache)
177
+ @name = name.to_sym
178
+ @base = base
179
+ @cache = cache
180
+ end
181
+
182
+ def value
183
+ @base.value[:value] if set?
184
+ end
185
+
186
+ def value=(v)
187
+ @base.value = {:stamp => Time.now.to_i, :value => v}
188
+ end
189
+
190
+ def set?
191
+ @base.value.is_a?(Hash)
192
+ end
193
+
194
+ def stamp
195
+ @base.value[:stamp] if set?
196
+ end
197
+
198
+ def update_allowed?
199
+ !set? || @cache.time_check_allow?(@name, @base.value[:stamp])
200
+ end
201
+ end
202
+
203
+ end
204
+ end
@@ -1,5 +1,23 @@
1
1
  module KnifeCloudformation
2
2
  module Utils
3
+
4
+ module Debug
5
+ module Output
6
+ def debug(msg)
7
+ puts "<KnifeCloudformation>: #{msg}" if ENV['DEBUG']
8
+ end
9
+ end
10
+
11
+ class << self
12
+ def included(klass)
13
+ klass.class_eval do
14
+ include Output
15
+ extend Output
16
+ end
17
+ end
18
+ end
19
+ end
20
+
3
21
  module JSON
4
22
 
5
23
  def try_json_compat
@@ -2,5 +2,5 @@ module KnifeCloudformation
2
2
  class Version < Gem::Version
3
3
  end
4
4
 
5
- VERSION = Version.new('0.1.12')
5
+ VERSION = Version.new('0.1.14')
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-cloudformation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.12
4
+ version: 0.1.14
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-18 00:00:00.000000000 Z
12
+ date: 2013-10-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chef
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - ~>
36
36
  - !ruby/object:Gem::Version
37
- version: '1.15'
37
+ version: '1.17'
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
- version: '1.15'
45
+ version: '1.17'
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: net-sftp
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -59,6 +59,38 @@ dependencies:
59
59
  - - ! '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sparkle_formation
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 0.1.2
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 0.1.2
78
+ - !ruby/object:Gem::Dependency
79
+ name: redis-objects
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
62
94
  - !ruby/object:Gem::Dependency
63
95
  name: attribute_struct
64
96
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +98,7 @@ dependencies:
66
98
  requirements:
67
99
  - - ~>
68
100
  - !ruby/object:Gem::Version
69
- version: 0.1.6
101
+ version: 0.1.8
70
102
  type: :runtime
71
103
  prerelease: false
72
104
  version_requirements: !ruby/object:Gem::Requirement
@@ -74,7 +106,7 @@ dependencies:
74
106
  requirements:
75
107
  - - ~>
76
108
  - !ruby/object:Gem::Version
77
- version: 0.1.6
109
+ version: 0.1.8
78
110
  description: Knife tooling for Cloud Formation
79
111
  email: chrisroberts.code@gmail.com
80
112
  executables: []
@@ -90,13 +122,13 @@ files:
90
122
  - lib/chef/knife/cloudformation_update.rb
91
123
  - lib/chef/knife/cloudformation_inspect.rb
92
124
  - lib/knife-cloudformation.rb
93
- - lib/knife-cloudformation/sparkle_attribute.rb
94
125
  - lib/knife-cloudformation/version.rb
95
126
  - lib/knife-cloudformation/aws_commons.rb
96
- - lib/knife-cloudformation/sparkle_formation.rb
127
+ - lib/knife-cloudformation/cache.rb
97
128
  - lib/knife-cloudformation/utils.rb
98
129
  - lib/knife-cloudformation/aws_commons/stack_parameter_validator.rb
99
130
  - lib/knife-cloudformation/aws_commons/stack.rb
131
+ - Gemfile
100
132
  - README.md
101
133
  - knife-cloudformation.gemspec
102
134
  - CHANGELOG.md
@@ -1,67 +0,0 @@
1
- require 'attribute_struct'
2
-
3
- module SparkleAttribute
4
-
5
- # TODO: look at the docs for Fn stuff. We can probably just map
6
- # simple ones with a bit of string manipulations
7
-
8
- def _cf_join(*args)
9
- options = args.detect{|i| i.is_a?(Hash) && i[:options]} || {:options => {}}
10
- args.delete(options)
11
- unless(args.size == 1)
12
- args = [args]
13
- end
14
- {'Fn::Join' => [options[:options][:delimiter] || '', *args]}
15
- end
16
-
17
- def _cf_ref(thing)
18
- thing = _process_key(thing, :force) if thing.is_a?(Symbol)
19
- {'Ref' => thing}
20
- end
21
-
22
- def _cf_map(thing, key, *suffix)
23
- suffix = suffix.map do |item|
24
- if(item.is_a?(Symbol))
25
- _process_key(item, :force)
26
- else
27
- item
28
- end
29
- end
30
- thing = _process_key(thing, :force) if thing.is_a?(Symbol)
31
- key = _process_key(key, :force) if key.is_a?(Symbol)
32
- {'Fn::FindInMap' => [_process_key(thing), {'Ref' => _process_key(key)}, *suffix]}
33
- end
34
-
35
- def _cf_attr(*args)
36
- args = args.map do |thing|
37
- if(thing.is_a?(Symbol))
38
- _process_key(thing, :force)
39
- else
40
- thing
41
- end
42
-
43
- end
44
- {'Fn::GetAtt' => args}
45
- end
46
-
47
- def _cf_base64(arg)
48
- {'Fn::Base64' => arg}
49
- end
50
-
51
- def rhel?
52
- !!@platform[:rhel]
53
- end
54
-
55
- def debian?
56
- !!@platform[:debian]
57
- end
58
-
59
- def _platform=(plat)
60
- @platform || __hashish
61
- @platform.clear
62
- @platform[plat.to_sym] = true
63
- end
64
-
65
- end
66
-
67
- AttributeStruct.send(:include, SparkleAttribute)
@@ -1,132 +0,0 @@
1
- require 'chef/mash'
2
- require 'attribute_struct'
3
- require 'knife-cloudformation/sparkle_attribute'
4
- require 'knife-cloudformation/utils'
5
-
6
- AttributeStruct.camel_keys = true
7
-
8
- module KnifeCloudformation
9
- class SparkleFormation
10
-
11
- include KnifeCloudformation::Utils::AnimalStrings
12
-
13
- class << self
14
-
15
- attr_reader :dynamics
16
- attr_reader :components_path
17
- attr_reader :dynamics_path
18
-
19
- def custom_paths
20
- @_paths ||= {}
21
- @_paths
22
- end
23
-
24
- def components_path=(path)
25
- custom_paths[:sparkle_path] = path
26
- end
27
-
28
- def dynamics_path=(path)
29
- custom_paths[:dynamics_directory] = path
30
- end
31
-
32
- def compile(path)
33
- formation = self.instance_eval(IO.read(path), path, 1)
34
- formation.compile._dump
35
- end
36
-
37
- def build(&block)
38
- struct = AttributeStruct.new
39
- struct.instance_exec(&block)
40
- struct
41
- end
42
-
43
- def load_component(path)
44
- self.instance_eval(IO.read(path), path, 1)
45
- end
46
-
47
- def load_dynamics!(directory)
48
- @loaded_dynamics ||= []
49
- Dir.glob(File.join(directory, '*.rb')).each do |dyn|
50
- dyn = File.expand_path(dyn)
51
- next if @loaded_dynamics.include?(dyn)
52
- self.instance_eval(IO.read(dyn), dyn, 1)
53
- @loaded_dynamics << dyn
54
- end
55
- @loaded_dynamics.uniq!
56
- true
57
- end
58
-
59
- def dynamic(name, &block)
60
- @dynamics ||= Mash.new
61
- @dynamics[name] = block
62
- end
63
-
64
- def insert(dynamic_name, struct, *args)
65
- if(@dynamics && @dynamics[dynamic_name])
66
- struct.instance_exec(*args, &@dynamics[dynamic_name])
67
- struct
68
- else
69
- raise "Failed to locate requested dynamic block for insertion: #{dynamic_name} (valid: #{@dynamics.keys.sort.join(', ')})"
70
- end
71
- end
72
-
73
- def from_hash(hash)
74
- struct = AttributeStruct.new
75
- struct._camel_keys_set(:auto_discovery)
76
- struct._load(hash)
77
- struct._camel_keys_set(nil)
78
- struct
79
- end
80
- end
81
-
82
- attr_reader :name
83
- attr_reader :sparkle_path
84
- attr_reader :components
85
- attr_reader :load_order
86
-
87
- def initialize(name, options={})
88
- @name = name
89
- @sparkle_path = options[:sparkle_path] ||
90
- self.class.custom_paths[:sparkle_path] ||
91
- File.join(Dir.pwd, 'cloudformation/components')
92
- @dynamics_directory = options[:dynamics_directory] ||
93
- self.class.custom_paths[:dynamics_directory] ||
94
- File.join(File.dirname(@sparkle_path), 'dynamics')
95
- self.class.load_dynamics!(@dynamics_directory)
96
- @components = Mash.new
97
- @load_order = []
98
- end
99
-
100
- def load(*args)
101
- args.each do |thing|
102
- if(thing.is_a?(Symbol))
103
- path = File.join(sparkle_path, "#{thing}.rb")
104
- else
105
- path = thing
106
- end
107
- key = File.basename(path).sub('.rb', '')
108
- components[key] = self.class.load_component(path)
109
- @load_order << key
110
- end
111
- self
112
- end
113
-
114
- def overrides(&block)
115
- @overrides = self.class.build(&block)
116
- self
117
- end
118
-
119
- # Returns compiled Mash instance
120
- def compile
121
- compiled = AttributeStruct.new
122
- @load_order.each do |key|
123
- compiled._merge!(components[key])
124
- end
125
- if(@overrides)
126
- compiled._merge!(@overrides)
127
- end
128
- compiled
129
- end
130
-
131
- end
132
- end