cloudformation-ruby-dsl-addedvars 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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,9 @@
1
+ {
2
+ "Mappings": {
3
+ "JsonExampleMap": {
4
+ "foo": "bar"
5
+ }
6
+ }
7
+
8
+ }
9
+
@@ -0,0 +1,5 @@
1
+ {
2
+ 'Mappings' => {
3
+ 'RubyExampleMap' => { 'baz' => 'blah' }
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ Mappings:
3
+ YamlExampleMap:
4
+ foo:
5
+ bar: yo
@@ -0,0 +1,8 @@
1
+ {
2
+ "Mappings": {
3
+ "ExampleMap1": {
4
+ "gregory": "smith"
5
+ }
6
+ }
7
+
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "Mappings": {
3
+ "ExampleMap2": {
4
+ "smith": "john"
5
+ }
6
+ }
7
+
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "Mappings": {
3
+ "ExampleMap3": {
4
+ "john": "gregory"
5
+ }
6
+ }
7
+
8
+ }
@@ -0,0 +1,5 @@
1
+ column0 column1 column2 column3 column4
2
+ foo baz qux quux
3
+ foo corge grault garply
4
+ bar waldo fred
5
+ bar plugh xyzzy thud
@@ -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,4 @@
1
+ #!/usr/bin/env bash
2
+ echo "put initialization script here"
3
+ echo "the time is {{ locals[:time] }}"
4
+ echo "the aws region is {{ ref('AWS::Region') }}"
@@ -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