sfn 2.2.0 → 3.0.0

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