sfn 2.1.6 → 2.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: 1b2ef775b264c538208dcd1cb1010fe50af9c5fd
4
- data.tar.gz: 3043b030b8b16a2a356cdba35b6299497c16ea27
3
+ metadata.gz: 610903a7a6be9347a5c0d51bff46e3eba9239e28
4
+ data.tar.gz: 0aacd95b68b649c70416a02c77c8bf06d60072cf
5
5
  SHA512:
6
- metadata.gz: 3ed7440587bfc33ff5d2224cb3306fdf5dbc5e546a750e5987aa3df3f5ce5788bca5f8738741dc3bdb9e4c893fb21708859897ce18be27be19b44866ffb906e1
7
- data.tar.gz: 2946e28dc3e74dfa06cc71b6db2f72e1cbe5a710d28c8406a9f3a596f3ddd43afd78000413a32373bc661f54c4c1221d523512e47889ed47855b79c67be107a5
6
+ metadata.gz: 3cce4831036082b7f85c80e7d29b9d71aff2ae54b645830030126732642edfac1c10fd3e3a9e36f8bc889666cfd3bdc98f1c1113f948f3d6bc61310834cea2b9
7
+ data.tar.gz: bb744d8e3639da9440321c325f1f70ce593fa0e748795e0f4184e7dd9ec492345b85956755c83fcafac2227bfdb54b7a45e399715e767b521ca58d7533fb5185
@@ -1,3 +1,10 @@
1
+ # v2.1.8
2
+ * [fix] Fix some planner errors caused by unexpected types (#146)
3
+ * [fix] Use common stack scrubbing implementation for create and update (#148)
4
+ * [fix] Properly expand nested stacks when displaying events (#148)
5
+ * [enhancement] Update internal stack data caching to reduce request numbers and size (#148)
6
+ * [feature] Add initial graph command implementation (currenty AWS only) (#152)
7
+
1
8
  # v2.1.6
2
9
  * [fix] Prevent configuration defaults overwriting user defined values (#144)
3
10
 
@@ -13,6 +13,7 @@ module Sfn
13
13
  autoload :Diff, 'sfn/command/diff'
14
14
  autoload :Events, 'sfn/command/events'
15
15
  autoload :Export, 'sfn/command/export'
16
+ autoload :Graph, 'sfn/command/graph'
16
17
  autoload :Import, 'sfn/command/import'
17
18
  autoload :Init, 'sfn/command/init'
18
19
  autoload :Inspect, 'sfn/command/inspect'
@@ -59,7 +59,7 @@ module Sfn
59
59
  )
60
60
 
61
61
  apply_stacks!(stack)
62
- stack.template = Sfn::Utils::StackParameterScrubber.scrub!(stack.template)
62
+ stack.template = Sfn::Utils::StackParameterScrubber.scrub!(scrub_template(stack.template))
63
63
 
64
64
  if(config[:print_only])
65
65
  ui.puts _format_json(translate_template(stack.template))
@@ -15,10 +15,8 @@ module Sfn
15
15
  name_required!
16
16
  name = name_args.first
17
17
  ui.info "Events for Stack: #{ui.color(name, :bold)}\n"
18
- @stacks = []
19
- @stack = provider.connection.stacks.get(name)
20
- @stacks << stack
21
- discover_stacks(stack)
18
+ @seen_events = []
19
+ @stack = provider.stack(name)
22
20
  if(stack)
23
21
  api_action!(:api_stack => stack) do
24
22
  table = ui.table(self) do
@@ -41,13 +39,13 @@ module Sfn
41
39
  end
42
40
  end.display
43
41
  if(config[:poll])
44
- while(stack.in_progress?)
42
+ while(stack.reload.in_progress?)
45
43
  to_wait = config.fetch(:poll_wait_time, 10).to_f
46
44
  while(to_wait > 0)
47
45
  sleep(0.1)
48
46
  to_wait -= 0.1
49
47
  end
50
- stack.reload
48
+ stack.resources.reload
51
49
  table.display
52
50
  end
53
51
  end
@@ -64,30 +62,36 @@ module Sfn
64
62
  # @param last_id [String] only return events after this ID
65
63
  # @return [Array<Hash>]
66
64
  def get_events(*args)
67
- discover_stacks(stack)
68
- stack_events = @stacks.map do |stack|
69
- stack.events.all.map do |e|
70
- e.attributes.merge(:stack_name => stack.name).to_smash
65
+ stack_events = discover_stacks(stack).map do |i_stack|
66
+ i_events = i_stack.events.reload.all
67
+ i_events.map do |e|
68
+ e.attributes.merge(:stack_name => i_stack.name).to_smash
71
69
  end
72
- end.flatten.compact.find_all{|e| e[:time] }
73
- stack_events.sort do |x,y|
74
- Time.parse(x[:time].to_s) <=> Time.parse(y[:time].to_s)
70
+ end.flatten.compact.find_all{|e| e[:time] }.reverse
71
+ stack_events.delete_if{|evt| @seen_events.include?(evt)}
72
+ @seen_events.concat(stack_events)
73
+ unless(@initial_complete)
74
+ stack_events = stack_events.sort_by{|e| e[:time] }
75
+ unless(config[:all_events])
76
+ start_index = stack_events.rindex do |item|
77
+ item[:stack_name] == stack.name &&
78
+ item[:resource_state].to_s.end_with?('in_progress') &&
79
+ item[:resource_status_reason].to_s.downcase.include?('user init')
80
+ end
81
+ if(start_index)
82
+ stack_events.slice!(0, start_index)
83
+ end
84
+ end
85
+ @initial_complete = true
75
86
  end
87
+ stack_events
76
88
  end
77
89
 
78
90
  # Discover stacks defined within the resources of given stack
79
91
  #
80
92
  # @param stack [Miasma::Models::Orchestration::Stack]
81
93
  def discover_stacks(stack)
82
- stack.resources.reload.all.each do |resource|
83
- if(resource.type == 'AWS::CloudFormation::Stack')
84
- nested_stack = provider.connection.stacks.get(resource.id)
85
- if(nested_stack)
86
- @stacks.push(nested_stack).uniq!
87
- discover_stacks(nested_stack)
88
- end
89
- end
90
- end
94
+ @stacks = [stack] + stack.nested_stacks.reverse
91
95
  end
92
96
 
93
97
  # @return [Array<String>] default attributes for events
@@ -0,0 +1,228 @@
1
+ require 'sfn'
2
+ require 'graph'
3
+
4
+ module Sfn
5
+ class Command
6
+ # Graph command
7
+ class Graph < Command
8
+
9
+ include Sfn::CommandModule::Base
10
+ include Sfn::CommandModule::Template
11
+ include Sfn::CommandModule::Stack
12
+
13
+ # Generate graph
14
+ def execute!
15
+ config[:print_only] = true
16
+ file = load_template_file
17
+ file.delete('sfn_nested_stack')
18
+ file = Sfn::Utils::StackParameterScrubber.scrub!(file)
19
+ file = translate_template(file)
20
+ @outputs = Smash.new
21
+ file = file.to_smash
22
+ ui.info 'Template resource graph generation'
23
+ if(config[:file])
24
+ ui.puts " -> path: #{config[:file]}"
25
+ end
26
+ run_action 'Pre-processing template for graphing' do
27
+ output_discovery(file, @outputs, nil, nil)
28
+ nil
29
+ end
30
+ graph = nil
31
+ run_action 'Generating resource graph' do
32
+ graph = generate_graph(file.to_smash)
33
+ nil
34
+ end
35
+ run_action 'Writing graph result' do
36
+ FileUtils.mkdir_p(File.dirname(config[:output_file]))
37
+ if(config[:output_type] == 'dot')
38
+ File.open("#{config[:output_file]}.dot", 'w') do |file|
39
+ file.puts graph.to_s
40
+ end
41
+ else
42
+ graph.save config[:output_file], config[:output_type]
43
+ end
44
+ nil
45
+ end
46
+ end
47
+
48
+ def generate_graph(template, args={})
49
+ graph = ::Graph.new
50
+ @root_graph = graph unless @root_graph
51
+ graph.graph_attribs << ::Graph::Attribute.new('overlap = false')
52
+ graph.graph_attribs << ::Graph::Attribute.new('splines = true')
53
+ graph.graph_attribs << ::Graph::Attribute.new('pack = true')
54
+ graph.graph_attribs << ::Graph::Attribute.new('start = "random"')
55
+ if(args[:name])
56
+ graph.name = "cluster_#{args[:name]}"
57
+ labelnode_key = "cluster_#{args[:name]}"
58
+ graph.plaintext << graph.node(labelnode_key)
59
+ graph.node(labelnode_key).label args[:name]
60
+ else
61
+ graph.name = 'root'
62
+ end
63
+ edge_detection(template, graph, args[:name].to_s.sub('cluster_', ''), args.fetch(:resource_names, []))
64
+ graph
65
+ end
66
+
67
+ def output_discovery(template, outputs, resource_name, parent_template, name='')
68
+ if(template['Resources'])
69
+ template['Resources'].each_pair do |r_name, r_info|
70
+ if(r_info['Type'] == 'AWS::CloudFormation::Stack')
71
+ output_discovery(r_info['Properties']['Stack'], outputs, r_name, template, r_name)
72
+ end
73
+ end
74
+ end
75
+ if(parent_template)
76
+ substack_parameters = Smash[
77
+ parent_template.fetch('Resources', resource_name, 'Properties', 'Parameters', {}).map do |key, value|
78
+ result = [key, value]
79
+ if(value.is_a?(Hash))
80
+ v_key = value.keys.first
81
+ v_value = value.values.first
82
+ if(v_key == 'Fn::GetAtt' && parent_template.fetch('Resources', {}).keys.include?(v_value.first) && v_value.last.start_with?('Outputs.'))
83
+ output_key = v_value.first << '__' << v_value.last.split('.', 2).last
84
+ if(outputs.key?(output_key))
85
+ new_value = outputs[output_key]
86
+ result = [key, new_value]
87
+ end
88
+ end
89
+ end
90
+ result
91
+ end
92
+ ]
93
+ processor = GraphProcessor.new({},
94
+ :parameters => substack_parameters
95
+ )
96
+ template['Resources'] = processor.dereference_processor(
97
+ template['Resources'], ['Ref']
98
+ )
99
+ template['Outputs'] = processor.dereference_processor(
100
+ template['Outputs'], ['Ref']
101
+ )
102
+ rename_processor = GraphProcessor.new({},
103
+ :parameters => Smash[
104
+ template.fetch('Resources', {}).keys.map do |r_key|
105
+ [r_key, {'Ref' => [name, r_key].join}]
106
+ end
107
+ ]
108
+ )
109
+ derefed_outs = rename_processor.dereference_processor(
110
+ template.fetch('Outputs', {})
111
+ ) || {}
112
+
113
+ derefed_outs.each do |o_name, o_data|
114
+ o_key = [name, o_name].join('__')
115
+ outputs[o_key] = o_data['Value']
116
+ end
117
+ end
118
+ outputs.dup.each do |key, value|
119
+ if(value.is_a?(Hash))
120
+ v_key = value.keys.first
121
+ v_value = value.values.first
122
+ if(v_key == 'Fn::GetAtt' && v_value.last.start_with?('Outputs.'))
123
+ output_key = v_value.first << '__' << v_value.last.split('.', 2).last
124
+ if(outputs.key?(output_key))
125
+ outputs[key] = outputs[output_key]
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def edge_detection(template, graph, name = '', resource_names = [])
133
+ resources = template.fetch('Resources', {})
134
+ node_prefix = name
135
+ resources.each do |resource_name, resource_data|
136
+ node_name = [node_prefix, resource_name].join
137
+ if(resource_data['Type'] == 'AWS::CloudFormation::Stack')
138
+ graph.subgraph << generate_graph(
139
+ resource_data['Properties'].delete('Stack'),
140
+ :name => resource_name,
141
+ :type => resource_data['Type'],
142
+ :resource_names => resource_names
143
+ )
144
+ next
145
+ else
146
+ graph.node(node_name).attributes << graph.fillcolor(colorize(node_prefix).inspect)
147
+ graph.box3d << graph.node(node_name)
148
+ end
149
+ graph.filled << graph.node(node_name)
150
+ graph.node(node_name).label "#{resource_name}\n<#{resource_data['Type']}>\n#{name}"
151
+ resource_dependencies(resource_data, resource_names + resources.keys).each do |dep_name|
152
+ if(resources.keys.include?(dep_name))
153
+ dep_name = [node_prefix, dep_name].join
154
+ end
155
+ @root_graph.edge(dep_name, node_name)
156
+ end
157
+ end
158
+ resource_names.concat resources.keys.map{|r_name| [node_prefix, r_name].join}
159
+ end
160
+
161
+ def resource_dependencies(data, names)
162
+ case data
163
+ when Hash
164
+ data.map do |key, value|
165
+ if(key == 'Ref' && names.include?(value))
166
+ value
167
+ elsif(key == 'Fn::GetAtt' && names.include?(res = [value].flatten.compact.first))
168
+ res
169
+ else
170
+ resource_dependencies(key, names) +
171
+ resource_dependencies(value, names)
172
+ end
173
+ end.flatten.compact.uniq
174
+ when Array
175
+ data.map do |item|
176
+ resource_dependencies(item, names)
177
+ end.flatten.compact.uniq
178
+ else
179
+ []
180
+ end
181
+ end
182
+
183
+ def colorize(string)
184
+ hash = string.chars.inject(0) do |memo, chr|
185
+ chr.ord + ((memo << 5) - memo)
186
+ end
187
+ color = '#'
188
+ 6.times do |count|
189
+ color << ('00' + ((hash >> count * 8) & 0xFF).to_s(16)).slice(-2)
190
+ end
191
+ color
192
+ end
193
+
194
+ class GraphProcessor < SparkleFormation::Translation
195
+ MAP = {}
196
+ REF_MAPPING = {}
197
+ FN_MAPPING = {}
198
+
199
+ attr_accessor :name
200
+
201
+ def initialize(template, args={})
202
+ super
203
+ @name = args[:name]
204
+ end
205
+
206
+ def apply_function(hash, funcs=[])
207
+ k, v = hash.first
208
+ if(hash.size == 1)
209
+ case k
210
+ when 'Ref'
211
+ parameters.key?(v) ? parameters[v] : hash
212
+ when 'Fn::Join'
213
+ v.last
214
+ when 'Fn::Select'
215
+ v.last[v.first.to_i]
216
+ else
217
+ hash
218
+ end
219
+ else
220
+ hash
221
+ end
222
+ end
223
+
224
+ end
225
+
226
+ end
227
+ end
228
+ end
@@ -12,7 +12,7 @@ module Sfn
12
12
  def execute!
13
13
  name_required!
14
14
  stack_name = name_args.last
15
- stack = provider.connection.stacks.get(stack_name)
15
+ stack = provider.stack(stack_name)
16
16
  ui.info "Stack inspection #{ui.color(stack_name, :bold)}:"
17
17
  outputs = api_action!(:api_stack => stack) do
18
18
  [:attribute, :nodes, :load_balancers, :instance_failure].map do |key|
@@ -95,7 +95,7 @@ module Sfn
95
95
  def display_attribute(stack)
96
96
  [config[:attribute]].flatten.compact.each do |stack_attribute|
97
97
  attr = stack_attribute.split('.').inject(stack) do |memo, key|
98
- args = key.scan(/\(([^)]*)\)/).flatten.first.to_s
98
+ args = key.scan(/\(([^\)]*)\)/).flatten.first.to_s
99
99
  if(args)
100
100
  args = args.split(',').map{|a| a.to_i.to_s == a ? a.to_i : a}
101
101
  key = key.split('(').first
@@ -129,7 +129,7 @@ module Sfn
129
129
  [
130
130
  asg.name,
131
131
  Smash[
132
- asg.servers.map(&:expand).map{|s|
132
+ asg.servers.map(&:expand).compact.map{|s|
133
133
  [s.id, Smash.new(
134
134
  :name => s.name,
135
135
  :addresses => s.addresses.map(&:address)
@@ -144,11 +144,13 @@ module Sfn
144
144
  resource.within?(:compute, :servers)
145
145
  end.map do |srv|
146
146
  srv = srv.instance
147
- [srv.id, Smash.new(
147
+ if(srv)
148
+ [srv.id, Smash.new(
148
149
  :name => srv.name,
149
150
  :addresses => srv.addresses.map(&:address)
150
- )]
151
- end
151
+ )]
152
+ end
153
+ end.compact
152
154
  ]
153
155
  unless(asg_nodes.empty?)
154
156
  ui.info ' AutoScale Group Instances:'
@@ -16,7 +16,7 @@ module Sfn
16
16
 
17
17
  stack_info = "#{ui.color('Name:', :bold)} #{name}"
18
18
  begin
19
- stack = provider.connection.stacks.get(name)
19
+ stack = provider.stacks.get(name)
20
20
  rescue Miasma::Error::ApiError::RequestError
21
21
  stack = nil
22
22
  end
@@ -132,7 +132,6 @@ module Sfn
132
132
  poll_stack(stack.name)
133
133
  if(stack.reload.state == :update_complete)
134
134
  ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
135
- stack.resources.reload
136
135
  namespace.const_get(:Describe).new({:outputs => true}, [name]).execute!
137
136
  else
138
137
  ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
@@ -256,18 +255,22 @@ module Sfn
256
255
  unless(val[:diffs].empty?)
257
256
  p_name = nil
258
257
  val[:diffs].each do |diff|
259
- if(diff[:updated] && diff[:original])
258
+ if(!diff[:updated].nil? || !diff[:original].nil?)
260
259
  p_name = diff.fetch(:property_name, :path)
261
260
  ui.print ' ' * 8
262
261
  ui.print "#{p_name}: "
263
262
  ui.print ' ' * (max_p - p_name.size)
264
- ui.print ui.color("-#{diff[:original]}", :red)
265
- ui.print ' ' * (max_o - diff[:original].size)
263
+ ui.print ui.color("-#{diff[:original]}", :red) unless diff[:original].nil?
264
+ ui.print ' ' * (max_o - diff[:original].to_s.size)
266
265
  ui.print ' '
267
266
  if(diff[:updated] == Sfn::Planner::RUNTIME_MODIFIED)
268
267
  ui.puts ui.color("+#{diff[:original]} <Dependency Modified>", :green)
269
268
  else
270
- ui.puts ui.color("+#{diff[:updated]}", :green)
269
+ if(diff[:updated].nil?)
270
+ ui.puts
271
+ else
272
+ ui.puts ui.color("+#{diff[:updated]}", :green)
273
+ end
271
274
  end
272
275
  end
273
276
  end
@@ -277,20 +280,6 @@ module Sfn
277
280
  end
278
281
  end
279
282
 
280
- # Scrub sparkle/sfn customizations from the stack resource data
281
- #
282
- # @param template [Hash]
283
- # @return [Hash]
284
- def scrub_template(template)
285
- template = Sfn::Utils::StackParameterScrubber.scrub!(template)
286
- (template['Resources'] || {}).each do |r_name, r_content|
287
- if(valid_stack_types.include?(r_content['Type']))
288
- (r_content['Properties'] || {}).delete('Stack')
289
- end
290
- end
291
- template
292
- end
293
-
294
283
  end
295
284
  end
296
285
  end
@@ -81,7 +81,8 @@ module Sfn
81
81
  #
82
82
  # @param name [String] name of stack
83
83
  # @return [Miasma::Models::Orchestration::Stack]
84
- def stack(name)
84
+ def stack(name=nil)
85
+ name = name_args.first unless name
85
86
  provider.stacks.get(name)
86
87
  end
87
88
 
@@ -214,16 +214,13 @@ module Sfn
214
214
  def process_nested_stack_shallow(sf, c_stack=nil)
215
215
  sf.apply_nesting(:shallow) do |stack_name, stack, resource|
216
216
  run_callbacks_for(:template, :stack_name => stack_name, :sparkle_stack => stack)
217
- stack_definition = stack.compile.dump!
218
217
  bucket = provider.connection.api_for(:storage).buckets.get(
219
218
  config[:nesting_bucket]
220
219
  )
221
220
  if(config[:print_only])
222
221
  template_url = "http://example.com/bucket/#{name_args.first}_#{stack_name}.json"
223
222
  else
224
- unless(config[:plan])
225
- resource.properties.delete!(:stack)
226
- end
223
+ stack_definition = dump_stack_for_storage(stack)
227
224
  unless(bucket)
228
225
  raise "Failed to locate configured bucket for stack template storage (#{bucket})!"
229
226
  end
@@ -277,12 +274,7 @@ module Sfn
277
274
  :current_parameters => current_parameters
278
275
  )
279
276
  )
280
- hold_stack = resource.properties.delete!(:stack)
281
- stack_definition = stack.compile.dump!
282
-
283
- if(config[:plan])
284
- resource.properties.stack hold_stack
285
- end
277
+ stack_definition = dump_stack_for_storage(stack)
286
278
  bucket = provider.connection.api_for(:storage).buckets.get(
287
279
  config[:nesting_bucket]
288
280
  )
@@ -308,6 +300,38 @@ module Sfn
308
300
  end
309
301
  end
310
302
 
303
+ # Remove internally used `Stack` property from Stack resources and
304
+ # and generate compiled Hash
305
+ #
306
+ # @param stack [SparkleFormation]
307
+ # @return [Hash]
308
+ def dump_stack_for_storage(stack)
309
+ nested_stacks = stack.nested_stacks.map do |nested_resource|
310
+ [nested_resource.resource_name!, stack.compile.resources.set!(nested_resource.resource_name!).properties.delete!(:stack)]
311
+ end
312
+ stack_definition = stack.compile.dump!
313
+ if(config[:plan])
314
+ nested_stacks.each do |nested_name, nested_data|
315
+ stack.compile.resources.set!(nested_name).properties.stack nested_data
316
+ end
317
+ end
318
+ stack_definition
319
+ end
320
+
321
+ # Scrub sparkle/sfn customizations from the stack resource data
322
+ #
323
+ # @param template [Hash]
324
+ # @return [Hash]
325
+ def scrub_template(template)
326
+ template = Sfn::Utils::StackParameterScrubber.scrub!(template)
327
+ (template['Resources'] || {}).each do |r_name, r_content|
328
+ if(valid_stack_types.include?(r_content['Type']))
329
+ result = (r_content['Properties'] || {}).delete('Stack')
330
+ end
331
+ end
332
+ template
333
+ end
334
+
311
335
  # Update the nested stack information for specific provider
312
336
  #
313
337
  # @param provider [Symbol]
@@ -48,6 +48,7 @@ module Sfn
48
48
  autoload :Diff, 'sfn/config/diff'
49
49
  autoload :Events, 'sfn/config/events'
50
50
  autoload :Export, 'sfn/config/export'
51
+ autoload :Graph, 'sfn/config/graph'
51
52
  autoload :Import, 'sfn/config/import'
52
53
  autoload :Init, 'sfn/config/init'
53
54
  autoload :Inspect, 'sfn/config/inspect'
@@ -23,6 +23,11 @@ module Sfn
23
23
  :description => 'Display all event attributes',
24
24
  :short_flag => 'A'
25
25
  )
26
+ attribute(
27
+ :all_events, [TrueClass, FalseClass],
28
+ :description => 'Display all available events',
29
+ :short_flag => 'L'
30
+ )
26
31
 
27
32
  end
28
33
  end
@@ -0,0 +1,24 @@
1
+ require 'sfn'
2
+
3
+ module Sfn
4
+ class Config
5
+ # Generate graph
6
+ class Graph < Validate
7
+
8
+ attribute(
9
+ :output_file, String,
10
+ :description => 'Directory to write graph files',
11
+ :short_flag => 'O',
12
+ :default => File.join(Dir.pwd, 'sfn-graph')
13
+ )
14
+
15
+ attribute(
16
+ :output_type, String,
17
+ :description => 'File output type (Requires graphviz package for non-dot types)',
18
+ :short_flag => 'e',
19
+ :default => 'dot'
20
+ )
21
+
22
+ end
23
+ end
24
+ end
@@ -194,7 +194,7 @@ module Sfn
194
194
  # @param recurse [TrueClass, FalseClass] recurse to fetch _all_ stacks
195
195
  # @return [Array<Miasma::Models::Orchestration::Stack>]
196
196
  def nested_stacks(recurse=true)
197
- resources.all.map do |resource|
197
+ resources.reload.all.map do |resource|
198
198
  if(api.data.fetch(:stack_types, []).include?(resource.type))
199
199
  # Custom remote load support
200
200
  if(resource.type == 'Custom::JackalStack')
@@ -130,7 +130,7 @@ module Sfn
130
130
  # @return [Hash] report
131
131
  def generate_plan(template, parameters)
132
132
  parameters = Smash[parameters.map{|k,v| [k, v.to_s]}]
133
- Smash.new(
133
+ result = Smash.new(
134
134
  :stacks => Smash.new(
135
135
  origin_stack.name => plan_stack(
136
136
  origin_stack,
@@ -145,10 +145,27 @@ module Sfn
145
145
  :unavailable => Smash.new,
146
146
  :unknown => Smash.new
147
147
  )
148
+ # scrub_stack_properties(template)
149
+ result
148
150
  end
149
151
 
150
152
  protected
151
153
 
154
+ # Remote custom Stack property from Stack resources within template
155
+ #
156
+ # @param template [Hash]
157
+ # @return [TrueClass]
158
+ def scrub_stack_properties(template)
159
+ if(template['Resources'])
160
+ template['Resources'].each do |name, info|
161
+ if(is_stack?(info['Type']) && info['Properties'].is_a?(Hash))
162
+ info['Properties'].delete('Stack')
163
+ end
164
+ end
165
+ end
166
+ true
167
+ end
168
+
152
169
  # Set global parameters available for all template translations.
153
170
  # These are pseudo-parameters that are provided by the
154
171
  # orchestration api runtime.
@@ -257,16 +274,17 @@ module Sfn
257
274
  o_nested_stacks = origin_template.fetch('Resources', {}).find_all do |s_name, s_val|
258
275
  is_stack?(s_val['Type'])
259
276
  end.map(&:first)
260
- n_nested_stacks = new_template_hash.fetch('Resources', {}).find_all do |s_name, s_val|
277
+ n_nested_stacks = (new_template_hash['Resources'] || {}).find_all do |s_name, s_val|
261
278
  is_stack?(s_val['Type'])
262
279
  end.map(&:first)
263
280
  [o_nested_stacks + n_nested_stacks].flatten.compact.uniq.each do |n_name|
264
281
  o_stack = stack.nested_stacks(false).detect{|s| s.data[:logical_id] == n_name}
265
- n_exists = is_stack?(new_template_hash['Resources'].fetch(n_name, {})['Type'])
266
- n_template = new_template_hash['Resources'].fetch(n_name, {}).fetch('Properties', {})['Stack']
267
- n_parameters = new_template_hash['Resources'].fetch(n_name, {}).fetch('Properties', {}).fetch('Parameters', {})
268
- n_type = new_template_hash['Resources'].fetch(n_name, {})['Type'] ||
269
- origin_template['Resources'][n_name]['Type']
282
+ n_exists = is_stack?(new_template_hash.get('Resources', n_name, 'Type'))
283
+ n_template = new_template_hash.get('Resources', n_name, 'Properties', 'Stack')
284
+ n_parameters = new_template_hash.fetch('Resources', n_name, 'Properties', 'Parameters', Smash.new)
285
+ n_type = new_template_hash.fetch('Resources', n_name, 'Type',
286
+ origin_template.get('Resources', n_name, 'Type')
287
+ )
270
288
  resource = Smash.new(
271
289
  :name => n_name,
272
290
  :type => n_type,
@@ -290,9 +308,7 @@ module Sfn
290
308
  end
291
309
  end
292
310
 
293
- n_nested_stacks.each do |ns_name|
294
- new_template_hash['Resources'][ns_name]['Properties'].delete('Stack')
295
- end
311
+ scrub_stack_properties(new_template_hash)
296
312
 
297
313
  update_template = dereference_template(
298
314
  t_key, new_template_hash, new_parameters,
@@ -354,10 +370,14 @@ module Sfn
354
370
  else
355
371
  if(p_path.include?('Properties'))
356
372
  resource_name = p_path[1]
357
- property_name = p_path[3].sub(/\[\d+\]$/, '')
373
+ if(p_path.size < 4 && p_path.last == 'Properties')
374
+ property_name = diff.flatten.compact.last.keys.first
375
+ else
376
+ property_name = p_path[3].to_s.sub(/\[\d+\]$/, '')
377
+ end
358
378
  type = templates[:origin]['Resources'][resource_name]['Type']
359
- info = SfnAws.registry.fetch(type, {})
360
- effect = info[:full_properties].fetch(property_name, {}).fetch(:update_causes, :unknown).to_sym
379
+ info = SfnAws.registry.fetch(type, {}).to_smash
380
+ effect = info.fetch(:full_properties, property_name, :update_causes, :unknown).to_sym
361
381
  resource = Smash.new(
362
382
  :name => resource_name,
363
383
  :type => type,
@@ -88,20 +88,26 @@ module Sfn
88
88
  end
89
89
 
90
90
  # @return [Miasma::Orchestration::Stacks]
91
- def stacks
92
- connection.stacks.from_json(cached_stacks)
91
+ def stacks(stack_id=nil)
92
+ connection.stacks.from_json(cached_stacks(stack_id))
93
93
  end
94
94
 
95
95
  # @return [String] json representation of cached stacks
96
- def cached_stacks
97
- fetch_stacks unless @initial_fetch_complete
96
+ def cached_stacks(stack_id=nil)
97
+ unless(@initial_fetch_complete || stack_id)
98
+ recache = true
99
+ if(stack_id && @initial_fetch_complete)
100
+ recache = !!stacks.get(stack_id)
101
+ end
102
+ fetch_stacks(stack_id) if recache
103
+ end
98
104
  value = cache[:stacks].value
99
105
  value ? MultiJson.dump(MultiJson.load(value).values) : '[]'
100
106
  end
101
107
 
102
108
  # @return [Miasma::Orchestration::Stack, NilClass]
103
109
  def stack(stack_id)
104
- stacks.get(stack_id)
110
+ stacks(stack_id).get(stack_id)
105
111
  end
106
112
 
107
113
  # Store stack attribute changes
@@ -162,23 +168,32 @@ module Sfn
162
168
  # Request stack information and store in cache
163
169
  #
164
170
  # @return [TrueClass]
165
- def fetch_stacks
171
+ def fetch_stacks(stack_id=nil)
166
172
  cache.locked_action(:stacks_lock) do
167
173
  logger.info "Lock aquired for stack update. Requesting stacks from upstream. (#{Thread.current})"
168
- stacks = Hash[
169
- connection.stacks.reload.all.map do |stack|
170
- [stack.id, stack.attributes]
171
- end
172
- ]
174
+ if(stack_id)
175
+ single_stack = connection.stacks.get(stack_id)
176
+ stacks = single_stack ? {single_stack.id => single_stack} : {}
177
+ else
178
+ stacks = Hash[
179
+ connection.stacks.reload.all.map do |stack|
180
+ [stack.id, stack.attributes]
181
+ end
182
+ ]
183
+ end
173
184
  if(cache[:stacks].value)
174
185
  existing_stacks = MultiJson.load(cache[:stacks].value)
175
186
  # Force common types
176
187
  stacks = MultiJson.load(MultiJson.dump(stacks))
177
- # Remove stacks that have been deleted
178
- stale_ids = existing_stacks.keys - stacks.keys
179
- stacks = existing_stacks.to_smash.deep_merge(stacks)
180
- stale_ids.each do |stale_id|
181
- stacks.delete(stale_id)
188
+ if(stack_id)
189
+ stacks = existing_stacks.to_smash.deep_merge(stacks)
190
+ else
191
+ # Remove stacks that have been deleted
192
+ stale_ids = existing_stacks.keys - stacks.keys
193
+ stacks = existing_stacks.to_smash.deep_merge(stacks)
194
+ stale_ids.each do |stale_id|
195
+ stacks.delete(stale_id)
196
+ end
182
197
  end
183
198
  end
184
199
  cache[:stacks].value = stacks.to_json
@@ -1,4 +1,4 @@
1
1
  module Sfn
2
2
  # Current library version
3
- VERSION = Gem::Version.new('2.1.6')
3
+ VERSION = Gem::Version.new('2.1.8')
4
4
  end
@@ -11,6 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.license = 'Apache-2.0'
12
12
  s.require_path = 'lib'
13
13
  s.add_runtime_dependency 'bogo-cli', '>= 0.2.4', '< 0.4'
14
+ s.add_runtime_dependency 'bogo-ui', '>= 0.1.13', '< 0.4'
14
15
  s.add_runtime_dependency 'miasma', '>= 0.3.0', '< 0.4'
15
16
  s.add_runtime_dependency 'miasma-aws', '>= 0.3.0', '< 0.4'
16
17
  s.add_runtime_dependency 'miasma-azure', '>= 0.1.0', '< 0.3'
@@ -19,7 +20,8 @@ Gem::Specification.new do |s|
19
20
  s.add_runtime_dependency 'net-ssh'
20
21
  s.add_runtime_dependency 'sparkle_formation', '>= 2.1.2', '< 3'
21
22
  s.add_runtime_dependency 'hashdiff', '~> 0.2.2'
22
- s.add_development_dependency 'rake'
23
+ s.add_runtime_dependency 'graph', '~> 2.8.1'
24
+ s.add_development_dependency 'rake', '~> 10'
23
25
  s.add_development_dependency 'minitest'
24
26
  s.add_development_dependency 'mocha'
25
27
  s.executables << 'sfn'
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: 2.1.6
4
+ version: 2.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: 2016-03-02 00:00:00.000000000 Z
11
+ date: 2016-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bogo-cli
@@ -30,6 +30,26 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '0.4'
33
+ - !ruby/object:Gem::Dependency
34
+ name: bogo-ui
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.13
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '0.4'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 0.1.13
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '0.4'
33
53
  - !ruby/object:Gem::Dependency
34
54
  name: miasma
35
55
  requirement: !ruby/object:Gem::Requirement
@@ -178,20 +198,34 @@ dependencies:
178
198
  - - "~>"
179
199
  - !ruby/object:Gem::Version
180
200
  version: 0.2.2
201
+ - !ruby/object:Gem::Dependency
202
+ name: graph
203
+ requirement: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - "~>"
206
+ - !ruby/object:Gem::Version
207
+ version: 2.8.1
208
+ type: :runtime
209
+ prerelease: false
210
+ version_requirements: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: 2.8.1
181
215
  - !ruby/object:Gem::Dependency
182
216
  name: rake
183
217
  requirement: !ruby/object:Gem::Requirement
184
218
  requirements:
185
- - - ">="
219
+ - - "~>"
186
220
  - !ruby/object:Gem::Version
187
- version: '0'
221
+ version: '10'
188
222
  type: :development
189
223
  prerelease: false
190
224
  version_requirements: !ruby/object:Gem::Requirement
191
225
  requirements:
192
- - - ">="
226
+ - - "~>"
193
227
  - !ruby/object:Gem::Version
194
- version: '0'
228
+ version: '10'
195
229
  - !ruby/object:Gem::Dependency
196
230
  name: minitest
197
231
  requirement: !ruby/object:Gem::Requirement
@@ -271,6 +305,7 @@ files:
271
305
  - lib/sfn/command/diff.rb
272
306
  - lib/sfn/command/events.rb
273
307
  - lib/sfn/command/export.rb
308
+ - lib/sfn/command/graph.rb
274
309
  - lib/sfn/command/import.rb
275
310
  - lib/sfn/command/init.rb
276
311
  - lib/sfn/command/inspect.rb
@@ -292,6 +327,7 @@ files:
292
327
  - lib/sfn/config/diff.rb
293
328
  - lib/sfn/config/events.rb
294
329
  - lib/sfn/config/export.rb
330
+ - lib/sfn/config/graph.rb
295
331
  - lib/sfn/config/import.rb
296
332
  - lib/sfn/config/init.rb
297
333
  - lib/sfn/config/inspect.rb