sfn 1.1.6 → 1.1.8

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.
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