knife-cloudformation 0.1.12 → 0.1.14

Sign up to get free protection for your applications and to get access to all the features.
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