sfn 0.5.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,100 +18,77 @@ module Sfn
18
18
  end
19
19
 
20
20
  stack_info = "#{ui.color('Name:', :bold)} #{name}"
21
- stack = provider.connection.stacks.get(name)
22
-
23
- config[:compile_parameters] ||= Smash.new
21
+ begin
22
+ stack = provider.connection.stacks.get(name)
23
+ rescue Miasma::Error::ApiError::RequestError
24
+ stack = nil
25
+ end
24
26
 
25
27
  if(config[:file])
26
- s_name = [name]
27
- c_setter = lambda do |c_stack|
28
- compile_params = c_stack.outputs.detect do |output|
29
- output.key == 'CompileState'
30
- end
31
- if(compile_params)
32
- compile_params = MultiJson.load(compile_params.value)
33
- c_current = config[:compile_parameters].fetch(s_name.join('_'), Smash.new)
34
- config[:compile_parameters][s_name.join('_')] = compile_params.merge(c_current)
35
- end
36
- end
37
-
38
- if(stack)
39
- c_setter.call(stack)
40
- stack.resources.all.each do |s_resource|
41
- if(s_resource.type == 'AWS::CloudFormation::Stack')
42
- s_name.push(s_resource.logical_id)
43
- c_setter.call(s_resource.expand)
44
- s_name.pop
45
- end
46
- end
47
- end
48
-
49
- file = load_template_file
28
+ file = load_template_file(:stack => stack)
50
29
  stack_info << " #{ui.color('Path:', :bold)} #{config[:file]}"
51
30
  nested_stacks = file.delete('sfn_nested_stack')
52
31
  end
53
32
 
54
33
  if(nested_stacks)
55
-
56
34
  unpack_nesting(name, file, :update)
57
-
58
35
  else
59
- stack = provider.connection.stacks.get(name)
36
+ unless(stack)
37
+ ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
38
+ raise "Failed to locate stack: #{name}"
39
+ end
60
40
 
61
- if(stack)
62
- ui.info "#{ui.color('SparkleFormation:', :bold)} #{ui.color('update', :green)}"
41
+ ui.info "#{ui.color('Cloud Formation:', :bold)} #{ui.color('update', :green)}"
63
42
 
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
43
+ unless(file)
44
+ if(config[:template])
45
+ file = config[:template]
46
+ stack_info << " #{ui.color('(template provided)', :green)}"
47
+ else
48
+ stack_info << " #{ui.color('(no template update)', :yellow)}"
71
49
  end
72
- ui.info " -> #{stack_info}"
50
+ end
51
+ ui.info " -> #{stack_info}"
73
52
 
53
+ apply_stacks!(stack)
74
54
 
75
- if(file)
76
- stack.template = translate_template(file)
77
- apply_stacks!(stack)
78
- populate_parameters!(file, stack.parameters)
79
- stack.parameters = config[:parameters]
80
- stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
81
- else
82
- apply_stacks!(stack)
83
- populate_parameters!(stack.template, stack.parameters)
84
- stack.parameters = config[:parameters]
55
+ if(file)
56
+ if(config[:print_only])
57
+ ui.puts _format_json(translate_template(file))
58
+ return
85
59
  end
60
+ populate_parameters!(file, :current_parameters => stack.parameters)
61
+ stack.template = translate_template(file)
62
+ stack.parameters = config_root_parameters
63
+ stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
64
+ else
65
+ populate_parameters!(stack.template, :current_parameters => stack.parameters)
66
+ stack.parameters = config_root_parameters
67
+ end
86
68
 
87
- begin
69
+ begin
70
+ api_action!(:api_stack => stack) do
88
71
  stack.save
89
- rescue Miasma::Error::ApiError::RequestError => e
90
- if(e.message.downcase.include?('no updates'))
91
- ui.warn "No updates detected for stack (#{stack.name})"
72
+ if(config[:poll])
73
+ poll_stack(stack.name)
74
+ if(stack.reload.state == :update_complete)
75
+ ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
76
+ namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
77
+ else
78
+ ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
79
+ raise
80
+ end
92
81
  else
