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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/sfn/command.rb +1 -0
- data/lib/sfn/command/create.rb +1 -1
- data/lib/sfn/command/events.rb +26 -22
- data/lib/sfn/command/graph.rb +228 -0
- data/lib/sfn/command/inspect.rb +8 -6
- data/lib/sfn/command/update.rb +9 -20
- data/lib/sfn/command_module/base.rb +2 -1
- data/lib/sfn/command_module/template.rb +34 -10
- data/lib/sfn/config.rb +1 -0
- data/lib/sfn/config/events.rb +5 -0
- data/lib/sfn/config/graph.rb +24 -0
- data/lib/sfn/monkey_patch/stack.rb +1 -1
- data/lib/sfn/planner/aws.rb +33 -13
- data/lib/sfn/provider.rb +31 -16
- data/lib/sfn/version.rb +1 -1
- data/sfn.gemspec +3 -1
- metadata +42 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 610903a7a6be9347a5c0d51bff46e3eba9239e28
|
4
|
+
data.tar.gz: 0aacd95b68b649c70416a02c77c8bf06d60072cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3cce4831036082b7f85c80e7d29b9d71aff2ae54b645830030126732642edfac1c10fd3e3a9e36f8bc889666cfd3bdc98f1c1113f948f3d6bc61310834cea2b9
|
7
|
+
data.tar.gz: bb744d8e3639da9440321c325f1f70ce593fa0e748795e0f4184e7dd9ec492345b85956755c83fcafac2227bfdb54b7a45e399715e767b521ca58d7533fb5185
|
data/CHANGELOG.md
CHANGED
@@ -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
|
|
data/lib/sfn/command.rb
CHANGED
@@ -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'
|
data/lib/sfn/command/create.rb
CHANGED
@@ -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))
|
data/lib/sfn/command/events.rb
CHANGED
@@ -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
|
-
@
|
19
|
-
@stack = provider.
|
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
|
-
|
69
|
-
|
70
|
-
e.attributes.merge(:stack_name =>
|
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.
|
74
|
-
|
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.
|
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
|
data/lib/sfn/command/inspect.rb
CHANGED
@@ -12,7 +12,7 @@ module Sfn
|
|
12
12
|
def execute!
|
13
13
|
name_required!
|
14
14
|
stack_name = name_args.last
|
15
|
-
stack = provider.
|
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(/\(([
|
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
|
-
|
147
|
+
if(srv)
|
148
|
+
[srv.id, Smash.new(
|
148
149
|
:name => srv.name,
|
149
150
|
:addresses => srv.addresses.map(&:address)
|
150
|
-
|
151
|
-
|
151
|
+
)]
|
152
|
+
end
|
153
|
+
end.compact
|
152
154
|
]
|
153
155
|
unless(asg_nodes.empty?)
|
154
156
|
ui.info ' AutoScale Group Instances:'
|
data/lib/sfn/command/update.rb
CHANGED
@@ -16,7 +16,7 @@ module Sfn
|
|
16
16
|
|
17
17
|
stack_info = "#{ui.color('Name:', :bold)} #{name}"
|
18
18
|
begin
|
19
|
-
stack = provider.
|
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]
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
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]
|
data/lib/sfn/config.rb
CHANGED
@@ -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'
|
data/lib/sfn/config/events.rb
CHANGED
@@ -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')
|
data/lib/sfn/planner/aws.rb
CHANGED
@@ -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
|
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
|
266
|
-
n_template = new_template_hash
|
267
|
-
n_parameters = new_template_hash
|
268
|
-
n_type = new_template_hash
|
269
|
-
origin_template
|
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
|
-
|
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
|
-
|
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
|
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,
|
data/lib/sfn/provider.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
169
|
-
connection.stacks.
|
170
|
-
|
171
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
stacks.
|
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
|
data/lib/sfn/version.rb
CHANGED
data/sfn.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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: '
|
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: '
|
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
|