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.
@@ -27,16 +27,18 @@ module Sfn
27
27
  s_name = [name]
28
28
 
29
29
  c_setter = lambda do |c_stack|
30
- compile_params = c_stack.outputs.detect do |output|
31
- output.key == 'CompileState'
30
+ if(c_stack.outputs)
31
+ compile_params = c_stack.outputs.detect do |output|
32
+ output.key == 'CompileState'
33
+ end
32
34
  end
33
35
  if(compile_params)
34
36
  compile_params = MultiJson.load(compile_params.value)
35
- c_current = config[:compile_parameters].fetch(s_name.join('_'), Smash.new)
36
- config[:compile_parameters][s_name.join('_')] = compile_params.merge(c_current)
37
+ c_current = config[:compile_parameters].fetch(s_name.join('__'), Smash.new)
38
+ config[:compile_parameters][s_name.join('__')] = compile_params.merge(c_current)
37
39
  end
38
40
  c_stack.nested_stacks(false).each do |n_stack|
39
- s_name.push(n_stack.name)
41
+ s_name.push(n_stack.data.fetch(:logical_id, n_stack.name))
40
42
  c_setter.call(n_stack)
41
43
  s_name.pop
42
44
  end
@@ -46,114 +48,114 @@ module Sfn
46
48
  c_setter.call(stack)
47
49
  end
48
50
 
51
+ ui.debug "Compile parameters - #{config[:compile_parameters]}"
49
52
  file = load_template_file(:stack => stack)
50
53
  stack_info << " #{ui.color('Path:', :bold)} #{config[:file]}"
51
- nested_stacks = file.delete('sfn_nested_stack')
52
54
  end
53
55
 
54
- if(nested_stacks)
55
- unpack_nesting(name, file, :update)
56
- else
57
- unless(stack)
58
- ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
59
- raise "Failed to locate stack: #{name}"
60
- end
56
+ unless(stack)
57
+ ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
58
+ raise "Failed to locate stack: #{name}"
59
+ end
61
60
 
62
- ui.info "#{ui.color('SparkleFormation:', :bold)} #{ui.color('update', :green)}"
61
+ ui.info "#{ui.color('SparkleFormation:', :bold)} #{ui.color('update', :green)}"
63
62
 
64
- unless(file)
65
- if(config[:template])
66
- file = config[:template]
67
- stack_info << " #{ui.color('(template provided)', :green)}"
68
- else
69
- stack_info << " #{ui.color('(no template update)', :yellow)}"
70
- end
63
+ unless(file)
64
+ if(config[:template])
65
+ file = config[:template]
66
+ stack_info << " #{ui.color('(template provided)', :green)}"
67
+ else
68
+ stack_info << " #{ui.color('(no template update)', :yellow)}"
71
69
  end
72
- ui.info " -> #{stack_info}"
70
+ end
71
+ ui.info " -> #{stack_info}"
73
72
 
74
- if(file)
75
- if(config[:print_only])
76
- ui.puts _format_json(translate_template(file))
77
- return
78
- end
73
+ if(file)
74
+ if(config[:print_only])
75
+ ui.puts format_json(parameter_scrub!(template_content(file)))
76
+ return
77
+ end
79
78
 
80
- original_template = stack.template
81
- original_parameters = stack.parameters
79
+ original_template = stack.template
80
+ original_parameters = stack.parameters
82
81
 
83
- stack.template = translate_template(file)
84
- apply_stacks!(stack)
82
+ apply_stacks!(stack)
85
83
 
86
- populate_parameters!(file, :current_parameters => stack.parameters)
87
- update_template = stack.template
84
+ populate_parameters!(file, :current_parameters => stack.root_parameters)
85
+ update_template = stack.template
88
86
 
