cfer 0.3.0 → 0.4.0
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/.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
|