cloudformation-ruby-dsl 0.5.4 → 1.0.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/bin/cfntemplate-to-ruby +2 -0
  4. data/cloudformation-ruby-dsl.gemspec +4 -3
  5. data/examples/cloudformation-ruby-script.rb +9 -9
  6. data/lib/cloudformation-ruby-dsl/cfntemplate.rb +187 -341
  7. data/lib/cloudformation-ruby-dsl/dsl.rb +249 -0
  8. data/lib/cloudformation-ruby-dsl/version.rb +1 -1
  9. metadata +34 -79
  10. data/vendor/AWSCloudFormation-1.0.12/README.TXT +0 -44
  11. data/vendor/AWSCloudFormation-1.0.12/RELEASENOTES.TXT +0 -6
  12. data/vendor/AWSCloudFormation-1.0.12/THIRDPARTYLICENSE.TXT +0 -824
  13. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cancel-update-stack +0 -9
  14. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cancel-update-stack.cmd +0 -19
  15. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cmd +0 -15
  16. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cmd.cmd +0 -42
  17. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-create-stack +0 -7
  18. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-create-stack.cmd +0 -19
  19. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-delete-stack +0 -7
  20. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-delete-stack.cmd +0 -19
  21. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-events +0 -7
  22. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-events.cmd +0 -19
  23. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resource +0 -7
  24. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resource.cmd +0 -19
  25. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resources +0 -7
  26. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resources.cmd +0 -19
  27. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stacks +0 -7
  28. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stacks.cmd +0 -19
  29. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-get-template +0 -7
  30. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-get-template.cmd +0 -19
  31. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stack-resources +0 -7
  32. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stack-resources.cmd +0 -19
  33. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stacks +0 -7
  34. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stacks.cmd +0 -19
  35. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-update-stack +0 -7
  36. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-update-stack.cmd +0 -19
  37. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-validate-template +0 -7
  38. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-validate-template.cmd +0 -19
  39. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-version +0 -7
  40. data/vendor/AWSCloudFormation-1.0.12/bin/cfn-version.cmd +0 -21
  41. data/vendor/AWSCloudFormation-1.0.12/bin/service +0 -29
  42. data/vendor/AWSCloudFormation-1.0.12/bin/service.cmd +0 -74
  43. data/vendor/AWSCloudFormation-1.0.12/credential-file-path.template +0 -2
  44. data/vendor/AWSCloudFormation-1.0.12/lib/CliCommando-1.0.jar +0 -0
  45. data/vendor/AWSCloudFormation-1.0.12/lib/activation-1.1.jar +0 -0
  46. data/vendor/AWSCloudFormation-1.0.12/lib/commons-cli-1.1.jar +0 -0
  47. data/vendor/AWSCloudFormation-1.0.12/lib/commons-codec-1.3.jar +0 -0
  48. data/vendor/AWSCloudFormation-1.0.12/lib/commons-discovery-0.2.jar +0 -0
  49. data/vendor/AWSCloudFormation-1.0.12/lib/commons-httpclient-3.0.jar +0 -0
  50. data/vendor/AWSCloudFormation-1.0.12/lib/commons-logging-1.0.4.jar +0 -0
  51. data/vendor/AWSCloudFormation-1.0.12/lib/commons-logging-api-1.1.1.jar +0 -0
  52. data/vendor/AWSCloudFormation-1.0.12/lib/httpclient-4.2.jar +0 -0
  53. data/vendor/AWSCloudFormation-1.0.12/lib/jaxb-api-2.0.jar +0 -0
  54. data/vendor/AWSCloudFormation-1.0.12/lib/jaxb-impl-2.0.1.jar +0 -0
  55. data/vendor/AWSCloudFormation-1.0.12/lib/jaxws-api-2.0.jar +0 -0
  56. data/vendor/AWSCloudFormation-1.0.12/lib/jdom-1.0.jar +0 -0
  57. data/vendor/AWSCloudFormation-1.0.12/lib/log4j.jar +0 -0
  58. data/vendor/AWSCloudFormation-1.0.12/lib/serializer.jar +0 -0
  59. data/vendor/AWSCloudFormation-1.0.12/lib/service.jar +0 -0
  60. data/vendor/AWSCloudFormation-1.0.12/lib/stax-api-1.0.1.jar +0 -0
  61. data/vendor/AWSCloudFormation-1.0.12/lib/wsdl4j-1.6.1.jar +0 -0
  62. data/vendor/AWSCloudFormation-1.0.12/lib/wss4j-1.5.7.jar +0 -0
  63. data/vendor/AWSCloudFormation-1.0.12/lib/wstx-asl-3.2.0.jar +0 -0
  64. data/vendor/AWSCloudFormation-1.0.12/lib/xalan-j2-2.7.0.jar +0 -0
  65. data/vendor/AWSCloudFormation-1.0.12/lib/xfire-all-1.2.6.jar +0 -0
  66. data/vendor/AWSCloudFormation-1.0.12/lib/xfire-jsr181-api-1.0-M1.jar +0 -0
  67. data/vendor/AWSCloudFormation-1.0.12/lib/xmlsec-1.4.2.jar +0 -0
  68. data/vendor/AWSCloudFormation-1.0.12/license.txt +0 -96
  69. data/vendor/AWSCloudFormation-1.0.12/notice.txt +0 -40
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3c4ac135faa6e385379b3156225d4408853fe025
4
- data.tar.gz: 683175d8ba2a0933803e4cdfdb07eaed10a00a2c
3
+ metadata.gz: 8876df582be39f256601ca2194db3405936c941a
4
+ data.tar.gz: 2fe2e643711d1a26327553436d95c45b28632107
5
5
  SHA512:
