cfncli 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZWQ3YjUwZTM2YzUyNzBlOGJjMzhjZmJjZTkxMDI1N2JhNTA3N2NmYw==
5
+ data.tar.gz: !binary |-
6
+ Yjc2NTBmY2Y5ZWY3YWZiMzNjZDdlMDdkYjI3NWY3ZjJkNmU2YWE4Mg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NjFlMTEzZWQ4ZDgxN2YzN2RlZDdmZDY1NzRhY2E4YWUyOGI4M2IwYTc0Yzg2
10
+ YWFkYjA3MTRjZGVlZmI4N2MxZDFlNjM2YjZkYTE3YjYwMTc4MWMzZjg0ZTFj
11
+ YTBkZmI0NDE0MWM4NTAyNjY1ZjU5Njk1ZmM2Zjk0YmE3OWU3M2Q=
12
+ data.tar.gz: !binary |-
13
+ YjgzNmY4N2QzNjQ5MDliOTRjYWIzNjM0ZjE2MjIyODFhN2EzYmFmYmVkYTBi
14
+ YTQzZmFhMTkyMGNkOTRlODk0ZDM3NDljMTY0NDBhYmFmYjBhNjQ0YjE3YzA5
15
+ MDc0YzU5ZjNjYzQ4MmFkZDRiNTE4NmQ0OWU3Njk0OWMyYzZkY2E=
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.swp
11
+ tags
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2
4
+ sudo: false
5
+ before_install:
6
+ - gem install bundler -v '~> 1.10'
7
+ script:
8
+ - bundle exec rake spec
9
+ deploy:
10
+ provider: rubygems
11
+ api_key:
12
+ secure: "qDh1BOlLLokI/gU/5HkAmbsAEE5A384IketR+BB4Pal8BsGGnxnbbSUU79pEU94wCHhUFYDnzq7DbkxbvuBYl2R1y98iq7SkLvLD2XWr6zAYwWM3oQOrv3NTWi3hc93Mz8YxVJ6GFzyKBFf4SmYjf18OkpOHuxKSAF95T6LcSKaVljM3Vy49FOE6DsStcgHs4U7kgjQMFiaN4WI8ul7XrmsC9zLrmMG7EH6ktTcpE0TuLrpmtq4Y1dmDGLA55yEDfnYZVz/TgDpkcCAMNGC92P/3EgW6nsYAb3LJ/LD5qbbMdnMUVwKx9lOyiYkTKM9QtPoEhAxtn5I0DiWgQ+UI1MQAmdiqYScjhle8DpaRhYCSz+WDQc3MDHPEZQsKksA92Cs26Np6JRLDkpAYw2MKjLMnqFe1mDxr9UpnEJbxZJtsV+N6zgEwDMh/rwJXVdns4wIDveOT5rFP9VCfsJxn25In+DNhmeiBhLYMTeUfOPP9+SrH2xLiVnHk0OPy/IDrYTEd0BkGAPQQDQ45MCT0TOoF/bpTFQ8NREOXndTP9RyHS442eIHiQ5+kv4P/kwhBEjebhzIT4W5LYsYOBv2ZInTP2xKoHXveyVaIwMyKc0VwNluCi/xVnc/NpVhiRxgr57drW6z/Piy2uxs26VVfcve+9+EoO+qP2fBhtQ5YOWA="
13
+ on:
14
+ tags: true
15
+ addons:
16
+ code_climate:
17
+ repo_token:
18
+ secure: "AKrgx5PtDa5/pek0hLgtk55SEE4DzCBM1li4eVCPK4CWfo7WgiODN51rnE7uhnABdaeI82t/YuJIyLE37PnsU4dqkXVBXtPmijBkbZy1SZ/bmTsSnOFoMTVWkaCb/iAcr+i5yqJ16bSGLV/t3K80ksyH80WgZTdzmh75pEDKaOBDLI42mgBf/eXkLZ68iuotatLnuLFwVa4mGW79yiJYBxHNstYHEBd0B4Ahutz3BMGFUiwCBx0wer+nVf+RxN+RKkiXRLnOeucwVEcbOOHbm1nkLDhL8U1KQ8lnWNV+RSUPNoOJNoMcgMRKMavDj6TC29e2pO44PWNgICCkUqbvc9Otrac5HG566ZsYZ0gp9BhQxEIpeYkzEUh7a/znLGiQYylrohIf9gsWz3KLGOPD68y4DZfmKgQDZG8ag1ErKmIv1Cna1PmsFA/lXUrZ60hIzgc0Vdw2jb7Ww2GHK35h8kSOCuDOOgmDpLhESvPVb7PPL2ftTLwSH6FT/P7wyfoeSqgotZTKZCw6JZAnV+HjwWB1uzR7dlt/H0qjduCVekWcK7Pq4lnlNI8S4tcAYiJSaL6EcCvrQ2qUAlw2xXYar0ouRgOQgM4H3HCleqkNIZwHxc3GYgdyhhPYqBr8Uq1ziT7NnQ4X94cCu9tuAcvKoqvQiJOKc4HUGCzgPnJiqL8="
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cfncli.gemspec
4
+ gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
@@ -0,0 +1,5 @@
1
+ guard :rspec, :cmd => "bundle exec rspec --color" do
2
+ watch('spec/spec_helper.rb') { "spec" }
3
+ watch(%r{^spec/.+_spec\.rb})
4
+ watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
5
+ end
@@ -0,0 +1,44 @@
1
+ # cfncli
2
+ [![Code Climate](https://codeclimate.com/github/lethalpaga/cfncli/badges/gpa.svg)](https://codeclimate.com/github/lethalpaga/cfncli)
3
+ [![Test Coverage](https://codeclimate.com/github/lethalpaga/cfncli/badges/coverage.svg)](https://codeclimate.com/github/lethalpaga/cfncli/coverage)
4
+
5
+ cfncli is a command line tool that simplifies the creation of Cloudformation stacks.
6
+ It's designed to be a very simple wrapper around the Cloudformation API, but adds the following features compared to
7
+ the AWS cli :
8
+ * Can wait for the stack creation/update/deletion to be complete before returning
9
+ * Prints the stack events on the console
10
+ * Gives back a return code indicating if the operation was a success or failure
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'cfncli'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install cfncli
27
+
28
+ ## Usage
29
+
30
+ ```
31
+ cfncli help
32
+ cfncli help apply
33
+ ```
34
+
35
+ ## Development
36
+
37
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
38
+
39
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lethalpaga/cfncli.
44
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cfncli"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cfncli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cfncli"
8
+ spec.version = CfnCli::VERSION
9
+ spec.authors = ["lethalpaga"]
10
+ spec.email = ["lethalpaga@gmail.com"]
11
+
12
+ spec.summary = %q{Creates cloudformation stacks}
13
+ spec.description = %q{Creates a Cloudformation stack synchronously}
14
+ spec.homepage = "https://github.com/lethalpaga/cfncli"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "rspec-its", "~>1.2"
25
+ spec.add_development_dependency "cucumber", "~> 2"
26
+ spec.add_development_dependency "guard", "~> 2"
27
+ spec.add_development_dependency "guard-rspec"
28
+
29
+ spec.add_dependency "thor"
30
+ spec.add_dependency "aws-sdk", "~> 2"
31
+ spec.add_dependency "waiting", "~> 0"
32
+ spec.add_dependency "activesupport", "~> 4"
33
+ spec.add_dependency "colorize", "~> 0"
34
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'cfncli/cli'
3
+
4
+ args = CfnCli::Config.load_from_file('cfncli.yml').to_thor(ARGV)
5
+ CfnCli::Cli.start(args)
@@ -0,0 +1,11 @@
1
+ require "cfncli/version"
2
+
3
+ require 'thor'
4
+
5
+ module CfnCli
6
+ class Cli < Thor
7
+ def create
8
+ puts "Creating stack"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ require 'aws-sdk'
2
+
3
+ module CfnCli
4
+ module CfnClient
5
+
6
+ attr_accessor :stub_responses
7
+
8
+ # CloudFormation client
9
+ # @see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html
10
+ def cfn_client
11
+ @@client ||= Aws::CloudFormation::Client.new(stub_responses: stub_responses || false)
12
+ end
13
+
14
+ # Clouformation Resource
15
+ # This is used to interact with the CloudFormation API
16
+ # @see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Resource.html
17
+ # @note this uses a class variable for the client and the resource so they can share
18
+ # the stubbed responses in the unit tests.
19
+ def cfn
20
+ @@resource ||= Aws::CloudFormation::Resource.new(client: cfn_client)
21
+ @@resource
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,254 @@
1
+ require 'thor'
2
+ require 'aws-sdk'
3
+
4
+ require 'cfncli/cloudformation'
5
+ require 'cfncli/config'
6
+
7
+ module CfnCli
8
+ class Cli < Thor
9
+
10
+ module ExitCode
11
+ OK = 0
12
+ STACK_ERROR = 1
13
+ VALIDATION_ERROR = 2
14
+ end
15
+
16
+ # Global options
17
+ class_option 'log_level',
18
+ type: :numeric,
19
+ default: 1,
20
+ desc: 'Log level to display (0=DEBUG, 1=INFO, 2=ERROR, 3=CRITICAL)'
21
+
22
+ # Stack options
23
+ method_option 'stack_name',
24
+ alias: '-n',
25
+ type: :string,
26
+ required: true,
27
+ desc: 'Cloudformation stack name'
28
+
29
+ method_option 'template_body',
30
+ type: :string,
31
+ desc: 'JSON string or file containing the template body.' \
32
+ ' This is exclusive with the template_url option. Use @filename to read' \
33
+ ' the template body from a file'
34
+
35
+ method_option 'template_url',
36
+ type: :string,
37
+ desc: 'S3 URL to the Cloudformation template.' \
38
+ ' This is exclusive with the template_body option'
39
+
40
+ method_option 'parameters',
41
+ type: :hash,
42
+ desc: 'Stack parameters. Pass each parameter in the form --parameters key1:value1 key2:value2 or use the @filename syntax to provide a JSON file'
43
+
44
+ method_option 'parameters_file',
45
+ type: :string,
46
+ desc: 'Stack parameters file. It should be a JSON file using the same syntax as for the AWS CLI'
47
+
48
+ method_option 'disable_rollback',
49
+ type: :boolean,
50
+ default: false,
51
+ desc: 'Disable rollbacks in case of a stack update failure'\
52
+ ' This is mutually exclusive with on_failure.'
53
+
54
+ method_option 'timeout_in_minutes',
55
+ type: :boolean,
56
+ desc: 'Stack creation timeout (in minutes)'
57
+
58
+ method_option 'notification_arns',
59
+ type: :array,
60
+ desc: 'List of SNS notification ARNs to publish stack related events'
61
+
62
+ method_option 'capabilities',
63
+ type: :array,
64
+ enum: ['CAPABILITY_IAM'],
65
+ desc: 'A list of capabilities that you must specify before AWS CloudFormation can create or update certain stacks'
66
+
67
+ method_option 'resource_types',
68
+ type: :array,
69
+ desc: 'The template resource types that you have permissions to work with for this create stack action, such as AWS::EC2::Instance, AWS::EC2::*, or Custom::MyCustomInstance'
70
+
71
+ method_option 'on_failure',
72
+ type: :string,
73
+ enum: ['DO_NOTHING', 'ROLLBACK', 'DELETE'],
74
+ desc: 'Determines what action will be taken if the stack creation fails.' \
75
+ ' This is mutually exclusive with disable_rollback'
76
+
77
+ method_option 'stack_policy_body',
78
+ type: :string,
79
+ desc: 'JSON String containing the stack policy body. The @filename syntax can be used.'
80
+
81
+ method_option 'stack_policy_url',
82
+ type: :string,
83
+ desc: 'S3 URL to a stack policy file.' \
84
+ ' This is mutually exclusive with stack_policy_body'
85
+
86
+ method_option 'tags',
87
+ type: :hash,
88
+ desc: 'Key-value pairs to associate with this stack'
89
+
90
+ # Application options
91
+ method_option 'list_events',
92
+ alias: '-l',
93
+ type: :boolean,
94
+ default: true,
95
+ desc: 'List the stack events during the operation'
96
+
97
+ method_option 'interval',
98
+ type: :numeric,
99
+ default: 10,
100
+ desc: 'Polling interval (in seconds) for the cloudformation events'
101
+
102
+ method_option 'timeout',
103
+ type: :numeric,
104
+ default: 1800,
105
+ desc: 'Timeout (in seconds) for the stack creation'
106
+
107
+ method_option 'fail_on_noop',
108
+ type: :boolean,
109
+ default: false,
110
+ desc: 'Fails if a stack has nothing to update'
111
+
112
+ desc 'apply', 'Creates a stack in Cloudformation'
113
+ def apply
114
+ opts = process_params(options)
115
+
116
+ stack_name = opts['stack_name']
117
+
118
+ interval = consume_option(opts, 'interval')
119
+ timeout = consume_option(opts, 'timeout')
120
+ retries = timeout / interval
121
+ fail_on_noop = consume_option(opts, 'fail_on_noop')
122
+ list_events = consume_option(opts, 'list_events')
123
+
124
+ ENV['CFNCLI_LOG_LEVEL'] = consume_option(opts, 'log_level').to_s
125
+
126
+ client_config = Config::CfnClient.new(interval, retries, fail_on_noop)
127
+
128
+ res = ExitCode::OK
129
+ cfn.create_stack(opts, client_config)
130
+ if list_events
131
+ cfn.events(stack_name, client_config)
132
+ res = ExitCode::STACK_ERROR unless cfn.stack_successful? stack_name
133
+ end
134
+
135
+ puts "Stack creation #{res == 0 ? 'successful' : 'failed'}"
136
+ exit res
137
+ rescue Aws::CloudFormation::Errors::ValidationError => e
138
+ puts e.message
139
+ exit ExitCode::VALIDATION_ERROR
140
+ end
141
+
142
+ method_option 'stack_name',
143
+ alias: '-n',
144
+ type: :string,
145
+ required: true,
146
+ desc: 'Name or ID of the Cloudformation stack'
147
+
148
+ # Application options
149
+ method_option 'interval',
150
+ type: :numeric,
151
+ default: 10,
152
+ desc: 'Polling interval (in seconds) for the cloudformation events'
153
+
154
+ method_option 'timeout',
155
+ type: :numeric,
156
+ default: 1800,
157
+ desc: 'Timeout (in seconds) for the stack event listing'
158
+
159
+ desc 'events', 'Displays the events for a stack in realtime'
160
+ def events
161
+ stack_name = options['stack_name']
162
+ config = Config::CfnClient.new(options['interval'], options['retries'])
163
+ cfn.events(stack_name, config)
164
+ end
165
+
166
+ no_tasks do
167
+ # Reads an option from a hash and deletes it
168
+ # @param opts [Hash] Hash containing the options
169
+ # @param option Key to consume
170
+ # @return value of Key option in opts
171
+ def consume_option(opts, option)
172
+ res = opts[option]
173
+ opts.delete(option)
174
+ res
175
+ end
176
+
177
+ # Process the parameters to make them compliant with the Cloudformation API
178
+ # @param opts [Hash] Hash containing the options. The hash will not be modified
179
+ # @return the processed options hash
180
+ def process_params(opts)
181
+ opts = opts.dup
182
+ check_exclusivity(opts.keys, ['template_body', 'template_url'])
183
+ check_exclusivity(opts.keys, ['disable_rollback', 'on_failure'])
184
+ check_exclusivity(opts.keys, ['stack_policy_body', 'stack_policy_url'])
185
+ check_exclusivity(opts.keys, ['parameters', 'parameters_file'])
186
+
187
+ opts['template_body'] = file_or_content(opts['template_body']) if opts['template_body']
188
+ opts['stack_policy_body'] = file_or_content(opts['stack_policy_body']) if opts['stack_policy_body']
189
+ opts['parameters'] = process_stack_parameters(opts['parameters']) if opts['parameters']
190
+ opts['parameters'] = process_stack_parameters_file(consume_option(opts, 'parameters_file')) if opts['parameters_file']
191
+
192
+ opts
193
+ end
194
+
195
+ # Check if only one of the arguments is specified in the options
196
+ # @param options [Arrray<String>] List of available options
197
+ # @param exclusives [Array<String>] List of mutually exclusive options
198
+ def check_exclusivity(opts, exclusives)
199
+ exclusive_options = opts & exclusives
200
+ if exclusive_options.size > 1
201
+ fail Thor::Error, "Error: #{exclusive_options} are mutually exclusive."
202
+ end
203
+ end
204
+
205
+ # Gets the content of a string that can either be the
206
+ # content itself or a filename if beginning by @
207
+ # @param str [String] String containing either the content or the filename to read
208
+ def file_or_content(str)
209
+ return str if str.nil?
210
+ return str unless file_param? str
211
+
212
+ content = File.read(str[1..-1])
213
+ content
214
+ end
215
+
216
+ # Indicates if the parameter is a file (as opposed to a value)
217
+ # This is indicated by a leading @
218
+ def file_param?(param)
219
+ return false unless param.is_a? String
220
+ param.start_with? '@'
221
+ end
222
+
223
+ # Converts a parameter JSON file to the format expected by CloudFormation
224
+ # @param filename Path to the JSON file containing the parameters description
225
+ # @return
226
+ def process_stack_parameters_file(filename)
227
+ content = File.read(filename)
228
+ return CloudFormation.parse_json_params(JSON.parse(content))
229
+ end
230
+
231
+ # Converts a parameters hash in the format expected by CloudFormation
232
+ # @param parameters [Hash] Hash containing the parameters to convert
233
+ def process_stack_parameters(parameters)
234
+ return {} unless parameters
235
+
236
+ # Returns the content of the file if parameters is a file
237
+ return file_or_content(parameters) if file_param? parameters
238
+
239
+ # Otherwise convert each param to the cfn structure
240
+ parameters.map do |key, value|
241
+ {
242
+ parameter_key: key,
243
+ parameter_value: value
244
+ }
245
+ end
246
+ end
247
+
248
+ # Cloudformation utility object
249
+ def cfn
250
+ @cfn ||= CfnCli::CloudFormation.new
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,78 @@
1
+ require 'cfncli/cfn_client'
2
+ require 'cfncli/stack'
3
+ require 'cfncli/logger'
4
+ require 'cfncli/event_poller'
5
+
6
+ require 'waiting'
7
+ require 'pp'
8
+
9
+ module CfnCli
10
+ class CloudFormation
11
+ include CfnClient
12
+ include Loggable
13
+
14
+ def initialize
15
+ end
16
+
17
+ # Creates a stack and wait for the creation to be finished
18
+ # @param options [Hash] Options for the stack creation
19
+ # (@see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html)
20
+ def create_stack(options, config = nil)
21
+ create_or_update_stack(options, config)
22
+ end
23
+
24
+ # Creates a stack if it doesn't exist otherwise update it
25
+ def create_or_update_stack(options, config = nil)
26
+ opts = process_params(options.dup)
27
+
28
+ stack_name = opts['stack_name']
29
+
30
+ stack = create_stack_obj(stack_name, config)
31
+
32
+ if stack.exists?
33
+ stack.update(opts)
34
+ else
35
+ stack.create(opts)
36
+ end
37
+
38
+ stack
39
+ end
40
+
41
+ # List stack events
42
+ def events(stack_name, config)
43
+ stack = create_stack_obj(stack_name, config)
44
+ stack.list_events(EventPoller.new, config)
45
+ end
46
+
47
+ # Returns the stack stack
48
+ def stack_successful?(stack_name)
49
+ Stack.new(stack_name).succeeded?
50
+ end
51
+
52
+ # Converts the 'standard' json stack parameters format to the format
53
+ # expected by the API
54
+ # (see https://blogs.aws.amazon.com/application-management/post/Tx1A23GYVMVFKFD/Passing-Parameters-to-CloudFormation-Stacks-with-the-AWS-CLI-and-Powershell)
55
+ def self.parse_json_params(params)
56
+ params.map do |param|
57
+ {
58
+ parameter_key: param.first.first,
59
+ parameter_value: param.first.last
60
+ }
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # Creates a new stack object
67
+ # Mainly useful to mock it in unit tests
68
+ def create_stack_obj(stack_name, config)
69
+ CfnCli::Stack.new(stack_name, config)
70
+ end
71
+
72
+ # Process the parameters
73
+ def process_params(opts)
74
+ opts.delete('disable_rollback')
75
+ opts
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,87 @@
1
+ require 'yaml'
2
+
3
+ module CfnCli
4
+ module Config
5
+ def self.load_from_file(filename)
6
+ content = YAML::load_file(filename)
7
+ Parameters.new(content)
8
+ rescue Errno::ENOENT
9
+ nil
10
+ end
11
+
12
+ class CfnClient
13
+ attr_accessor :interval
14
+ attr_accessor :retries
15
+ attr_accessor :fail_on_noop
16
+
17
+ def initialize(interval = 10, retries = 30, fail_on_noop = false)
18
+ @interval = interval
19
+ @retries = retries
20
+ @fail_on_noop = fail_on_noop
21
+ end
22
+ end
23
+
24
+ class Parameters
25
+ def initialize(content)
26
+ @content = content
27
+ end
28
+
29
+ # Converts parameters to command-line arguments
30
+ def to_args(content = nil)
31
+ content ||= to_a
32
+ content.join(' ')
33
+ end
34
+
35
+ # Get an array of parameters
36
+ def to_a
37
+ from_hash(@content) if @content.is_a? Hash
38
+ end
39
+
40
+ # Format parameters for thor
41
+ # @param given_args [Array] Optional array of existing arguments
42
+ def to_thor(given_args = nil)
43
+ args = []
44
+
45
+ if given_args
46
+ given_args = given_args.dup
47
+ args << given_args.shift
48
+ end
49
+ args += to_a
50
+ args += given_args if given_args
51
+
52
+ args
53
+ end
54
+
55
+ protected
56
+
57
+ def from_hash(content)
58
+ args = []
59
+ content.each_pair do |key, value|
60
+ case value
61
+ when Hash
62
+ value = parse_hash(value)
63
+ when Array
64
+ value = parse_array(value)
65
+ end
66
+
67
+ args += ["--#{key}", value]
68
+ end
69
+
70
+ args
71
+ end
72
+
73
+ def parse_hash(content)
74
+ args = []
75
+ content.each_pair do |key, value|
76
+ args += ["#{key}:#{value}"]
77
+ end
78
+
79
+ args
80
+ end
81
+
82
+ def parse_array(value)
83
+ "[#{value.join(',')}]"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,27 @@
1
+ require 'cfncli/states'
2
+
3
+ module CfnCli
4
+ class Event
5
+ include CfnStates
6
+
7
+ attr_reader :event
8
+
9
+ def initialize(event)
10
+ @event = event
11
+ end
12
+
13
+ def status
14
+ event.resource_status
15
+ end
16
+
17
+ def color
18
+ return :green if succeeded?
19
+ return :yellow if in_progress?
20
+ return :red if failed?
21
+ end
22
+
23
+ def to_s
24
+ "#{event.timestamp} #{event.resource_status} #{event.resource_type} #{event.logical_resource_id} #{event.resource_status_reason}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ require 'colorize'
2
+ require 'cfncli/event'
3
+
4
+ module CfnCli
5
+ class EventPoller
6
+ def initialize
7
+ end
8
+
9
+ def event(event)
10
+ colorize Event.new(event)
11
+ end
12
+
13
+ def colorize(event)
14
+ puts event.to_s.colorize(event.color)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ module CfnCli
2
+ class EventStreamer
3
+ attr_reader :stack
4
+ attr_reader :config
5
+
6
+ def initialize(stack, config = nil)
7
+ @stack = stack
8
+ @config = config || default_config
9
+ @seen_events = []
10
+ end
11
+
12
+ def default_config
13
+ Config::CfnClient.new
14
+ end
15
+
16
+ # Wait for events. This will exit when the
17
+ # stack reaches a finished state
18
+ # @yields [CfnEvent] Events for the stack
19
+ def each_event
20
+ Waiting.wait(interval: config.interval, max_attempts: config.retries) do |waiter|
21
+ @next_token = stack.events(@next_token).each do |event|
22
+ yield event unless seen?(event)
23
+ end
24
+
25
+ waiter.done if stack.finished?
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_accessor :seen_events
32
+
33
+ # Indicates if an event has already been seen
34
+ def seen?(event)
35
+ res = seen_events.include? event.id
36
+ seen_events << event.id
37
+ res
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ require 'logger'
2
+
3
+ module CfnCli
4
+ # Logger Mixin
5
+ module Loggable
6
+ # Gets the logger object
7
+ #
8
+ # By default it logs to STDOUT with the date-time format +%Y-%m-%d %H:%M:%S +
9
+ #
10
+ # @return [Logger]
11
+ def logger
12
+ if @logger.nil?
13
+ @logger = Logger.new(STDOUT)
14
+ @logger.level = ENV['CFNCLI_LOG_LEVEL'].to_i || Logger::INFO
15
+ @logger.formatter = proc do |severity, datetime, progname, msg|
16
+ severity = severity.ljust(7)
17
+ progname = "#{progname}: " if progname
18
+ "#{datetime} [ #{severity} ] #{progname}#{msg}\n"
19
+ end
20
+ end
21
+ @logger
22
+ end
23
+
24
+ # Sets the logger object
25
+ # @param obj [Logger]
26
+ def logger=(obj)
27
+ @logger = obj
28
+ end
29
+
30
+ # Gets the current log level as a symbol
31
+ # @return [Symbol]
32
+ def log_level
33
+ level_map = {
34
+ Logger::DEBUG => :debug,
35
+ Logger::INFO => :info,
36
+ Logger::WARN => :warn,
37
+ Logger::ERROR => :error,
38
+ Logger::FATAL => :fatal,
39
+ Logger::UNKNOWN => :unknown
40
+ }
41
+ level_map[logger.level]
42
+ end
43
+
44
+ # Sets the current log level
45
+ def log_level=(level)
46
+ if level.is_a?(Fixnum)
47
+ logger.level = level
48
+ else
49
+ logger.level = Logger.const_get(level.to_s.upcase)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,122 @@
1
+ require 'cfncli/cfn_client'
2
+ require 'cfncli/logger'
3
+ require 'cfncli/config'
4
+ require 'cfncli/event_streamer'
5
+ require 'cfncli/states'
6
+
7
+ require 'waiting'
8
+
9
+ module CfnCli
10
+ class Stack
11
+ include CfnCli::CfnClient
12
+ include CfnCli::CfnStates
13
+ include Loggable
14
+
15
+ attr_reader :stack_name
16
+
17
+ class StackNotFoundError < StandardError; end
18
+
19
+ def initialize(stack_name, config = nil)
20
+ @stack = nil
21
+ @stack_id = nil
22
+ @stack_name = stack_name
23
+ @config = config || default_config
24
+ end
25
+
26
+ def default_config
27
+ Config::CfnClient.new
28
+ end
29
+
30
+ def fail_on_noop?
31
+ @config.fail_on_noop
32
+ end
33
+
34
+ def stack_id
35
+ @stack_id || @stack_name
36
+ end
37
+
38
+ def stack
39
+ fetch_stack
40
+ end
41
+
42
+ def exists?
43
+ stack.exists?
44
+ end
45
+
46
+ # Creates a new stack
47
+ # @param opts Hash containing the options for `create_stack`
48
+ # (see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Resource.html#create_stack-instance_method)
49
+ def create(opts)
50
+ logger.debug "Creating stack #{stack_name} (#{opts.inspect})"
51
+ @stack = cfn.create_stack(opts)
52
+ stack.wait_until_exists
53
+ @stack_id = stack.stack_id
54
+ end
55
+
56
+ # Updates an existing stack
57
+ # @param opts Hash containing the options for `update_stack`
58
+ # (see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html#update_stack-instance_method)
59
+ def update(opts)
60
+ logger.debug "Updating stack #{stack_name} (#{opts.inspect})"
61
+ resp = cfn.client.update_stack(opts)
62
+ @stack_id = resp.stack_id
63
+ rescue Aws::CloudFormation::Errors::ValidationError => e
64
+ unless !fail_on_noop? && e.message.include?('No updates are to be performed')
65
+ raise e
66
+ end
67
+ end
68
+
69
+ # Waits for a stack to be in a finished state
70
+ # @return A boolean indicating if the operation was succesful
71
+ def wait_for_completion
72
+ Waiting.wait(max_attempts: @config.retries, interval: @config.interval) do |waiter|
73
+ waiter.done if finished?
74
+ end
75
+ succeeded?
76
+ rescue Waiting::TimedOutError => e
77
+ logger.error "Timed out while waiting for the stack #{inspect} to be created(#{e.message})"
78
+ false
79
+ end
80
+
81
+ # List all events in real time
82
+ # @param poller [CfnCli::Poller] Poller class to display events
83
+ def list_events(poller, config = nil)
84
+ streamer = EventStreamer.new(self, config)
85
+ streamer.each_event do |event|
86
+ poller.event(event)
87
+ end
88
+ end
89
+
90
+ # Get the events from the cfn stack
91
+ def events(next_token)
92
+ stack.events(next_token)
93
+ end
94
+
95
+ # Indicates if the stack is in a finished state
96
+ def finished?
97
+ return false if stack.nil?
98
+ finished_states.include? stack.stack_status
99
+ end
100
+
101
+ # Indicates if the stack is in a successful state
102
+ def succeeded?
103
+ res = success_states.include? stack.stack_status
104
+ return false if stack.nil?
105
+ res
106
+ end
107
+
108
+ # Indicates if the stack is in a transition state
109
+ def in_progress?
110
+ return false if stack.nil?
111
+ transitive_states.include? stack.stack_status
112
+ end
113
+
114
+ private
115
+
116
+ # Gets stack info from the cfn API
117
+ def fetch_stack
118
+ @stack = cfn.stack(stack_id)
119
+ @stack
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,72 @@
1
+ module CfnCli
2
+ module CfnStates
3
+ # Indicates if the state is finished
4
+ def finished?
5
+ finished_states.include? status
6
+ end
7
+
8
+ # Indicates if the state is successful
9
+ def succeeded?
10
+ success_states.include? status
11
+ end
12
+
13
+ # Indicates if the state is a transition
14
+ def in_progress?
15
+ transitive_states.include? status
16
+ end
17
+
18
+ # Indicates if the state is failed
19
+ def failed?
20
+ !succeeded? && !in_progress?
21
+ end
22
+
23
+ # List of possible states
24
+ def states
25
+ [
26
+ 'CREATE_IN_PROGRESS',
27
+ 'CREATE_IN_PROGRESS',
28
+ 'CREATE_FAILED',
29
+ 'CREATE_COMPLETE',
30
+ 'ROLLBACK_IN_PROGRESS',
31
+ 'ROLLBACK_FAILED',
32
+ 'ROLLBACK_COMPLETE',
33
+ 'DELETE_IN_PROGRESS',
34
+ 'DELETE_FAILED',
35
+ 'DELETE_COMPLETE',
36
+ 'UPDATE_IN_PROGRESS',
37
+ 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS',
38
+ 'UPDATE_COMPLETE',
39
+ 'UPDATE_ROLLBACK_IN_PROGRESS',
40
+ 'UPDATE_ROLLBACK_FAILED',
41
+ 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS',
42
+ 'UPDATE_ROLLBACK_COMPLETE',
43
+ ]
44
+ end
45
+
46
+ # List of successful states
47
+ def success_states
48
+ [
49
+ 'CREATE_COMPLETE',
50
+ 'DELETE_COMPLETE',
51
+ 'UPDATE_COMPLETE'
52
+ ]
53
+ end
54
+
55
+ # List of transitive states
56
+ def transitive_states
57
+ states.select do |state|
58
+ state.end_with? 'IN_PROGRESS'
59
+ end
60
+ end
61
+
62
+ # List of finished states
63
+ def finished_states
64
+ states - transitive_states
65
+ end
66
+
67
+ # List of failed or unknown states
68
+ def failed_states
69
+ states - success_states - transitive_states
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ module CfnCli
2
+ VERSION = "0.3.0"
3
+ end
metadata ADDED
@@ -0,0 +1,235 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cfncli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - lethalpaga
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-its
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: cucumber
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: thor
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: aws-sdk
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: '2'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: '2'
139
+ - !ruby/object:Gem::Dependency
140
+ name: waiting
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ~>
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: activesupport
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ~>
158
+ - !ruby/object:Gem::Version
159
+ version: '4'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ~>
165
+ - !ruby/object:Gem::Version
166
+ version: '4'
167
+ - !ruby/object:Gem::Dependency
168
+ name: colorize
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ~>
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ~>
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Creates a Cloudformation stack synchronously
182
+ email:
183
+ - lethalpaga@gmail.com
184
+ executables:
185
+ - cfncli
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - .gitignore
190
+ - .rspec
191
+ - .travis.yml
192
+ - Gemfile
193
+ - Guardfile
194
+ - README.md
195
+ - Rakefile
196
+ - bin/console
197
+ - bin/setup
198
+ - cfncli.gemspec
199
+ - exe/cfncli
200
+ - lib/cfncli.rb
201
+ - lib/cfncli/cfn_client.rb
202
+ - lib/cfncli/cli.rb
203
+ - lib/cfncli/cloudformation.rb
204
+ - lib/cfncli/config.rb
205
+ - lib/cfncli/event.rb
206
+ - lib/cfncli/event_poller.rb
207
+ - lib/cfncli/event_streamer.rb
208
+ - lib/cfncli/logger.rb
209
+ - lib/cfncli/stack.rb
210
+ - lib/cfncli/states.rb
211
+ - lib/cfncli/version.rb
212
+ homepage: https://github.com/lethalpaga/cfncli
213
+ licenses: []
214
+ metadata: {}
215
+ post_install_message:
216
+ rdoc_options: []
217
+ require_paths:
218
+ - lib
219
+ required_ruby_version: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ! '>='
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ required_rubygems_version: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ! '>='
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ requirements: []
230
+ rubyforge_project:
231
+ rubygems_version: 2.4.5
232
+ signing_key:
233
+ specification_version: 4
234
+ summary: Creates cloudformation stacks
235
+ test_files: []