sfn 1.1.6 → 1.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 +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
|