89
- if(config[:plan])
90
- begin
91
- stack.template = original_template
92
- stack.parameters = original_parameters
93
- plan = build_planner(stack)
94
- if(plan)
95
- result = plan.generate_plan(file, config_root_parameters)
96
- display_plan_information(result)
97
- end
98
- rescue => e
99
- unless(e.message.include?('Confirmation declined'))
100
- ui.error "Unexpected error when generating plan information: #{e.class} - #{e}"
101
- ui.debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
102
- ui.confirm 'Continue with stack update?' unless config[:plan_only]
103
- else
104
- raise
105
- end
87
+ if(config[:plan])
88
+ begin
89
+ stack.template = original_template
90
+ stack.parameters = original_parameters
91
+ plan = build_planner(stack)
92
+ if(plan)
93
+ result = plan.generate_plan(file.dump, config_root_parameters)
94
+ display_plan_information(result)
106
95
  end
107
- if(config[:plan_only])
108
- ui.info 'Plan only mode requested. Exiting.'
109
- exit 0
96
+ rescue => e
97
+ unless(e.message.include?('Confirmation declined'))
98
+ ui.error "Unexpected error when generating plan information: #{e.class} - #{e}"
99
+ ui.debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
100
+ ui.confirm 'Continue with stack update?' unless config[:plan_only]
101
+ else
102
+ raise
110
103
  end
111
104
  end
112
-
113
- stack.parameters = config_root_parameters
114
- stack.template = scrub_template(update_template)
115
- else
116
- apply_stacks!(stack)
117
- original_parameters = stack.parameters
118
- populate_parameters!(stack.template, :current_parameters => stack.parameters)
119
- stack.parameters = config_root_parameters
105
+ if(config[:plan_only])
106
+ ui.info 'Plan only mode requested. Exiting.'
107
+ exit 0
108
+ end
120
109
  end
110
+ stack.parameters = config_root_parameters
111
+ else
112
+ apply_stacks!(stack)
113
+ original_parameters = stack.parameters
114
+ populate_parameters!(file, :current_parameters => stack.root_parameters)
115
+ stack.parameters = config_root_parameters
116
+ end
121
117
 
122
- # Set options defined within config into stack instance for update request
123
- if(config[:merge_api_options])
124
- config.fetch(:options, Smash.new).each_pair do |key, value|
125
- if(stack.respond_to?("#{key}="))
126
- stack.send("#{key}=", value)
127
- end
118
+ if(config[:upload_root_template])
119
+ upload_result = store_template(name, file, Smash.new)
120
+ stack.template_url = upload_result[:url]
121
+ else
122
+ stack.template = parameter_scrub!(template_content(file, :scrub))
123
+ end
124
+
125
+ # Set options defined within config into stack instance for update request
126
+ if(config[:merge_api_options])
127
+ config.fetch(:options, Smash.new).each_pair do |key, value|
128
+ if(stack.respond_to?("#{key}="))
129
+ stack.send("#{key}=", value)
128
130
  end
129
131
  end
132
+ end
130
133
 
131
- begin
132
- api_action!(:api_stack => stack) do
133
- stack.save
134
- if(config[:poll])
135
- poll_stack(stack.name)
136
- if(stack.reload.state == :update_complete)
137
- ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
138
- namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
139
- else
140
- ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
141
- raise 'Stack did not reach a successful update completion state.'
142
- end
134
+ begin
135
+ api_action!(:api_stack => stack) do
136
+ stack.save
137
+ if(config[:poll])
138
+ poll_stack(stack.name)
139
+ if(stack.reload.state == :update_complete)
140
+ ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
141
+ namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
143
142
  else
144
- ui.warn 'Stack state polling has been disabled.'
145
- ui.info "Stack update initialized for #{ui.color(name, :green)}"
143
+ ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
144
+ raise 'Stack did not reach a successful update completion state.'
146
145
  end
147
- end
148
- rescue Miasma::Error::ApiError::RequestError => e
149
- if(e.message.downcase.include?('no updates'))
150
- ui.warn "No updates detected for stack (#{stack.name})"
151
146
  else
152
- raise
147
+ ui.warn 'Stack state polling has been disabled.'
148
+ ui.info "Stack update initialized for #{ui.color(name, :green)}"
153
149
  end
