sfn 2.1.6 → 2.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: 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