93
- raise
82
+ ui.warn 'Stack state polling has been disabled.'
83
+ ui.info "Stack update initialized for #{ui.color(name, :green)}"
94
84
  end
95
85
  end
96
-
97
- if(config[:poll])
98
- poll_stack(stack.name)
99
- if(stack.reload.state == :update_complete)
100
- ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
101
- namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
102
- else
103
- ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
104
- ui.info ""
105
- namespace.const_get(:Inspect).new({:instance_failure => true}, [name]).execute!
106
- raise
107
- end
86
+ rescue Miasma::Error::ApiError::RequestError => e
87
+ if(e.message.downcase.include?('no updates'))
88
+ ui.warn "No updates detected for stack (#{stack.name})"
108
89
  else
109
- ui.warn 'Stack state polling has been disabled.'
110
- ui.info "Stack update initialized for #{ui.color(name, :green)}"
90
+ raise
111
91
  end
112
- else
113
- ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
114
- raise
115
92
  end
116
93
 
117
94
  end
@@ -8,23 +8,49 @@ module Sfn
8
8
 
9
9
  include Sfn::CommandModule::Base
10
10
  include Sfn::CommandModule::Template
11
+ include Sfn::CommandModule::Stack
11
12
 
12
13
  def execute!
14
+ print_only_original = config[:print_only]
15
+ config[:print_only] = true
13
16
  file = load_template_file
14
17
  file.delete('sfn_nested_stack')