154
150
  end
155
-
151
+ rescue Miasma::Error::ApiError::RequestError => e
152
+ if(e.message.downcase.include?('no updates'))
153
+ ui.warn "No updates detected for stack (#{stack.name})"
154
+ else
155
+ raise
156
+ end
156
157
  end
158
+
157
159
  end
158
160
 
159
161
  def build_planner(stack)
@@ -14,21 +14,25 @@ module Sfn
14
14
  print_only_original = config[:print_only]
15
15
  config[:print_only] = true
16
16
  file = load_template_file
17
- file.delete('sfn_nested_stack')
18
17
  ui.info "#{ui.color("Template Validation (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, '').sub(%r{^/}, '')}"
19
- file = Sfn::Utils::StackParameterScrubber.scrub!(file)
20
- file = translate_template(file)
21
18
  config[:print_only] = print_only_original
22
19
 
20
+ raw_template = _format_json(parameter_scrub!(template_content(file)))
21
+
23
22
  if(config[:print_only])
24
- ui.puts _format_json(file)
23
+ ui.puts raw_template
25
24
  else
26
- validate_stack(file, sparkle_collection.get(:template, config[:file])[:name])
25
+ validate_stack(file.dump, sparkle_collection.get(:template, config[:file])[:name])
27
26
  end
28
27
  end
29
28
 
30
- def validate_stack(stack, name)
31
- resources = stack.fetch('Resources', {})
29
+ # Validate template with remote API and unpack nested templates if required
30
+ #
31
+ # @param template [Hash] template data structure
32
+ # @param name [String] name of template
33
+ # @return [TrueClass]
34
+ def validate_stack(template, name)
35
+ resources = template.fetch('Resources', {})
32
36
  nested_stacks = resources.find_all do |r_name, r_value|
33
37
  r_value.is_a?(Hash) &&
34
38
  provider.connection.data[:stack_types].include?(r_value['Type'])
@@ -39,14 +43,23 @@ module Sfn
39
43
  end
40
44
  begin
41
45
  ui.info "Validating: #{ui.color(name, :bold)}"
42
- stack = provider.connection.stacks.build(
43
- :name => 'validation-stack',
44
- :template => Sfn::Utils::StackParameterScrubber.scrub!(stack)
45
- )
46
+ if(config[:upload_root_template])
47
+ upload_result = store_template('validation-stack', template, Smash.new)
48
+ stack = provider.connection.stacks.build(
49
+ :name => 'validation-stack',
50
+ :template_url => upload_result[:url]
51
+ )
52
+ else
53
+ stack = provider.connection.stacks.build(
54
+ :name => 'validation-stack',
55
+ :template => parameter_scrub!(template)
56
+ )
57
+ end
46
58
  result = api_action!(:api_stack => stack) do
47
59
  stack.validate
48
60
  end
49
61
  ui.info ui.color(' -> VALID', :bold, :green)
62
+ true
50
63
  rescue => e
51
64
  ui.info ui.color(' -> INVALID', :bold, :red)
52
65
  ui.fatal e.message
@@ -110,18 +110,7 @@ module Sfn
110
110
  def poll_stack(name)
111
111
  provider.connection.stacks.reload
112
112
  retry_attempts = 0
113
- begin
114
- events = Sfn::Command::Events.new({:poll => true}, [name]).execute!
115
- rescue => e
116
- if(retry_attempts < config.fetch(:max_poll_retries, 5).to_i)
117
- retry_attempts += 1
118
- warn "Unexpected error encountered (#{e.class}: #{e}) Retrying [retry count: #{retry_attempts}]"
119
- sleep(1)
120
- retry
121
- else
122
- raise
123
- end
124
- end
113
+ events = Sfn::Command::Events.new({:poll => true}, [name]).execute!
125
114
  end
126
115
 
127
116
  # Wrapper for information retrieval. Provides consistent error
@@ -8,8 +8,6 @@ module Sfn
8
8
 