6
- metadata.gz: 8d25c190a403f173dd70def2b7b59bad1682e969cf3ccf6b786e2901a6a33f20c15ca59ce00520bf51e13acabd383ddcb65c57d7ee78efc974064ab071ccf808
7
- data.tar.gz: 034d390533c3197d2f2fbffeeed5b4ccb352a6c40475d389cacb997b9a255e3f4111f7e39caa2f05906566499ee54629e497ac3b7525db7255e51cc537b716f9
6
+ metadata.gz: f96a60535eb7da50a6b7e21f38670a58186f8b7b8e09c33c44f638d4e333dfa2c951c8a20b96b361013b89f79ee147498c04cbd8056935c64d1363d852cc0c5c
7
+ data.tar.gz: df0e8ec6cfaa0497aa90bf8fa1d7eb68fa154133ab67ff1941ca1eaa8beb759d72b08bf14bb9db6de40dc61f853cf26e66c13535ab06e4378441fa8199a0ad26
data/README.md CHANGED
@@ -39,9 +39,9 @@ You may need to preface this with `bundle exec` if you installed via Bundler.
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
41
  - `diff`: compare output with existing JSON for a stack
42
- - `cfn-validate-template`: run validation against the stack definition
43
- - `cfn-create-stack`: create a new stack from the output
44
- - `cfn-update-stack`: update an existing stack from the output
42
+ - `validate`: run validation against the stack definition
43
+ - `create`: create a new stack from the output
44
+ - `update`: update an existing stack from the output
45
45
 
46
46
  Below are the various functions currently available in the DSL. See [the example script](examples/cloudformation-ruby-script.rb) for more usage information.
47
47
 
@@ -119,6 +119,8 @@ def pprint_cfn_template(tpl)
119
119
  v.each { |name, options| pprint_cfn_section 'mapping', name, options }
120
120
  when 'Resources'
121
121
  v.each { |name, options| pprint_cfn_resource name, options }
122
+ when 'Conditions'
123
+ v.each { |name, options| pprint_cfn_section 'condition', name, options }
122
124
  when 'Outputs'
123
125
  v.each { |name, options| pprint_cfn_section 'output', name, options }
124
126
  else
@@ -23,8 +23,8 @@ Gem::Specification.new do |gem|
23
23
  gem.version = Cfn::Ruby::Dsl::VERSION
24
24
  gem.authors = ["Shawn Smith", "Dave Barcelo", "Morgan Fletcher", "Csongor Gyuricza", "Igor Polishchuk", "Nathaniel Eliot", "Jona Fenocchi", "Tony Cui"]
25
25
  gem.email = ["Shawn.Smith@bazaarvoice.com", "Dave.Barcelo@bazaarvoice.com", "Morgan.Fletcher@bazaarvoice.com", "Csongor.Gyuricza@bazaarvoice.com", "Igor.Polishchuk@bazaarvoice.com", "Nathaniel.Eliot@bazaarvoice.com", "Jona.Fenocchi@bazaarvoice.com", "Tony.Cui@bazaarvoice.com"]
