cfer 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/README.md +39 -1
- data/Rakefile +1 -56
- data/cfer.gemspec +13 -14
- data/examples/common/instance_deps.rb +3 -3
- data/examples/instance.rb +15 -21
- data/lib/cfer.rb +83 -7
- data/lib/cfer/block.rb +42 -0
- data/lib/cfer/cfn/cfer_credentials_provider.rb +72 -0
- data/lib/cfer/cfn/client.rb +121 -18
- data/lib/cfer/cli.rb +40 -1
- data/lib/cfer/core/client.rb +11 -0
- data/lib/cfer/core/resource.rb +16 -18
- data/lib/cfer/core/stack.rb +85 -14
- data/lib/cfer/util/error.rb +8 -0
- data/lib/cfer/version.rb +7 -1
- data/lib/cferext/aws/auto_scaling/auto_scaling_group.rb +6 -0
- data/lib/cferext/aws/iam/policy.rb +30 -0
- data/lib/cferext/aws/iam/policy_generator.rb +54 -0
- metadata +84 -58
- data/examples/chef_instance.rb +0 -56
- data/lib/cferext/aws/auto_scaling/launch_configuration.rb +0 -15
- data/lib/cferext/aws/ec2/instance.rb +0 -15
- data/lib/cferext/provisioning.rb +0 -16
- data/lib/cferext/provisioning/cfn-bootstrap.rb +0 -186
- data/lib/cferext/provisioning/chef.rb +0 -75
data/lib/cfer/cfn/client.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require_relative '../core/client'
|
2
|
+
require 'uri'
|
3
|
+
|
2
4
|
module Cfer::Cfn
|
3
5
|
|
4
6
|
class Client < Cfer::Core::Client
|
@@ -6,9 +8,11 @@ module Cfer::Cfn
|
|
6
8
|
attr_reader :stack
|
7
9
|
|
8
10
|
def initialize(options)
|
11
|
+
super
|
9
12
|
@name = options[:stack_name]
|
10
|
-
options
|
11
|
-
@
|
13
|
+
@options = options
|
14
|
+
@options.delete :stack_name
|
15
|
+
@cfn = Aws::CloudFormation::Client.new(@options)
|
12
16
|
flush_cache
|
13
17
|
end
|
14
18
|
|
@@ -20,6 +24,17 @@ module Cfer::Cfn
|
|
20
24
|
end
|
21
25
|
end
|
22
26
|
|
27
|
+
|
28
|
+
def delete_stack(stack_name)
|
29
|
+
begin
|
30
|
+
@cfn.delete_stack({
|
31
|
+
stack_name: stack_name, # required
|
32
|
+
})
|
33
|
+
rescue Aws::CloudFormation::Errors
|
34
|
+
raise CferError, "Stack delete #{stack_name}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
23
38
|
def responds_to?(method)
|
24
39
|
@cfn.responds_to? method
|
25
40
|
end
|
@@ -28,6 +43,29 @@ module Cfer::Cfn
|
|
28
43
|
@cfn.send(method, *args, &block)
|
29
44
|
end
|
30
45
|
|
46
|
+
def estimate(stack, options = {})
|
47
|
+
response = validate_template(template_body: stack.to_cfn)
|
48
|
+
|
49
|
+
estimate_params = []
|
50
|
+
response.parameters.each do |tmpl_param|
|
51
|
+
input_param = stack.input_parameters[tmpl_param.parameter_key]
|
52
|
+
if input_param
|
53
|
+
output_val = tmpl_param.no_echo ? '*****' : input_param
|
54
|
+
Cfer::LOGGER.debug "Parameter #{tmpl_param.parameter_key}=#{output_val}"
|
55
|
+
p = {
|
56
|
+
parameter_key: tmpl_param.parameter_key,
|
57
|
+
parameter_value: input_param,
|
58
|
+
use_previous_value: false
|
59
|
+
}
|
60
|
+
|
61
|
+
estimate_params << p
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
estimate_response = estimate_template_cost(template_body: stack.to_cfn, parameters: estimate_params)
|
66
|
+
estimate_response.url
|
67
|
+
end
|
68
|
+
|
31
69
|
def converge(stack, options = {})
|
32
70
|
Preconditions.check(@name).is_not_nil
|
33
71
|
Preconditions.check(stack) { is_not_nil and has_type(Cfer::Core::Stack) }
|
@@ -37,12 +75,15 @@ module Cfer::Cfn
|
|
37
75
|
create_params = []
|
38
76
|
update_params = []
|
39
77
|
|
40
|
-
previous_parameters =
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
78
|
+
previous_parameters = fetch_parameters rescue nil
|
79
|
+
|
80
|
+
current_version = Cfer::SEMANTIC_VERSION
|
81
|
+
previous_version = fetch_cfer_version rescue nil
|
82
|
+
|
83
|
+
current_hash = stack.git_version
|
84
|
+
previous_hash = fetch_git_hash rescue nil
|
85
|
+
|
86
|
+
# Compare current and previous versions and hashes?
|
46
87
|
|
47
88
|
response.parameters.each do |tmpl_param|
|
48
89
|
input_param = stack.input_parameters[tmpl_param.parameter_key]
|
@@ -81,7 +122,6 @@ module Cfer::Cfn
|
|
81
122
|
|
82
123
|
stack_options = {
|
83
124
|
stack_name: name,
|
84
|
-
template_body: stack.to_cfn,
|
85
125
|
capabilities: response.capabilities
|
86
126
|
}
|
87
127
|
|
@@ -91,11 +131,19 @@ module Cfer::Cfn
|
|
91
131
|
stack_options.merge! parse_stack_policy(:stack_policy, options[:stack_policy])
|
92
132
|
stack_options.merge! parse_stack_policy(:stack_policy_during_update, options[:stack_policy_during_update])
|
93
133
|
|
134
|
+
stack_options.merge! upload_or_return_template(stack.to_cfn, options)
|
135
|
+
|
94
136
|
cfn_stack =
|
95
137
|
begin
|
96
138
|
create_stack stack_options.merge parameters: create_params
|
139
|
+
:created
|
97
140
|
rescue Cfer::Util::StackExistsError
|
98
|
-
|
141
|
+
if options[:change]
|
142
|
+
create_change_set stack_options.merge change_set_name: options[:change], description: options[:change_description], parameters: update_params
|
143
|
+
else
|
144
|
+
update_stack stack_options.merge parameters: update_params
|
145
|
+
end
|
146
|
+
:updated
|
99
147
|
end
|
100
148
|
|
101
149
|
flush_cache
|
@@ -150,21 +198,59 @@ module Cfer::Cfn
|
|
150
198
|
end
|
151
199
|
end
|
152
200
|
|
201
|
+
def stack_cache(stack_name)
|
202
|
+
@stack_cache[stack_name] ||= {}
|
203
|
+
end
|
204
|
+
|
153
205
|
def fetch_stack(stack_name = @name)
|
154
206
|
raise Cfer::Util::StackDoesNotExistError, 'Stack name must be specified' if stack_name == nil
|
155
207
|
begin
|
156
|
-
|
208
|
+
stack_cache(stack_name)[:stack] ||= describe_stacks(stack_name: stack_name).stacks.first.to_h
|
157
209
|
rescue Aws::CloudFormation::Errors::ValidationError => e
|
158
210
|
raise Cfer::Util::StackDoesNotExistError, e.message
|
159
211
|
end
|
160
212
|
end
|
161
213
|
|
214
|
+
def fetch_summary(stack_name = @name)
|
215
|
+
begin
|
216
|
+
stack_cache(stack_name)[:summary] ||= get_template_summary(stack_name: stack_name)
|
217
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
218
|
+
raise Cfer::Util::StackDoesNotExistError, e.message
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def fetch_metadata(stack_name = @name)
|
223
|
+
md = fetch_summary(stack_name).metadata
|
224
|
+
stack_cache(stack_name)[:metadata] ||=
|
225
|
+
if md
|
226
|
+
JSON.parse(md)
|
227
|
+
else
|
228
|
+
{}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def remove(stack_name, options = {})
|
233
|
+
delete_stack(stack_name)
|
234
|
+
end
|
235
|
+
|
236
|
+
def fetch_cfer_version(stack_name = @name)
|
237
|
+
previous_version = Semantic::Version.new('0.0.0')
|
238
|
+
if previous_version_hash = fetch_metadata(stack_name).fetch('Cfer', {}).fetch('Version', nil)
|
239
|
+
previous_version_hash.each { |k, v| previous_version.send(k + '=', v) }
|
240
|
+
previous_version
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def fetch_git_hash(stack_name = @name)
|
245
|
+
fetch_metadata(stack_name).fetch('Cfer', {}).fetch('Git', {}).fetch('Rev', nil)
|
246
|
+
end
|
247
|
+
|
162
248
|
def fetch_parameters(stack_name = @name)
|
163
|
-
|
249
|
+
stack_cache(stack_name)[:parameters] ||= cfn_list_to_hash('parameter', fetch_stack(stack_name)[:parameters])
|
164
250
|
end
|
165
251
|
|
166
252
|
def fetch_outputs(stack_name = @name)
|
167
|
-
|
253
|
+
stack_cache(stack_name)[:outputs] ||= cfn_list_to_hash('output', fetch_stack(stack_name)[:outputs])
|
168
254
|
end
|
169
255
|
|
170
256
|
def fetch_output(stack_name, output_name)
|
@@ -181,22 +267,37 @@ module Cfer::Cfn
|
|
181
267
|
|
182
268
|
private
|
183
269
|
|
270
|
+
def upload_or_return_template(cfn_hash, options = {})
|
271
|
+
if cfn_hash.bytesize <= 51200 && !options[:force_s3]
|
272
|
+
{ template_body: cfn_hash }
|
273
|
+
else
|
274
|
+
raise Cfer::Util::CferError, 'Cfer needs to upload the template to S3, but no bucket was specified.' unless options[:s3_path]
|
275
|
+
|
276
|
+
uri = URI(options[:s3_path])
|
277
|
+
template = Aws::S3::Object.new bucket_name: uri.host, key: uri.path.reverse.chomp('/').reverse
|
278
|
+
template.put body: cfn_hash
|
279
|
+
|
280
|
+
template_url = template.public_url
|
281
|
+
template_url = template_url + '?versionId=' + template.version_id if template.version_id
|
282
|
+
|
283
|
+
{ template_url: template_url }
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
184
287
|
def cfn_list_to_hash(attribute, list)
|
288
|
+
return {} unless list
|
289
|
+
|
185
290
|
key = :"#{attribute}_key"
|
186
291
|
value = :"#{attribute}_value"
|
187
292
|
|
188
|
-
|
293
|
+
HashWithIndifferentAccess[ *list.map { |kv| [ kv[key].to_s, kv[value].to_s ] }.flatten ]
|
189
294
|
end
|
190
295
|
|
191
296
|
def flush_cache
|
192
297
|
Cfer::LOGGER.debug "*********** FLUSH CACHE ***************"
|
193
298
|
Cfer::LOGGER.debug "Stack cache: #{@stack_cache}"
|
194
|
-
Cfer::LOGGER.debug "Stack parameters: #{@stack_parameters}"
|
195
|
-
Cfer::LOGGER.debug "Stack outputs: #{@stack_outputs}"
|
196
299
|
Cfer::LOGGER.debug "***************************************"
|
197
300
|
@stack_cache = {}
|
198
|
-
@stack_parameters = {}
|
199
|
-
@stack_outputs = {}
|
200
301
|
end
|
201
302
|
|
202
303
|
def for_each_event(stack_name)
|
@@ -224,6 +325,8 @@ module Cfer::Cfn
|
|
224
325
|
Cfer::LOGGER.debug "Using #{name} from: #{value}"
|
225
326
|
if value.nil?
|
226
327
|
{}
|
328
|
+
elsif value.is_a?(Hash)
|
329
|
+
{"#{name}_body".to_sym => value.to_json}
|
227
330
|
elsif value.match(/\A#{URI::regexp(%w[http https s3])}\z/) # looks like a URL
|
228
331
|
{"#{name}_url".to_sym => value}
|
229
332
|
elsif File.exist?(value) # looks like a file to read
|
data/lib/cfer/cli.rb
CHANGED
@@ -4,6 +4,7 @@ require 'table_print'
|
|
4
4
|
|
5
5
|
module Cfer
|
6
6
|
class Cli < Thor
|
7
|
+
map '-v' => :version, '--version' => :version
|
7
8
|
|
8
9
|
namespace 'cfer'
|
9
10
|
class_option :verbose, type: :boolean, default: false
|
@@ -12,11 +13,16 @@ module Cfer
|
|
12
13
|
class_option :pretty_print, type: :boolean, default: :true, desc: 'Render JSON in a more human-friendly format'
|
13
14
|
|
14
15
|
def self.template_options
|
15
|
-
|
16
16
|
method_option :parameters,
|
17
17
|
type: :hash,
|
18
18
|
desc: 'The CloudFormation parameters to pass to the stack',
|
19
19
|
default: {}
|
20
|
+
method_option :parameter_file,
|
21
|
+
type: :string,
|
22
|
+
desc: 'A YAML or JSON file with CloudFormation parameters to pass to the stack'
|
23
|
+
method_option :parameter_environment,
|
24
|
+
type: :string,
|
25
|
+
desc: 'If parameter_file is set, will merge the subkey of this into the parameter list.'
|
20
26
|
end
|
21
27
|
|
22
28
|
def self.stack_options
|
@@ -59,6 +65,20 @@ module Cfer
|
|
59
65
|
method_option :timeout,
|
60
66
|
type: :numeric,
|
61
67
|
desc: 'The timeout (in minutes) before the stack operation aborts'
|
68
|
+
method_option :s3_path,
|
69
|
+
type: :string,
|
70
|
+
desc: 'Specifies an S3 path in case the stack is created with a URL.'
|
71
|
+
method_option :force_s3,
|
72
|
+
type: :boolean,
|
73
|
+
default: false,
|
74
|
+
desc: 'Forces Cfer to upload the template to S3 and pass CloudFormation a URL.'
|
75
|
+
method_option :change,
|
76
|
+
type: :string,
|
77
|
+
desc: 'Issues updates as a Cfn change set.'
|
78
|
+
method_option :change_description,
|
79
|
+
type: :string,
|
80
|
+
desc: 'The description of this Cfn change'
|
81
|
+
|
62
82
|
template_options
|
63
83
|
stack_options
|
64
84
|
def converge(stack_name)
|
@@ -71,6 +91,12 @@ module Cfer
|
|
71
91
|
Cfer.describe! stack_name, options
|
72
92
|
end
|
73
93
|
|
94
|
+
desc 'delete <stack>', 'Deletes a CloudFormation stack'
|
95
|
+
stack_options
|
96
|
+
def delete(stack_name)
|
97
|
+
Cfer.delete! stack_name, options
|
98
|
+
end
|
99
|
+
|
74
100
|
desc 'tail <stack>', 'Follows stack events on standard output as they occur'
|
75
101
|
method_option :follow,
|
76
102
|
aliases: :f,
|
@@ -96,6 +122,14 @@ module Cfer
|
|
96
122
|
Cfer.generate! tmpl, options
|
97
123
|
end
|
98
124
|
|
125
|
+
desc 'estimate [OPTIONS] <template.rb>', 'Prints a link to the Amazon cost caculator estimating the cost of the resulting CloudFormation stack'
|
126
|
+
long_desc <<-LONGDESC
|
127
|
+
LONGDESC
|
128
|
+
template_options
|
129
|
+
def estimate(tmpl)
|
130
|
+
Cfer.estimate! tmpl, options
|
131
|
+
end
|
132
|
+
|
99
133
|
def self.main(args)
|
100
134
|
Cfer::LOGGER.debug "Cfer version #{Cfer::VERSION}"
|
101
135
|
begin
|
@@ -128,6 +162,11 @@ module Cfer
|
|
128
162
|
end
|
129
163
|
end
|
130
164
|
|
165
|
+
desc 'version', 'Prints the current version of Cfer'
|
166
|
+
def version
|
167
|
+
puts Cfer::VERSION
|
168
|
+
end
|
169
|
+
|
131
170
|
private
|
132
171
|
|
133
172
|
def cfn(opts = {})
|
data/lib/cfer/core/client.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
|
+
require 'git'
|
2
|
+
|
1
3
|
module Cfer::Core
|
2
4
|
class Client
|
5
|
+
attr_reader :git
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
path = options[:working_directory] || '.'
|
9
|
+
if File.exist?("#{path}/.git")
|
10
|
+
@git = Git.open(path) rescue nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
3
14
|
def converge
|
4
15
|
raise Cfer::Util::CferError, 'converge not implemented on this client'
|
5
16
|
end
|
data/lib/cfer/core/resource.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
module Cfer::
|
2
|
-
class Resource < Cfer::
|
3
|
-
|
1
|
+
module Cfer::Core
|
2
|
+
class Resource < Cfer::BlockHash
|
3
|
+
@@types = {}
|
4
4
|
|
5
5
|
def initialize(name, type, **options, &block)
|
6
6
|
@name = name
|
@@ -9,36 +9,34 @@ module Cfer::Cfn
|
|
9
9
|
self.merge!(options)
|
10
10
|
self[:Properties] = HashWithIndifferentAccess.new
|
11
11
|
build_from_block(&block)
|
12
|
+
|
12
13
|
end
|
13
14
|
|
15
|
+
|
14
16
|
def tag(k, v, **options)
|
15
17
|
self[:Properties][:Tags] ||= []
|
16
18
|
self[:Properties][:Tags].unshift({"Key" => k, "Value" => v}.merge(options))
|
17
19
|
end
|
18
20
|
|
19
|
-
def properties(
|
21
|
+
def properties(keyvals = {})
|
20
22
|
self[:Properties].merge!(keyvals)
|
21
23
|
end
|
22
24
|
|
23
|
-
def
|
24
|
-
|
25
|
+
def get_property(key)
|
26
|
+
puts self[:Properties]
|
27
|
+
self[:Properties].fetch key
|
25
28
|
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
else
|
35
|
-
properties key => arguments
|
30
|
+
class << self
|
31
|
+
def resource_class(type)
|
32
|
+
@@types[type] ||= "CferExt::#{type}".split('::').inject(Object) { |o, c| o.const_get c if o && o.const_defined?(c) } || Class.new(Cfer::Core::Resource)
|
33
|
+
end
|
34
|
+
|
35
|
+
def extend_resource(type, &block)
|
36
|
+
resource_class(type).instance_eval(&block)
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
39
40
|
private
|
40
|
-
def camelize_property(sym)
|
41
|
-
sym.to_s.camelize.to_sym
|
42
|
-
end
|
43
41
|
end
|
44
42
|
end
|
data/lib/cfer/core/stack.rb
CHANGED
@@ -13,16 +13,18 @@ module Cfer::Core
|
|
13
13
|
|
14
14
|
attr_reader :options
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
attr_reader :git_version
|
17
|
+
|
18
|
+
def client
|
19
|
+
@options[:client] || raise('No client set on this stack')
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
def converge!(options = {})
|
23
|
+
client.converge self, options
|
24
|
+
end
|
25
|
+
|
26
|
+
def tail!(options = {}, &block)
|
27
|
+
client.tail self, options, &block
|
26
28
|
end
|
27
29
|
|
28
30
|
def initialize(options = {})
|
@@ -31,12 +33,25 @@ module Cfer::Core
|
|
31
33
|
|
32
34
|
@options = options
|
33
35
|
|
36
|
+
self[:Metadata] = {
|
37
|
+
:Cfer => {
|
38
|
+
:Version => Cfer::SEMANTIC_VERSION.to_h.delete_if { |k, v| v === nil }
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
34
42
|
self[:Parameters] = {}
|
35
43
|
self[:Mappings] = {}
|
36
44
|
self[:Conditions] = {}
|
37
45
|
self[:Resources] = {}
|
38
46
|
self[:Outputs] = {}
|
39
47
|
|
48
|
+
if options[:client] && git = options[:client].git && @git_version = (git.object('HEAD^').sha rescue nil)
|
49
|
+
self[:Metadata][:Cfer][:Git] = {
|
50
|
+
Rev: @git_version,
|
51
|
+
Clean: git.status.changed.empty?
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
40
55
|
@parameters = HashWithIndifferentAccess.new
|
41
56
|
@input_parameters = HashWithIndifferentAccess.new
|
42
57
|
|
@@ -84,6 +99,8 @@ module Cfer::Core
|
|
84
99
|
def parameter(name, options = {})
|
85
100
|
param = {}
|
86
101
|
options.each do |key, v|
|
102
|
+
next if v === nil
|
103
|
+
|
87
104
|
k = key.to_s.camelize.to_sym
|
88
105
|
param[k] =
|
89
106
|
case k
|
@@ -119,9 +136,7 @@ module Cfer::Core
|
|
119
136
|
def resource(name, type, options = {}, &block)
|
120
137
|
Preconditions.check_argument(/[[:alnum:]]+/ =~ name, "Resource name must be alphanumeric")
|
121
138
|
|
122
|
-
clazz =
|
123
|
-
Preconditions.check_argument clazz <= Cfer::Cfn::Resource, "#{type} is not a valid resource type because CferExt::#{type} does not inherit from `Cfer::Cfn::Resource`"
|
124
|
-
|
139
|
+
clazz = Cfer::Core::Resource.resource_class(type)
|
125
140
|
rc = clazz.new(name, type, options, &block)
|
126
141
|
|
127
142
|
self[:Resources][name] = rc
|
@@ -150,10 +165,9 @@ module Cfer::Core
|
|
150
165
|
# Includes template code from one or more files, and evals it in the context of this stack.
|
151
166
|
# Filenames are relative to the file containing the invocation of this method.
|
152
167
|
def include_template(*files)
|
153
|
-
|
154
|
-
dirname = File.dirname(calling_file)
|
168
|
+
include_base = options[:include_base] || File.dirname(caller.first.split(/:\d/,2).first)
|
155
169
|
files.each do |file|
|
156
|
-
path = File.join(
|
170
|
+
path = File.join(include_base, file)
|
157
171
|
instance_eval(File.read(path), path)
|
158
172
|
end
|
159
173
|
end
|
@@ -162,6 +176,63 @@ module Cfer::Core
|
|
162
176
|
client = @options[:client] || raise(Cfer::Util::CferError, "Can not fetch stack outputs without a client")
|
163
177
|
client.fetch_output(stack, out)
|
164
178
|
end
|
179
|
+
|
180
|
+
def lookup_outputs(stack)
|
181
|
+
client = @options[:client] || raise(Cfer::Util::CferError, "Can not fetch stack outputs without a client")
|
182
|
+
client.fetch_outputs(stack)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def post_block
|
188
|
+
begin
|
189
|
+
validate_stack!(self)
|
190
|
+
rescue Cfer::Util::CferValidationError => e
|
191
|
+
Cfer::LOGGER.error "Cfer detected #{e.errors.size > 1 ? 'errors' : 'an error'} when generating the stack:"
|
192
|
+
e.errors.each do |err|
|
193
|
+
Cfer::LOGGER.error "* #{err[:error]} in Stack#{validation_contextualize(err[:context])}"
|
194
|
+
end
|
195
|
+
raise e
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def validate_stack!(hash)
|
200
|
+
errors = []
|
201
|
+
context = []
|
202
|
+
_inner_validate_stack!(hash, errors, context)
|
203
|
+
|
204
|
+
raise Cfer::Util::CferValidationError, errors unless errors.empty?
|
205
|
+
end
|
206
|
+
|
207
|
+
def _inner_validate_stack!(hash, errors = [], context = [])
|
208
|
+
case hash
|
209
|
+
when Hash
|
210
|
+
hash.each do |k, v|
|
211
|
+
_inner_validate_stack!(v, errors, context + [k])
|
212
|
+
end
|
213
|
+
when Array
|
214
|
+
hash.each_index do |i|
|
215
|
+
_inner_validate_stack!(hash[i], errors, context + [i])
|
216
|
+
end
|
217
|
+
when nil
|
218
|
+
errors << {
|
219
|
+
error: "CloudFormation does not allow nulls in templates",
|
220
|
+
context: context
|
221
|
+
}
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def validation_contextualize(err_ctx)
|
226
|
+
err_ctx.inject("") do |err_str, ctx|
|
227
|
+
err_str <<
|
228
|
+
case ctx
|
229
|
+
when String
|
230
|
+
".#{ctx}"
|
231
|
+
when Numeric
|
232
|
+
"[#{ctx}]"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
165
236
|
end
|
166
237
|
|
167
238
|
end
|