9
9
  module InstanceMethods
10
10
 
11
- # unpacked stack name joiner/identifier
12
- UNPACK_NAME_JOINER = '-sfn-'
13
11
  # maximum number of attempts to get valid parameter value
14
12
  MAX_PARAMETER_ATTEMPTS = 5
15
13
  # Template parameter locations
@@ -24,12 +22,14 @@ module Sfn
24
22
  def apply_stacks!(stack)
25
23
  remote_stacks = [config[:apply_stack]].flatten.compact
26
24
  remote_stacks.each do |stack_name|
27
- remote_stack = provider.connection.stacks.get(stack_name)
25
+ remote_stack = provider.stack(stack_name)
28
26
  if(remote_stack)
29
27
  apply_nested_stacks!(remote_stack, stack)
30
- stack.apply_stack(remote_stack)
28
+ mappings = generate_custom_apply_mappings(remote_stack)
29
+ execute_apply_stack(remote_stack, stack, mappings)
31
30
  else
32
- apply_unpacked_stack!(stack_name, stack)
31
+ ui.error "Failed to apply requested stack. Unable to locate. (#{stack_name})"
32
+ raise "Failed to locate stack: #{stack}"
33
33
  end
34
34
  end
35
35
  stack
@@ -42,67 +42,47 @@ module Sfn
42
42
  # @return [Miasma::Models::Orchestration::Stack]
43
43
  def apply_nested_stacks!(remote_stack, stack)
44
44
  remote_stack.resources.all.each do |resource|
45
- if(resource.type == 'AWS::CloudFormation::Stack')
45
+ if(valid_stack_types.include?(resource.type))
46
46
  nested_stack = resource.expand
47
47
  apply_nested_stacks!(nested_stack, stack)
48
- stack.apply_stack(nested_stack)
48
+ mappings = generate_custom_apply_mappings(nested_stack)
49
+ execute_apply_stack(nested_stack, stack, mappings)
49
50
  end
50
51
  end
51
52
  stack
52
53
  end
53
54
 
54
- # Apply all stacks from an unpacked stack
55
+ # Build apply mappings valid for given provider stack
55
56
  #
