sfn 1.1.6 → 1.1.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 33cc7d74a1c8f4392f0582809856400ed3853ba7
4
- data.tar.gz: 8d8c74ef584bedcf110a8af780b83f0cb8b484e9
3
+ metadata.gz: 698f214f1cdff652dc16b36120697090de470f87
4
+ data.tar.gz: 7efbb32522675152516584455f8f317b9816e6f8
5
5
  SHA512:
6
- metadata.gz: 4806b2694db30ba48e71aaa4c5de7c6c514a930e61529471ad4a3c23561b229086a501146f234356926a52a3c91f5e167db3f82f1158a655e998d059b5955406
7
- data.tar.gz: 05fde4e6095043513c2c76cd19d7c72346fdbfb34679cbaec3bea85a810cdf1f8131d8a1bf43d8b60588717c508134ad7580c8fb1d3f6e1c1bef5130f9610c89
6
+ metadata.gz: 0f2de1bf189064aa4213a6211730d7a0d0760ff613ce5974db6e8957ee7a5301574fcaa15411be1235ca7c7f9f251bd34c9274288d809f1683f540ffa85c7a71
7
+ data.tar.gz: c268b90db20e18cb7534ef63b19883026d73e47e62d3cffcfae0d894cb7ad41d58032c715d172d0005f772212250803595c62b06eb9253b92d210ae75940b141
@@ -1,3 +1,8 @@
1
+ ## v1.1.8
2
+ * [fix] Disable knife config mashing to get expected values (#72)
3
+ * [feature] Add new `conf` command (#72)
4
+ * [feature] Add planning support for stack updates (#69)
5
+
1
6
  ## v1.1.6
2
7
  * [fix] set proper parameter hash on apply stack (#67)
3
8
 
@@ -112,6 +112,7 @@ These commands are used for inspection or removal of existing stacks:
112
112
 
113
113
  * `sfn describe`
114
114
  * `sfn inspect`
115
+ * `sfn diff`
115
116
  * `sfn events`
116
117
  * `sfn destroy`
117
118
 
@@ -63,6 +63,7 @@ unless(defined?(Chef::Knife::CloudformationCreate))
63
63
  base[k]
64
64
  end.compact.first || {}
65
65
  cmd_config = cmd_config.to_smash
66
+
66
67
  reconfig = config.find_all do |k,v|
67
68
  !v.nil?
68
69
  end
@@ -73,11 +74,16 @@ unless(defined?(Chef::Knife::CloudformationCreate))
73
74
  end
74
75
  [k,v]
75
76
  end
76
- config = Smash[reconfig]
77
- cmd_config = cmd_config.deep_merge(config)
77
+ n_config = Smash[reconfig]
78
+ cmd_config = cmd_config.deep_merge(n_config)
78
79
  self.class.sfn_class.new(cmd_config, name_args).execute!
79
80
  end
80
81
 
82
+ # NOOP this merge as it breaks expected state
83
+ def merge_configs
84
+ config
85
+ end
86
+
81
87
  end
82
88
  knife_klass.instance_variable_set(:@name, "Chef::Knife::#{command_class}")
83
89
  knife_klass.instance_variable_set(
data/lib/sfn.rb CHANGED
@@ -14,5 +14,6 @@ module Sfn
14
14
  autoload :Knife, 'sfn/knife'
15
15
  autoload :Command, 'sfn/command'
16
16
  autoload :CommandModule, 'sfn/command_module'
17
+ autoload :Planner, 'sfn/planner'
17
18
 
18
19
  end
@@ -17,10 +17,10 @@ module Sfn
17
17
 
18
18
  # Create a new callback instance
19
19
  #
20
- # @param [Bogo::Ui]
21
- # @param [Smash] configuration hash
22
- # @param [Array<String>] arguments from the CLI
23
- # @param [Provider] API connection
20
+ # @param ui [Bogo::Ui]
21
+ # @param config [Smash] configuration hash
22
+ # @param arguments [Array<String>] arguments from the CLI
23
+ # @param api [Provider] API connection
24
24
  #
25
25
  # @return [self]
26
26
  def initialize(ui, config, arguments, api)
@@ -4,6 +4,9 @@ require 'bogo-cli'
4
4
  module Sfn
5
5
  class Command < Bogo::Cli::Command
6
6
 
7
+ include CommandModule::Callbacks
8
+
9
+ autoload :Conf, 'sfn/command/conf'
7
10
  autoload :Create, 'sfn/command/create'
8
11
  autoload :Describe, 'sfn/command/describe'
9
12
  autoload :Destroy, 'sfn/command/destroy'
@@ -0,0 +1,39 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ class Command
5
+ # Config command
6
+ class Conf < Command
7
+
8
+ # Run the list command
9
+ def execute!
10
+ ui.info ui.color("Current configuration state:")
11
+ Config::Conf.attributes.sort_by(&:first).each do |k, val|
12
+ if(config.has_key?(k))
13
+ ui.print " #{ui.color(k, :bold, :green)}: "
14
+ format_value(config[k], ' ')
15
+ end
16
+ end
17
+ end
18
+
19
+ def format_value(value, indent='')
20
+ if(value.is_a?(Hash))
21
+ ui.puts
22
+ value.sort_by(&:first).each do |k,v|
23
+ ui.print "#{indent} #{ui.color(k, :bold)}: "
24
+ format_value(v, indent + ' ')
25
+ end
26
+ elsif(value.is_a?(Array))
27
+ ui.puts
28
+ value.map(&:to_s).sort.each do |v|
29
+ ui.print "#{indent} "
30
+ format_value(v, indent + ' ')
31
+ end
32
+ else
33
+ ui.puts value.to_s
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -76,14 +76,42 @@ module Sfn
76
76
  ui.puts _format_json(translate_template(file))
77
77
  return
78
78
  end
79
+
80
+ original_template = stack.template
81
+ original_parameters = stack.parameters
82
+
79
83
  stack.template = translate_template(file)
80
84
  apply_stacks!(stack)
85
+
81
86
  populate_parameters!(file, :current_parameters => stack.parameters)
87
+ update_template = stack.template
88
+
89
+ if(config[:plan])
90
+ stack.template = original_template
91
+ stack.parameters = original_parameters
92
+ plan = build_planner(stack)
93
+ if(plan)
94
+ result = plan.generate_plan(file, config_root_parameters)
95
+ display_plan_information(result)
96
+ end
97
+ end
98
+
82
99
  stack.parameters = config_root_parameters
83
- stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
100
+ stack.template = Sfn::Utils::StackParameterScrubber.scrub!(update_template)
84
101
  else
85
102
  apply_stacks!(stack)
103
+
104
+ original_parameters = stack.parameters
86
105
  populate_parameters!(stack.template, :current_parameters => stack.parameters)
106
+
107
+ if(config[:plan])
108
+ stack.parameters = original_parameters
109
+ plan = Planner::Aws.new(ui, config, arguments, stack)
110
+ if(plan)
111
+ result = plan.generate_plan(stack.template, config_root_parameters)
112
+ display_plan_information(result)
113
+ end
114
+ end
87
115
  stack.parameters = config_root_parameters
88
116
  end
89
117
 
@@ -115,6 +143,103 @@ module Sfn
115
143
  end
116
144
  end
117
145
 
146
+ def build_planner(stack)
147
+ klass_name = stack.api.class.to_s.split('::').last
148
+ klass = Planner.const_get(klass_name)
149
+ if(klass)
150
+ klass.new(ui, config, arguments, stack)
151
+ else
152
+ warn "Failed to build planner for current provider. No provider implemented. (`#{klass_name}`)"
153
+ nil
154
+ end
155
+ end
156
+
157
+ def display_plan_information(result)
158
+ ui.info ui.color('Pre-update resource planning report:', :bold)
159
+ unless(print_plan_result(result))
160
+ ui.info 'No resources life cycle changes detected in this update!'
161
+ end
162
+ ui.confirm 'Apply this stack update?'
163
+ end
164
+
165
+
166
+ def print_plan_result(info, names=[])
167
+ said_things = false
168
+ unless(info[:stacks].empty?)
169
+ info[:stacks].each do |s_name, s_info|
170
+ said_things = print_plan_result(s_info, [*names, s_name].compact)
171
+ end
172
+ end
173
+ unless(names.flatten.compact.empty?)
174
+ ui.puts
175
+ ui.puts " #{ui.color('Update plan for:', :bold)} #{ui.color(names.join(' > '), :blue)}"
176
+ unless(info[:unknown].empty?)
177
+ ui.puts " #{ui.color('!!! Unknown update effect:', :red, :bold)}"
178
+ print_plan_items(info, :unknown, :red)
179
+ ui.puts
180
+ said_things = true
181
+ end
182
+ unless(info[:unavailable].empty?)
183
+ ui.puts " #{ui.color('Update request not allowed:', :red, :bold)}"
184
+ print_plan_items(info, :unavailable, :red)
185
+ ui.puts
186
+ said_things = true
187
+ end
188
+ unless(info[:replace].empty?)
189
+ ui.puts " #{ui.color('Resources to be replaced:', :red, :bold)}"
190
+ print_plan_items(info, :replace, :red)
191
+ ui.puts
192
+ said_things = true
193
+ end
194
+ unless(info[:interrupt].empty?)
195
+ ui.puts " #{ui.color('Resources to be interrupted:', :yellow, :bold)}"
196
+ print_plan_items(info, :interrupt, :yellow)
197
+ ui.puts
198
+ said_things = true
199
+ end
200
+ unless(info[:removed].empty?)
201
+ ui.puts " #{ui.color('Resources to be removed:', :red, :bold)}"
202
+ print_plan_items(info, :removed, :red)
203
+ ui.puts
204
+ said_things = true
205
+ end
206
+ unless(info[:added].empty?)
207
+ ui.puts " #{ui.color('Resources to be added:', :green, :bold)}"
208
+ print_plan_items(info, :added, :green)
209
+ ui.puts
210
+ said_things = true
211
+ end
212
+ unless(said_things)
213
+ ui.puts " #{ui.color('No resource lifecycle changes detected!', :green)}"
214
+ ui.puts
215
+ end
216
+ end
217
+ said_things
218
+ end
219
+
220
+ # Print planning items
221
+ #
222
+ # @param info [Hash] plan
223
+ # @param key [Symbol] key of items
224
+ # @param color [Symbol] color to flag
225
+ def print_plan_items(info, key, color)
226
+ max_name = info[key].keys.map(&:size).max
227
+ max_type = info[key].values.map{|i|i[:type]}.map(&:size).max
228
+ info[key].each do |name, val|
229
+ ui.print ' ' * 6
230
+ ui.print ui.color("[#{val[:type]}]", color)
231
+ ui.print ' ' * (max_type - val[:type].size)
232
+ ui.print ' ' * 4
233
+ ui.print ui.color(name, :bold)
234
+ unless(val[:properties].nil? || val[:properties].empty?)
235
+ ui.print ' ' * (max_name - name.size)
236
+ ui.print ' ' * 4
237
+ ui.print "Reason: Updated properties: `#{val[:properties].join('`, `')}`"
238
+ end
239
+ ui.puts
240
+ end
241
+ end
242
+
118
243
  end
119
244
  end
120
245
  end
@@ -123,11 +123,6 @@ module Sfn
123
123
  stack_parameters = sparkle.fetch('Parameters', Smash.new)
124
124
  end
125
125
  unless(stack_parameters.empty?)
126
- if(sparkle.is_a?(SparkleFormation))
127
- ui.info "#{ui.color('Stack runtime parameters:', :bold)} - template: #{ui.color(sparkle.root_path.map(&:name).map(&:to_s).join(' > '), :green, :bold)}"
128
- else
129
- ui.info ui.color('Stack runtime parameters:', :bold)
130
- end
131
126
  if(config.get(:parameter).is_a?(Array))
132
127
  config[:parameter] = Smash[
133
128
  *config.get(:parameter).map(&:to_a).flatten
@@ -140,6 +135,7 @@ module Sfn
140
135
  else
141
136
  config.set(:parameters, config.fetch(:parameter, Smash.new))
142
137
  end
138
+ param_banner = false
143
139
  stack_parameters.each do |k,v|
144
140
  ns_k = (parameter_prefix + [k]).compact.join('__')
145
141
  next if config[:parameters][ns_k]
@@ -167,6 +163,14 @@ module Sfn
167
163
  end
168
164
  end
169
165
  attempt = 0
166
+ if(!valid && !param_banner)
167
+ if(sparkle.is_a?(SparkleFormation))
168
+ ui.info "#{ui.color('Stack runtime parameters:', :bold)} - template: #{ui.color(sparkle.root_path.map(&:name).map(&:to_s).join(' > '), :green, :bold)}"
169
+ else
170
+ ui.info ui.color('Stack runtime parameters:', :bold)
171
+ end
172
+ param_banner = true
173
+ end
170
174
  until(valid)
171
175
  attempt += 1
172
176
  default = config[:parameters].fetch(
@@ -139,11 +139,13 @@ module Sfn
139
139
  if(formation.compile_state)
140
140
  current_state = current_state.merge(formation.compile_state)
141
141
  end
142
- ui.info "#{ui.color('Compile time parameters:', :bold)} - template: #{ui.color(pathed_name, :green, :bold)}" unless config[:print_only]
143
- formation.parameters.each do |k,v|
144
- current_state[k] = request_compile_parameter(k, v, current_state[k], !!formation.parent)
142
+ unless(formation.parameters.empty?)
143
+ ui.info "#{ui.color('Compile time parameters:', :bold)} - template: #{ui.color(pathed_name, :green, :bold)}" unless config[:print_only]
144
+ formation.parameters.each do |k,v|
145
+ current_state[k] = request_compile_parameter(k, v, current_state[k], !!formation.parent)
146
+ end
147
+ formation.compile_state = current_state
145
148
  end
146
- formation.compile_state = current_state
147
149
  end
148
150
  sparkle_packs.each do |pack|
149
151
  sf.sparkle.add_sparkle(pack)
@@ -197,7 +199,9 @@ module Sfn
197
199
  if(config[:print_only])
198
200
  template_url = "http://example.com/bucket/#{name_args.first}_#{stack_name}.json"
199
201
  else
200
- resource.properties.delete!(:stack)
202
+ unless(config[:plan])
203
+ resource.properties.delete!(:stack)
204
+ end
201
205
  unless(bucket)
202
206
  raise "Failed to locate configured bucket for stack template storage (#{bucket})!"
203
207
  end
@@ -241,7 +245,9 @@ module Sfn
241
245
  :current_parameters => current_parameters
242
246
  )
243
247
  )
244
- resource.properties.delete!(:stack)
248
+ unless(config[:plan])
249
+ resource.properties.delete!(:stack)
250
+ end
245
251
  bucket = provider.connection.api_for(:storage).buckets.get(
246
252
  config[:nesting_bucket]
247
253
  )
@@ -9,6 +9,7 @@ module Sfn
9
9
  # Only values allowed designating bool type
10
10
  BOOLEAN_VALUES = [TrueClass, FalseClass]
11
11
 
12
+ autoload :Conf, 'sfn/config/conf'
12
13
  autoload :Create, 'sfn/config/create'
13
14
  autoload :Describe, 'sfn/config/describe'
14
15
  autoload :Destroy, 'sfn/config/destroy'
@@ -67,6 +68,7 @@ module Sfn
67
68
  :description => 'Automatically accept any requests for confirmation'
68
69
  )
69
70
 
71
+ attribute :conf, Conf, :coerce => proc{|v| Conf.new(v)}
70
72
  attribute :create, Create, :coerce => proc{|v| Create.new(v)}
71
73
  attribute :update, Update, :coerce => proc{|v| Update.new(v)}
72
74
  attribute :destroy, Destroy, :coerce => proc{|v| Destroy.new(v)}
@@ -0,0 +1,9 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ class Config
5
+ # Config command configuration (subclass create to get all the configs)
6
+ class Conf < Create
7
+ end
8
+ end
9
+ end
@@ -28,6 +28,10 @@ module Sfn
28
28
  :multiple => true,
29
29
  :description => 'Notification endpoints for stack events'
30
30
  )
31
+ attribute(
32
+ :plan, FalseClass,
33
+ :default => false
34
+ )
31
35
 
32
36
  end
33
37
  end
@@ -18,6 +18,11 @@ module Sfn
18
18
  v.is_a?(String) ? Smash[*v.split(/[=:]/, 2)] : v
19
19
  }
20
20
  )
21
+ attribute(
22
+ :plan, [TrueClass, FalseClass],
23
+ :default => true,
24
+ :description => 'Provide planning information prior to update'
25
+ )
21
26
 
22
27
  end
23
28
  end
@@ -0,0 +1,44 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ # Interface for generating plan report
5
+ class Planner
6
+
7
+ autoload :Aws, 'sfn/planner/aws'
8
+
9
+ # @return [Bogo::Ui]
10
+ attr_reader :ui
11
+ # @return [Smash]
12
+ attr_reader :config
13
+ # @return [Array<String>] CLI arguments
14
+ attr_reader :arguments
15
+ # @return [Miasma::Models::Orchestration::Stack] existing remote stack
16
+ attr_reader :origin_stack
17
+
18
+ # Create a new planner instance
19
+ #
20
+ # @param ui [Bogo::Ui]
21
+ # @param config [Smash]
22
+ # @param arguments [Array<String>]
23
+ # @param stack [Miasma::Models::Orchestration::Stack]
24
+ #
25
+ # @return [self]
26
+ def initialize(ui, config, arguments, stack)
27
+ @ui = ui
28
+ @config = config
29
+ @arguments = arguments
30
+ @origin_stack = stack
31
+ end
32
+
33
+ # Generate update report
34
+ #
35
+ # @param template [Hash] updated template
36
+ # @param parameters [Hash] runtime parameters for update
37
+ #
38
+ # @return [Hash] report
39
+ def generate_plan(template, parameters)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,410 @@
1
+ require 'pp'
2
+
3
+
4
+ require 'sfn'
5
+ require 'sparkle_formation/aws'
6
+ require 'hashdiff'
7
+
8
+ module Sfn
9
+ class Planner
10
+ # AWS specific planner
11
+ class Aws < Planner
12
+
13
+ # Customized translator to dereference template
14
+ class Translator < SparkleFormation::Translation
15
+
16
+ # @return [Array<String>] flagged items for value replacement
17
+ attr_reader :flagged
18
+
19
+ # Override to init flagged array
20
+ def initialize(*_)
21
+ super
22
+ @flagged = []
23
+ end
24
+
25
+ # Flag a reference as modified
26
+ #
27
+ # @param ref_name [String]
28
+ # @return [Array<String>]
29
+ def flag_ref(ref_name)
30
+ @flagged << ref_name.to_s
31
+ @flagged.uniq!
32
+ end
33
+
34
+ # Check if resource name is flagged
35
+ #
36
+ # @param name [String]
37
+ # @return [TrueClass, FalseClass]
38
+ def flagged?(name)
39
+ @flagged.include?(name.to_s)
40
+ end
41
+
42
+ # Apply function if possible
43
+ #
44
+ # @param hash [Hash]
45
+ # @param funcs [Array] allowed functions
46
+ # @return [Hash]
47
+ # @note also allows 'Ref' within funcs to provide mapping
48
+ # replacements using the REF_MAPPING constant
49
+ def apply_function(hash, funcs=[])
50
+ k,v = hash.first
51
+ if(hash.size == 1 && (k.start_with?('Fn') || k == 'Ref') && (funcs.empty? || funcs.include?(k)))
52
+ case k
53
+ when 'Fn::Join'
54
+ v.last.join(v.first)
55
+ when 'Fn::FindInMap'
56
+ map_holder = mappings[v[0]]
57
+ if(map_holder)
58
+ map_item = map_holder[dereference(v[1])]
59
+ if(map_item)
60
+ map_item[v[2]]
61
+ else
62
+ raise "Failed to find mapping item! (#{v[0]} -> #{v[1]})"
63
+ end
64
+ else
65
+ raise "Failed to find mapping! (#{v[0]})"
66
+ end
67
+ when 'Fn::GetAtt'
68
+ func.include?('DEREF') ? dereference(hash) : hash
69
+ when 'Ref'
70
+ if(funcs.include?('DEREF'))
71
+ dereference(hash)
72
+ else
73
+ {'Ref' => self.class.const_get(:REF_MAPPING).fetch(v, v)}
74
+ end
75
+ else
76
+ hash
77
+ end
78
+ else
79
+ hash
80
+ end
81
+ end
82
+
83
+ # Override the parent dereference behavior to return junk
84
+ # value on flagged resource match
85
+ #
86
+ # @param hash [Hash]
87
+ # @return [Hash, String]
88
+ def dereference(hash)
89
+ result = nil
90
+ if(hash.is_a?(Hash))
91
+ if(hash.keys.first == 'Ref' && flagged?(hash.values.first))
92
+ result = '__MODIFIED_REFERENCE_VALUE__'
93
+ elsif(hash.keys.first == 'Fn::GetAtt')
94
+ if(hash.values.last.last.start_with?('Outputs.'))
95
+ if(flagged?(hash.values.join('_')))
96
+ result = '__MODIFIED_REFERENCE_VALUE__'
97
+ end
98
+ elsif(flagged?(hash.values.first))
99
+ result = '__MODIFIED_REFERENCE_VALUE__'
100
+ end
101
+ end
102
+ end
103
+ result.nil? ? super : result
104
+ end
105
+
106
+ end
107
+
108
+ # Resources that will be replaced on metadata init updates
109
+ REPLACE_ON_CFN_INIT_UPDATE = [
110
+ 'AWS::AutoScaling::LaunchConfiguration'
111
+ ]
112
+
113
+ # @return [Smash] initialized translators
114
+ attr_accessor :translators
115
+
116
+ # Simple overload to load in aws resource set from
117
+ # sparkleformation
118
+ def initialize(*_)
119
+ super
120
+ SfnAws.load!
121
+ @translators = Smash.new
122
+ end
123
+
124
+ # Generate update report
125
+ #
126
+ # @param template [Hash] updated template
127
+ # @param parameters [Hash] runtime parameters for update
128
+ #
129
+ # @return [Hash] report
130
+ def generate_plan(template, parameters)
131
+ Smash.new(
132
+ :stacks => Smash.new(
133
+ origin_stack.name => plan_stack(
134
+ origin_stack,
135
+ template,
136
+ parameters
137
+ )
138
+ ),
139
+ :added => Smash.new,
140
+ :removed => Smash.new,
141
+ :replace => Smash.new,
142
+ :interrupt => Smash.new,
143
+ :unavailable => Smash.new,
144
+ :unknown => Smash.new
145
+ )
146
+ end
147
+
148
+ protected
149
+
150
+ # Generate plan for stack
151
+ #
152
+ # @param stack [Miasma::Models::Orchestration::Stack]
153
+ # @param new_template [Hash]
154
+ # @param new_parameters [Hash]
155
+ # @return [Hash]
156
+ def plan_stack(stack, new_template, new_parameters)
157
+ plan_results = Smash.new(
158
+ :stacks => Smash.new,
159
+ :added => Smash.new,
160
+ :removed => Smash.new,
161
+ :replace => Smash.new,
162
+ :interrupt => Smash.new,
163
+ :unavailable => Smash.new,
164
+ :unknown => Smash.new,
165
+ :outputs => Smash.new,
166
+ :n_outputs => []
167
+ )
168
+
169
+ origin_template = dereference_template("#{stack.data.checksum}_origin", stack.template, stack.parameters)
170
+
171
+ t_key = "#{stack.data.checksum}_#{stack.data.fetch(:logical_id, stack.name)}"
172
+ run_stack_diff(stack, t_key, plan_results, origin_template, new_template, new_parameters)
173
+
174
+ new_checksum = nil
175
+ current_checksum = false
176
+ until(new_checksum == current_checksum)
177
+ current_checksum = plan_results.checksum
178
+ run_stack_diff(stack, t_key, plan_results, origin_template, new_template, new_parameters)
179
+ new_checksum = plan_results.checksum
180
+ end
181
+ scrub_plan(plan_results)
182
+ plan_results
183
+ end
184
+
185
+ # Check if resource type is stack resource type
186
+ #
187
+ # @param type [String]
188
+ # @return [TrueClass, FalseClass]
189
+ def is_stack?(type)
190
+ origin_stack.api.data.fetch(:stack_types, ['AWS::CloudFormation::Stack']).include?(type)
191
+ end
192
+
193
+ # Scrub the plan results to only provide highest precedence diff
194
+ # items
195
+ #
196
+ # @param results [Hash]
197
+ # @return [NilClass]
198
+ def scrub_plan(results)
199
+ precedence = [:unavailable, :replace, :interrupt, :unavailable, :unknown]
200
+ until(precedence.empty?)
201
+ key = precedence.shift
202
+ results[key].keys.each do |k|
203
+ precedence.each do |p_key|
204
+ results[p_key].delete(k)
205
+ end
206
+ end
207
+ end
208
+ nil
209
+ end
210
+
211
+ # Run the stack diff and populate the result set
212
+ #
213
+ # @param stack [Miasma::Models::Orchestration::Stack] existing stack
214
+ # @param plan_result [Smash] plan data to populate
215
+ # @param origin_template [Smash] template of existing stack
216
+ # @param new_template [Smash] template to replace existing
217
+ # @param new_parameters [Smash] parameters to be applied to update
218
+ # @return [NilClass]
219
+ def run_stack_diff(stack, t_key, plan_results, origin_template, new_template, new_parameters)
220
+ translator = translator_for(t_key)
221
+
222
+ new_parameters = new_parameters.dup
223
+ stack.parameters.each do |k,v|
224
+ if(new_parameters[k].is_a?(Hash))
225
+ val = translator.dereference(new_parameters[k])
226
+ new_parameters[k] = val == new_parameters[k] ? v : val
227
+ end
228
+ end
229
+
230
+ update_template = dereference_template(
231
+ t_key, new_template.to_smash, new_parameters,
232
+ plan_results[:replace].keys + plan_results[:unavailable].keys
233
+ )
234
+
235
+ o_nested_stacks = origin_template['Resources'].find_all do |s_name, s_val|
236
+ is_stack?(s_val['Type'])
237
+ end.map(&:first)
238
+ n_nested_stacks = new_template['Resources'].find_all do |s_name, s_val|
239
+ is_stack?(s_val['Type'])
240
+ end.map(&:first)
241
+ [o_nested_stacks + n_nested_stacks].flatten.compact.uniq.each do |n_name|
242
+ o_stack = stack.nested_stacks(false).detect{|s| s.data[:logical_id] == n_name}
243
+ n_exists = is_stack?(update_template['Resources'].fetch(n_name, {})['Type'])
244
+ n_template = update_template['Resources'].fetch(n_name, {}).fetch('Properties', {})['Stack']
245
+ n_parameters = update_template['Resources'].fetch(n_name, {}).fetch('Properties', {})['Parameters']
246
+ n_type = update_template['Resources'].fetch(n_name, {}).fetch('Type',
247
+ origin_template['Resources'][n_name]['Type']
248
+ )
249
+ resource = Smash.new(
250
+ :name => n_name,
251
+ :type => n_type,
252
+ :properties => []
253
+ )
254
+ if(o_stack && n_template)
255
+ n_parameters.keys.each do |n_key|
256
+ n_parameters[n_key] = translator.dereference(n_parameters[n_key])
257
+ end
258
+ n_results = plan_stack(o_stack, n_template, n_parameters)
259
+ unless(n_results[:outputs].empty?)
260
+ n_results[:outputs].keys.each do |n_output|
261
+ translator.flag_ref("#{n_name}_Outputs.#{n_output}")
262
+ end
263
+ end
264
+ plan_results[:stacks][n_name] = n_results
265
+ elsif(o_stack && (!n_template && !n_exists))
266
+ plan_results[:removed][n_name] = resource
267
+ elsif(n_template && !o_stack)
268
+ plan_results[:added][n_name] = resource
269
+ end
270
+ end
271
+ n_nested_stacks.each do |ns_name|
272
+ update_template['Resources'][ns_name]['Properties'].delete('Stack')
273
+ end
274
+ HashDiff.diff(origin_template, MultiJson.load(MultiJson.dump(update_template))).group_by do |item|
275
+ item[1]
276
+ end.each do |a_path, diff_items|
277
+ register_diff(
278
+ plan_results, a_path, diff_items, translator_for(t_key),
279
+ :origin => origin_template,
280
+ :update => update_template
281
+ )
282
+ end
283
+ nil
284
+ end
285
+
286
+ # Register a diff item into the results set
287
+ #
288
+ # @param results [Hash]
289
+ # @param path [String]
290
+ # @param diff [Array]
291
+ # @param templates [Hash]
292
+ # @option :templates [Hash] :origin
293
+ # @option :templates [Hash] :update
294
+ def register_diff(results, path, diff, translator, templates)
295
+ if(path.start_with?('Resources'))
296
+ p_path = path.split('.')
297
+ if(p_path.size == 2)
298
+ diff = diff.first
299
+ key = diff.first == '+' ? :added : :removed
300
+ type = (key == :added ? templates[:update] : templates[:origin])['Resources'][p_path.last]['Type']
301
+ results[key][p_path.last] = Smash.new(
302
+ :name => p_path.last,
303
+ :type => type,
304
+ :properties => []
305
+ )
306
+ else
307
+ if(p_path.include?('Properties'))
308
+ resource_name = p_path[1]
309
+ property_name = p_path[3].sub(/\[\d+\]$/, '')
310
+ type = templates[:origin]['Resources'][resource_name]['Type']
311
+ info = SfnAws.registry[type]
312
+ effect = info[:full_properties].fetch(property_name, {}).fetch(:update_causes, :unknown)
313
+ resource = Smash.new(
314
+ :name => resource_name,
315
+ :type => type,
316
+ :properties => [property_name]
317
+ )
318
+ case effect
319
+ when :replacement
320
+ set_resource(:replace, results, resource_name, resource)
321
+ when :interrupt
322
+ set_resource(:interrupt, results, resource_name, resource)
323
+ when :unavailable
324
+ set_resource(:unavailable, results, resource_name, resource)
325
+ when :none
326
+ # \o/
327
+ else
328
+ set_resource(:unknown, results, resource_name, resource)
329
+ end
330
+ elsif(p_path.include?('AWS::CloudFormation::Init'))
331
+ resource_name = p_path[1]
332
+ type = templates[:origin]['Resources'][resource_name]['Type']
333
+ if(REPLACE_ON_CFN_INIT_UPDATE.include?(type))
334
+ set_resource(:replace, results, resource_name,
335
+ Smash.new(
336
+ :name => resource_name,
337
+ :type => type,
338
+ :properties => ['AWS::CloudFormation::Init']
339
+ )
340
+ )
341
+ end
342
+ end
343
+ end
344
+ elsif(path.start_with?('Outputs'))
345
+ set_resource(:outputs, results, path.split('.')[1], {:properties => []})
346
+ end
347
+ end
348
+
349
+ # Set resource item into result set
350
+ #
351
+ # @param kind [Symbol]
352
+ # @param results [Hash]
353
+ # @param name [String]
354
+ # @param resource [Hash]
355
+ def set_resource(kind, results, name, resource)
356
+ if(results[kind][name])
357
+ results[kind][name][:properties] += resource[:properties]
358
+ results[kind][name][:properties].uniq!
359
+ else
360
+ results[kind][name] = resource
361
+ end
362
+ end
363
+
364
+ # Dereference all parameters within template to allow for
365
+ # processing using real values
366
+ #
367
+ # @param t_key [String]
368
+ # @param template [Hash]
369
+ # @param parameters [Hash]
370
+ # @param flagged [Array<String>]
371
+ #
372
+ # @return [Hash]
373
+ def dereference_template(t_key, template, parameters, flagged=[])
374
+ translator = translator_for(t_key, template, parameters)
375
+ flagged.each do |item|
376
+ translator.flag_ref(item)
377
+ end
378
+ template['Resources'] = translator.dereference_processor(template['Resources'], ['Ref', 'Fn', 'DEREF'])
379
+ template['Outputs'] = translator.dereference_processor(template['Outputs'], ['Ref', 'Fn', 'DEREF'])
380
+ template
381
+ end
382
+
383
+ # Provide a translator instance for given key (new or cached instance)
384
+ #
385
+ # @param t_key [String] identifier
386
+ # @param template [Hash] stack template
387
+ # @param parameters [Hash] stack parameters
388
+ # @return [Translator]
389
+ def translator_for(t_key, template=nil, parameters=nil)
390
+ o_translator = translators[t_key]
391
+ if(template)
392
+ translator = Translator.new(template, :parameters => parameters)
393
+ if(o_translator)
394
+ o_translator.flagged.each do |i|
395
+ translator.flag_ref(i)
396
+ end
397
+ end
398
+ translators[t_key] = translator
399
+ o_translator = translator
400
+ else
401
+ unless(o_translator)
402
+ o_translator = Translator.new({}, :parameters => {})
403
+ end
404
+ end
405
+ o_translator
406
+ end
407
+
408
+ end
409
+ end
410
+ end
@@ -1,4 +1,4 @@
1
1
  module Sfn
2
2
  # Current library version
3
- VERSION = Gem::Version.new('1.1.6')
3
+ VERSION = Gem::Version.new('1.1.8')
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sfn
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Roberts
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-30 00:00:00.000000000 Z
11
+ date: 2015-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bogo-cli
@@ -124,6 +124,7 @@ files:
124
124
  - lib/sfn/callback.rb
125
125
  - lib/sfn/callback/stack_policy.rb
126
126
  - lib/sfn/command.rb
127
+ - lib/sfn/command/conf.rb
127
128
  - lib/sfn/command/create.rb
128
129
  - lib/sfn/command/describe.rb
129
130
  - lib/sfn/command/destroy.rb
@@ -143,6 +144,7 @@ files:
143
144
  - lib/sfn/command_module/stack.rb
144
145
  - lib/sfn/command_module/template.rb
145
146
  - lib/sfn/config.rb
147
+ - lib/sfn/config/conf.rb
146
148
  - lib/sfn/config/create.rb
147
149
  - lib/sfn/config/describe.rb
148
150
  - lib/sfn/config/destroy.rb
@@ -158,6 +160,8 @@ files:
158
160
  - lib/sfn/config/validate.rb
159
161
  - lib/sfn/monkey_patch.rb
160
162
  - lib/sfn/monkey_patch/stack.rb
163
+ - lib/sfn/planner.rb
164
+ - lib/sfn/planner/aws.rb
161
165
  - lib/sfn/provider.rb
162
166
  - lib/sfn/utils.rb
163
167
  - lib/sfn/utils/debug.rb