sfn 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,6 +15,23 @@ module Sfn
15
15
 
16
16
  module InstanceMethods
17
17
 
18
+ # Extract template content based on type
19
+ #
20
+ # @param thing [SparkleFormation, Hash]
21
+ # @param scrub [Truthy, Falsey] scrub nested templates
22
+ # @return [Hash]
23
+ def template_content(thing, scrub=false)
24
+ if(thing.is_a?(SparkleFormation))
25
+ if(scrub)
26
+ dump_stack_for_storage(thing)
27
+ else
28
+ config[:sparkle_dump] ? thing.sparkle_dump : thing.dump
29
+ end
30
+ else
31
+ thing
32
+ end
33
+ end
34
+
18
35
  # Request compile time parameter value
19
36
  #
20
37
  # @param p_name [String, Symbol] name of parameter
@@ -70,7 +87,7 @@ module Sfn
70
87
  ui.fatal "Failed to receive allowed parameter! (Parameter: #{p_name})"
71
88
  exit 1
72
89
  else
73
- ui.error "Invalid value provided for parameter. Must be type: `#{p_config[:type].to_s.capitalize}`"
90
+ ui.error "Invalid value provided for parameter `#{p_name}`. Must be type: `#{p_config[:type].to_s.capitalize}`"
74
91
  end
75
92
  end
76
93
  end
@@ -100,12 +117,19 @@ module Sfn
100
117
  # @return [SparkleFormation::SparkleCollection]
101
118
  def sparkle_collection
102
119
  memoize(:sparkle_collection) do
103
- collection = SparkleFormation::SparkleCollection.new
120
+ collection = SparkleFormation::SparkleCollection.new(
121
+ :provider => config.get(:credentials, :provider)
122
+ )
104
123
  begin
105
124
  if(config[:base_directory])
106
- root_pack = SparkleFormation::SparklePack.new(:root => config[:base_directory])
125
+ root_pack = SparkleFormation::SparklePack.new(
126
+ :root => config[:base_directory],
127
+ :provider => config.get(:credentials, :provider)
128
+ )
107
129
  else
108
- root_pack = SparkleFormation::SparklePack.new
130
+ root_pack = SparkleFormation::SparklePack.new(
131
+ :provider => config.get(:credentials, :provider)
132
+ )
109
133
  end
110
134
  collection.set_root(root_pack)
111
135
  rescue Errno::ENOENT
@@ -175,9 +199,6 @@ module Sfn
175
199
  sf.stack_resource_types.push(s_type)
176
200
  end
177
201
  end
178
- if(sf.nested? && !sf.isolated_nests?)
179
- raise TypeError.new('Template does not contain isolated stack nesting! Sfn does not support mixed mixed resources within root stack!')
180
- end
181
202
  run_callbacks_for(:template, :stack_name => arguments.first, :sparkle_stack => sf)
182
203
  if(sf.nested? && config[:apply_nesting])
183
204
  validate_nesting_bucket!
@@ -190,12 +211,13 @@ module Sfn
190
211
  when :shallow
191
212
  process_nested_stack_shallow(sf, c_stack)
192
213
  when :none
193
- sf.dump.merge('sfn_nested_stack' => !!sf.nested?)
214
+ sf
194
215
  else
195
216
  raise ArgumentError.new "Unknown nesting style requested: #{config[:apply_nesting].inspect}!"
196
217
  end
218
+ sf
197
219
  else
198
- sf.dump.merge('sfn_nested_stack' => !!sf.nested?)
220
+ sf
199
221
  end
200
222
  else
201
223
  template = _from_json(File.read(config[:file]))
@@ -249,20 +271,13 @@ module Sfn
249
271
  #
250
272
  # @param sf [SparkleFormation] stack
251
273
  # @param c_stack [Miasma::Models::Orchestration::Stack] existing stack
252
- # @return [Hash] dumped stack
274
+ # @return [SparkleFormation::SparkleStruct] compiled structure
253
275
  def process_nested_stack_deep(sf, c_stack=nil)
254
276
  sf.apply_nesting(:deep) do |stack_name, stack, resource|
255
277
  run_callbacks_for(:template, :stack_name => stack_name, :sparkle_stack => stack)
256
-
257
278
  stack_resource = resource._dump
