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