sfn 2.2.0 → 3.0.0

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.
@@ -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