cloudformation-ruby-dsl 1.1.0 → 1.2.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/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
|