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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/bin/cfntemplate-to-ruby +2 -0
- data/cloudformation-ruby-dsl.gemspec +4 -3
- data/examples/cloudformation-ruby-script.rb +9 -9
- data/lib/cloudformation-ruby-dsl/cfntemplate.rb +187 -341
- data/lib/cloudformation-ruby-dsl/dsl.rb +249 -0
- data/lib/cloudformation-ruby-dsl/version.rb +1 -1
- metadata +34 -79
- data/vendor/AWSCloudFormation-1.0.12/README.TXT +0 -44
- data/vendor/AWSCloudFormation-1.0.12/RELEASENOTES.TXT +0 -6
- data/vendor/AWSCloudFormation-1.0.12/THIRDPARTYLICENSE.TXT +0 -824
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cancel-update-stack +0 -9
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cancel-update-stack.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cmd +0 -15
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-cmd.cmd +0 -42
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-create-stack +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-create-stack.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-delete-stack +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-delete-stack.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-events +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-events.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resource +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resource.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resources +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stack-resources.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stacks +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-describe-stacks.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-get-template +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-get-template.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stack-resources +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stack-resources.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stacks +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-list-stacks.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-update-stack +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-update-stack.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-validate-template +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-validate-template.cmd +0 -19
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-version +0 -7
- data/vendor/AWSCloudFormation-1.0.12/bin/cfn-version.cmd +0 -21
- data/vendor/AWSCloudFormation-1.0.12/bin/service +0 -29
- data/vendor/AWSCloudFormation-1.0.12/bin/service.cmd +0 -74
- data/vendor/AWSCloudFormation-1.0.12/credential-file-path.template +0 -2
- data/vendor/AWSCloudFormation-1.0.12/lib/CliCommando-1.0.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/activation-1.1.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/commons-cli-1.1.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/commons-codec-1.3.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/commons-discovery-0.2.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/commons-httpclient-3.0.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/commons-logging-1.0.4.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/commons-logging-api-1.1.1.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/httpclient-4.2.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/jaxb-api-2.0.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/jaxb-impl-2.0.1.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/jaxws-api-2.0.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/jdom-1.0.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/log4j.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/serializer.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/service.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/stax-api-1.0.1.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/wsdl4j-1.6.1.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/wss4j-1.5.7.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/wstx-asl-3.2.0.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/xalan-j2-2.7.0.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/xfire-all-1.2.6.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/xfire-jsr181-api-1.0-M1.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/lib/xmlsec-1.4.2.jar +0 -0
- data/vendor/AWSCloudFormation-1.0.12/license.txt +0 -96
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8876df582be39f256601ca2194db3405936c941a
|
|
4
|
+
data.tar.gz: 2fe2e643711d1a26327553436d95c45b28632107
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
- `
|
|
43
|
-
- `
|
|
44
|
-
- `
|
|
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
|
|
data/bin/cfntemplate-to-ruby
CHANGED
|
@@ -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
|
|
27
|
-
gem.summary = %q{Ruby DSL library that provides a wrapper around the
|
|
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>
|
|
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
|
|
98
|
-
#
|
|
99
|
-
#
|
|
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
|
|
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(
|
|
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(
|
|
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 '
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
49
|
+
############################# Command-line support
|
|
32
50
|
|
|
33
|
-
# Parse command-line arguments
|
|
34
|
-
def
|
|
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
|
|
38
|
-
nopretty
|
|
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
|
|
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
|
|
57
|
-
$stderr.puts "usage: #{$PROGRAM_NAME} <expand|diff|
|
|
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
|
-
|
|
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
|
-
#
|
|
114
|
-
|
|
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
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
175
|
+
if !params_diff.empty?
|
|
176
|
+
puts "====== Parameters ======"
|
|
177
|
+
puts params_diff
|
|
178
|
+
puts "========================"
|
|
179
|
+
puts
|
|
180
|
+
end
|
|
151
181
|
|
|
152
|
-
|
|
153
|
-
|
|
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 '
|
|
158
|
-
|
|
159
|
-
|
|
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 '
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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 =
|
|
194
|
-
|
|
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 -
|
|
253
|
+
"\n" + (old_cfn_tags - cfn_tags_ary).map {|tag| "< #{tag}" }.join("\n") +
|
|
197
254
|
"\n" + "---" +
|
|
198
|
-
"\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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
475
|
-
def
|
|
476
|
-
|
|
477
|
-
|
|
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
|