258
-
259
- if(stack.parent)
260
- current_parameters = stack.parent.compile.resources.set!(stack_name).properties.parameters
261
- current_parameters = current_parameters.nil? ? Smash.new : current_parameters._dump
262
- else
263
- current_parameters = Smash.new
264
- end
265
279
  current_stack = c_stack ? c_stack.nested_stacks.detect{|s| s.data[:logical_id] == stack_name} : nil
280
+ current_parameters = extract_current_nested_template_parameters(stack, stack_name, current_stack)
266
281
  if(current_stack && current_stack.data[:parent_stack])
267
282
  current_parameters.merge!(
268
283
  current_stack.data[:parent_stack].template.fetch(
@@ -283,21 +298,7 @@ module Sfn
283
298
  :current_parameters => current_parameters
284
299
  )
285
300
  )
286
- stack_definition = dump_stack_for_storage(stack)
287
- bucket = provider.connection.api_for(:storage).buckets.get(
288
- config[:nesting_bucket]
289
- )
290
- unless(bucket)
291
- raise "Failed to locate configured bucket for stack template storage (#{config[:nesting_bucket]})!"
292
- end
293
- file = bucket.files.build
294
- file.name = "#{full_stack_name}.json"
295
- file.content_type = 'text/json'
296
- file.body = MultiJson.dump(Sfn::Utils::StackParameterScrubber.scrub!(stack_definition))
297
- file.save
298
- result.merge!(
299
- :url => file.url
300
- )
301
+ store_template(full_stack_name, stack, result)
301
302
  else
302
303
  result = Smash.new(
303
304
  :url => "http://example.com/bucket/#{full_stack_name}.json"
@@ -309,6 +310,45 @@ module Sfn
309
310
  end
310
311
  end
311
312
 
313
+ # Extract currently defined parameters for nested template
314
+ #
315
+ # @param stack [SparkleFormation]
316
+ # @param stack_name [String]
317
+ # @param c_stack [Miasma::Models::Orchestration::Stack]
318
+ # @return [Hash]
319
+ def extract_current_nested_template_parameters(stack, stack_name, c_stack=nil)
320
+ if(stack.parent)
321
+ current_parameters = stack.parent.compile.resources.set!(stack_name).properties.parameters
322
+ current_parameters.nil? ? Smash.new : current_parameters._dump
323
+ else
324
+ Smash.new
325
+ end
326
+ end
327
+
328
+ # Store template in remote bucket and update given result hash
329
+ #
330
+ # @param full_stack_name [String] unique resource name for template
331
+ # @param stack [SparkleFormation] template instance
332
+ # @param result [Hash]
333
+ # @return [Hash]
334
+ def store_template(full_stack_name, stack, result)
335
+ stack_definition = stack.is_a?(SparkleFormation) ? dump_stack_for_storage(stack) : stack
336
+ bucket = provider.connection.api_for(:storage).buckets.get(
337
+ config[:nesting_bucket]
338
+ )
339
+ unless(bucket)
340
+ raise "Failed to locate configured bucket for stack template storage (#{config[:nesting_bucket]})!"
341
+ end
342
+ file = bucket.files.build
343
+ file.name = "#{full_stack_name}.json"
344
+ file.content_type = 'text/json'
345
+ file.body = MultiJson.dump(parameter_scrub!(stack_definition))
346
+ file.save
347
+ result.merge!(
348
+ :url => file.url
349
+ )
350
+ end
351
+
312
352
  # Remove internally used `Stack` property from Stack resources and
313
353
  # and generate compiled Hash
314
354
  #
@@ -522,6 +562,7 @@ module Sfn
522
562
  extend Sfn::CommandModule::Template::ClassMethods
523
563
  include Sfn::CommandModule::Template::InstanceMethods
524
564
  include Sfn::Utils::PathSelector
565
+ include Sfn::Utils::StackParameterScrubber
525
566
  end
526
567
  end
527
568
 
@@ -11,6 +11,11 @@ module Sfn
11
11
  :short_flag => 'w'
12
12
  )
13
13
 
14
+ attribute(
15
+ :sparkle_dump, [TrueClass, FalseClass],
16
+ :description => 'Do not use provider customized dump behavior'
17
+ )
18
+
14
19
  end
