cloudformation-ruby-dsl 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -3
- data/examples/cloudformation-ruby-script.rb +19 -7
- data/lib/cloudformation-ruby-dsl/cfntemplate.rb +65 -21
- data/lib/cloudformation-ruby-dsl/dsl.rb +29 -7
- data/lib/cloudformation-ruby-dsl/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddd63bd7d011689a102e8a40eaa4104cc4c41014
|
4
|
+
data.tar.gz: c6f98b6601e0e24022b99d6b5e94682b5f2fd943
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42c017c5ed7f1ed8633b157adf374a20a84c67e2419e6bf2118700d9cfbf26510b80718163257882f1baae537e75943ab6857131e848915b7fe1b9b52b4eb760
|
7
|
+
data.tar.gz: 0f7af272130c77afc3188de7c0b245570ffd3df02861c9febb6909ba83abf843d804c6ba3b416e55f2ebb1bb5b0a56115b86124c5a6a7c423ce2dad335d6b63d
|
data/README.md
CHANGED
@@ -38,10 +38,19 @@ You may need to preface this with `bundle exec` if you installed via Bundler.
|
|
38
38
|
|
39
39
|
Make the resulting file executable (`chmod +x [NEW_NAME.rb]`). It can respond to the following subcommands (which are listed if you run without parameters):
|
40
40
|
- `expand`: output the JSON template to the command line (takes optional `--nopretty` to minimize the output)
|
41
|
-
- `diff`: compare an existing stack with your template
|
41
|
+
- `diff`: compare an existing stack with your template. Produces following exit codes:
|
42
|
+
```
|
43
|
+
0 - no differences, nothing to update
|
44
|
+
1 - stack does not exist, template Validation error
|
45
|
+
2 - there are differences between an existing stack and your template
|
46
|
+
```
|
42
47
|
- `validate`: run validation against the stack definition
|
43
48
|
- `create`: create a new stack from the output
|
44
|
-
- `update`: update an existing stack from the output
|
49
|
+
- `update`: update an existing stack from the output. Produces following exit codes:
|
50
|
+
```
|
51
|
+
0 - update finished successfully
|
52
|
+
1 - no updates to perform, stack doesn't exist, unable to update immutable parameter or tag, AWS ServiceError exception
|
53
|
+
```
|
45
54
|
- `cancel-update`: cancel updating a stack
|
46
55
|
- `delete`: delete a stack (with prompt)
|
47
56
|
- `describe`: get output of an existing stack and output it (takes optional `--nopretty` to minimize output)
|
@@ -89,7 +98,7 @@ Reference a CloudFormation pseudo parameter.
|
|
89
98
|
### Utility Functions
|
90
99
|
|
91
100
|
Additional capabilities for file inclusion, etc.
|
92
|
-
- `tag(
|
101
|
+
- `tag(tag_name, tag_options_hash)`: add tags to the stack, which are inherited by all resources in that stack. `tag_options_hash` includes `:Value=>value` and `:Immutable=>true` properties. `tag(tag_value_hash)` is deprecated and will be removed in a future version.
|
93
102
|
- `file(name)`: return the named file as a string, for further use
|
94
103
|
- `load_from_file(filename)`: load the named file by a given type; currently handles YAML, JSON, and Ruby
|
95
104
|
- `interpolate(string)`: embed CFN references into a string (`{{ref('Service')}}`) for later interpretation by the CFN engine
|
@@ -111,17 +111,29 @@ template do
|
|
111
111
|
|
112
112
|
|
113
113
|
# The tag type is a DSL extension; it is not a property of actual CloudFormation templates.
|
114
|
-
# These tags are excised from the template and used to generate a series of --tag arguments
|
114
|
+
# These tags are excised from the template and used to generate a series of --tag arguments
|
115
|
+
# which are passed to CloudFormation when a stack is created.
|
115
116
|
# They do not ultimately appear in the expanded CloudFormation template.
|
116
|
-
# The diff subcommand will compare tags with the running stack and identify any changes,
|
117
|
-
#
|
117
|
+
# The diff subcommand will compare tags with the running stack and identify any changes,
|
118
|
+
# but a stack update will do the diff and throw an error on any immutable tags update attempt.
|
119
|
+
# The tags are propagated to all resources created by the stack, including the stack itself.
|
120
|
+
# If a resource has its own tag with the same name as CF's it's not overwritten.
|
118
121
|
#
|
119
122
|
# Amazon has set the following restrictions on CloudFormation tags:
|
120
123
|
# => limit 10
|
121
|
-
#
|
122
|
-
|
123
|
-
tag :
|
124
|
-
|
124
|
+
# CloudFormation tags declaration examples:
|
125
|
+
|
126
|
+
tag 'My:New:Tag',
|
127
|
+
:Value => 'ImmutableTagValue',
|
128
|
+
:Immutable => true
|
129
|
+
|
130
|
+
tag :MyOtherTag,
|
131
|
+
:Value => 'My Value With Spaces'
|
132
|
+
|
133
|
+
tag(:"tag:name", :Value => 'tag_value', :Immutable => true)
|
134
|
+
|
135
|
+
# Following format is deprecated and not advised. Please declare CloudFormation tags as described above.
|
136
|
+
tag :TagName => 'tag_value' # It's immutable.
|
125
137
|
|
126
138
|
resource 'SecurityGroup', :Type => 'AWS::EC2::SecurityGroup', :Properties => {
|
127
139
|
:GroupDescription => 'Lets any vpc traffic in.',
|
@@ -36,6 +36,7 @@ class AwsCfn
|
|
36
36
|
|
37
37
|
def initialize(args)
|
38
38
|
Aws.config[:region] = args[:region] if args.key?(:region)
|
39
|
+
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: args[:aws_profile]) unless args[:aws_profile].nil?
|
39
40
|
end
|
40
41
|
|
41
42
|
def cfn_client
|
@@ -72,6 +73,7 @@ def parse_args
|
|
72
73
|
stack_name = nil
|
73
74
|
parameters = {}
|
74
75
|
region = default_region
|
76
|
+
profile = nil
|
75
77
|
nopretty = false
|
76
78
|
ARGV.slice_before(/^--/).each do |name, value|
|
77
79
|
case name
|
@@ -81,11 +83,13 @@ def parse_args
|
|
81
83
|
parameters = Hash[value.split(/;/).map { |pair| pair.split(/=/, 2) }] #/# fix for syntax highlighting
|
82
84
|
when '--region'
|
83
85
|
region = value
|
86
|
+
when '--profile'
|
87
|
+
profile = value
|
84
88
|
when '--nopretty'
|
85
89
|
nopretty = true
|
86
90
|
end
|
87
91
|
end
|
88
|
-
[stack_name, parameters, region, nopretty]
|
92
|
+
[stack_name, parameters, region, profile, nopretty]
|
89
93
|
end
|
90
94
|
|
91
95
|
def validate_action(action)
|
@@ -132,7 +136,7 @@ def validate_action(action)
|
|
132
136
|
end
|
133
137
|
|
134
138
|
def cfn(template)
|
135
|
-
aws_cfn = AwsCfn.new({:region => template.aws_region})
|
139
|
+
aws_cfn = AwsCfn.new({:region => template.aws_region, :aws_profile => template.aws_profile})
|
136
140
|
cfn_client = aws_cfn.cfn_client
|
137
141
|
|
138
142
|
action = validate_action( ARGV[0] )
|
@@ -145,6 +149,12 @@ def cfn(template)
|
|
145
149
|
# Remove them from the template as well, so that the template is valid.
|
146
150
|
cfn_tags = template.excise_tags!
|
147
151
|
|
152
|
+
# Find tags where extension attribute `:Immutable` is true then remove it from the
|
153
|
+
# tag's properties hash since it can't be passed to CloudFormation.
|
154
|
+
immutable_tags = template.get_tag_attribute(cfn_tags, :Immutable)
|
155
|
+
|
156
|
+
cfn_tags.each {|k, v| cfn_tags[k] = v[:Value].to_s}
|
157
|
+
|
148
158
|
if action == 'diff' or (action == 'expand' and not template.nopretty)
|
149
159
|
template_string = JSON.pretty_generate(template)
|
150
160
|
else
|
@@ -152,7 +162,7 @@ def cfn(template)
|
|
152
162
|
end
|
153
163
|
|
154
164
|
# Derive stack name from ARGV
|
155
|
-
_, options = extract_options(ARGV[1..-1], %w(--nopretty), %w(--stack-name --region --parameters --tag))
|
165
|
+
_, options = extract_options(ARGV[1..-1], %w(--nopretty), %w(--profile --stack-name --region --parameters --tag))
|
156
166
|
# If the first argument is not an option and stack_name is undefined, assume it's the stack name
|
157
167
|
# The second argument, if present, is the resource name used by the describe-resource command
|
158
168
|
if template.stack_name.nil?
|
@@ -177,7 +187,10 @@ def cfn(template)
|
|
177
187
|
# example: <template.rb> diff my-stack-name --parameters "Env=prod" --region eu-west-1
|
178
188
|
# Diff the current template for an existing stack with the expansion of this template.
|
179
189
|
|
180
|
-
#
|
190
|
+
# `diff` operation exit codes are:
|
191
|
+
# 0 - no differences are found. Outputs nothing to make it easy to use the output of the diff call from within other scripts.
|
192
|
+
# 1 - produced by any ValidationError exception (e.g. "Stack with id does not exist")
|
193
|
+
# 2 - there are changes to update (tags, params, template)
|
181
194
|
# If you want output of the entire file, simply use this option with a large number, i.e., -U 10000
|
182
195
|
# In fact, this is what Diffy does by default; we just don't want that, and we can't support passing arbitrary options to diff
|
183
196
|
# because Diffy's "context" configuration is mutually exclusive with the configuration to pass arbitrary options to diff
|
@@ -246,7 +259,11 @@ def cfn(template)
|
|
246
259
|
puts
|
247
260
|
end
|
248
261
|
|
249
|
-
|
262
|
+
if tags_diff.empty? && params_diff.empty? && template_diff.empty?
|
263
|
+
exit(true)
|
264
|
+
else
|
265
|
+
exit(2)
|
266
|
+
end
|
250
267
|
|
251
268
|
when 'validate'
|
252
269
|
begin
|
@@ -400,32 +417,58 @@ def cfn(template)
|
|
400
417
|
old_stack = old_stack[0]
|
401
418
|
end
|
402
419
|
|
403
|
-
# If updating a stack and some parameters are marked as immutable,
|
420
|
+
# If updating a stack and some parameters or tags are marked as immutable, set the variable to true.
|
421
|
+
immutables_exist = nil
|
422
|
+
|
404
423
|
if not immutable_parameters.empty?
|
405
424
|
old_parameters = Hash[old_stack.parameters.map { |p| [p.parameter_key, p.parameter_value]}]
|
406
425
|
new_parameters = template.parameters
|
407
|
-
|
408
426
|
immutable_parameters.sort.each do |param|
|
409
|
-
if old_parameters[param].to_s != new_parameters[param].to_s
|
427
|
+
if old_parameters[param].to_s != new_parameters[param].to_s && old_parameters.key?(param)
|
410
428
|
$stderr.puts "Error: unable to update immutable parameter " +
|
411
429
|
"'#{param}=#{old_parameters[param]}' to '#{param}=#{new_parameters[param]}'."
|
412
|
-
|
430
|
+
immutables_exist = true
|
413
431
|
end
|
414
432
|
end
|
415
433
|
end
|
416
434
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
435
|
+
if not immutable_tags.empty?
|
436
|
+
old_cfn_tags = Hash[old_stack.tags.map { |t| [t.key, t.value]}]
|
437
|
+
cfn_tags_ary = Hash[cfn_tags.map { |k,v| [k, v]}]
|
438
|
+
immutable_tags.sort.each do |tag|
|
439
|
+
if old_cfn_tags[tag].to_s != cfn_tags_ary[tag].to_s && old_cfn_tags.key?(tag)
|
440
|
+
$stderr.puts "Error: unable to update immutable tag " +
|
441
|
+
"'#{tag}=#{old_cfn_tags[tag]}' to '#{tag}=#{cfn_tags_ary[tag]}'."
|
442
|
+
immutables_exist = true
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
# Fail if some parameters or tags were marked as immutable.
|
448
|
+
if immutables_exist
|
426
449
|
exit(false)
|
427
450
|
end
|
428
451
|
|
452
|
+
# Compare the sorted arrays of parameters for an exact match and print difference.
|
453
|
+
old_parameters = old_stack.parameters.map { |p| [p.parameter_key, p.parameter_value]}.sort
|
454
|
+
new_parameters = template.parameters.sort
|
455
|
+
if new_parameters != old_parameters
|
456
|
+
puts "\nCloudFormation stack parameters that do not match and will be updated:" +
|
457
|
+
"\n" + (old_parameters - new_parameters).map {|param| "< #{param}" }.join("\n") +
|
458
|
+
"\n" + "---" +
|
459
|
+
"\n" + (new_parameters - old_parameters).map {|param| "> #{param}"}.join("\n")
|
460
|
+
end
|
461
|
+
|
462
|
+
# Compare the sorted arrays of tags for an exact match and print difference.
|
463
|
+
old_cfn_tags = old_stack.tags.map { |t| [t.key, t.value]}.sort
|
464
|
+
cfn_tags_ary = cfn_tags.map { |k,v| [k, v]}.sort
|
465
|
+
if cfn_tags_ary != old_cfn_tags
|
466
|
+
puts "\nCloudFormation stack tags that do not match and will be updated:" +
|
467
|
+
"\n" + (old_cfn_tags - cfn_tags_ary).map {|tag| "< #{tag}" }.join("\n") +
|
468
|
+
"\n" + "---" +
|
469
|
+
"\n" + (cfn_tags_ary - old_cfn_tags).map {|tag| "> #{tag}"}.join("\n")
|
470
|
+
end
|
471
|
+
|
429
472
|
# update the stack
|
430
473
|
begin
|
431
474
|
|
@@ -434,6 +477,7 @@ def cfn(template)
|
|
434
477
|
stack_name: stack_name,
|
435
478
|
template_body: template_string,
|
436
479
|
parameters: template.parameters.map { |k,v| {parameter_key: k, parameter_value: v}}.to_a,
|
480
|
+
tags: cfn_tags.map { |k,v| {"key" => k.to_s, "value" => v.to_s} }.to_a,
|
437
481
|
capabilities: ["CAPABILITY_IAM"],
|
438
482
|
}
|
439
483
|
|
@@ -460,7 +504,7 @@ end
|
|
460
504
|
# Example:
|
461
505
|
#
|
462
506
|
# desired, unknown = extract_options("arg1 --option withvalue --optionwithoutvalue", %w(--option), %w())
|
463
|
-
#
|
507
|
+
#
|
464
508
|
# puts desired => Array{"arg1", "--option", "withvalue"}
|
465
509
|
# puts unknown => Array{}
|
466
510
|
#
|
@@ -515,6 +559,6 @@ end
|
|
515
559
|
|
516
560
|
# Main entry point
|
517
561
|
def template(&block)
|
518
|
-
stack_name, parameters, aws_region, nopretty = parse_args
|
519
|
-
raw_template(parameters, stack_name, aws_region, nopretty, &block)
|
562
|
+
stack_name, parameters, aws_region, aws_profile, nopretty = parse_args
|
563
|
+
raw_template(parameters, stack_name, aws_region, aws_profile, nopretty, &block)
|
520
564
|
end
|
@@ -44,8 +44,8 @@ end
|
|
44
44
|
############################# CloudFormation DSL
|
45
45
|
|
46
46
|
# Main entry point
|
47
|
-
def raw_template(parameters = {}, stack_name = nil, aws_region = default_region, nopretty = false, &block)
|
48
|
-
TemplateDSL.new(parameters, stack_name, aws_region, nopretty, &block)
|
47
|
+
def raw_template(parameters = {}, stack_name = nil, aws_region = default_region, aws_profile = nil, nopretty = false, &block)
|
48
|
+
TemplateDSL.new(parameters, stack_name, aws_region, aws_profile, nopretty, &block)
|
49
49
|
end
|
50
50
|
|
51
51
|
def default_region
|
@@ -54,12 +54,13 @@ end
|
|
54
54
|
|
55
55
|
# Core interpreter for the DSL
|
56
56
|
class TemplateDSL < JsonObjectDSL
|
57
|
-
attr_reader :parameters, :aws_region, :nopretty, :stack_name
|
57
|
+
attr_reader :parameters, :aws_region, :nopretty, :stack_name, :aws_profile
|
58
58
|
|
59
|
-
def initialize(parameters = {}, stack_name = nil, aws_region = default_region, nopretty = false)
|
59
|
+
def initialize(parameters = {}, stack_name = nil, aws_region = default_region, aws_profile = nil, nopretty = false)
|
60
60
|
@parameters = parameters
|
61
61
|
@stack_name = stack_name
|
62
62
|
@aws_region = aws_region
|
63
|
+
@aws_profile = aws_profile
|
63
64
|
@nopretty = nopretty
|
64
65
|
super()
|
65
66
|
end
|
@@ -110,15 +111,36 @@ class TemplateDSL < JsonObjectDSL
|
|
110
111
|
contents
|
111
112
|
end
|
112
113
|
|
114
|
+
# Find tags where the specified attribute is true then remove this attribute.
|
115
|
+
def get_tag_attribute(tags, attribute)
|
116
|
+
marked_tags = []
|
117
|
+
tags.each do |tag, options|
|
118
|
+
if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
|
119
|
+
marked_tags << tag
|
120
|
+
end
|
121
|
+
end
|
122
|
+
marked_tags
|
123
|
+
end
|
124
|
+
|
113
125
|
def excise_tags!
|
114
126
|
tags = @dict.fetch(:Tags, {})
|
115
127
|
@dict.delete(:Tags)
|
116
128
|
tags
|
117
129
|
end
|
118
130
|
|
119
|
-
def tag(tag)
|
120
|
-
tag.
|
121
|
-
default(:Tags, {})[
|
131
|
+
def tag(tag, *args)
|
132
|
+
if (tag.is_a?(String) || tag.is_a?(Symbol)) && !args.empty?
|
133
|
+
default(:Tags, {})[tag.to_s] = args[0]
|
134
|
+
# For backward-compatibility, transform `tag_name=>value` format to `tag_name, :Value=>value, :Immutable=>true`
|
135
|
+
# Tags declared this way remain immutable and won't be updated.
|
136
|
+
elsif tag.is_a?(Hash) && tag.size == 1 && args.empty?
|
137
|
+
$stderr.puts "WARNING: #{tag} tag declaration format is deprecated and will be removed in a future version. Please use resource-like style instead."
|
138
|
+
tag.each do |name, value|
|
139
|
+
default(:Tags, {})[name.to_s] = {:Value => value, :Immutable => true}
|
140
|
+
end
|
141
|
+
else
|
142
|
+
$stderr.puts "Error: #{tag} tag validation error. Please verify tag's declaration format."
|
143
|
+
exit(false)
|
122
144
|
end
|
123
145
|
end
|
124
146
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cloudformation-ruby-dsl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shawn Smith
|
@@ -15,7 +15,7 @@ authors:
|
|
15
15
|
autorequire:
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
|
-
date: 2016-
|
18
|
+
date: 2016-03-28 00:00:00.000000000 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: detabulator
|