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 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