15
20
  end
16
21
  end
@@ -11,6 +11,10 @@ module Sfn
11
11
  :description => 'Apply outputs from stack to input parameters',
12
12
  :short_flag => 'A'
13
13
  )
14
+ attribute(
15
+ :apply_mapping, Smash,
16
+ :description => 'Customize apply stack mapping as [StackName__]OutputName:ParameterName'
17
+ )
14
18
  attribute(
15
19
  :parameter, Smash,
16
20
  :multiple => true,
@@ -94,6 +94,10 @@ module Sfn
94
94
  end
95
95
  }
96
96
  )
97
+ attribute(
98
+ :upload_root_template, [TrueClass, FalseClass],
99
+ :description => 'Upload root template to storage bucket'
100
+ )
97
101
 
98
102
  end
99
103
  end
@@ -7,7 +7,12 @@ module Sfn
7
7
  # Expand stack model functionality
8
8
  module Stack
9
9
 
10
+ autoload :Azure, 'sfn/monkey_patch/stack/azure'
11
+ autoload :Google, 'sfn/monkey_patch/stack/google'
12
+
10
13
  include Bogo::AnimalStrings
14
+ include Azure
15
+ include Google
11
16
 
12
17
  ## Status helpers
13
18
 
@@ -135,58 +140,85 @@ module Sfn
135
140
  # @param min [Integer] lowest allowed return value (defaults 5)
136
141
  # @return [Integer] percent complete (0..100)
137
142
  def percent_complete(min = 5)
138
- if(in_progress?)
139
- total_resources = template.fetch('Resources', []).size
140
- total_complete = resources.all.find_all do |resource|
141
- resource.status.downcase.end_with?('complete')
142
- end.size
143
- result = ((total_complete.to_f / total_resources) * 100).to_i
144
- result > min.to_i ? result : min
143
+ if(self.respond_to?("percent_complete_#{api.provider}"))
144
+ self.send("percent_complete_#{api.provider}", min)
145
145
  else
146
- 100
146
+ if(in_progress?)
147
+ total_resources = template.fetch('Resources', []).size
148
+ total_complete = resources.all.find_all do |resource|
149
+ resource.status.downcase.end_with?('complete')
150
+ end.size
151
+ result = ((total_complete.to_f / total_resources) * 100).to_i
152
+ result > min.to_i ? result : min
153
+ else
154
+ 100
155
+ end
147
156
  end
148
157
  end
149
158
 
150
159
  # Apply stack outputs to current stack parameters
151
160
  #
161
+ # @param opts [Hash]
162
+ # @option opts [String] :parameter_key key used for parameters block
163
+ # @option opts [String] :default_key key used within parameter for default value
164
+ # @option opts [Hash] :mapping custom output -> parameter name mapping
152
165
  # @param remote_stack [Miasma::Orchestration::Stack]
153
166
  # @return [self]
154
167
  # @note setting `DisableApply` within parameter hash will
155
168
  # prevent parameters being overridden
156
- def apply_stack(remote_stack, ignore_params=nil)
157
- default_key = 'Default'
158
- stack_parameters = template['Parameters']
159
- if(stack_parameters)
160
- valid_parameters = Smash[
161
- stack_parameters.map do |key, val|
162
- unless(val['DisableApply'])
163
- [snake(key), key]
164
- end
165
- end.compact
166
- ]
167
- if(ignore_params)
168
- valid_parameters = Hash[
169
- valid_parameters.map do |snake_param, camel_param|
170
- unless(ignore_params.include?(camel_param))
171
- [snake_param, camel_param]
172
- end
173
- end.compact
174
- ]
169
+ def apply_stack(remote_stack, opts={}, ignore_params=nil)
170
+ if(self.respond_to?("apply_stack_#{api.provider}"))
171
+ self.send("apply_stack_#{api.provider}", remote_stack, opts, ignore_params)
172
+ else
173
+ unless(opts[:mapping])
174
+ opts[:mapping] = {}
175
175
  end
