sparkle_formation 2.1.8 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/docs/README.md +1 -0
- data/docs/helper-methods.md +1 -0
- data/docs/provider-restrictions.md +174 -0
- data/docs/translation.md +3 -0
- data/lib/sparkle_formation.rb +3 -0
- data/lib/sparkle_formation/function_struct.rb +132 -4
- data/lib/sparkle_formation/provider.rb +1 -0
- data/lib/sparkle_formation/provider/aws.rb +4 -4
- data/lib/sparkle_formation/provider/azure.rb +4 -4
- data/lib/sparkle_formation/provider/google.rb +200 -0
- data/lib/sparkle_formation/provider/heat.rb +4 -4
- data/lib/sparkle_formation/resources.rb +106 -21
- data/lib/sparkle_formation/resources/aws.rb +252 -0
- data/lib/sparkle_formation/resources/aws_resources.json +117 -29
- data/lib/sparkle_formation/resources/google.rb +47 -0
- data/lib/sparkle_formation/resources/google_resources.json +667 -0
- data/lib/sparkle_formation/sparkle.rb +145 -120
- data/lib/sparkle_formation/sparkle_attribute.rb +60 -21
- data/lib/sparkle_formation/sparkle_attribute/aws.rb +27 -18
- data/lib/sparkle_formation/sparkle_attribute/azure.rb +25 -12
- data/lib/sparkle_formation/sparkle_attribute/google.rb +149 -0
- data/lib/sparkle_formation/sparkle_attribute/heat.rb +33 -14
- data/lib/sparkle_formation/sparkle_collection.rb +37 -15
- data/lib/sparkle_formation/sparkle_formation.rb +48 -22
- data/lib/sparkle_formation/sparkle_struct.rb +43 -0
- data/lib/sparkle_formation/version.rb +1 -1
- metadata +7 -2
@@ -26,7 +26,7 @@ class SparkleFormation
|
|
26
26
|
# @yieldparam resource [AttributeStruct] the stack resource
|
27
27
|
# @yieldparam s_name [String] stack resource name
|
28
28
|
# @yieldreturn [Hash] key/values to be merged into resource properties
|
29
|
-
# @return [
|
29
|
+
# @return [SparkleFormation::SparkleStruct] compiled structure
|
30
30
|
def apply_deep_nesting(*args, &block)
|
31
31
|
outputs = collect_outputs
|
32
32
|
nested_stacks(:with_resource).each do |stack, resource|
|
@@ -44,7 +44,7 @@ class SparkleFormation
|
|
44
44
|
if(block_given?)
|
45
45
|
extract_templates(&block)
|
46
46
|
end
|
47
|
-
compile
|
47
|
+
compile
|
48
48
|
end
|
49
49
|
|
50
50
|
# Apply shallow nesting. This style of nesting will bubble
|
@@ -55,7 +55,7 @@ class SparkleFormation
|
|
55
55
|
# @yieldparam resource_name [String] name of stack resource
|
56
56
|
# @yieldparam stack [SparkleFormation] nested stack
|
57
57
|
# @yieldreturn [String] Remote URL storage for template
|
58
|
-
# @return [
|
58
|
+
# @return [SparkleFormation::SparkleStruct] compiled structure
|
59
59
|
def apply_shallow_nesting(*args, &block)
|
60
60
|
parameters = compile.parameters
|
61
61
|
output_map = {}
|
@@ -68,7 +68,7 @@ class SparkleFormation
|
|
68
68
|
compile.outputs._set(o_name).value compile._stack_output(*o_val)
|
69
69
|
end
|
70
70
|
end
|
71
|
-
compile
|
71
|
+
compile
|
72
72
|
end
|
73
73
|
|
74
74
|
# Extract output to make available for stack parameter usage at the
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'sparkle_formation'
|
2
|
+
|
3
|
+
class SparkleFormation
|
4
|
+
module Provider
|
5
|
+
# Google specific implementation
|
6
|
+
module Google
|
7
|
+
|
8
|
+
# Always return as nested since nesting is our final form
|
9
|
+
def nested?(*_)
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Extract nested stack templates and store in root level files
|
14
|
+
#
|
15
|
+
# @param template_hash [Hash] template hash to process
|
16
|
+
# @param dump_copy [Smash] translated dump
|
17
|
+
# @param parent_names [Array<String>] name of parent resources
|
18
|
+
# @return [Smash] dump_copy
|
19
|
+
def google_template_extractor(template_hash, dump_copy, parent_names=[])
|
20
|
+
template_hash.fetch('resources', []).each do |t_resource|
|
21
|
+
if(t_resource['type'] == stack_resource_type)
|
22
|
+
full_names = parent_names + [t_resource['name']]
|
23
|
+
stack = t_resource['properties'].delete('stack')
|
24
|
+
if(t_resource['properties'].empty?)
|
25
|
+
t_resource.delete('properties')
|
26
|
+
end
|
27
|
+
google_template_extractor(stack, dump_copy, full_names)
|
28
|
+
new_type = generate_template_files(full_names.join('-'), stack, dump_copy)
|
29
|
+
t_resource['type'] = new_type
|
30
|
+
end
|
31
|
+
end
|
32
|
+
dump_copy
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sets stack template files into target copy and extracts parameters
|
36
|
+
# into schema files if available
|
37
|
+
#
|
38
|
+
# @param r_name [String] name used for template file name
|
39
|
+
# @param r_stack [Hash] template to store
|
40
|
+
# @param dump_copy [Smash] translated dump
|
41
|
+
# @return [String] new type for stack
|
42
|
+
def generate_template_files(r_name, r_stack, dump_copy)
|
43
|
+
f_name = "#{r_name}.jinja"
|
44
|
+
r_parameters = r_stack.delete('parameters')
|
45
|
+
dump_copy[:imports].push(
|
46
|
+
Smash.new(
|
47
|
+
:name => f_name,
|
48
|
+
:content => r_stack
|
49
|
+
)
|
50
|
+
)
|
51
|
+
if(r_parameters)
|
52
|
+
dump_copy[:imports].push(
|
53
|
+
Smash.new(
|
54
|
+
:name => "#{f_name}.schema",
|
55
|
+
:content => Smash.new.tap{|schema|
|
56
|
+
schema.set(:info, :title, "#{f_name} template")
|
57
|
+
schema.set(:info, :description, "#{f_name} template schema")
|
58
|
+
schema.set(:properties, r_parameters)
|
59
|
+
}
|
60
|
+
)
|
61
|
+
)
|
62
|
+
end
|
63
|
+
f_name
|
64
|
+
end
|
65
|
+
|
66
|
+
# Customized dump to break out templates into consumable structures for
|
67
|
+
# passing to the deployment manager API
|
68
|
+
#
|
69
|
+
# @return [Hash]
|
70
|
+
def google_dump
|
71
|
+
result = non_google_dump
|
72
|
+
if(root?)
|
73
|
+
dump_copy = Smash.new(:imports => [])
|
74
|
+
google_template_extractor(result, dump_copy)
|
75
|
+
dump_copy.set(:config, :content, result)
|
76
|
+
dump_copy.set(:config, :content, :imports,
|
77
|
+
dump_copy[:imports].map{|i| i[:name]}
|
78
|
+
)
|
79
|
+
dump_copy.to_hash
|
80
|
+
else
|
81
|
+
result
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Properly remap dumping methods
|
86
|
+
def self.included(klass)
|
87
|
+
klass.class_eval do
|
88
|
+
alias_method :non_google_dump, :dump
|
89
|
+
alias_method :dump, :google_dump
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Properly remap dumping methods
|
94
|
+
def self.extended(klass)
|
95
|
+
klass.instance_eval do
|
96
|
+
alias :non_google_dump :dump
|
97
|
+
alias :dump :google_dump
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [String] Type string for Google Deployment Manager stack resource
|
102
|
+
# @note Nested templates aren't defined as a specific type thus no "real"
|
103
|
+
# type exists. So we'll create a custom one!
|
104
|
+
def stack_resource_type
|
105
|
+
'sparkleformation.stack'
|
106
|
+
end
|
107
|
+
|
108
|
+
# Generate policy for stack
|
109
|
+
#
|
110
|
+
# @return [Hash]
|
111
|
+
def generate_policy
|
112
|
+
{}
|
113
|
+
end
|
114
|
+
|
115
|
+
# Apply deeply nested stacks. This is the new nesting approach and
|
116
|
+
# does not bubble parameters up to the root stack. Parameters are
|
117
|
+
# isolated to the stack resource itself and output mapping is
|
118
|
+
# automatically applied.
|
119
|
+
#
|
120
|
+
# @yieldparam stack [SparkleFormation] stack instance
|
121
|
+
# @yieldparam resource [AttributeStruct] the stack resource
|
122
|
+
# @yieldparam s_name [String] stack resource name
|
123
|
+
# @yieldreturn [Hash] key/values to be merged into resource properties
|
124
|
+
# @return [SparkleFormation::SparkleStruct] compiled structure
|
125
|
+
def apply_deep_nesting(*args, &block)
|
126
|
+
outputs = collect_outputs
|
127
|
+
nested_stacks(:with_resource).each do |stack, resource|
|
128
|
+
unless(stack.nested_stacks.empty?)
|
129
|
+
stack.apply_deep_nesting(*args)
|
130
|
+
end
|
131
|
+
stack.compile.parameters.keys!.each do |parameter_name|
|
132
|
+
if(output_name = output_matched?(parameter_name, outputs.keys))
|
133
|
+
next if outputs[output_name] == stack
|
134
|
+
stack_output = stack.make_output_available(output_name, outputs, self)
|
135
|
+
# NOTE: Only set value if not already explicitly set
|
136
|
+
if(resource.properties._set(parameter_name).nil?)
|
137
|
+
resource.properties._set(parameter_name, stack_output)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
if(block_given?)
|
143
|
+
extract_templates(&block)
|
144
|
+
end
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
# Forcibly disable shallow nesting as support for it with Google templates doesn't
|
149
|
+
# really make much sense.
|
150
|
+
def apply_shallow_nesting(*args, &block)
|
151
|
+
raise NotImplementedError.new 'Shallow nesting is not supported for this provider!'
|
152
|
+
end
|
153
|
+
|
154
|
+
# Extract output to make available for stack parameter usage at the
|
155
|
+
# current depth
|
156
|
+
#
|
157
|
+
# @param output_name [String] name of output
|
158
|
+
# @param outputs [Hash] listing of outputs
|
159
|
+
# @param source_stack [SparkleFormation] requesting stack
|
160
|
+
# @reutrn [Hash] reference to output value (used for setting parameter)
|
161
|
+
def make_output_available(output_name, outputs, source_stack)
|
162
|
+
bubble_path = outputs[output_name].root_path - root_path
|
163
|
+
drip_path = root_path - outputs[output_name].root_path
|
164
|
+
bubble_path.each_slice(2) do |base_sparkle, ref_sparkle|
|
165
|
+
next unless ref_sparkle
|
166
|
+
base_sparkle.compile.outputs._set(output_name)._set(
|
167
|
+
:value, base_sparkle.compile._stack_output(
|
168
|
+
ref_sparkle.name, output_name
|
169
|
+
)
|
170
|
+
)
|
171
|
+
end
|
172
|
+
if(bubble_path.empty?)
|
173
|
+
if(drip_path.size == 1)
|
174
|
+
parent = drip_path.first.parent
|
175
|
+
if(parent && !parent.compile.parameters._set(output_name).nil?)
|
176
|
+
return compile.parameter!(output_name)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
raise ArgumentError.new "Failed to detect available bubbling path for output `#{output_name}`. " <<
|
180
|
+
'This may be due to a circular dependency! ' <<
|
181
|
+
"(Output Path: #{outputs[output_name].root_path.map(&:name).join(' > ')} " <<
|
182
|
+
"Requester Path: #{root_path.map(&:name).join(' > ')})"
|
183
|
+
end
|
184
|
+
result = source_stack.compile._stack_output(bubble_path.first.name, output_name)
|
185
|
+
if(drip_path.size > 1)
|
186
|
+
parent = drip_path.first.parent
|
187
|
+
drip_path.unshift(parent) if parent
|
188
|
+
drip_path.each_slice(2) do |base_sparkle, ref_sparkle|
|
189
|
+
next unless ref_sparkle
|
190
|
+
base_sparkle.compile.resources[ref_sparkle.name].properties.parameters.value._set(output_name, result)
|
191
|
+
ref_sparkle.compile.parameters._set(output_name).type 'string' # TODO: <<<<------ type check and prop
|
192
|
+
result = compile._parameter(output_name)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
result
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -26,7 +26,7 @@ class SparkleFormation
|
|
26
26
|
# @yieldparam resource [AttributeStruct] the stack resource
|
27
27
|
# @yieldparam s_name [String] stack resource name
|
28
28
|
# @yieldreturn [Hash] key/values to be merged into resource properties
|
29
|
-
# @return [
|
29
|
+
# @return [SparkleFormation::SparkleStruct] compiled structure
|
30
30
|
def apply_deep_nesting(*args, &block)
|
31
31
|
outputs = collect_outputs
|
32
32
|
nested_stacks(:with_resource).each do |stack, resource|
|
@@ -44,7 +44,7 @@ class SparkleFormation
|
|
44
44
|
if(block_given?)
|
45
45
|
extract_templates(&block)
|
46
46
|
end
|
47
|
-
compile
|
47
|
+
compile
|
48
48
|
end
|
49
49
|
|
50
50
|
# Apply shallow nesting. This style of nesting will bubble
|
@@ -55,7 +55,7 @@ class SparkleFormation
|
|
55
55
|
# @yieldparam resource_name [String] name of stack resource
|
56
56
|
# @yieldparam stack [SparkleFormation] nested stack
|
57
57
|
# @yieldreturn [String] Remote URL storage for template
|
58
|
-
# @return [
|
58
|
+
# @return [SparkleFormation::SparkleStruct] compiled structure
|
59
59
|
def apply_shallow_nesting(*args, &block)
|
60
60
|
parameters = compile.parameters
|
61
61
|
output_map = {}
|
@@ -68,7 +68,7 @@ class SparkleFormation
|
|
68
68
|
compile.outputs._set(o_name).value compile._stack_output(*o_val)
|
69
69
|
end
|
70
70
|
end
|
71
|
-
compile
|
71
|
+
compile
|
72
72
|
end
|
73
73
|
|
74
74
|
# Extract output to make available for stack parameter usage at the
|
@@ -6,6 +6,7 @@ class SparkleFormation
|
|
6
6
|
|
7
7
|
autoload :Aws, 'sparkle_formation/resources/aws'
|
8
8
|
autoload :Azure, 'sparkle_formation/resources/azure'
|
9
|
+
autoload :Google, 'sparkle_formation/resources/google'
|
9
10
|
autoload :Heat, 'sparkle_formation/resources/heat'
|
10
11
|
autoload :Rackspace, 'sparkle_formation/resources/rackspace'
|
11
12
|
|
@@ -13,6 +14,64 @@ class SparkleFormation
|
|
13
14
|
RESOURCE_TYPE_TR = '_'
|
14
15
|
# String to split for resource namespacing
|
15
16
|
RESOURCE_TYPE_NAMESPACE_SPLITTER = '::'
|
17
|
+
# Property update conditionals
|
18
|
+
# Format: Smash.new(RESOURCE_TYPE => {PROPERTY_NAME => [PropertyConditional]})
|
19
|
+
PROPERTY_UPDATE_CONDITIONALS = Smash.new
|
20
|
+
|
21
|
+
# Defines a resource type
|
22
|
+
#
|
23
|
+
# @param name [String] name of resource type
|
24
|
+
# @param properties [Array<Property>] resource properties
|
25
|
+
# @param raw [Hash] raw resource information
|
26
|
+
Resource = Struct.new(:name, :properties, :raw) do
|
27
|
+
# Get property by name
|
28
|
+
#
|
29
|
+
# @param name [String] name of property
|
30
|
+
# @return [Property, NilClass]
|
31
|
+
def property(name)
|
32
|
+
properties.detect do |prop|
|
33
|
+
prop.name == name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines conditional result for cause of property update
|
39
|
+
#
|
40
|
+
# @param update_causes [String] one of: 'replacement', 'interrupt', 'unknown', 'none'
|
41
|
+
# @param conditional [Proc, TrueClass] condition logic. passed two values: Hash of resource "final" state and
|
42
|
+
# Hash of resource "original" state
|
43
|
+
UpdateCausesConditional = Struct.new(:update_causes, :conditional)
|
44
|
+
|
45
|
+
# Defines a resource property
|
46
|
+
#
|
47
|
+
# @param name [String] property name
|
48
|
+
# @param description [String] property descrition
|
49
|
+
# @param type [String] property data type
|
50
|
+
# @param required [TrueClass, FalseClass] property is required
|
51
|
+
# @param update_causes [String] one of: 'replacement', 'interrupt', 'unknown', 'none'
|
52
|
+
# @param conditionals [Array<UpdateCausesConditional>] conditionals for update causes
|
53
|
+
Property = Struct.new(:name, :description, :type, :required, :update_causes, :conditionals) do
|
54
|
+
# Determine result of property update
|
55
|
+
#
|
56
|
+
# @param final_resource [Hash] desired resource structure containing this property
|
57
|
+
# @return ['replacement', 'interrupt', 'unknown', 'none']
|
58
|
+
def update_causes(final_resource=nil, original_resource=nil)
|
59
|
+
if(conditionals && final_resource)
|
60
|
+
final_resource = final_resource.to_smash
|
61
|
+
original_resource = original_resource.to_smash
|
62
|
+
result = conditionals.detect do |p_cond|
|
63
|
+
p_cond == true || p_cond.conditional.call(final_resource, original_resource)
|
64
|
+
end
|
65
|
+
if(result)
|
66
|
+
result.update_causes
|
67
|
+
else
|
68
|
+
'unknown'
|
69
|
+
end
|
70
|
+
else
|
71
|
+
self[:update_causes]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
16
75
|
|
17
76
|
class << self
|
18
77
|
|
@@ -80,27 +139,31 @@ class SparkleFormation
|
|
80
139
|
# @param key [String, Symbol]
|
81
140
|
# @return [String, NilClass]
|
82
141
|
def registry_key(key)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
snake_parts =
|
89
|
-
|
90
|
-
|
91
|
-
snake_parts.
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
split_ref = ref.downcase.split(resource_type_splitter)
|
98
|
-
ref = split_ref.slice(split_ref.size - snake_parts.size, split_ref.size).join('')
|
99
|
-
key == ref
|
142
|
+
if(registry[key])
|
143
|
+
result = key
|
144
|
+
else
|
145
|
+
o_key = key
|
146
|
+
key = key.to_s.tr(self.const_get(:RESOURCE_TYPE_TR), '') # rubocop:disable Style/RedundantSelf
|
147
|
+
snake_parts = nil
|
148
|
+
result = @@registry[base_key].keys.detect do |ref|
|
149
|
+
ref = ref.downcase
|
150
|
+
snake_parts = ref.split(resource_type_splitter)
|
151
|
+
until(snake_parts.empty?)
|
152
|
+
break if snake_parts.join('') == key
|
153
|
+
snake_parts.shift
|
154
|
+
end
|
155
|
+
!snake_parts.empty?
|
100
156
|
end
|
101
|
-
if(
|
102
|
-
|
103
|
-
|
157
|
+
if(result)
|
158
|
+
collisions = @@registry[base_key].keys.find_all do |ref|
|
159
|
+
split_ref = ref.downcase.split(resource_type_splitter)
|
160
|
+
ref = split_ref.slice(split_ref.size - snake_parts.size, split_ref.size).join('')
|
161
|
+
key == ref
|
162
|
+
end
|
163
|
+
if(collisions.size > 1)
|
164
|
+
raise ArgumentError.new 'Ambiguous dynamic name returned multiple matches! ' \
|
165
|
+
"`#{o_key.inspect}` -> #{collisions.sort.join(', ')}"
|
166
|
+
end
|
104
167
|
end
|
105
168
|
end
|
106
169
|
result
|
@@ -121,7 +184,7 @@ class SparkleFormation
|
|
121
184
|
# @param key [String, Symbol]
|
122
185
|
# @return [Hashish, NilClass]
|
123
186
|
def lookup(key)
|
124
|
-
@@registry[base_key][registry_key(key)]
|
187
|
+
@@registry[base_key][key] || @@registry[base_key][registry_key(key)]
|
125
188
|
end
|
126
189
|
|
127
190
|
# @return [Hashish] currently loaded AWS registry
|
@@ -142,6 +205,28 @@ class SparkleFormation
|
|
142
205
|
struct
|
143
206
|
end
|
144
207
|
|
208
|
+
# Information about specific resource type
|
209
|
+
#
|
210
|
+
# @param type [String] resource type
|
211
|
+
# @return [Resource]
|
212
|
+
def resource_lookup(type)
|
213
|
+
result = registry[type]
|
214
|
+
if(result)
|
215
|
+
properties = result.fetch('full_properties', {}).map do |p_name, p_info|
|
216
|
+
Property.new(p_name,
|
217
|
+
p_info[:description],
|
218
|
+
p_info[:type],
|
219
|
+
p_info[:required],
|
220
|
+
p_info[:update_causes],
|
221
|
+
self.const_get(:PROPERTY_UPDATE_CONDITIONALS).get(type, p_name)
|
222
|
+
)
|
223
|
+
end
|
224
|
+
Resource.new(type, properties, result)
|
225
|
+
else
|
226
|
+
raise KeyError.new "Failed to locate requested resource type: `#{type}`"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
145
230
|
end
|
146
231
|
end
|
147
232
|
end
|
@@ -8,6 +8,258 @@ class SparkleFormation
|
|
8
8
|
# AWS specific resources collection
|
9
9
|
class Aws < Resources
|
10
10
|
|
11
|
+
# Conditionals for property updates
|
12
|
+
PROPERTY_UPDATE_CONDITIONALS = Smash.new(
|
13
|
+
'AWS::DynamoDB::Table' => {
|
14
|
+
'GlobalSecondaryIndexes' => [
|
15
|
+
# Updates not really supported here. Set as unknown to
|
16
|
+
# prompt user to investigate
|
17
|
+
UpdateCausesConditional.new('unknown', true)
|
18
|
+
]
|
19
|
+
},
|
20
|
+
'AWS::EC2::EIPAssociation' => {
|
21
|
+
'AllocationId' => [
|
22
|
+
UpdateCausesConditional.new('replacement',
|
23
|
+
lambda{|final, original|
|
24
|
+
original.get('Properties', 'InstanceId') != final.get('Properties', 'InstanceId') ||
|
25
|
+
original.get('Properties', 'NetworkInterfaceId') != final.get('Properties', 'NewtorkInterfaceId')
|
26
|
+
}
|
27
|
+
),
|
28
|
+
UpdateCausesConditional.new('none', true)
|
29
|
+
],
|
30
|
+
'EIP' => [
|
31
|
+
UpdateCausesConditional.new('replacement',
|
32
|
+
lambda{|final, original|
|
33
|
+
original.get('Properties', 'InstanceId') != final.get('Properties', 'InstanceId') ||
|
34
|
+
original.get('Properties', 'NetworkInterfaceId') != final.get('Properties', 'NewtorkInterfaceId')
|
35
|
+
}
|
36
|
+
),
|
37
|
+
UpdateCausesConditional.new('none', true)
|
38
|
+
],
|
39
|
+
'InstanceId' => [
|
40
|
+
UpdateCausesConditional.new('replacement',
|
41
|
+
lambda{|final, original|
|
42
|
+
original.get('Properties', 'AllocationId') != final.get('Properties', 'AllocationId') ||
|
43
|
+
original.get('Properties', 'EIP') != final.get('Properties', 'EIP')
|
44
|
+
}
|
45
|
+
),
|
46
|
+
UpdateCausesConditional.new('none', true)
|
47
|
+
],
|
48
|
+
'NetworkInterfaceId' => [
|
49
|
+
UpdateCausesConditional.new('replacement',
|
50
|
+
lambda{|final, original|
|
51
|
+
original.get('Properties', 'AllocationId') != final.get('Properties', 'AllocationId') ||
|
52
|
+
original.get('Properties', 'EIP') != final.get('Properties', 'EIP')
|
53
|
+
}
|
54
|
+
),
|
55
|
+
UpdateCausesConditional.new('none', true)
|
56
|
+
]
|
57
|
+
},
|
58
|
+
'AWS::EC2::Instance' => {
|
59
|
+
'AdditionalInfo' => [
|
60
|
+
UpdateCausesConditional.new('unknown', true) # EBS AMI dependent
|
61
|
+
],
|
62
|
+
'BlockDeviceMappings' => [
|
63
|
+
UpdateCausesConditional.new('replacement',
|
64
|
+
lambda{|final, original|
|
65
|
+
f_maps = final.fetch('Properties', 'BlockDeviceMappings', [])
|
66
|
+
o_maps = original.fetch('Properties', 'BlockDeviceMappings', [])
|
67
|
+
f_maps.map! do |m|
|
68
|
+
m.delete('DeleteOnTermination')
|
69
|
+
m.to_smash(:sorted)
|
70
|
+
end
|
71
|
+
o_maps.map! do |m|
|
72
|
+
m.delete('DeleteOnTermination')
|
73
|
+
m.to_smash(:sorted)
|
74
|
+
end
|
75
|
+
f_maps.size != o_maps.size ||
|
76
|
+
!f_maps.all?{|m| o_maps.include?(m)}
|
77
|
+
}
|
78
|
+
),
|
79
|
+
UpdateCausesConditional.new('none', true)
|
80
|
+
],
|
81
|
+
'EbsOptimized' => [
|
82
|
+
UpdateCausesConditional.new('unknown', true) # EBS AMI dependent
|
83
|
+
],
|
84
|
+
'InstanceType' => [
|
85
|
+
UpdateCausesConditional.new('unknown', true) # EBS AMI dependent
|
86
|
+
],
|
87
|
+
'KernelId' => [
|
88
|
+
UpdateCausesConditional.new('unknown', true) # EBS AMI dependent
|
89
|
+
],
|
90
|
+
'RamdiskId' => [
|
91
|
+
UpdateCausesConditional.new('unknown', true) # EBS AMI dependent
|
92
|
+
],
|
93
|
+
'SecurityGroupIds' => [
|
94
|
+
UpdateCausesConditional.new('none',
|
95
|
+
lambda{|final, _orig|
|
96
|
+
final.get('Properties', 'SubnetId') ||
|
97
|
+
final.fetch('Properties', 'NetworkInterface', {}).values.include?('SubnetId')
|
98
|
+
}
|
99
|
+
),
|
100
|
+
UpdateCausesConditional.new('replacement', true)
|
101
|
+
],
|
102
|
+
'UserData' => [
|
103
|
+
UpdateCausesConditional.new('unknown', true) # EBS AMI dependent
|
104
|
+
]
|
105
|
+
},
|
106
|
+
'AWS::EC2::NetworkInterface' => {
|
107
|
+
'PrivateIpAddresses' => [
|
108
|
+
UpdateCausesConditional.new('replacement',
|
109
|
+
lambda{|final, original|
|
110
|
+
f_primary = final.fetch('Properties', 'PrivateIpAddresses', []).detect do |addr|
|
111
|
+
addr['Primary']
|
112
|
+
end || Smash.new
|
113
|
+
o_primary = original.fetch('Properties', 'PrivateIpAddresses', []).detect do |addr|
|
114
|
+
addr['Primary']
|
115
|
+
end || Smash.new
|
116
|
+
f_primary.to_smash(:sorted) != o_primary.to_smash(:sorted)
|
117
|
+
}
|
118
|
+
),
|
119
|
+
UpdateCausesConditional.new('none', true)
|
120
|
+
]
|
121
|
+
},
|
122
|
+
'AWS::ElastiCache::CacheCluster' => {
|
123
|
+
'NumCacheNodes' => [
|
124
|
+
UpdateCausesConditional.new('replacement',
|
125
|
+
lambda{|final, original|
|
126
|
+
[
|
127
|
+
final.get('Properties', 'PreferredAvailabilityZone'),
|
128
|
+
final.get('Properties', 'PreferredAvailabilityZones'),
|
129
|
+
original.get('Properties', 'PreferredAvailabilityZone'),
|
130
|
+
original.get('Properties', 'PreferredAvailabilityZones')
|
131
|
+
].all?{|i| i.nil? || i.empty? }
|
132
|
+
}
|
133
|
+
),
|
134
|
+
UpdateCausesConditional.new('none', true)
|
135
|
+
],
|
136
|
+
'PreferredAvailabilityZones' => [
|
137
|
+
UpdateCausesConditional.new('interrupt',
|
138
|
+
lambda{|final, original|
|
139
|
+
original.get('Properties', 'PreferredAvailabilityZones') ||
|
140
|
+
final.fetch('Properties', 'PreferredAvailabilityZones', []).include?(
|
141
|
+
original.get('Properties', 'PreferredAvailabilityZone')
|
142
|
+
)
|
143
|
+
}
|
144
|
+
),
|
145
|
+
UpdateCausesConditional.new('replacement', true)
|
146
|
+
]
|
147
|
+
},
|
148
|
+
'AWS::ElasticLoadBalancing::LoadBalancer' => {
|
149
|
+
'AvailabilityZones' => [
|
150
|
+
UpdateCausesConditional.new('replacement',
|
151
|
+
lambda{|final, original|
|
152
|
+
original.fetch('Properties', 'AvailabilityZones', []).empty? ||
|
153
|
+
final.fetch('Properties', 'AvailabilityZones', []).empty?
|
154
|
+
}
|
155
|
+
),
|
156
|
+
UpdateCausesConditional.new('none', true)
|
157
|
+
],
|
158
|
+
'HealthCheck' => [
|
159
|
+
UpdateCausesConditional.new('replacement',
|
160
|
+
lambda{|final, original|
|
161
|
+
original.fetch('Properties', 'HealthCheck', {}).empty? ||
|
162
|
+
final.fetch('Properties', 'HealthCheck', {}).empty?
|
163
|
+
}
|
164
|
+
),
|
165
|
+
UpdateCausesConditional.new('none', true)
|
166
|
+
],
|
167
|
+
'Subnets' => [
|
168
|
+
UpdateCausesConditional.new('replacement',
|
169
|
+
lambda{|final, original|
|
170
|
+
original.fetch('Properties', 'Subnets', []).empty? ||
|
171
|
+
final.fetch('Properties', 'Subnets', []).empty?
|
172
|
+
}
|
173
|
+
),
|
174
|
+
UpdateCausesConditional.new('none', true)
|
175
|
+
]
|
176
|
+
},
|
177
|
+
'AWS::RDS::DBCluster' => {
|
178
|
+
'BackupRetentionPeriod' => [
|
179
|
+
UpdateCausesConditional.new('interrupt',
|
180
|
+
lambda{|final, original|
|
181
|
+
fp = final.get('Properties', 'BackupRetentionPeriod').to_i
|
182
|
+
op = original.get('Properties', 'BackupRetentionPeriod').to_i
|
183
|
+
(fp == 0 && op != 0) ||
|
184
|
+
(op == 0 && fp != 0)
|
185
|
+
}
|
186
|
+
),
|
187
|
+
UpdateCausesConditional.new('none', true)
|
188
|
+
],
|
189
|
+
'PreferredMaintenanceWindow' => [
|
190
|
+
# can interrupt if apply immediately is set on api call but
|
191
|
+
# no way to know
|
192
|
+
UpdateCausesConditional.new('unknown', true)
|
193
|
+
]
|
194
|
+
},
|
195
|
+
'AWS::RDS::DBClusterParameterGroup' => {
|
196
|
+
'Parameters' => [
|
197
|
+
# dependent on what parameters have been changed. doesn't
|
198
|
+
# look like parameter modifications are applied immediately?
|
199
|
+
# set as unknown for safety
|
200
|
+
UpdateCausesConditional.new('unknown', true)
|
201
|
+
]
|
202
|
+
},
|
203
|
+
'AWS::RDS::DBInstance' => {
|
204
|
+
'AutoMinorVersionUpgrade' => [
|
205
|
+
# can cause interrupts based on future actions (enables
|
206
|
+
# auto patching) so leave as unknown for safety
|
207
|
+
UpdateCausesConditional.new('unknown', true)
|
208
|
+
],
|
209
|
+
'BackupRetentionPeriod' => [
|
210
|
+
UpdateCausesConditional.new('interrupt',
|
211
|
+
lambda{|final, original|
|
212
|
+
fp = final.get('Properties', 'BackupRetentionPeriod').to_i
|
213
|
+
op = original.get('Properties', 'BackupRetentionPeriod').to_i
|
214
|
+
(fp == 0 && op != 0) ||
|
215
|
+
(op == 0 && fp != 0)
|
216
|
+
}
|
217
|
+
),
|
218
|
+
UpdateCausesConditional.new('none', true)
|
219
|
+
],
|
220
|
+
'DBParameterGroupName' => [
|
221
|
+
# changes are not applied until reboot, but it could
|
222
|
+
# still be considered an interrupt? setting as unknown
|
223
|
+
# for safety
|
224
|
+
UpdateCausesConditional.new('unknown', true)
|
225
|
+
],
|
226
|
+
'PreferredMaintenanceWindow' => [
|
227
|
+
# can interrupt if apply immediately is set on api call but
|
228
|
+
# no way to know
|
229
|
+
UpdateCausesConditional.new('unknown', true)
|
230
|
+
]
|
231
|
+
},
|
232
|
+
'AWS::RDS::DBParameterGroup' => {
|
233
|
+
'Parameters' => [
|
234
|
+
# dependent on what parameters have been changed. doesn't
|
235
|
+
# look like parameter modifications are applied immediately?
|
236
|
+
# set as unknown for safety
|
237
|
+
UpdateCausesConditional.new('unknown', true)
|
238
|
+
]
|
239
|
+
},
|
240
|
+
'AWS::RDS::EventSubscription' => {
|
241
|
+
'SourceType' => [
|
242
|
+
UpdateCausesConditional.new('replacement',
|
243
|
+
lambda{|final, original|
|
244
|
+
!final.get('Properties', 'SourceType')
|
245
|
+
}
|
246
|
+
),
|
247
|
+
UpdateCausesConditional.new('none', true)
|
248
|
+
]
|
249
|
+
},
|
250
|
+
'AWS::Route53::HostedZone' => {
|
251
|
+
'VPCs' => [
|
252
|
+
UpdateCausesConditional.new('replacement',
|
253
|
+
lambda{|final, original|
|
254
|
+
!final.get('Properties', 'VPCs') ||
|
255
|
+
!original.get('Properties', 'VPCs')
|
256
|
+
}
|
257
|
+
),
|
258
|
+
UpdateCausesConditional.new('none', true)
|
259
|
+
]
|
260
|
+
}
|
261
|
+
)
|
262
|
+
|
11
263
|
class << self
|
12
264
|
|
13
265
|
include Bogo::Memoization
|