15
18
  ui.info "#{ui.color("Template Validation (#{provider.connection.provider}): ", :bold)} #{config[:file].sub(Dir.pwd, '').sub(%r{^/}, '')}"
16
19
  file = Sfn::Utils::StackParameterScrubber.scrub!(file)
17
20
  file = translate_template(file)
21
+ config[:print_only] = print_only_original
22
+
23
+ if(config[:print_only])
24
+ ui.puts _format_json(file)
25
+ else
26
+ validate_stack(file, sparkle_collection.get(:template, config[:file])[:name])
27
+ end
28
+ end
29
+
30
+ def validate_stack(stack, name)
31
+ resources = stack.fetch('Resources', {})
32
+ nested_stacks = resources.find_all do |r_name, r_value|
33
+ r_value.is_a?(Hash) &&
34
+ provider.connection.class.const_get(:RESOURCE_MAPPING).fetch(r_value['Type'], {})[:api] == :orchestration
35
+ end
36
+ nested_stacks.each do |n_name, n_resource|
37
+ validate_stack(n_resource.fetch('Properties', {}).fetch('Stack', {}), "#{name} > #{n_name}")
38
+ n_resource['Properties'].delete('Stack')
39
+ end
18
40
  begin
19
- result = provider.connection.stacks.build(
41
+ ui.info "Validating: #{ui.color(name, :bold)}"
42
+ stack = provider.connection.stacks.build(
20
43
  :name => 'validation-stack',
21
- :template => file
22
- ).validate
44
+ :template => Sfn::Utils::StackParameterScrubber.scrub!(stack)
45
+ )
46
+ result = api_action!(:api_stack => stack) do
47
+ stack.validate
48
+ end
23
49
  ui.info ui.color(' -> VALID', :bold, :green)
24
50
  rescue => e
25
51
  ui.info ui.color(' -> INVALID', :bold, :red)
26
52
  ui.fatal e.message
27
- failed = true
53
+ raise e
28
54
  end
29
55
  end
30
56
 
data/lib/sfn/command.rb CHANGED
@@ -18,8 +18,7 @@ module Sfn
18
18
 
19
19
  # Override to provide config file searching
20
20
  def initialize(cli_opts, args)
21
- unless(cli_opts[:config])
22
- cli_opts = cli_opts.to_hash.to_smash(:snake)
21
+ unless(cli_opts['config'])
23
22
  discover_config(cli_opts)
24
23
  end
25
24
  super(cli_opts, args)
@@ -37,14 +36,18 @@ module Sfn
37
36
  # Start with current working directory and traverse to root
38
37
  # looking for a `.sfn` configuration file
39
38
  #
40
- # @param opts [Smash]
41
- # @return [Smash]
39
+ # @param opts [Slop]
40
+ # @return [Slop]
42
41
  def discover_config(opts)
43
42
  cwd = Dir.pwd.split(File::SEPARATOR)
44
43
  until(cwd.empty? || File.exists?(cwd.push('.sfn').join(File::SEPARATOR)))
45
44
  cwd.pop(2)
46
45
  end
47
- opts[:config] = cwd.join(File::SEPARATOR) unless cwd.empty?
46
+ if(opts.respond_to?(:fetch_option))
47
+ opts.fetch_option('config').value = cwd.join(File::SEPARATOR) unless cwd.empty?
48
+ else
49
+ opts['config'] = cwd.join(File::SEPARATOR) unless cwd.empty?
50
+ end
48
51
  opts
49
52
  end
50
53
 
@@ -11,11 +11,16 @@ module Sfn
11
11
  # @return [KnifeCloudformation::Provider]
12
12
  def provider
13
13
  memoize(:provider, :direct) do
14
- Sfn::Provider.new(
14
+ result = Sfn::Provider.new(
15
15
  :miasma => config[:credentials],
16
16
  :async => false,
17
17
  :fetch => false
18
18
  )
19
+ result.connection.data[:retry_ui] = ui
20
+ result.connection.data[:retry_type] = config.fetch(:retry, :type, :exponential)
21
+ result.connection.data[:retry_interval] = config.fetch(:retry, :interval, 5)
22
+ result.connection.data[:retry_max] = config.fetch(:retry, :max_attempts, 20)
23
+ result
19
24
  end
20
25
  end
21
26
 
@@ -117,9 +122,18 @@ module Sfn
117
122
  arguments
118
123
  end
119
124
 
125
+ # Override config method to memoize the result allowing for
126
+ # modifications to the configuration during runtime
127
+ #
128
+ # @return [Smash]
129
+ # @note callback requires are also loaded here
120
130
  def config
121
131
  memoize(:config) do
122
- super
132
+ result = super
133
+ result.fetch(:callbacks, :require, []).each do |c_loader|
134
+ require c_loader
135
+ end
136
+ result
123
137
  end
124
138
  end
125
139
 
@@ -130,6 +144,7 @@ module Sfn
130
144
  klass.instance_eval do
131
145
 
132
146
  include Sfn::CommandModule::Base::InstanceMethods
147
+ include Sfn::CommandModule::Callbacks
133
148
  include Sfn::Utils::JSON
134
149
  include Sfn::Utils::Output
135
150
  include Bogo::AnimalStrings
@@ -0,0 +1,69 @@
1
+ require 'sfn'
2
+ require 'sparkle_formation'
3
+
4
+ module Sfn
5
+ module CommandModule
6
+ # Callback processor helpers
7
+ module Callbacks
8
+
9
+ include Bogo::Memoization
10
+
11
+ # Run expected callbacks around action
12
+ #
13
+ # @yieldblock api action to run
14
+ # @yieldresult [Object] result from call
15
+ # @return [Object] result of yield block
16
+ def api_action!(*args)
17
+ type = self.class.name.split('::').last.downcase
18
+ run_callbacks_for(["before_#{type}", :before], *args)
19
+ result = yield if block_given?
20
+ run_callbacks_for(["after_#{type}", :after], *args)
21
+ result
22
+ end
23
+
24
+ # Process requested callbacks
25
+ #
26
+ # @param type [Symbol, String] name of callback type
27
+ # @return [NilClass]
28
+ def run_callbacks_for(type, *args)
29
+ types = [type].flatten.compact
30
+ type = types.first
31
+ clbks = types.map do |c_type|
32
+ callbacks_for(c_type)
33
+ end.flatten(1).compact.uniq.each do |item|
34
+ callback_name, callback = item
35
+ ui.info "Callback #{ui.color(type.to_s, :bold)} #{callback_name}: #{ui.color('starting', :yellow)}"
36
+ if(args.empty?)
37
+ callback.call
38
+ else
39
+ callback.call(*args)
40
+ end
41
+ ui.info "Callback #{ui.color(type.to_s, :bold)} #{callback_name}: #{ui.color('complete', :green)}"
42
+ end
43
+ nil
44
+ end
45
+
46
+ # Fetch valid callbacks for given type
47
+ #
48
+ # @param type [Symbol, String] name of callback type
49
+ # @param responder [Array<String, Symbol>] matching response methods
50
+ # @return [Array<Method>]
51
+ def callbacks_for(type)
52
+ ([config.fetch(:callbacks, type, [])].flatten.compact + [config.fetch(:callbacks, :default, [])].flatten.compact).map do |c_name|
53
+ instance = memoize(c_name) do
54
+ begin
55
+ klass = Sfn::Callback.const_get(Bogo::Utility.camel(c_name.to_s))
56
+ klass.new(ui, config, arguments, provider)
57
+ rescue NameError
58
+ raise "Unknown #{type} callback requested: #{c_name} (not found)"
59
+ end
60
+ end
61
+ if(instance.respond_to?(type))
62
+ [c_name, instance.method(type)]
63
+ end
64
+ end.compact
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -104,10 +104,25 @@ module Sfn
104
104
 
105
105
  # Prompt for parameter values and store result
106
106
  #
107
- # @param stack [Hash] stack template
107
+ # @param sparkle [SparkleFormation, Hash]
108
+ # @param opts [Hash]
109
+ # @option opts [Hash] :current_parameters current stack parameter values
110
+ # @option opts [Miasma::Models::Orchestration::Stack] :stack existing stack
108
111
  # @return [Hash]
109
- def populate_parameters!(stack, current_params={})
110
- if(stack['Parameters'])
112
+ def populate_parameters!(sparkle, opts={})
113
+ current_parameters = opts.fetch(:current_parameters, {})
114
+ current_stack = opts[:stack]
115
+ if(sparkle.is_a?(SparkleFormation))
116
+ parameter_prefix = sparkle.root? ? [] : (sparkle.root_path - [sparkle.root]).map do |s|
117
+ Bogo::Utility.camel(s.name)
118
+ end
119
+ stack_parameters = sparkle.compile.parameters
120
+ stack_parameters = stack_parameters.nil? ? Smash.new : stack_parameters._dump
121
+ else
122
+ parameter_prefix = []
123
+ stack_parameters = sparkle.fetch('Parameters', Smash.new)
124
+ end
125
+ unless(stack_parameters.empty?)
111
126
  if(config.get(:parameter).is_a?(Array))
112
127
  config[:parameter] = Smash[
113
128
  *config.get(:parameter).map(&:to_a).flatten
@@ -120,14 +135,37 @@ module Sfn
120
135
  else
121
136
  config.set(:parameters, config.fetch(:parameter, Smash.new))
122
137
  end
123
- stack.fetch('Parameters', {}).each do |k,v|
124
- next if config[:parameters][k]
125
- attempt = 0
138
+ stack_parameters.each do |k,v|
139
+ ns_k = (parameter_prefix + [k]).compact.join('__')
140
+ next if config[:parameters][ns_k]
126
141
  valid = false
142
+ # When parameter is a hash type, it is being set via
143
+ # intrinsic function and we don't modify
144
+ if(current_parameters[k].is_a?(Hash))
145
+ if(current_stack)
146
+ enable_set = validate_stack_parameter(current_stack, k, ns_k, current_parameters[k])
147
+ else
148
+ enable_set = true
149
+ end
150
+ if(enable_set)
151
+ # NOTE: direct set dumps the stack (nfi). Smash will
152
+ # auto dup it, and works, so yay i guess.
153
+ config[:parameters][ns_k] = Smash.new(current_parameters[k])
154
+ valid = true
155
+ end
156
+ else
157
+ if(current_stack && current_stack.data[:parent_stack])
158
+ use_expected = validate_stack_parameter(current_stack, k, ns_k, current_parameters[k])
159
+ unless(use_expected)
160
+ current_parameters[k] = current_stack.parameters[k]
161
+ end
162
+ end
163
+ end
164
+ attempt = 0
127
165
  until(valid)
128
166
  attempt += 1
129
167
  default = config[:parameters].fetch(
130
- k, current_params.fetch(
168
+ ns_k, current_parameters.fetch(
131
169
  k, v['Default']
132
170
  )
133
171
  )
@@ -138,7 +176,7 @@ module Sfn
138
176
  end
139
177
  validation = Sfn::Utils::StackParameterValidator.validate(answer, v)
140
178
  if(validation == true)
141
- config[:parameters][k] = answer
179
+ config[:parameters][ns_k] = answer
142
180
  valid = true
143
181
  else
144
182
  validation.each do |validation_error|
@@ -152,7 +190,67 @@ module Sfn
152
190
  end
153
191
  end
154
192
  end
155
- stack
193
+ Smash[
194
+ config.fetch(:parameters, {}).map do |k,v|
195
+ strip_key = parameter_prefix ? k.sub(/#{parameter_prefix.join('__')}_{2}?/, '') : k
196
+ unless(strip_key.include?('__'))
197
+ [strip_key, v]
198
+ end
199
+ end.compact
200
+ ]
201
+ end
202
+
203
+ # @return [Hash] parameters for root stack create/update
204
+ def config_root_parameters
205
+ Hash[
206
+ config.fetch(:parameters, {}).find_all do |k,v|
207
+ !k.include?('__')
208
+ end
209
+ ]
210
+ end
211
+
212
+ # Validate stack parameter is properly set via stack resource
213
+ # from parent stack. If not properly set, prompt user for
214
+ # expected behavior. This accounts for states encountered when
215
+ # a nested stack's parameters are adjusted directly but the
216
+ # resource sets value via intrinsic function.
217
+ #
218
+ # @param c_stack [Miasma::Models::Orchestration::Stack] current stack
219
+ # @param p_key [String] stack parameter key
220
+ # @param p_ns_key [String] namespaced stack parameter key
221
+ # @param c_value [Hash] currently set value (via intrinsic function)
222
+ # @return [TrueClass, FalseClass] value is validated
223
+ def validate_stack_parameter(c_stack, p_key, p_ns_key, c_value)
224
+ stack_value = c_stack.parameters[p_key]
225
+ p_stack = c_stack.data[:parent_stack]
226
+ if(c_value.is_a?(Hash))
227
+ case c_value.keys.first
228
+ when 'Ref'
229
+ current_value = p_stack.parameters[c_value.values.first]
230
+ when 'Fn::Att'
231
+ resource_name, output_name = c_value.values.first.split('.', 2)
232
+ ref_stack = p_stack.nested_stacks.detect{|i| i.data[:logical_id] == resource_name}
233
+ if(ref_stack)
234
+ output = ref_stack.outputs.detect do |o|
235
+ o.key == output_name
236
+ end
237
+ if(output)
238
+ current_value = output.value
239
+ end
240
+ end
241
+ end
242
+ else
243
+ current_value = c_value
244
+ end
245
+ if(current_value && current_value != stack_value)
246
+ ui.warn 'Nested stack has been altered directly! This update may cause unexpected modifications!'
247
+ ui.warn "Stack name: #{c_stack.name}. Parameter: #{p_key}. Current value: #{stack_value}. Expected value: #{current_value} (via: #{c_value.inspect})"
248
+ answer = ui.ask_question("Use current value or expected value for #{p_key} [current/expected]?", :valid => ['current', 'expected'])
249
+ answer == 'expected'
250
+ else
251
+ ui.warn "Unable to check #{p_key} for direct value modification. (Cannot auto-check expected value #{c_value.inspect})"
252
+ true
253
+ end
156
254
  end
157
255
 
158
256
  end