176
- if(persisted?)
177
- remote_stack.outputs.each do |output|
178
- if(param_key = valid_parameters[snake(output.key)])
179
- parameters.merge!(param_key => output.value)
176
+ if(opts[:parameter_key])
177
+ stack_parameters = template[opts[:parameter_key]]
178
+ default_key = opts.fetch(
179
+ :default_key,
180
+ opts[:parameter_key].to_s[0,1].match(/[a-z]/) ? 'default' : 'Default'
181
+ )
182
+ else
183
+ if(template['Parameters'])
184
+ default_key = 'Default'
185
+ stack_parameters = template['Parameters']
186
+ else
187
+ default_key = 'default'
188
+ stack_parameters = template['parameters']
189
+ end
190
+ end
191
+ if(stack_parameters)
192
+ valid_parameters = stack_parameters.find_all do |key, val|
193
+ !val['DisableApply'] && !val['disable_apply']
194
+ end.map(&:first)
195
+ if(ignore_params)
196
+ valid_parameters.reject! do |key|
197
+ ignore_params.include?(key)
180
198
  end
181
199
  end
182
- else
183
200
  remote_stack.outputs.each do |output|
184
- if(param_key = valid_parameters[snake(output.key)])
185
- stack_parameters[param_key][default_key] = output.value
201
+ o_key = output.key.downcase.tr('_', '')
202
+ p_key = valid_parameters.detect do |v_param|
203
+ v_param.downcase.tr('_', '') == o_key
204
+ end
205
+ unless(p_key)
206
+ map_key = opts[:mapping].keys.detect do |map_key|
207
+ map_key.downcase.tr('_', '') == o_key
208
+ end
209
+ if(map_key)
210
+ p_key = valid_parameters.detect do |v_param|
211
+ v_param.downcase.tr('_', '') == opts[:mapping][map_key].downcase.tr('_', '')
212
+ end
213
+ end
214
+ end
215
+ if(p_key)
216
+ parameters.merge!(p_key => output.value)
186
217
  end
187
218
  end
188
219
  end
189
220
  end
221
+ self
190
222
  end
191
223
 
192
224
  # Return all stacks contained within this stack
@@ -194,39 +226,47 @@ module Sfn
194
226
  # @param recurse [TrueClass, FalseClass] recurse to fetch _all_ stacks
195
227
  # @return [Array<Miasma::Models::Orchestration::Stack>]
196
228
  def nested_stacks(recurse=true)
197
- resources.reload.all.map do |resource|
198
- if(api.data.fetch(:stack_types, []).include?(resource.type))
199
- # Custom remote load support
200
- if(resource.type == 'Custom::JackalStack')
201
- location, stack_id = resource.id.to_s.split('-', 2)
202
- if(l_conf = api.data[:locations][location])
203
- n_stack = Miasma.api(
204
- :type => :orchestration,
205
- :provider => l_conf[:provider],
206
- :credentials => l_conf
207
- ).stacks.get(stack_id)
208
- end
209
- else
210
- n_stack = resource.expand
211
- end
212
- if(n_stack)
213
- n_stack.data[:logical_id] = resource.name
214
- n_stack.data[:parent_stack] = self
215
- n_stack.api.data[:stack_types] = api.data[:stack_types]
216
- if(recurse)
217
- [n_stack] + n_stack.nested_stacks(recurse)
229
+ if(self.respond_to?("nested_stacks_#{api.provider}"))
230
+ self.send("nested_stacks_#{api.provider}", recurse)
231
+ else
232
+ resources.reload.all.map do |resource|
233
+ if(api.data.fetch(:stack_types, []).include?(resource.type))
234
+ # Custom remote load support
235
+ if(resource.type == 'Custom::JackalStack')
236
+ location, stack_id = resource.id.to_s.split('-', 2)
237
+ if(l_conf = api.data[:locations][location])
238
+ n_stack = Miasma.api(
239
+ :type => :orchestration,
240
+ :provider => l_conf[:provider],
241
+ :credentials => l_conf
242
+ ).stacks.get(stack_id)
243
+ end
218
244
  else
219
- n_stack
245
+ n_stack = resource.expand
246
+ end
247
+ if(n_stack)
248
+ n_stack.data[:logical_id] = resource.name
249
+ n_stack.data[:parent_stack] = self
250
+ n_stack.api.data[:stack_types] = api.data[:stack_types]
251
+ if(recurse)
252
+ [n_stack] + n_stack.nested_stacks(recurse)
253
+ else
254
+ n_stack
255
+ end
220
256
  end