26
- gem.description = %q{Ruby DSL library that provides a wrapper around the cfn-cmd.}
27
- gem.summary = %q{Ruby DSL library that provides a wrapper around the cfn-cmd. Written by [Bazaarvoice](http://www.bazaarvoice.com).}
26
+ gem.description = %q{Ruby DSL library that provides a wrapper around the CloudFormation.}
27
+ gem.summary = %q{Ruby DSL library that provides a wrapper around the CloudFormation. Written by [Bazaarvoice](http://www.bazaarvoice.com).}
28
28
  gem.homepage = "http://github.com/bazaarvoice/cloudformation-ruby-dsl"
29
29
 
30
30
  gem.files = `git ls-files`.split($/)
@@ -34,6 +34,7 @@ Gem::Specification.new do |gem|
34
34
 
35
35
  gem.add_runtime_dependency 'detabulator'
36
36
  gem.add_runtime_dependency 'json'
37
- gem.add_runtime_dependency 'xml-simple'
38
37
  gem.add_runtime_dependency 'bundler'
38
+ gem.add_runtime_dependency 'aws-sdk'
39
+ gem.add_runtime_dependency 'diffy'
39
40
  end
@@ -30,7 +30,7 @@ template do
30
30
  :MaxLength => '25',
31
31
  :AllowedPattern => '[_a-zA-Z0-9]*',
32
32
  :ConstraintDescription => 'Maximum length of the Label parameter may not exceed 25 characters and may only contain letters, numbers and underscores.',
33
- # The :Immutable attribute is a Ruby CFN extension. It affects the behavior of the '<template> cfn-update-stack ...'
33
+ # The :Immutable attribute is a Ruby CFN extension. It affects the behavior of the '<template> update ...'
34
34
  # operation in that a stack update may not change the values of parameters marked w/:Immutable => true.
35
35
  :Immutable => true
36
36
 
@@ -94,18 +94,18 @@ template do
94
94
  mapping 'TableExampleMultimap',
95
95
  vpc.get_multimap({ :visibility => 'private', :zone => ['a', 'c'] }, :env, :region, :subnet)
96
96
 
97
- # The tag type is a Ruby CFN extension. These tags are excised from the template and used to generate a series of --tag arguments
98
- # which are passed to cfn-cmd. They do not ultimately appear in the expanded CloudFormation template. The diff subcommand will
99
- # compare tags with the running stack and identify any changes, but cfn-update-stack will do the diff and throw an error on any
97
+ # The tag type is a DSL extension; it is not a property of actual CloudFormation templates.
98
+ # 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.
99
+ # They do not ultimately appear in the expanded CloudFormation template.
100
+ # 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
100
101
  # changes. The tags are propagated to all resources created by the stack, including the stack itself.
101
102
  #
102
103
  # Amazon has set the following restrictions on CloudFormation tags:
103
104
  # => limit 10
104
- # => immutable (you may not cfn-update-stack with new tags or different values for existing tags -- they will be rejected)
105
+ # => immutable (you may not update a stack with new tags or different values for existing tags -- they will be rejected)
105
106
  #
106
- # Additionally, cfn-cmd throws an error if your tag value contains spaces. This limitation will be lifted when we move from cfn-cmd
107
- # to the new unified CLI.
108
107
  tag :MyTag => 'MyValue'
108
+ tag :MyOtherTag => 'My Value With Spaces'
109
109
 
110
110
  resource 'SecurityGroup', :Type => 'AWS::EC2::SecurityGroup', :Properties => {
111
111
  :GroupDescription => 'Lets any vpc traffic in.',
@@ -173,7 +173,7 @@ template do
173
173
 
174
174
  resource 'InstanceProfile', :Type => 'AWS::IAM::InstanceProfile', :Properties => {
175
175
  # use cfn intrinsic conditional to choose the 2nd value because the expression evaluates to false
176
- :Path => fn_if(equals(3, 0), '/unselected/', '/'),
176
+ :Path => fn_if(equal(3, 0), '/unselected/', '/'),
177
177
  :Roles => [ ref('InstanceRole') ],
178
178
  }
179
179
 
@@ -191,7 +191,7 @@ template do
191
191
  }
192
192
 
193
193
  # add conditions that can be used elsewhere in the template
194
- condition 'myCondition', fn_and(equals("one", "two"), not_equals("three", "four"))
194
+ condition 'myCondition', fn_and(equal("one", "two"), not_equal("three", "four"))
195
195
 
196
196
  output 'EmailSNSTopicARN',
197
197
  :Value => ref('EmailSNSTopic'),
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require 'cloudformation-ruby-dsl/dsl'
16
+
15
17
  unless RUBY_VERSION >= '1.9'
16
18
  # This script uses Ruby 1.9 functions such as Enumerable.slice_before and Enumerable.chunk
17
19
  $stderr.puts "This script requires ruby 1.9+. On OS/X use Homebrew to install ruby 1.9:"
@@ -23,25 +25,41 @@ require 'rubygems'
23
25
  require 'json'
24
26
  require 'yaml'
25
27
  require 'erb'
26
- require 'xmlsimple'
27
-
28
- VENDOR_PATH = File.expand_path("../../../vendor/AWSCloudFormation-1.0.12", __FILE__)
29
- SYSTEM_ENV = "export PATH=#{VENDOR_PATH}/bin:$PATH; export AWS_CLOUDFORMATION_HOME=#{VENDOR_PATH}"
28
+ require 'aws-sdk'
29
+ require 'diffy'
30
+
31
+ ############################# AWS SDK Support
32
+
33
+ class AwsCfn
34
+ attr_accessor :cfn_client_instance
35
+
36
+ def cfn_client
37
+ if @cfn_client_instance == nil
38
+ # region and credentials are loaded from the environment; see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html
39
+ @cfn_client_instance = Aws::CloudFormation::Client.new(
40
+ # we don't validate parameters because the aws-ruby-sdk gets a number parameter and expects it to be a string and fails the validation
41
+ # see: https://github.com/aws/aws-sdk-ruby/issues/848
42
+ validate_params: false,
43
+ )
44
+ end
45
+ @cfn_client_instance
46
+ end
47
+ end
30
48
 
31
- ############################# Command-line and "cfn-cmd" Support
49
+ ############################# Command-line support
32
50
 
33
- # Parse command-line arguments based on cfn-cmd syntax (cfn-create-stack etc.) and return the parameters and region
34
- def cfn_parse_args
51
+ # Parse command-line arguments and return the parameters and region
52
+ def parse_args
35
53
  stack_name = nil
36
54
  parameters = {}
37
- region = ENV['EC2_REGION'] || ENV['AWS_DEFAULT_REGION'] || 'us-east-1'
38
- nopretty = false
55
+ region = default_region
56
+ nopretty = false
39
57
  ARGV.slice_before(/^--/).each do |name, value|
40
58
  case name
41
59
  when '--stack-name'
42
60
  stack_name = value
43
61
  when '--parameters'
44
- parameters = Hash[value.split(/;/).map { |pair| pair.split(/=/, 2) }]
62
+ parameters = Hash[value.split(/;/).map { |pair| pair.split(/=/, 2) }] #/# fix for syntax highlighting
45
63
  when '--region'
46
64
  region = value
47
65
  when '--nopretty'
@@ -51,14 +69,13 @@ def cfn_parse_args
51
69
  [stack_name, parameters, region, nopretty]
52
70
  end
53
71
 
54
- def cfn_cmd(template)
72
+ def cfn(template)
73
+ aws_cfn = AwsCfn.new
74
+ cfn_client = aws_cfn.cfn_client
75
+
55
76
  action = ARGV[0]
56
- unless %w(expand diff cfn-validate-template cfn-create-stack cfn-update-stack).include? action
57
- $stderr.puts "usage: #{$PROGRAM_NAME} <expand|diff|cfn-validate-template|cfn-create-stack|cfn-update-stack>"
58
- exit(2)
59
- end
60
- unless (ARGV & %w(--template-file --template-url)).empty?
61
- $stderr.puts "#{File.basename($PROGRAM_NAME)}: The --template-file and --template-url command-line options are not allowed."
77
+ unless %w(expand diff validate create update).include? action
78
+ $stderr.puts "usage: #{$PROGRAM_NAME} <expand|diff|validate|create|update>"
62
79
  exit(2)
63
80
  end
64
81
 
@@ -66,27 +83,27 @@ def cfn_cmd(template)
66
83
  # cfn template since we can't pass it to CloudFormation.
67
84
  immutable_parameters = template.excise_parameter_attribute!(:Immutable)
68
85
 
69
- # Tag CloudFormation stacks based on :Tags defined in the template
86
+ # Tag CloudFormation stacks based on :Tags defined in the template.
87
+ # Remove them from the template as well, so that the template is valid.
70
88
  cfn_tags = template.excise_tags!
71
89
 
72
- # Can't currently support spaces because the system() call escapes them and that fouls up the CLI
73
- unless cfn_tags.select { |i| i =~ /\s+/ }.empty?
74
- $stderr.puts "ERROR: Tag names or values cannot currently contain spaces. Please remove spaces and try again."
75
- exit(2)
76
- end
77
-
78
- # The command line string looks like: --tag "Key=key; Value=value" --tag "Key2=key2; Value2=value"
79
- cfn_tags_options = cfn_tags.sort.map { |tag| ["--tag", "Key=%s; Value=%s" % tag.split('=')] }.flatten
80
-
81
- # example: <template.rb> cfn-create-stack my-stack-name --parameters "Env=prod" --region eu-west-1
82
- # Execute the AWS CLI cfn-cmd command to validate/create/update a CloudFormation stack.
83
90
  if action == 'diff' or (action == 'expand' and not template.nopretty)
84
91
  template_string = JSON.pretty_generate(template)
85
92
  else
86
93
  template_string = JSON.generate(template)
87
94
  end
88
95
 
89
- if action == 'expand'
96
+ # Derive stack name from ARGV
97
+ _, options = extract_options(ARGV[1..-1], %w(--nopretty), %w(--stack-name --region --parameters --tag))
98
+ # If the first argument is not an option and stack_name is undefined, assume it's the stack name
99
+ if template.stack_name.nil?
100
+ stack_name = options.shift if options[0] && !(/^-/ =~ options[0])
101
+ else
102
+ stack_name = template.stack_name
103
+ end
104
+
105
+ case action
106
+ when 'expand'
90
107
  # Write the pretty-printed JSON template to stdout and exit. [--nopretty] option writes output with minimal whitespace
91
108
  # example: <template.rb> expand --parameters "Env=prod" --region eu-west-1 --nopretty
92
109
  if template.nopretty
@@ -95,146 +112,189 @@ def cfn_cmd(template)
95
112
  puts template_string
96
113
  end
97
114
  exit(true)
98
- end
99
-
100
- temp_file = File.absolute_path("#{$PROGRAM_NAME}.expanded.json")
101
- File.write(temp_file, template_string)
102
-
103
- cmdline = ['cfn-cmd'] + ARGV + ['--template-file', temp_file] + cfn_tags_options
104
115
 
105
- # Add the required default capability if no capabilities were specified
106
- cmdline = cmdline + ['-c', 'CAPABILITY_IAM'] if not ARGV.include?('--capabilities') or ARGV.include?('-c')
107
-
108
- case action
109
116
  when 'diff'
110
117
  # example: <template.rb> diff my-stack-name --parameters "Env=prod" --region eu-west-1
111
118
  # Diff the current template for an existing stack with the expansion of this template.
112
119
 
113
- # The --parameters and --tag options were used to expand the template but we don't need them anymore. Discard.
114
- _, cfn_options = extract_options(ARGV[1..-1], %w(), %w(--parameters --tag))
120
+ # 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
121
+ # If you want output of the entire file, simply use this option with a large number, i.e., -U 10000
122
+ # 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
123
+ # because Diffy's "context" configuration is mutually exclusive with the configuration to pass arbitrary options to diff
124
+ if !options.include? '-U'
125
+ options.push('-U', '0')
126
+ end
115
127
 
116
- # Separate the remaining command-line options into options for 'cfn-cmd' and options for 'diff'.
117
- cfn_options, diff_options = extract_options(cfn_options, %w(),
118
- %w(--stack-name --region --parameters --connection-timeout -I --access-key-id -S --secret-key -K --ec2-private-key-file-path -U --url))
128
+ # Ensure a stack name was provided
129
+ if stack_name.empty?
130
+ $stderr.puts "Error: a stack name is required"
131
+ exit(false)
132
+ end
119
133
 
120
- # If the first argument is a stack name then shift it from diff_options over to cfn_options.
121
- if diff_options[0] && !(/^-/ =~ diff_options[0])
122
- cfn_options.unshift(diff_options.shift)
134
+ # describe the existing stack
135
+ begin
136
+ old_template_body = cfn_client.get_template({stack_name: stack_name}).template_body
137
+ rescue Aws::CloudFormation::Errors::ValidationError => e
138
+ $stderr.puts "Error: #{e}"
139
+ exit(false)
123
140
  end
124
141
 
125
- # Run CloudFormation commands to describe the existing stack
126
- cfn_options_string = cfn_options.map { |arg| "'#{arg}'" }.join(' ')
127
- old_template_raw = exec_capture_stdout("cfn-cmd cfn-get-template #{cfn_options_string}")
128
- # ec2 template output is not valid json: TEMPLATE "<json>\n"\n
129
- old_template_object = JSON.parse(old_template_raw[11..-3])
130
- old_template_string = JSON.pretty_generate(old_template_object)
131
- old_stack_attributes = exec_describe_stack(cfn_options_string)
132
- old_tags_string = old_stack_attributes["TAGS"]
133
- old_parameters_string = old_stack_attributes["PARAMETERS"]
142
+ # parse the string into a Hash, then convert back into a string; this is the only way Ruby JSON lets us pretty print a JSON string
143
+ old_template = JSON.pretty_generate(JSON.parse(old_template_body))
144
+ # there is only ever one stack, since stack names are unique
145
+ old_attributes = cfn_client.describe_stacks({stack_name: stack_name}).stacks[0]
146
+ old_tags = old_attributes.tags
147
+ old_parameters = old_attributes.parameters
134
148
 
135
149
  # Sort the tag strings alphabetically to make them easily comparable
136
- old_tags_string = (old_tags_string || '').split(';').sort.map { |tag| %Q(TAG "#{tag}"\n) }.join
150
+ old_tags_string = old_tags.sort.map { |tag| %Q(TAG "#{tag.key}=#{tag.value}"\n) }.join
137
151
  tags_string = cfn_tags.sort.map { |tag| "TAG \"#{tag}\"\n" }.join
138
152
 
139
153
  # Sort the parameter strings alphabetically to make them easily comparable
140
- old_parameters_string = (old_parameters_string || '').split(';').sort.map { |param| %Q(PARAMETER "#{param}"\n) }.join
154
+ old_parameters_string = old_parameters.sort! {|pCurrent, pNext| pCurrent.parameter_key <=> pNext.parameter_key }.map { |param| %Q(PARAMETER "#{param.parameter_key}=#{param.parameter_value}"\n) }.join
141
155
  parameters_string = template.parameters.sort.map { |key, value| "PARAMETER \"#{key}=#{value}\"\n" }.join
142
156
 
143
- # Diff the expanded template with the template from CloudFormation.
144
- old_temp_file = File.absolute_path("#{$PROGRAM_NAME}.current.json")
145
- new_temp_file = File.absolute_path("#{$PROGRAM_NAME}.expanded.json")
146
- File.write(old_temp_file, old_tags_string + old_parameters_string + old_template_string)
147
- File.write(new_temp_file, tags_string + parameters_string + template_string)
157
+ # set default diff options
158
+ Diffy::Diff.default_options.merge!(
159
+ :diff => "#{options.join(' ')}",
160
+ )
161
+ # set default diff output
162
+ Diffy::Diff.default_format = :color
163
+
164
+ tags_diff = Diffy::Diff.new(old_tags_string, tags_string).to_s.strip!
165
+ params_diff = Diffy::Diff.new(old_parameters_string, parameters_string).to_s.strip!
166
+ template_diff = Diffy::Diff.new(old_template, template_string).to_s.strip!
167
+
168
+ if !tags_diff.empty?
169
+ puts "====== Tags ======"
170
+ puts tags_diff
171
+ puts "=================="
172
+ puts
173
+ end
148
174
 
149
- # Compare templates
150
- puts %x( #{SYSTEM_ENV}; #{(["diff"] + diff_options + [old_temp_file, new_temp_file]).join(' ')} )
175
+ if !params_diff.empty?
176
+ puts "====== Parameters ======"
177
+ puts params_diff
178
+ puts "========================"
179
+ puts
180
+ end
151
181
 
152
- File.delete(old_temp_file)
153
- File.delete(new_temp_file)
182
+ if !template_diff.empty?
183
+ puts "====== Template ======"
184
+ puts template_diff
185
+ puts "======================"
186
+ puts
187
+ end
154
188
 
155
189
  exit(true)
156
190
 
157
- when 'cfn-validate-template'
158
- # The cfn-validate-template command doesn't support --parameters so remove it if it was provided for template expansion.
159
- _, cmdline = extract_options(cmdline, %w(), %w(--parameters --tag))
191
+ when 'validate'
192
+ begin
193
+ valid = cfn_client.validate_template({template_body: template_string})
194
+ exit(valid.successful?)
195
+ rescue Aws::CloudFormation::Errors::ValidationError => e
196
+ $stderr.puts "Validation error: #{e}"
197
+ exit(false)
198
+ end
160
199
 
161
- when 'cfn-update-stack'
162
- # Pick out the subset of cfn-update-stack options that apply to cfn-describe-stacks.
163
- cfn_options, other_options = extract_options(ARGV[1..-1], %w(),
164
- %w(--stack-name --region --connection-timeout -I --access-key-id -S --secret-key -K --ec2-private-key-file-path -U --url))
200
+ when 'create'
165
201
 
166
- # If the first argument is a stack name then shift it over to cfn_options.
167
- if other_options[0] && !(/^-/ =~ other_options[0])
168
- cfn_options.unshift(other_options.shift)
202
+ begin
203
+ create_result = cfn_client.create_stack({
204
+ stack_name: stack_name,
205
+ template_body: template_string,
206
+ parameters: template.parameters.map { |k,v| {parameter_key: k, parameter_value: v}}.to_a,
207
+ tags: cfn_tags.map { |k,v| {"key" => k.to_s, "value" => v} }.to_a,
208
+ capabilities: ["CAPABILITY_IAM"],
209
+ })
210
+ if create_result.successful?
211
+ puts create_result.stack_id
212
+ exit(true)
213
+ end
214
+ rescue Aws::CloudFormation::Errors::ServiceError => e
215
+ $stderr.puts "Failed to create stack: #{e}"
216
+ exit(false)
169
217
  end
170
218
 
219
+ when 'update'
220
+
171
221
  # Run CloudFormation command to describe the existing stack
172
- cfn_options_string = cfn_options.map { |arg| "'#{arg}'" }.join(' ')
173
- old_stack_attributes = exec_describe_stack(cfn_options_string)
222
+ old_stack = cfn_client.describe_stacks({stack_name: stack_name}).stacks
223
+
224
+ # this might happen if, for example, stack_name is an empty string and the Cfn client returns ALL stacks
225
+ if old_stack.length > 1
226
+ $stderr.puts "Error: found too many stacks with this name. There should only be one."
227
+ exit(false)
228
+ else
229
+ # grab the first (and only) result
230
+ old_stack = old_stack[0]
231
+ end
174
232
 
175
233
  # If updating a stack and some parameters are marked as immutable, fail if the new parameters don't match the old ones.
176
234
  if not immutable_parameters.empty?
177
- old_parameters_string = old_stack_attributes["PARAMETERS"]
178
- old_parameters = Hash[(old_parameters_string || '').split(';').map { |pair| pair.split('=', 2) }]
235
+ old_parameters = Hash[old_stack.parameters.map { |p| [p.parameter_key, p.parameter_value]}]
179
236
  new_parameters = template.parameters
180
237
 
181
238
  immutable_parameters.sort.each do |param|
182
239
  if old_parameters[param].to_s != new_parameters[param].to_s
183
- $stderr.puts "Error: cfn-update-stack may not update immutable parameter " +
240
+ $stderr.puts "Error: unable to update immutable parameter " +
184
241
  "'#{param}=#{old_parameters[param]}' to '#{param}=#{new_parameters[param]}'."
185
242
  exit(false)
186
243
  end
187
244
  end
188
245
  end
189
246
 
190
- # Tags are immutable in CloudFormation. The cfn-update-stack command doesn't support --tag options, so remove
191
- # the argument (if it exists) and validate against the existing stack to ensure tags haven't changed.
247
+ # Tags are immutable in CloudFormation. Validate against the existing stack to ensure tags haven't changed.
192
248
  # Compare the sorted arrays for an exact match
193
- old_cfn_tags = old_stack_attributes['TAGS'].split(';').sort rescue [] # Use empty Array if .split fails
194
- if cfn_tags.sort != old_cfn_tags
249
+ old_cfn_tags = old_stack.tags.map { |p| [p.key.to_sym, p.value]}
250
+ cfn_tags_ary = cfn_tags.to_a
251
+ if cfn_tags_ary.sort != old_cfn_tags
195
252
  $stderr.puts "CloudFormation stack tags do not match and cannot be updated. You must either use the same tags or create a new stack." +
196
- "\n" + (old_cfn_tags - cfn_tags).map {|tag| "< #{tag}" }.join("\n") +
253
+ "\n" + (old_cfn_tags - cfn_tags_ary).map {|tag| "< #{tag}" }.join("\n") +
197
254
  "\n" + "---" +
198
- "\n" + (cfn_tags - old_cfn_tags).map {|tag| "> #{tag}"}.join("\n")
255
+ "\n" + (cfn_tags_ary - old_cfn_tags).map {|tag| "> #{tag}"}.join("\n")
199
256
  exit(false)
200
257
  end
201
- _, cmdline = extract_options(cmdline, %w(), %w(--tag))
202
- end
203
-
204
- # Execute command cmdline
205
- puts %x( #{SYSTEM_ENV}; #{cmdline.map {|i| "\"#{i}\" "}.join} )
206
- unless $?
207
- $stderr.puts "\nExecution of 'cfn-cmd' failed. To facilitate debugging, the generated JSON template " +
208
- "file was not deleted. You may delete the file manually if it isn't needed: #{temp_file}"
209
- exit(false)
210
- end
211
-
212
- File.delete(temp_file)
213
-
214
- exit(true)
215
- end
216
258
 
217
- def extract_kv_string(hash, prefix='')
218
- key = "#{prefix}Key"
219
- value = "#{prefix}Value"
220
- hash["member"].map {|a| "#{a[key]}=#{a[value]}" }.join(';') rescue ''
221
- end
222
-
223
- def exec_describe_stack cfn_options_string
224
- xml_data = exec_capture_stdout("cfn-cmd cfn-describe-stacks #{cfn_options_string} --show-xml")
225
- xml = XmlSimple.xml_in(xml_data, :ForceArray => false)["DescribeStacksResult"]["Stacks"]["member"]
226
- { "TAGS" => extract_kv_string(xml["Tags"]), "PARAMETERS" => extract_kv_string(xml["Parameters"], "Parameter") }
227
- end
259
+ # update the stack
260
+ begin
261
+ update_result = cfn_client.update_stack({
262
+ stack_name: stack_name,
263
+ template_body: template_string,
264
+ parameters: template.parameters.map { |k,v| {parameter_key: k, parameter_value: v}}.to_a,
265
+ capabilities: ["CAPABILITY_IAM"],
266
+ })
267
+ if update_result.successful?
268
+ puts update_result.stack_id
269
+ exit(true)
270
+ end
271
+ rescue Aws::CloudFormation::Errors::ServiceError => e
272
+ $stderr.puts "Failed to update stack: #{e}"
273
+ exit(false)
274
+ end
228
275
 
229
- def exec_capture_stdout command
230
- stdout = %x( #{SYSTEM_ENV}; #{command} )
231
- unless $?.success?
232
- $stderr.puts stdout unless stdout.empty? # cfn-cmd sometimes writes error messages to stdout
233
- exit(false)
234
276
  end
235
- stdout
236
277
  end
237
278
 
279
+ # extract options and arguments from a command line string
280
+ #
281
+ # Example:
282
+ #
283
+ # desired, unknown = extract_options("arg1 --option withvalue --optionwithoutvalue", %w(--option), %w())
284
+ #
285
+ # puts desired => Array{"arg1", "--option", "withvalue"}
286
+ # puts unknown => Array{}
287
+ #
288
+ # @param args
289
+ # the Array of arguments (split the command line string by whitespace)
290
+ # @param opts_no_val
291
+ # the Array of options with no value, i.e., --force
292
+ # @param opts_1_val
293
+ # the Array of options with exaclty one value, i.e., --retries 3
294
+ # @returns
295
+ # an Array of two Arrays.
296
+ # The first array contains all the options that were extracted (both those with and without values) as a flattened enumerable.
297
+ # The second array contains all the options that were not extracted.
238
298
  def extract_options(args, opts_no_val, opts_1_val)
239
299
  args = args.clone
240
300
  opts = []
@@ -252,230 +312,16 @@ def extract_options(args, opts_no_val, opts_1_val)
252
312
  [opts, rest]
253
313
  end
254
314
 
255
- ############################# Generic DSL
256
-
257
- class JsonObjectDSL
258
- def initialize(&block)
259
- @dict = {}
260
- instance_eval &block
261
- end
262
-
263
- def value(values)
264
- @dict.update(values)
265
- end
266
-
267
- def default(key, value)
268
- @dict[key] ||= value
269
- end
270
-
271
- def to_json(*args)
272
- @dict.to_json(*args)
273
- end
274
-
275
- def print()
276
- puts JSON.pretty_generate(self)
277
- end
278
- end
279
-
280
- ############################# CloudFormation DSL
281
-
282
- # Main entry point
283
- def template(&block)
284
- TemplateDSL.new(&block)
285
- end
286
-
315
+ ##################################### Additional dsl logic
287
316
  # Core interpreter for the DSL
288
317
  class TemplateDSL < JsonObjectDSL
289
- attr_reader :parameters, :aws_region, :nopretty, :stack_name
290
-
291
- def initialize()
292
- @stack_name, @parameters, @aws_region, @nopretty = cfn_parse_args
293
- super
294
- end
295
-
296
318
  def exec!()
297
- cfn_cmd(self)
298
- end
299
-
300
- def parameter(name, options)
301
- default(:Parameters, {})[name] = options
302
- @parameters[name] ||= options[:Default]
303
- end
304
-
305
- # Find parameters where the specified attribute is true then remove the attribute from the cfn template.
306
- def excise_parameter_attribute!(attribute)
307
- marked_parameters = []
308
- @dict.fetch(:Parameters, {}).each do |param, options|
309
- if options.delete(attribute.to_sym) or options.delete(attribute.to_s)
310
- marked_parameters << param
311
- end
312
- end
313
- marked_parameters
314
- end
315
-
316
- def mapping(name, options)
317
- # if options is a string and a valid file then the script will process the external file.
318
- default(:Mappings, {})[name] = \
319
- if options.is_a?(Hash); options
320
- elsif options.is_a?(String); load_from_file(options)['Mappings'][name]
321
- else; raise("Options for mapping #{name} is neither a string or a hash. Error!")
322
- end
323
- end
324
-
325
- def load_from_file(filename)
326
- file = File.open(filename)
327
-
328
- begin
329
- # Figure out what the file extension is and process accordingly.
330
- contents = case File.extname(filename)
331
- when ".rb"; eval(file.read, nil, filename)
332
- when ".json"; JSON.load(file)
333
- when ".yaml"; YAML::load(file)
334
- else; raise("Do not recognize extension of #{filename}.")
335
- end
336
- ensure
337
- file.close
338
- end
339
- contents
340
- end
341
-
342
- def excise_tags!
343
- tags = []
344
- @dict.fetch(:Tags, {}).each do | tag_name, tag_value |
345
- tags << "#{tag_name}=#{tag_value}"
346
- end
347
- @dict.delete(:Tags)
348
- tags
349
- end
350
-
351
- def tag(tag)
352
- tag.each do | name, value |
353
- default(:Tags, {})[name] = value
354
- end
355
- end
356
-
357
- def condition(name, options) default(:Conditions, {})[name] = options end
358
-
359
- def resource(name, options) default(:Resources, {})[name] = options end
360
-
361
- def output(name, options) default(:Outputs, {})[name] = options end
362
-
363
- def find_in_map(map, key, name)
364
- # Eagerly evaluate mappings when all keys are known at template expansion time
365
- if map.is_a?(String) && key.is_a?(String) && name.is_a?(String)
366
- # We don't know whether the map was built with string keys or symbol keys. Try both.
367
- def get(map, key) map[key] || map.fetch(key.to_sym) end
368
- get(get(@dict.fetch(:Mappings).fetch(map), key), name)
369
- else
370
- { :'Fn::FindInMap' => [ map, key, name ] }
371
- end
372
- end
373
- end
374
-
375
- def base64(value) { :'Fn::Base64' => value } end
376
-
377
- def find_in_map(map, key, name) { :'Fn::FindInMap' => [ map, key, name ] } end
378
-
379
- def get_att(resource, attribute) { :'Fn::GetAtt' => [ resource, attribute ] } end
380
-
381
- def get_azs(region = '') { :'Fn::GetAZs' => region } end
382
-
383
- def join(delim, *list)
384
- case list.length
385
- when 0 then ''
386
- when 1 then list[0]
387
- else join_list(delim,list)
388
- end
389
- end
390
-
391
- # Variant of join that matches the native CFN syntax.
392
- def join_list(delim, list) { :'Fn::Join' => [ delim, list ] } end
393
-
394
- def equal(one, two) { :'Fn::Equals' => [one, two] } end
395
-
396
- def fn_not(condition) { :'Fn::Not' => [condition] } end
397
-
398
- def fn_or(*condition_list)
399
- case condition_list.length
400
- when 0..1 then raise "fn_or needs at least 2 items."
401
- when 2..10 then { :'Fn::Or' => condition_list }
402
- else raise "fn_or needs a list of 2-10 items that evaluate to true/false."
403
- end
404
- end
405
-
406
- def fn_and(*condition_list)
407
- case condition_list.length
408
- when 0..1 then raise "fn_and needs at least 2 items."
409
- when 2..10 then { :'Fn::And' => condition_list }
410
- else raise "fn_and needs a list of 2-10 items that evaluate to true/false."
411
- end
412
- end
413
-
414
- def fn_if(cond, if_true, if_false) { :'Fn::If' => [cond, if_true, if_false] } end
415
-
416
- def not_equal(one, two) fn_not(equal(one,two)) end
417
-
418
- def select(index, list) { :'Fn::Select' => [ index, list ] } end
419
-
420
- def ref(name) { :Ref => name } end
421
-
422
- def aws_account_id() ref("AWS::AccountId") end
423
-
424
- def aws_notification_arns() ref("AWS::NotificationARNs") end
425
-
426
- def aws_no_value() ref("AWS::NoValue") end
427
-
428
- def aws_stack_id() ref("AWS::StackId") end
429
-
430
- def aws_stack_name() ref("AWS::StackName") end
431
-
432
- # deprecated, for backward compatibility
433
- def no_value()
434
- warn_deprecated('no_value()', 'aws_no_value()')
435
- aws_no_value()
436
- end
437
-
438
- # Read the specified file and return its value as a string literal
439
- def file(filename) File.read(File.absolute_path(filename, File.dirname($PROGRAM_NAME))) end
440
-
441
- # Interpolates a string like "NAME={{ref('Service')}}" and returns a CloudFormation "Fn::Join"
442
- # operation to collect the results. Anything between {{ and }} is interpreted as a Ruby expression
443
- # and eval'd. This is especially useful with Ruby "here" documents.
444
- # Local variables may also be exposed to the string via the `locals` hash.
445
- def interpolate(string, locals={})
446
- list = []
447
- while string.length > 0
448
- head, match, string = string.partition(/\{\{.*?\}\}/)
449
- list << head if head.length > 0
450
- list << eval(match[2..-3], nil, 'interpolated string') if match.length > 0
451
- end
452
-
453
- # Split out strings in an array by newline, for visibility
454
- list = list.flat_map {|value| value.is_a?(String) ? value.lines.to_a : value }
455
- join('', *list)
456
- end
457
-
458
- def join_interpolate(delim, string)
459
- $stderr.puts "join_interpolate(delim,string) has been deprecated; use interpolate(string) instead"
460
- interpolate(string)
461
- end
462
-
463
- # This class is used by erb templates so they can access the parameters passed
464
- class Namespace
465
- attr_accessor :params
466
- def initialize(hash)
467
- @params = hash
468
- end
469
- def get_binding
470
- binding
319
+ cfn(self)
471
320
  end
472
321
  end
473
322
 
474
- # Combines the provided ERB template with optional parameters
475
- def erb_template(filename, params = {})
476
- ERB.new(file(filename), nil, '-').result(Namespace.new(params).get_binding)
477
- end
478
-
479
- def warn_deprecated(old, new)
480
- $stderr.puts "Warning: '#{old}' has been deprecated. Please update your template to use '#{new}' instead."
323
+ # Main entry point
324
+ def template(&block)
325
+ stack_name, parameters, aws_region, nopretty = parse_args
326
+ raw_template(parameters, stack_name, aws_region, nopretty, &block)
481
327
  end