sfn 0.5.0 → 1.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.
@@ -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