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