56
- # @param stack_name [String] name of parent stack
57
- # @param stack [Miasma::Models::Orchestration::Stack]
58
- # @return [Miasma::Models::Orchestration::Stack]
59
- def apply_unpacked_stack!(stack_name, stack)
60
- result = provider.connection.stacks.all.find_all do |remote_stack|
61
- remote_stack.name.start_with?("#{stack_name}#{UNPACK_NAME_JOINER}")
62
- end.sort_by(&:name).map do |remote_stack|
63
- stack.apply_stack(remote_stack)
64
- end
65
- unless(result.empty?)
66
- stack
67
- else
68
- ui.error "Failed to apply requested stack. Unable to locate. (#{stack_name})"
69
- raise "Failed to locate stack: #{stack_name}"
57
+ # @param provider_stack [Miasma::Models::Orchestration::Stack] stack providing outputs
58
+ # @return [Hash] output to parameter mapping
59
+ def generate_custom_apply_mappings(provider_stack)
60
+ if(config[:apply_mapping])
61
+ valid_keys = config[:apply_mapping].keys.find_all do |a_key|
62
+ a_key = a_key.to_s
63
+ !a_key.include?('__') ||
64
+ a_key.split('__').first == provider_stack.name
65
+ end
66
+ to_remove = valid_keys.find_all do |key|
67
+ valid_keys.any?{|v_key| v_key.match(/__#{Regexp.escape(key)}$/)}
68
+ end
69
+ valid_keys -= to_remove
70
+ Hash[
71
+ valid_keys.map do |a_key|
72
+ cut_key = a_key.include?('__') ? a_key.slice(a_key.index('__') + 2, a_key.length) : a_key
73
+ [cut_key, config[:apply_mapping][a_key]]
74
+ end
75
+ ]
70
76
  end
71
77
  end
72
78
 
73
- # Unpack nested stack and run action on each stack, applying
74
- # the previous stacks automatically
79
+ # Apply provider stack outputs to receiver stack parameters
75
80
  #
76
- # @param name [String] container stack name
77
- # @param file [Hash] stack template
78
- # @param action [String] create or update
81
+ # @param provider_stack [Miasma::Models::Orchestration::Stack] stack providing outputs
82
+ # @param receiver_stack [Miasma::Models::Orchestration::Stack] stack receiving outputs for parameters
79
83
  # @return [TrueClass]
80
- def unpack_nesting(name, file, action)
81
- config[:apply_stacks] ||= []
82
- stack_count = 0
83
- file['Resources'].each do |stack_resource_name, stack_resource|
84
-
85
- nested_stack_name = "#{name}#{UNPACK_NAME_JOINER}#{Kernel.sprintf('%0.3d', stack_count)}-#{stack_resource_name}"
86
- nested_stack_template = stack_resource['Properties']['Stack']
87
-
88
- namespace.const_get(action.to_s.capitalize).new(
89
- Smash.new(
90
- :print_only => config[:print_only],
91
- :template => nested_stack_template,
92
- :parameters => config.fetch(:parameters, Smash.new).to_smash,
93
- :apply_stacks => config[:apply_stacks],
94
- :options => config[:options]
95
- ),
96
- [nested_stack_name]
97
- ).execute!
98
- unless(config[:print_only])
99
- config[:apply_stacks].push(nested_stack_name).uniq!
100
- end
101
- config[:template] = nil
102
- provider.connection.stacks.reload
103
- stack_count += 1
104
- end
105
-
84
+ def execute_apply_stack(provider_stack, receiver_stack, mappings)
85
+ receiver_stack.apply_stack(provider_stack, :mapping => mappings)
106
86
  true
107
87
  end
108
88
 
@@ -148,7 +128,7 @@ module Sfn
148
128
  valid = false
149
129
  # When parameter is a hash type, it is being set via
150
130
  # intrinsic function and we don't modify
151
- if(current_parameters[k].is_a?(Hash))
131
+ if(function_set_parameter?(current_parameters[k]))
152
132
  if(current_stack)
153
133
  enable_set = validate_stack_parameter(current_stack, k, ns_k, current_parameters[k])
154
134
  else
@@ -157,7 +137,7 @@ module Sfn
157
137
  if(enable_set)
158
138
  # NOTE: direct set dumps the stack (nfi). Smash will
159
139
  # auto dup it, and works, so yay i guess.
160
- config[:parameters][ns_k] = Smash.new(current_parameters[k])
140
+ config[:parameters][ns_k] = current_parameters[k].is_a?(Hash) ? Smash.new(current_parameters[k]) : current_parameters[k].dup
161
141
  valid = true
162
142
  end
163
143
  else
@@ -189,7 +169,7 @@ module Sfn
189
169
  else
190
170
  answer = default
191
171
  end
192
- validation = Sfn::Utils::StackParameterValidator.validate(answer, v)
172
+ validation = validate_parameter(answer, v)
193
173
  if(validation == true)
194
174
  config[:parameters][ns_k] = answer
195
175
  valid = true
@@ -215,6 +195,14 @@ module Sfn
215
195
  ]
216
196
  end
217
197
 
198
+ # Determine if parameter was set via intrinsic function
199
+ #
200
+ # @param val [Object]
201
+ # @return [TrueClass, FalseClass]
202
+ def function_set_parameter?(val)
203
+ val.is_a?(Hash)
204
+ end
205
+
218
206
  # @return [Hash] parameters for root stack create/update
219
207
  def config_root_parameters
220
208
  Hash[
@@ -280,6 +268,7 @@ module Sfn
280
268
  klass.class_eval do
281
269
  extend Sfn::CommandModule::Stack::ClassMethods
282
270
  include Sfn::CommandModule::Stack::InstanceMethods
271
+ include Utils::StackParameterValidator
283
272
  end
284
273
  end
285
274