cloudformation-ruby-dsl 0.5.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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