cloudformation-ruby-dsl-addedvars 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/Gemfile +4 -0
- data/LICENSE +13 -0
- data/OWNERS +4 -0
- data/README.md +125 -0
- data/Rakefile +1 -0
- data/bin/aws-sdk-patch +3 -0
- data/bin/cfntemplate-to-ruby +345 -0
- data/cloudformation-ruby-dsl-addedvars.gemspec +42 -0
- data/docs/Contributing.md +21 -0
- data/docs/Releasing.md +20 -0
- data/examples/cloudformation-ruby-script.rb +232 -0
- data/examples/maps/domains.txt +4 -0
- data/examples/maps/map.json +9 -0
- data/examples/maps/map.rb +5 -0
- data/examples/maps/map.yaml +5 -0
- data/examples/maps/more_maps/map1.json +8 -0
- data/examples/maps/more_maps/map2.json +8 -0
- data/examples/maps/more_maps/map3.json +8 -0
- data/examples/maps/table.txt +5 -0
- data/examples/maps/vpc.txt +25 -0
- data/examples/userdata.sh +4 -0
- data/initial_contributions.md +5 -0
- data/lib/cloudformation-ruby-dsl-addedvars.rb +1 -0
- data/lib/cloudformation-ruby-dsl-addedvars/cfntemplate.rb +595 -0
- data/lib/cloudformation-ruby-dsl-addedvars/dsl.rb +270 -0
- data/lib/cloudformation-ruby-dsl-addedvars/spotprice.rb +50 -0
- data/lib/cloudformation-ruby-dsl-addedvars/table.rb +123 -0
- data/lib/cloudformation-ruby-dsl-addedvars/version.rb +21 -0
- data/share/aws-sdk-patch.sh +108 -0
- metadata +192 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# Copyright 2013-2014 Bazaarvoice, Inc.
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
lib = File.expand_path('../lib', __FILE__)
|
18
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
19
|
+
require 'cloudformation-ruby-dsl-addedvars/version'
|
20
|
+
|
21
|
+
Gem::Specification.new do |gem|
|
22
|
+
gem.name = "cloudformation-ruby-dsl-addedvars"
|
23
|
+
gem.version = Cfn::Ruby::Dsl::VERSION
|
24
|
+
gem.authors = ["Shawn Smith", "Dave Barcelo", "Morgan Fletcher", "Csongor Gyuricza", "Igor Polishchuk", "Nathaniel Eliot", "Jona Fenocchi", "Tony Cui", 'Troy Ready']
|
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", 'troy@troyready.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
|
+
gem.homepage = "http://github.com/bazaarvoice/cloudformation-ruby-dsl"
|
29
|
+
|
30
|
+
gem.files = `git ls-files`.split($/)
|
31
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
32
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
33
|
+
gem.require_paths = %w{lib bin}
|
34
|
+
|
35
|
+
gem.add_runtime_dependency 'detabulator'
|
36
|
+
gem.add_runtime_dependency 'json'
|
37
|
+
gem.add_runtime_dependency 'bundler'
|
38
|
+
gem.add_runtime_dependency 'aws-sdk'
|
39
|
+
gem.add_runtime_dependency 'diffy'
|
40
|
+
gem.add_runtime_dependency 'highline'
|
41
|
+
gem.add_runtime_dependency 'rake'
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
Thanks for your interest in contributing! Here are some things you should know.
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
To get started:
|
8
|
+
|
9
|
+
- fork this project on github
|
10
|
+
- create a new branch named after the change you want to make; i.e., `git checkout -b mynewfeature`
|
11
|
+
- make your changes and commit them
|
12
|
+
- send a pull request to this project from your fork/branch
|
13
|
+
|
14
|
+
Once you've sent your pull request, one of the project collaborators will review it and provide feedback. Please accept this commentary as constructive! It is intended as such.
|
15
|
+
|
16
|
+
## git
|
17
|
+
|
18
|
+
We're opinionated about git. Don't be surprised if we ask you to update your pull request to meet the following standards.
|
19
|
+
|
20
|
+
- rebase+squash your branch into a single commit. For clean git history, we'd prefer we merged in just 1 commit that contains the entire set of changes.
|
21
|
+
- don't write commit messages longer than 50 characters. See [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/) for some examples of how to achieve this, and why.
|
data/docs/Releasing.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Releasing
|
2
|
+
|
3
|
+
## Performing releases
|
4
|
+
|
5
|
+
0. Merge the desired commits to master. But merge them cleanly! See: [merging](#merging)
|
6
|
+
1. Edit and commit the version file in `lib/cloudformation-ruby-dsl-addedvars/version.rb`. Bump the version based on the [version specification](#versioning-specification)
|
7
|
+
2. `git push` to origin/master
|
8
|
+
3. `rake release`
|
9
|
+
|
10
|
+
## Versioning specification
|
11
|
+
|
12
|
+
For this project, we will follow the methodology proposed by http://semver.org/spec/v2.0.0.html.
|
13
|
+
|
14
|
+
1. Major versions break existing interfaces.
|
15
|
+
2. Minor versions are additive only.
|
16
|
+
3. Patch versions are for backward-compatible bug fixes.
|
17
|
+
|
18
|
+
## Merging
|
19
|
+
|
20
|
+
When you use the shiny green "Merge" button on a pull request, github creates a separate commit for the merge (because of the use of the `--no-ff` option). This is noisy and makes git history confusing. Instead of using the green merge button, merge the branch into master using [git-land](https://github.com/bazaarvoice/git-land#git-land) (or manually follow the steps described in the project).
|
@@ -0,0 +1,232 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright 2013-2014 Bazaarvoice, Inc.
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'bundler/setup'
|
18
|
+
require 'cloudformation-ruby-dsl-addedvars/cfntemplate'
|
19
|
+
require 'cloudformation-ruby-dsl-addedvars/table'
|
20
|
+
|
21
|
+
# Note: this is only intended to demonstrate the cloudformation-ruby-dsl-addedvars. It compiles
|
22
|
+
# and validates correctly, but won't produce a viable CloudFormation stack.
|
23
|
+
|
24
|
+
template do
|
25
|
+
|
26
|
+
parameter 'Label',
|
27
|
+
:Description => 'The label to apply to the servers.',
|
28
|
+
:Type => 'String',
|
29
|
+
:MinLength => '2',
|
30
|
+
:MaxLength => '25',
|
31
|
+
:AllowedPattern => '[_a-zA-Z0-9]*',
|
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> update ...'
|
34
|
+
# operation in that a stack update may not change the values of parameters marked w/:Immutable => true.
|
35
|
+
:Immutable => true
|
36
|
+
|
37
|
+
parameter 'InstanceType',
|
38
|
+
:Description => 'EC2 instance type',
|
39
|
+
:Type => 'String',
|
40
|
+
:Default => 'm2.xlarge',
|
41
|
+
:AllowedValues => %w(t1.micro m1.small m1.medium m1.large m1.xlarge m2.xlarge m2.2xlarge m2.4xlarge c1.medium c1.xlarge),
|
42
|
+
:ConstraintDescription => 'Must be a valid EC2 instance type.'
|
43
|
+
|
44
|
+
parameter 'ImageId',
|
45
|
+
:Description => 'EC2 Image ID',
|
46
|
+
:Type => 'String',
|
47
|
+
:Default => 'ami-255bbc4c',
|
48
|
+
:AllowedPattern => 'ami-[a-f0-9]{8}',
|
49
|
+
:ConstraintDescription => 'Must be ami-XXXXXXXX (where X is a hexadecimal digit)'
|
50
|
+
|
51
|
+
parameter 'KeyPairName',
|
52
|
+
:Description => 'Name of KeyPair to use.',
|
53
|
+
:Type => 'String',
|
54
|
+
:MinLength => '1',
|
55
|
+
:MaxLength => '40',
|
56
|
+
:Default => parameters['Label']
|
57
|
+
|
58
|
+
parameter 'EmailAddress',
|
59
|
+
:Type => 'String',
|
60
|
+
:Description => 'Email address at which to send notification events.'
|
61
|
+
|
62
|
+
mapping 'InlineExampleMap',
|
63
|
+
:team1 => {
|
64
|
+
:name => 'test1',
|
65
|
+
:email => 'test1@example.com',
|
66
|
+
},
|
67
|
+
:team2 => {
|
68
|
+
:name => 'test2',
|
69
|
+
:email => 'test2@example.com',
|
70
|
+
}
|
71
|
+
|
72
|
+
# Generates mappings from external files with various formats.
|
73
|
+
mapping 'JsonExampleMap', 'maps/map.json'
|
74
|
+
|
75
|
+
mapping 'RubyExampleMap', 'maps/map.rb'
|
76
|
+
|
77
|
+
mapping 'YamlExampleMap', 'maps/map.yaml'
|
78
|
+
|
79
|
+
# Loads JSON mappings dynamically from example directory.
|
80
|
+
Dir.entries('maps/more_maps').each_with_index do |path, index|
|
81
|
+
next if path == "." or path == ".."
|
82
|
+
mapping "ExampleMap#{index - 1}", "maps/more_maps/#{path}"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Selects all rows in the table which match the name/value pairs of the predicate object and returns a
|
86
|
+
# set of nested maps, where the key for the map at level n is the key at index n in the specified keys,
|
87
|
+
# except for the last key in the specified keys which is used to determine the value of the leaf-level map.
|
88
|
+
text = Table.load 'maps/table.txt'
|
89
|
+
mapping 'TableExampleMap',
|
90
|
+
text.get_map({ :column0 => 'foo' }, :column1, :column2, :column3)
|
91
|
+
|
92
|
+
# Shows how to create a table useful for looking up subnets that correspond to a particular env/region for eg. vpc placement.
|
93
|
+
vpc = Table.load 'maps/vpc.txt'
|
94
|
+
mapping 'TableExampleMultimap',
|
95
|
+
vpc.get_multimap({ :visibility => 'private', :zone => ['a', 'c'] }, :env, :region, :subnet)
|
96
|
+
|
97
|
+
# Shows how to use a table for iterative processing.
|
98
|
+
domains = Table.load 'maps/domains.txt'
|
99
|
+
domains.get_multihash(:purpose, {:product => 'demo', :alias => 'true'}, :prefix, :target, :alias_hosted_zone_id).each_pair do |key, value|
|
100
|
+
resource key+'Route53RecordSet', :Type => 'AWS::Route53::RecordSet', :Properties => {
|
101
|
+
:Comment => '',
|
102
|
+
:HostedZoneName => 'bazaarvoice.com',
|
103
|
+
:Name => value[:prefix]+'.bazaarvoice.com',
|
104
|
+
:Type => 'A',
|
105
|
+
:AliasTarget => {
|
106
|
+
:DNSName => value[:target],
|
107
|
+
:HostedZoneId => value[:alias_hosted_zone_id]
|
108
|
+
}
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# The tag type is a DSL extension; it is not a property of actual CloudFormation templates.
|
114
|
+
# These tags are excised from the template and used to generate a series of --tag arguments
|
115
|
+
# which are passed to CloudFormation when a stack is created.
|
116
|
+
# They do not ultimately appear in the expanded CloudFormation template.
|
117
|
+
# The diff subcommand will compare tags with the running stack and identify any changes,
|
118
|
+
# but a stack update will do the diff and throw an error on any immutable tags update attempt.
|
119
|
+
# The tags are propagated to all resources created by the stack, including the stack itself.
|
120
|
+
# If a resource has its own tag with the same name as CF's it's not overwritten.
|
121
|
+
#
|
122
|
+
# Amazon has set the following restrictions on CloudFormation tags:
|
123
|
+
# => limit 10
|
124
|
+
# CloudFormation tags declaration examples:
|
125
|
+
|
126
|
+
tag 'My:New:Tag',
|
127
|
+
:Value => 'ImmutableTagValue',
|
128
|
+
:Immutable => true
|
129
|
+
|
130
|
+
tag :MyOtherTag,
|
131
|
+
:Value => 'My Value With Spaces'
|
132
|
+
|
133
|
+
tag(:"tag:name", :Value => 'tag_value', :Immutable => true)
|
134
|
+
|
135
|
+
# Following format is deprecated and not advised. Please declare CloudFormation tags as described above.
|
136
|
+
tag :TagName => 'tag_value' # It's immutable.
|
137
|
+
|
138
|
+
resource 'SecurityGroup', :Type => 'AWS::EC2::SecurityGroup', :Properties => {
|
139
|
+
:GroupDescription => 'Lets any vpc traffic in.',
|
140
|
+
:SecurityGroupIngress => {:IpProtocol => '-1', :FromPort => '0', :ToPort => '65535', :CidrIp => "10.0.0.0/8"}
|
141
|
+
}
|
142
|
+
|
143
|
+
resource "ASG", :Type => 'AWS::AutoScaling::AutoScalingGroup', :Properties => {
|
144
|
+
:AvailabilityZones => 'us-east-1',
|
145
|
+
:HealthCheckType => 'EC2',
|
146
|
+
:LaunchConfigurationName => ref('LaunchConfig'),
|
147
|
+
:MinSize => 1,
|
148
|
+
:MaxSize => 5,
|
149
|
+
:NotificationConfiguration => {
|
150
|
+
:TopicARN => ref('EmailSNSTopic'),
|
151
|
+
:NotificationTypes => %w(autoscaling:EC2_INSTANCE_LAUNCH autoscaling:EC2_INSTANCE_LAUNCH_ERROR autoscaling:EC2_INSTANCE_TERMINATE autoscaling:EC2_INSTANCE_TERMINATE_ERROR),
|
152
|
+
},
|
153
|
+
:Tags => [
|
154
|
+
{
|
155
|
+
:Key => 'Name',
|
156
|
+
# Grabs a value in an external map file.
|
157
|
+
:Value => find_in_map('TableExampleMap', 'corge', 'grault'),
|
158
|
+
:PropagateAtLaunch => 'true',
|
159
|
+
},
|
160
|
+
{
|
161
|
+
:Key => 'Label',
|
162
|
+
:Value => parameters['Label'],
|
163
|
+
:PropagateAtLaunch => 'true',
|
164
|
+
}
|
165
|
+
],
|
166
|
+
}
|
167
|
+
|
168
|
+
resource 'EmailSNSTopic', :Type => 'AWS::SNS::Topic', :Properties => {
|
169
|
+
:Subscription => [
|
170
|
+
{
|
171
|
+
:Endpoint => ref('EmailAddress'),
|
172
|
+
:Protocol => 'email',
|
173
|
+
},
|
174
|
+
],
|
175
|
+
}
|
176
|
+
|
177
|
+
resource 'WaitConditionHandle', :Type => 'AWS::CloudFormation::WaitConditionHandle', :Properties => {}
|
178
|
+
|
179
|
+
resource 'WaitCondition', :Type => 'AWS::CloudFormation::WaitCondition', :DependsOn => 'ASG', :Properties => {
|
180
|
+
:Handle => ref('WaitConditionHandle'),
|
181
|
+
:Timeout => 1200,
|
182
|
+
:Count => "1"
|
183
|
+
}
|
184
|
+
|
185
|
+
resource 'LaunchConfig', :Type => 'AWS::AutoScaling::LaunchConfiguration', :Properties => {
|
186
|
+
:ImageId => parameters['ImageId'],
|
187
|
+
:KeyName => ref('KeyPairName'),
|
188
|
+
:IamInstanceProfile => ref('InstanceProfile'),
|
189
|
+
:InstanceType => ref('InstanceType'),
|
190
|
+
:InstanceMonitoring => 'false',
|
191
|
+
:SecurityGroups => [ref('SecurityGroup')],
|
192
|
+
:BlockDeviceMappings => [
|
193
|
+
{:DeviceName => '/dev/sdb', :VirtualName => 'ephemeral0'},
|
194
|
+
{:DeviceName => '/dev/sdc', :VirtualName => 'ephemeral1'},
|
195
|
+
{:DeviceName => '/dev/sdd', :VirtualName => 'ephemeral2'},
|
196
|
+
{:DeviceName => '/dev/sde', :VirtualName => 'ephemeral3'},
|
197
|
+
],
|
198
|
+
# Loads an external userdata script with an interpolated argument.
|
199
|
+
:UserData => base64(interpolate(file('userdata.sh'), time: Time.now)),
|
200
|
+
}
|
201
|
+
|
202
|
+
resource 'InstanceProfile', :Type => 'AWS::IAM::InstanceProfile', :Properties => {
|
203
|
+
# use cfn intrinsic conditional to choose the 2nd value because the expression evaluates to false
|
204
|
+
:Path => fn_if(equal(3, 0), '/unselected/', '/'),
|
205
|
+
:Roles => [ ref('InstanceRole') ],
|
206
|
+
}
|
207
|
+
|
208
|
+
resource 'InstanceRole', :Type => 'AWS::IAM::Role', :Properties => {
|
209
|
+
:AssumeRolePolicyDocument => {
|
210
|
+
:Statement => [
|
211
|
+
{
|
212
|
+
:Effect => 'Allow',
|
213
|
+
:Principal => { :Service => [ 'ec2.amazonaws.com' ] },
|
214
|
+
:Action => [ 'sts:AssumeRole' ],
|
215
|
+
},
|
216
|
+
],
|
217
|
+
},
|
218
|
+
:Path => '/',
|
219
|
+
}
|
220
|
+
|
221
|
+
# add conditions that can be used elsewhere in the template
|
222
|
+
condition 'myCondition', fn_and(equal("one", "two"), not_equal("three", "four"))
|
223
|
+
|
224
|
+
output 'EmailSNSTopicARN',
|
225
|
+
:Value => ref('EmailSNSTopic'),
|
226
|
+
:Description => 'ARN of SNS Topic used to send emails on events.'
|
227
|
+
|
228
|
+
output 'MappingLookup',
|
229
|
+
:Value => find_in_map('TableExampleMap', 'corge', 'grault'),
|
230
|
+
:Description => 'An example map lookup.'
|
231
|
+
|
232
|
+
end.exec!
|
@@ -0,0 +1,4 @@
|
|
1
|
+
purpose product prefix target alias alias_hosted_zone_id has_origin_domain origin_target
|
2
|
+
CDN demo code dabc123.cloudfront.net true Z2FDTNDATAQYW2 false
|
3
|
+
Assets demo assets ddef456.cloudfront.net true Z2FDTNDATAQYW2 false
|
4
|
+
API demo api prod-api false false prod-api-origin.whatever.net.
|
@@ -0,0 +1,25 @@
|
|
1
|
+
env region zone visibility subnet
|
2
|
+
qa us-east-1 a public subnet-aaaa
|
3
|
+
qa us-east-1 a private subnet-bbbb
|
4
|
+
qa us-east-1 b public subnet-cccc
|
5
|
+
qa us-east-1 b private subnet-dddd
|
6
|
+
qa us-east-1 c public subnet-eeee
|
7
|
+
qa us-east-1 c private subnet-ffff
|
8
|
+
qa eu-west-1 a public subnet-gggg
|
9
|
+
qa eu-west-1 a private subnet-hhhh
|
10
|
+
qa eu-west-1 b public subnet-iiii
|
11
|
+
qa eu-west-1 b private subnet-jjjj
|
12
|
+
qa eu-west-1 c public subnet-kkkk
|
13
|
+
qa eu-west-1 c private subnet-llll
|
14
|
+
prod us-east-1 a public subnet-mmmm
|
15
|
+
prod us-east-1 a private subnet-nnnn
|
16
|
+
prod us-east-1 b public subnet-oooo
|
17
|
+
prod us-east-1 b private subnet-pppp
|
18
|
+
prod us-east-1 c public subnet-qqqq
|
19
|
+
prod us-east-1 c private subnet-rrrr
|
20
|
+
prod eu-west-1 a public subnet-ssss
|
21
|
+
prod eu-west-1 a private subnet-tttt
|
22
|
+
prod eu-west-1 b public subnet-uuuu
|
23
|
+
prod eu-west-1 b private subnet-vvvv
|
24
|
+
prod eu-west-1 c public subnet-wwww
|
25
|
+
prod eu-west-1 c private subnet-xxxx
|
@@ -0,0 +1,5 @@
|
|
1
|
+
Several people were instrumental in building this project during its incubation stage. Because the process of open sourcing involved squashing the repository to remove confidential information, some of their contributions have been lost from the history that Github displays.
|
2
|
+
|
3
|
+
- [Shawn Smith](https://github.com/shawnsmith): initial implementation
|
4
|
+
- [Nathaniel Eliot](https://github.com/temujin9): converted from raw library to gem, refactored table code, cleaned and prepared code for open sourcing
|
5
|
+
- [Jona Fenocchi](http://github.com/jonaf): added support for CloudFormation tags
|
@@ -0,0 +1 @@
|
|
1
|
+
require "cloudformation-ruby-dsl-addedvars/version"
|
@@ -0,0 +1,595 @@
|
|
1
|
+
# Copyright 2013-2014 Bazaarvoice, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'cloudformation-ruby-dsl-addedvars/dsl'
|
16
|
+
|
17
|
+
unless RUBY_VERSION >= '1.9'
|
18
|
+
# This script uses Ruby 1.9 functions such as Enumerable.slice_before and Enumerable.chunk
|
19
|
+
$stderr.puts "This script requires ruby 1.9+. On OS/X use Homebrew to install ruby 1.9:"
|
20
|
+
$stderr.puts " brew install ruby"
|
21
|
+
exit(2)
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require 'json'
|
26
|
+
require 'yaml'
|
27
|
+
require 'erb'
|
28
|
+
require 'aws-sdk'
|
29
|
+
require 'diffy'
|
30
|
+
require 'highline/import'
|
31
|
+
|
32
|
+
############################# AWS SDK Support
|
33
|
+
|
34
|
+
class AwsCfn
|
35
|
+
attr_accessor :cfn_client_instance
|
36
|
+
|
37
|
+
def initialize(args)
|
38
|
+
Aws.config[:region] = args[:region] if args.key?(:region)
|
39
|
+
Aws.config[:credentials] = Aws::SharedCredentials.new(profile_name: args[:aws_profile]) unless args[:aws_profile].nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
def cfn_client
|
43
|
+
if @cfn_client_instance == nil
|
44
|
+
# credentials are loaded from the environment; see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html
|
45
|
+
@cfn_client_instance = Aws::CloudFormation::Client.new(
|
46
|
+
# 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
|
47
|
+
# see: https://github.com/aws/aws-sdk-ruby/issues/848
|
48
|
+
validate_params: false
|
49
|
+
)
|
50
|
+
end
|
51
|
+
@cfn_client_instance
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# utility class to deserialize Structs as JSON
|
56
|
+
# borrowed from http://ruhe.tumblr.com/post/565540643/generate-json-from-ruby-struct
|
57
|
+
class Struct
|
58
|
+
def to_map
|
59
|
+
map = Hash.new
|
60
|
+
self.members.each { |m| map[m] = self[m] }
|
61
|
+
map
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_json(*a)
|
65
|
+
to_map.to_json(*a)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
############################# Command-line support
|
70
|
+
|
71
|
+
# Parse command-line arguments and return the parameters and region
|
72
|
+
def parse_args
|
73
|
+
stack_name = nil
|
74
|
+
parameters = {}
|
75
|
+
region = default_region
|
76
|
+
profile = nil
|
77
|
+
nopretty = false
|
78
|
+
ARGV.slice_before(/^--/).each do |name, value|
|
79
|
+
case name
|
80
|
+
when '--stack-name'
|
81
|
+
stack_name = value
|
82
|
+
when '--parameters'
|
83
|
+
parameters = Hash[value.split(/;/).map { |pair| pair.split(/=/, 2) }] #/# fix for syntax highlighting
|
84
|
+
parameters = $stack_params.merge(parameters) if $stack_params
|
85
|
+
when '--region'
|
86
|
+
region = value
|
87
|
+
# Because of the use of global variables here and below, here we
|
88
|
+
# must ensure the command-line set version isn't overridden
|
89
|
+
$aws_region = nil
|
90
|
+
when '--profile'
|
91
|
+
profile = value
|
92
|
+
when '--nopretty'
|
93
|
+
nopretty = true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
# Allow these same settings to be set without using the command-line flags
|
97
|
+
stack_name ||= $stack_name
|
98
|
+
parameters = $stack_params if parameters == {} && $stack_params
|
99
|
+
profile ||= $aws_profile
|
100
|
+
region = $aws_region unless $aws_region == nil
|
101
|
+
[stack_name, parameters, region, profile, nopretty]
|
102
|
+
end
|
103
|
+
|
104
|
+
def validate_action(action)
|
105
|
+
valid = %w[
|
106
|
+
expand
|
107
|
+
diff
|
108
|
+
validate
|
109
|
+
create
|
110
|
+
update
|
111
|
+
cancel-update
|
112
|
+
delete
|
113
|
+
describe
|
114
|
+
describe-resource
|
115
|
+
get-template
|
116
|
+
]
|
117
|
+
removed = %w[
|
118
|
+
cfn-list-stack-resources
|
119
|
+
cfn-list-stacks
|
120
|
+
]
|
121
|
+
deprecated = {
|
122
|
+
"cfn-validate-template" => "validate",
|
123
|
+
"cfn-create-stack" => "create",
|
124
|
+
"cfn-update-stack" => "update",
|
125
|
+
"cfn-cancel-update-stack" => "cancel-update",
|
126
|
+
"cfn-delete-stack" => "delete",
|
127
|
+
"cfn-describe-stack-events" => "describe",
|
128
|
+
"cfn-describe-stack-resources" => "describe",
|
129
|
+
"cfn-describe-stack-resource" => "describe-resource",
|
130
|
+
"cfn-get-template" => "get-template"
|
131
|
+
}
|
132
|
+
if deprecated.keys.include? action
|
133
|
+
replacement = deprecated[action]
|
134
|
+
$stderr.puts "WARNING: '#{action}' is deprecated and will be removed in a future version. Please use '#{replacement}' instead."
|
135
|
+
action = replacement
|
136
|
+
end
|
137
|
+
unless valid.include? action
|
138
|
+
if removed.include? action
|
139
|
+
$stderr.puts "ERROR: native command #{action} is no longer supported by cloudformation-ruby-dsl-addedvars."
|
140
|
+
end
|
141
|
+
$stderr.puts "usage: #{$PROGRAM_NAME} <#{valid.join('|')}>"
|
142
|
+
exit(2)
|
143
|
+
end
|
144
|
+
action
|
145
|
+
end
|
146
|
+
|
147
|
+
def cfn(template)
|
148
|
+
aws_cfn = AwsCfn.new({:region => template.aws_region, :aws_profile => template.aws_profile})
|
149
|
+
cfn_client = aws_cfn.cfn_client
|
150
|
+
|
151
|
+
# Validate the stack action, whether setup via command line or global options
|
152
|
+
action = validate_action( ARGV[0].class == String ? ARGV[0] : $cf_action )
|
153
|
+
|
154
|
+
# Find parameters where extension attribute :Immutable is true then remove it from the
|
155
|
+
# cfn template since we can't pass it to CloudFormation.
|
156
|
+
immutable_parameters = template.excise_parameter_attribute!(:Immutable)
|
157
|
+
|
158
|
+
# Find parameters where extension attribute :UsePreviousValue is true then
|
159
|
+
# remove it from the cfn template since it's sent as a special option.
|
160
|
+
use_prev_val_params = template.excise_parameter_attribute!(:UsePreviousValue)
|
161
|
+
|
162
|
+
# Tag CloudFormation stacks based on :Tags defined in the template.
|
163
|
+
# Remove them from the template as well, so that the template is valid.
|
164
|
+
cfn_tags = template.excise_tags!
|
165
|
+
|
166
|
+
# Find tags where extension attribute `:Immutable` is true then remove it from the
|
167
|
+
# tag's properties hash since it can't be passed to CloudFormation.
|
168
|
+
immutable_tags = template.get_tag_attribute(cfn_tags, :Immutable)
|
169
|
+
|
170
|
+
cfn_tags.each {|k, v| cfn_tags[k] = v[:Value].to_s}
|
171
|
+
|
172
|
+
if action == 'diff' or (action == 'expand' and not template.nopretty)
|
173
|
+
template_string = JSON.pretty_generate(template)
|
174
|
+
else
|
175
|
+
template_string = JSON.generate(template)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Derive stack name from ARGV
|
179
|
+
_, options = extract_options(ARGV[1..-1], %w(--nopretty), %w(--profile --stack-name --region --parameters --tag))
|
180
|
+
# If the first argument is not an option and stack_name is undefined, assume it's the stack name
|
181
|
+
# The second argument, if present, is the resource name used by the describe-resource command
|
182
|
+
if template.stack_name.nil?
|
183
|
+
stack_name = options.shift if options[0] && !(/^-/ =~ options[0])
|
184
|
+
resource_name = options.shift if options[0] && !(/^-/ =~ options[0])
|
185
|
+
else
|
186
|
+
stack_name = template.stack_name
|
187
|
+
end
|
188
|
+
|
189
|
+
case action
|
190
|
+
when 'expand'
|
191
|
+
# Write the pretty-printed JSON template to stdout and exit. [--nopretty] option writes output with minimal whitespace
|
192
|
+
# example: <template.rb> expand --parameters "Env=prod" --region eu-west-1 --nopretty
|
193
|
+
if template.nopretty
|
194
|
+
puts template_string
|
195
|
+
else
|
196
|
+
puts template_string
|
197
|
+
end
|
198
|
+
exit(true)
|
199
|
+
|
200
|
+
when 'diff'
|
201
|
+
# example: <template.rb> diff my-stack-name --parameters "Env=prod" --region eu-west-1
|
202
|
+
# Diff the current template for an existing stack with the expansion of this template.
|
203
|
+
|
204
|
+
# `diff` operation exit codes are:
|
205
|
+
# 0 - no differences are found. Outputs nothing to make it easy to use the output of the diff call from within other scripts.
|
206
|
+
# 1 - produced by any ValidationError exception (e.g. "Stack with id does not exist")
|
207
|
+
# 2 - there are changes to update (tags, params, template)
|
208
|
+
# If you want output of the entire file, simply use this option with a large number, i.e., -U 10000
|
209
|
+
# 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
|
210
|
+
# because Diffy's "context" configuration is mutually exclusive with the configuration to pass arbitrary options to diff
|
211
|
+
if !options.include? '-U'
|
212
|
+
options.push('-U', '0')
|
213
|
+
end
|
214
|
+
|
215
|
+
# Ensure a stack name was provided
|
216
|
+
if stack_name.empty?
|
217
|
+
$stderr.puts "Error: a stack name is required"
|
218
|
+
exit(false)
|
219
|
+
end
|
220
|
+
|
221
|
+
# describe the existing stack
|
222
|
+
begin
|
223
|
+
old_template_body = cfn_client.get_template({stack_name: stack_name}).template_body
|
224
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
225
|
+
$stderr.puts "Error: #{e}"
|
226
|
+
exit(false)
|
227
|
+
end
|
228
|
+
|
229
|
+
# 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
|
230
|
+
old_template = JSON.pretty_generate(JSON.parse(old_template_body))
|
231
|
+
# there is only ever one stack, since stack names are unique
|
232
|
+
old_attributes = cfn_client.describe_stacks({stack_name: stack_name}).stacks[0]
|
233
|
+
old_tags = old_attributes.tags
|
234
|
+
old_parameters = old_attributes.parameters
|
235
|
+
|
236
|
+
# Sort the tag strings alphabetically to make them easily comparable
|
237
|
+
old_tags_string = old_tags.map { |tag| %Q(TAG "#{tag.key}=#{tag.value}"\n) }.sort.join
|
238
|
+
tags_string = cfn_tags.map { |k, v| %Q(TAG "#{k.to_s}=#{v}"\n) }.sort.join
|
239
|
+
|
240
|
+
# For any parameters to to UsePreviousValue, ensure here that they don't
|
241
|
+
# show up on the diff report by setting their template value to match the
|
242
|
+
# current stack value
|
243
|
+
template_diff_params = template.parameters
|
244
|
+
use_prev_val_params.each do |p|
|
245
|
+
template_diff_params[p] = (old_parameters.find {|i| i.parameter_key == p}).parameter_value
|
246
|
+
end if !use_prev_val_params.nil?
|
247
|
+
|
248
|
+
# Sort the parameter strings alphabetically to make them easily comparable
|
249
|
+
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
|
250
|
+
parameters_string = template_diff_params.sort.map { |key, value| "PARAMETER \"#{key}=#{value}\"\n" }.join
|
251
|
+
|
252
|
+
# set default diff options
|
253
|
+
Diffy::Diff.default_options.merge!(
|
254
|
+
:diff => "#{options.join(' ')}",
|
255
|
+
)
|
256
|
+
# set default diff output
|
257
|
+
Diffy::Diff.default_format = :color
|
258
|
+
|
259
|
+
tags_diff = Diffy::Diff.new(old_tags_string, tags_string).to_s.strip!
|
260
|
+
params_diff = Diffy::Diff.new(old_parameters_string, parameters_string).to_s.strip!
|
261
|
+
template_diff = Diffy::Diff.new(old_template, template_string).to_s.strip!
|
262
|
+
|
263
|
+
if !tags_diff.empty?
|
264
|
+
puts "====== Tags ======"
|
265
|
+
puts tags_diff
|
266
|
+
puts "=================="
|
267
|
+
puts
|
268
|
+
end
|
269
|
+
|
270
|
+
if !params_diff.empty?
|
271
|
+
puts "====== Parameters ======"
|
272
|
+
puts params_diff
|
273
|
+
puts "========================"
|
274
|
+
puts
|
275
|
+
end
|
276
|
+
|
277
|
+
if !template_diff.empty?
|
278
|
+
puts "====== Template ======"
|
279
|
+
puts template_diff
|
280
|
+
puts "======================"
|
281
|
+
puts
|
282
|
+
end
|
283
|
+
|
284
|
+
if tags_diff.empty? && params_diff.empty? && template_diff.empty?
|
285
|
+
exit(true)
|
286
|
+
else
|
287
|
+
exit(2)
|
288
|
+
end
|
289
|
+
|
290
|
+
when 'validate'
|
291
|
+
begin
|
292
|
+
valid = cfn_client.validate_template({template_body: template_string})
|
293
|
+
if valid.successful?
|
294
|
+
puts "Validation successful"
|
295
|
+
exit(true)
|
296
|
+
end
|
297
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
298
|
+
$stderr.puts "Validation error: #{e}"
|
299
|
+
exit(false)
|
300
|
+
end
|
301
|
+
|
302
|
+
when 'create'
|
303
|
+
begin
|
304
|
+
|
305
|
+
# default options (not overridable)
|
306
|
+
create_stack_opts = {
|
307
|
+
stack_name: stack_name,
|
308
|
+
template_body: template_string,
|
309
|
+
parameters: template.parameters.map { |k,v| {parameter_key: k, parameter_value: v}}.to_a,
|
310
|
+
tags: cfn_tags.map { |k,v| {"key" => k.to_s, "value" => v} }.to_a,
|
311
|
+
capabilities: ["CAPABILITY_IAM"],
|
312
|
+
}
|
313
|
+
|
314
|
+
# fill in options from the command line
|
315
|
+
extra_options = parse_arg_array_as_hash(options)
|
316
|
+
create_stack_opts = extra_options.merge(create_stack_opts)
|
317
|
+
|
318
|
+
# create stack
|
319
|
+
create_result = cfn_client.create_stack(create_stack_opts)
|
320
|
+
if create_result.successful?
|
321
|
+
puts create_result.stack_id
|
322
|
+
exit(true)
|
323
|
+
end
|
324
|
+
rescue Aws::CloudFormation::Errors::ServiceError => e
|
325
|
+
$stderr.puts "Failed to create stack: #{e}"
|
326
|
+
exit(false)
|
327
|
+
end
|
328
|
+
|
329
|
+
when 'cancel-update'
|
330
|
+
begin
|
331
|
+
cancel_update_result = cfn_client.cancel_update_stack({stack_name: stack_name})
|
332
|
+
if cancel_update_result.successful?
|
333
|
+
$stderr.puts "Canceled updating stack #{stack_name}."
|
334
|
+
exit(true)
|
335
|
+
end
|
336
|
+
rescue Aws::CloudFormation::Errors::ServiceError => e
|
337
|
+
$stderr.puts "Failed to cancel updating stack: #{e}"
|
338
|
+
exit(false)
|
339
|
+
end
|
340
|
+
|
341
|
+
when 'delete'
|
342
|
+
begin
|
343
|
+
if HighLine.agree("Really delete #{stack_name} in #{cfn_client.config.region}? [Y/n]")
|
344
|
+
delete_result = cfn_client.delete_stack({stack_name: stack_name})
|
345
|
+
if delete_result.successful?
|
346
|
+
$stderr.puts "Deleted stack #{stack_name}."
|
347
|
+
exit(true)
|
348
|
+
end
|
349
|
+
else
|
350
|
+
$stderr.puts "Canceled deleting stack #{stack_name}."
|
351
|
+
exit(true)
|
352
|
+
end
|
353
|
+
rescue Aws::CloudFormation::Errors::ServiceError => e
|
354
|
+
$stderr.puts "Failed to delete stack: #{e}"
|
355
|
+
exit(false)
|
356
|
+
end
|
357
|
+
|
358
|
+
when 'describe'
|
359
|
+
begin
|
360
|
+
describe_stack = cfn_client.describe_stacks({stack_name: stack_name})
|
361
|
+
describe_stack_resources = cfn_client.describe_stack_resources({stack_name: stack_name})
|
362
|
+
if describe_stack.successful? and describe_stack_resources.successful?
|
363
|
+
stacks = {}
|
364
|
+
stack_resources = {}
|
365
|
+
describe_stack_resources.stack_resources.each { |stack_resource|
|
366
|
+
if stack_resources[stack_resource.stack_name].nil?
|
367
|
+
stack_resources[stack_resource.stack_name] = []
|
368
|
+
end
|
369
|
+
stack_resources[stack_resource.stack_name].push({
|
370
|
+
logical_resource_id: stack_resource.logical_resource_id,
|
371
|
+
physical_resource_id: stack_resource.physical_resource_id,
|
372
|
+
resource_type: stack_resource.resource_type,
|
373
|
+
timestamp: stack_resource.timestamp,
|
374
|
+
resource_status: stack_resource.resource_status,
|
375
|
+
resource_status_reason: stack_resource.resource_status_reason,
|
376
|
+
description: stack_resource.description,
|
377
|
+
})
|
378
|
+
}
|
379
|
+
describe_stack.stacks.each { |stack| stacks[stack.stack_name] = stack.to_map.merge!({resources: stack_resources[stack.stack_name]}) }
|
380
|
+
unless template.nopretty
|
381
|
+
puts JSON.pretty_generate(stacks)
|
382
|
+
else
|
383
|
+
puts JSON.generate(stacks)
|
384
|
+
end
|
385
|
+
exit(true)
|
386
|
+
end
|
387
|
+
rescue Aws::CloudFormation::Errors::ServiceError => e
|
388
|
+
$stderr.puts "Failed describe stack #{stack_name}: #{e}"
|
389
|
+
exit(false)
|
390
|
+
end
|
391
|
+
|
392
|
+
when 'describe-resource'
|
393
|
+
begin
|
394
|
+
describe_stack_resource = cfn_client.describe_stack_resource({
|
395
|
+
stack_name: stack_name,
|
396
|
+
logical_resource_id: resource_name,
|
397
|
+
})
|
398
|
+
if describe_stack_resource.successful?
|
399
|
+
unless template.nopretty
|
400
|
+
puts JSON.pretty_generate(describe_stack_resource.stack_resource_detail)
|
401
|
+
else
|
402
|
+
puts JSON.generate(describe_stack_resource.stack_resource_detail)
|
403
|
+
end
|
404
|
+
exit(true)
|
405
|
+
end
|
406
|
+
rescue Aws::CloudFormation::Errors::ServiceError => e
|
407
|
+
$stderr.puts "Failed get stack resource details: #{e}"
|
408
|
+
exit(false)
|
409
|
+
end
|
410
|
+
|
411
|
+
when 'get-template'
|
412
|
+
begin
|
413
|
+
get_template_result = cfn_client.get_template({stack_name: stack_name})
|
414
|
+
template_body = JSON.parse(get_template_result.template_body)
|
415
|
+
if get_template_result.successful?
|
416
|
+
unless template.nopretty
|
417
|
+
puts JSON.pretty_generate(template_body)
|
418
|
+
else
|
419
|
+
puts JSON.generate(template_body)
|
420
|
+
end
|
421
|
+
exit(true)
|
422
|
+
end
|
423
|
+
rescue Aws::CloudFormation::Errors::ServiceError => e
|
424
|
+
$stderr.puts "Failed get stack template: #{e}"
|
425
|
+
exit(false)
|
426
|
+
end
|
427
|
+
|
428
|
+
when 'update'
|
429
|
+
|
430
|
+
# Run CloudFormation command to describe the existing stack
|
431
|
+
old_stack = cfn_client.describe_stacks({stack_name: stack_name}).stacks
|
432
|
+
|
433
|
+
# this might happen if, for example, stack_name is an empty string and the Cfn client returns ALL stacks
|
434
|
+
if old_stack.length > 1
|
435
|
+
$stderr.puts "Error: found too many stacks with this name. There should only be one."
|
436
|
+
exit(false)
|
437
|
+
else
|
438
|
+
# grab the first (and only) result
|
439
|
+
old_stack = old_stack[0]
|
440
|
+
end
|
441
|
+
|
442
|
+
# If updating a stack and some parameters or tags are marked as immutable, set the variable to true.
|
443
|
+
immutables_exist = nil
|
444
|
+
|
445
|
+
if not immutable_parameters.empty?
|
446
|
+
old_parameters = Hash[old_stack.parameters.map { |p| [p.parameter_key, p.parameter_value]}]
|
447
|
+
new_parameters = template.parameters
|
448
|
+
immutable_parameters.sort.each do |param|
|
449
|
+
if old_parameters[param].to_s != new_parameters[param].to_s && old_parameters.key?(param)
|
450
|
+
$stderr.puts "Error: unable to update immutable parameter " +
|
451
|
+
"'#{param}=#{old_parameters[param]}' to '#{param}=#{new_parameters[param]}'."
|
452
|
+
immutables_exist = true
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
if not immutable_tags.empty?
|
458
|
+
old_cfn_tags = Hash[old_stack.tags.map { |t| [t.key, t.value]}]
|
459
|
+
cfn_tags_ary = Hash[cfn_tags.map { |k,v| [k, v]}]
|
460
|
+
immutable_tags.sort.each do |tag|
|
461
|
+
if old_cfn_tags[tag].to_s != cfn_tags_ary[tag].to_s && old_cfn_tags.key?(tag)
|
462
|
+
$stderr.puts "Error: unable to update immutable tag " +
|
463
|
+
"'#{tag}=#{old_cfn_tags[tag]}' to '#{tag}=#{cfn_tags_ary[tag]}'."
|
464
|
+
immutables_exist = true
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# Fail if some parameters or tags were marked as immutable.
|
470
|
+
if immutables_exist
|
471
|
+
exit(false)
|
472
|
+
end
|
473
|
+
|
474
|
+
# Compare the sorted arrays of parameters for an exact match and print difference.
|
475
|
+
old_parameters = old_stack.parameters.map { |p| [p.parameter_key, p.parameter_value]}.sort
|
476
|
+
new_parameters = template.parameters.sort
|
477
|
+
if new_parameters != old_parameters
|
478
|
+
puts "\nCloudFormation stack parameters that do not match and will be updated:" +
|
479
|
+
"\n" + (old_parameters - new_parameters).map {|param| "< #{param}" }.join("\n") +
|
480
|
+
"\n" + "---" +
|
481
|
+
"\n" + (new_parameters - old_parameters).map {|param| "> #{param}"}.join("\n")
|
482
|
+
end
|
483
|
+
|
484
|
+
# Compare the sorted arrays of tags for an exact match and print difference.
|
485
|
+
old_cfn_tags = old_stack.tags.map { |t| [t.key, t.value]}.sort
|
486
|
+
cfn_tags_ary = cfn_tags.map { |k,v| [k, v]}.sort
|
487
|
+
if cfn_tags_ary != old_cfn_tags
|
488
|
+
puts "\nCloudFormation stack tags that do not match and will be updated:" +
|
489
|
+
"\n" + (old_cfn_tags - cfn_tags_ary).map {|tag| "< #{tag}" }.join("\n") +
|
490
|
+
"\n" + "---" +
|
491
|
+
"\n" + (cfn_tags_ary - old_cfn_tags).map {|tag| "> #{tag}"}.join("\n")
|
492
|
+
end
|
493
|
+
|
494
|
+
# update the stack
|
495
|
+
begin
|
496
|
+
|
497
|
+
# default options (not overridable)
|
498
|
+
update_stack_opts = {
|
499
|
+
stack_name: stack_name,
|
500
|
+
template_body: template_string,
|
501
|
+
parameters: template.parameters.map { |k,v| {parameter_key: k, parameter_value: v}}.to_a,
|
502
|
+
tags: cfn_tags.map { |k,v| {"key" => k.to_s, "value" => v.to_s} }.to_a,
|
503
|
+
capabilities: ["CAPABILITY_IAM"],
|
504
|
+
}
|
505
|
+
|
506
|
+
# For any parameter previously set to UsePreviousValue, set that option
|
507
|
+
use_prev_val_params.each do |p|
|
508
|
+
index = update_stack_opts[:parameters].index (update_stack_opts[:parameters].find {|k| k[:parameter_key] == p})
|
509
|
+
update_stack_opts[:parameters][index][:use_previous_value] = true
|
510
|
+
update_stack_opts[:parameters][index].delete :parameter_value
|
511
|
+
end if !use_prev_val_params.nil?
|
512
|
+
|
513
|
+
# fill in options from the command line
|
514
|
+
extra_options = parse_arg_array_as_hash(options)
|
515
|
+
update_stack_opts = extra_options.merge(update_stack_opts)
|
516
|
+
|
517
|
+
# update the stack
|
518
|
+
update_result = cfn_client.update_stack(update_stack_opts)
|
519
|
+
if update_result.successful?
|
520
|
+
puts update_result.stack_id
|
521
|
+
exit(true)
|
522
|
+
end
|
523
|
+
rescue Aws::CloudFormation::Errors::ServiceError => e
|
524
|
+
$stderr.puts "Failed to update stack: #{e}"
|
525
|
+
exit(false)
|
526
|
+
end
|
527
|
+
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# extract options and arguments from a command line string
|
532
|
+
#
|
533
|
+
# Example:
|
534
|
+
#
|
535
|
+
# desired, unknown = extract_options("arg1 --option withvalue --optionwithoutvalue", %w(--option), %w())
|
536
|
+
#
|
537
|
+
# puts desired => Array{"arg1", "--option", "withvalue"}
|
538
|
+
# puts unknown => Array{}
|
539
|
+
#
|
540
|
+
# @param args
|
541
|
+
# the Array of arguments (split the command line string by whitespace)
|
542
|
+
# @param opts_no_val
|
543
|
+
# the Array of options with no value, i.e., --force
|
544
|
+
# @param opts_1_val
|
545
|
+
# the Array of options with exaclty one value, i.e., --retries 3
|
546
|
+
# @returns
|
547
|
+
# an Array of two Arrays.
|
548
|
+
# The first array contains all the options that were extracted (both those with and without values) as a flattened enumerable.
|
549
|
+
# The second array contains all the options that were not extracted.
|
550
|
+
def extract_options(args, opts_no_val, opts_1_val)
|
551
|
+
opts = []
|
552
|
+
rest = []
|
553
|
+
unless args == nil
|
554
|
+
args = args.clone
|
555
|
+
while (arg = args.shift) != nil
|
556
|
+
if opts_no_val.include?(arg)
|
557
|
+
opts.push(arg)
|
558
|
+
elsif opts_1_val.include?(arg)
|
559
|
+
opts.push(arg)
|
560
|
+
opts.push(arg) if (arg = args.shift) != nil
|
561
|
+
else
|
562
|
+
rest.push(arg)
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
[opts, rest]
|
567
|
+
end
|
568
|
+
|
569
|
+
# convert an array of option strings to a hash
|
570
|
+
# example input: ["--option", "value", "--optionwithnovalue"]
|
571
|
+
# example output: {:option => "value", :optionwithnovalue: true}
|
572
|
+
def parse_arg_array_as_hash(options)
|
573
|
+
result = {}
|
574
|
+
options.slice_before(/\A--[a-zA-Z_-]\S/).each { |o|
|
575
|
+
key = ((o[0].sub '--', '').gsub '-', '_').downcase.to_sym
|
576
|
+
value = if o.length > 1 then o.drop(1) else true end
|
577
|
+
value = value[0] if value.is_a?(Array) and value.length == 1
|
578
|
+
result[key] = value
|
579
|
+
}
|
580
|
+
result
|
581
|
+
end
|
582
|
+
|
583
|
+
##################################### Additional dsl logic
|
584
|
+
# Core interpreter for the DSL
|
585
|
+
class TemplateDSL < JsonObjectDSL
|
586
|
+
def exec!()
|
587
|
+
cfn(self)
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
# Main entry point
|
592
|
+
def template(&block)
|
593
|
+
stack_name, parameters, aws_region, aws_profile, nopretty = parse_args
|
594
|
+
raw_template(parameters, stack_name, aws_region, aws_profile, nopretty, &block)
|
595
|
+
end
|