221
257
  end
222
- end
223
- end.flatten.compact
258
+ end.flatten.compact
259
+ end
224
260
  end
225
261
 
226
262
  # @return [TrueClass, FalseClass] stack contains nested stacks
227
263
  def nested?
228
- !!resources.detect do |resource|
229
- api.data.fetch(:stack_types, []).include?(resource.type)
264
+ if(self.respond_to?("nested_#{api.provider}?"))
265
+ self.send("nested_#{api.provider}?")
266
+ else
267
+ !!resources.detect do |resource|
268
+ api.data.fetch(:stack_types, []).include?(resource.type)
269
+ end
230
270
  end
231
271
  end
232
272
 
@@ -234,22 +274,26 @@ module Sfn
234
274
  #
235
275
  # @return [Smash, NilClass]
236
276
  def policy
237
- if(self.api.provider == :aws) # cause this is the only one
238
- begin
239
- result = self.api.request(
240
- :path => '/',
241
- :form => Smash.new(
242
- 'Action' => 'GetStackPolicy',
243
- 'StackName' => self.id
277
+ if(self.respond_to?("policy_#{api.provider}"))
278
+ self.send("policy_#{api.provider}")
279
+ else
280
+ if(self.api.provider == :aws) # cause this is the only one
281
+ begin
282
+ result = self.api.request(
283
+ :path => '/',
284
+ :form => Smash.new(
285
+ 'Action' => 'GetStackPolicy',
286
+ 'StackName' => self.id
287
+ )
244
288
  )
245
- )
246
- serialized_policy = result.get(:body, 'GetStackPolicyResult', 'StackPolicyBody')
247
- MultiJson.load(serialized_policy).to_smash
248
- rescue Miasma::Error::ApiError::RequestError => e
249
- if(e.response.code == 404)
250
- nil
251
- else
252
- raise
289
+ serialized_policy = result.get(:body, 'GetStackPolicyResult', 'StackPolicyBody')
290
+ MultiJson.load(serialized_policy).to_smash
291
+ rescue Miasma::Error::ApiError::RequestError => e
292
+ if(e.response.code == 404)
293
+ nil
294
+ else
295
+ raise
296
+ end
253
297
  end
254
298
  end
255
299
  end
@@ -263,14 +307,40 @@ module Sfn
263
307
  # contain any direct values for parameters (which is what we
264
308
  # are testing for)
265
309
  def nesting_style
266
- if(nested?)
267
- self.template['Resources'].find_all do |t_resource|
268
- t_resource['Type'] == self.api.class.const_get(:RESOURCE_MAPPING).key(self.class)
269
- end.detect do |t_resource|
270
- t_resource['Properties'].fetch('Parameters', {}).values.detect do |t_value|
271
- !t_value.is_a?(Hash)
272
- end
273
- end ? :deep : :shallow
310
+ if(self.respond_to?("nesting_style_#{api.provider}"))
311
+ self.send("nesting_style_#{api.provider}")
312
+ else
313
+ if(nested?)
314
+ self.template['Resources'].find_all do |t_resource|
315
+ t_resource['Type'] == self.api.class.const_get(:RESOURCE_MAPPING).key(self.class)
316
+ end.detect do |t_resource|
317
+ t_resource['Properties'].fetch('Parameters', {}).values.detect do |t_value|
318
+ !t_value.is_a?(Hash)
319
+ end
320
+ end ? :deep : :shallow
321
+ end
322
+ end
323
+ end
324
+
325
+ # Reformat template data structure to SparkleFormation style structure
326
+ #
327
+ # @return [Hash]
328
+ def sparkleish_template(*args)
329
+ if(self.respond_to?("sparkleish_template_#{api.provider}"))
330
+ self.send("sparkleish_template_#{api.provider}", *args)
331
+ else
332
+ template
333
+ end
334
+ end
335
+
336
+ # Provide easy parameters override
337
+ #
338
+ # @return [Hash]
339
+ def root_parameters
340
+ if(self.respond_to?("root_parameters_#{api.provider}"))
341
+ self.send("root_parameters_#{api.provider}")
342
+ else
343
+ parameters
274
344
  end
275
345
  end
276
346