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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4d86a3e1c9ae992f10540eb8bc343c788a111089
4
- data.tar.gz: 388ea8ad0dd651ff9889e3dbe068ecec4ae4a4a4
3
+ metadata.gz: ddd63bd7d011689a102e8a40eaa4104cc4c41014
4
+ data.tar.gz: c6f98b6601e0e24022b99d6b5e94682b5f2fd943
5
5
  SHA512:
6
- metadata.gz: b8f8a87b3e8af88daaf3752f95047c6e1aa124ba49c994c7d918b185960c323cdaf796d950b0ac50c5eff43774cfba4818cd2697f0a0ca0f28492ac6c157c818
7
- data.tar.gz: 42ad392c8638195e397e6ea59529aac1cec3bc97fdcfd4d14eb22d232a787921e831f3eb9647841c6a8fbcef4954d8bec15cb3f6ebf8cc74b66eccadbfb88f15
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(tag)`: add tags to the stack, which are inherited by all resources in that stack; can only be used at launch
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 which are passed to CloudFormation when a stack is created.
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, but a stack update will do the diff and throw an error on any
117
- # changes. The tags are propagated to all resources created by the stack, including the stack itself.
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
- # => immutable (you may not update a stack with new tags or different values for existing tags -- they will be rejected)
122
- #
123
- tag :MyTag => 'MyValue'
124
- tag :MyOtherTag => 'My Value With Spaces'
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
- # We default to "output nothing if no differences are found" to make it easy to use the output of the diff call from within other scripts
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
- exit(true)
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, fail if the new parameters don't match the old ones.
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
- exit(false)
430
+ immutables_exist = true
413
431
  end
414
432
  end
415
433
  end
416
434
 
417
- # Tags are immutable in CloudFormation. Validate against the existing stack to ensure tags haven't changed.
418
- # Compare the sorted arrays for an exact match
419
- old_cfn_tags = old_stack.tags.map { |p| [p.key.to_sym, p.value]}.sort
420
- cfn_tags_ary = cfn_tags.to_a.sort
421
- if cfn_tags_ary != old_cfn_tags
422
- $stderr.puts "CloudFormation stack tags do not match and cannot be updated. You must either use the same tags or create a new stack." +
423
- "\n" + (old_cfn_tags - cfn_tags_ary).map {|tag| "< #{tag}" }.join("\n") +
424
- "\n" + "---" +
425
- "\n" + (cfn_tags_ary - old_cfn_tags).map {|tag| "> #{tag}"}.join("\n")
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.each do | name, value |
121
- default(:Tags, {})[name] = value
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
 
@@ -15,7 +15,7 @@
15
15
  module Cfn
16
16
  module Ruby
17
17
  module Dsl
18
- VERSION = "1.1.0"
18
+ VERSION = "1.2.0"
19
19
  end
20
20
  end
21
21
  end
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.1.0
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-01-20 00:00:00.000000000 Z
18
+ date: 2016-03-28 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: detabulator