stackit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 426eae8667b341e8834521f00605632edd98b19d
4
+ data.tar.gz: dc9890de3f68a1ee7198ea22597f46bbd7266a39
5
+ SHA512:
6
+ metadata.gz: 8cc56791184964fbf5752e5cf493b3c4cb5845c43beae200d33845dbc0bce23b5e34ffe3a4903a1c223943817fe942d3f7cc9198150b2279a0053d98039fb52f
7
+ data.tar.gz: 196c75963e0d8fe4c4ebde6645a2d4eab0103028c18622856b87ae3c198dfe3ca287b74a6fc878e70fb3944e94f86ad406cb7be84b4f82c2135724ad86fae9fa
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.6
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in stackit.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,16 @@
1
+ stackit :: Simple, elegant CloudFormation dependency management.
2
+ Copyright (C) 2016 Jeremy Hahn
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # stackit
2
+
3
+ Simple, elegant CloudFormation dependency management.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'stackit'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install stackit
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Development
26
+
27
+ 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.
28
+
29
+ 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).
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jeremyhahn/stackit.
34
+
35
+
36
+ ## License
37
+
38
+ The gem is available as open source under the terms of the [GNU GENERAL PUBLIC LICENSE](http://www.gnu.org/licenses/gpl-3.0.en.html).
data/Rakefile ADDED
@@ -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
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "stackit"
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
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/stackit ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $:.push File.expand_path('../../lib', __FILE__)
3
+ require 'stackit/cli'
4
+ Stackit::Cli.start
@@ -0,0 +1,3 @@
1
+ module Stackit
2
+ class Aws < Awsclient::Connection; end
3
+ end
@@ -0,0 +1,168 @@
1
+ require 'stackit'
2
+ require 'thor'
3
+
4
+ module Stackit
5
+ class Cli < Thor
6
+
7
+ class_option :environment, :aliases => '-e', :desc => "Your stack environment (dev, staging, prod, etc)", :default => 'default'
8
+ class_option :profile, desc: 'AWS profile'
9
+ class_option :region, desc: 'AWS region', default: 'us-east-1'
10
+ class_option :debug, type: :boolean, default: false
11
+ class_option :verbose, type: :boolean, default: false
12
+ class_option :assume_role, type: :hash, :desc => 'IAM role name and optional duration to keep the STS token valid'
13
+
14
+ def initialize(*args)
15
+ super(*args)
16
+ init_cli
17
+ end
18
+
19
+ def self.banner(task, namespace = true, subcommand = false)
20
+ #"#{basename} #{subcommand_prefix} #{task.usage}"
21
+ "#{basename} #{task.usage}"
22
+ end
23
+
24
+ def self.subcommand_prefix
25
+ self.name.gsub(%r{.*::}, '').gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
26
+ end
27
+
28
+ desc 'create', 'Creates a new CloudFormation stack'
29
+ method_option :template, aliases: '-t', desc: 'The cloudformation template', :required => true
30
+ method_option :stack_name, aliases: '-n', desc: 'The stack name. Defaults to the camelized template file name', :required => true
31
+ method_option :stack_policy, :aliases => '-p', :desc => 'A local file system or S3 (HTTPS) path to the stack policy'
32
+ method_option :depends, :aliases => '-d', :type => :array, :default => [], :desc => 'Space delimited list of stack names to automatically map parameter values from'
33
+ method_option :parameters, aliases: '-p', type: :hash, desc: 'Parameters supplied to the cloudformation template', default: {}
34
+ method_option :parameters_file, desc: 'Parameters supplied to the cloudformation template'
35
+ method_option :parameter_map, :aliases => '-pm', type: :hash, default: {}, desc: 'Parameter map used to direct dependent parameter values to stack template parameters'
36
+ method_option :wait, :aliases => '-w', type: :boolean, default: false, desc: 'Wait for the stack to enter STATUS_COMPLETE before returning or raise an exception if it times out'
37
+ method_option :force, :desc => 'Force a stack update on unchanged templates'
38
+ method_option :dry_run, :type => :boolean, :default => false, :desc => 'Run all code except AWS API calls'
39
+ def create
40
+ ManagedStack.new({
41
+ template: options[:template],
42
+ stack_name: options[:stack_name],
43
+ stack_policy: options[:stack_policy],
44
+ depends: options[:depends],
45
+ user_defined_parameters: options[:parameters],
46
+ parameters_file: options[:parameters_file],
47
+ parameter_map: options[:parameter_map],
48
+ wait: options[:wait],
49
+ force: options[:force],
50
+ dry_run: options[:dry_run],
51
+ debug: !!options[:debug]
52
+ }).create!
53
+ end
54
+
55
+ desc 'update', 'Updates an existing CloudFormation stack'
56
+ method_option :template, aliases: '-t', desc: 'The cloudformation template', :required => true
57
+ method_option :stack_name, aliases: '-n', desc: 'The stack name. Defaults to the camelized template file name', :required => true
58
+ method_option :stack_policy, :aliases => '-p', :desc => 'A local file system or S3 (HTTPS) path to the stack policy'
59
+ method_option :stack_policy_during_update, :aliases => '-pu', :desc => 'A local file system or S3 (HTTPS) path to the stack policy to use during update'
60
+ method_option :depends, :aliases => '-d', :type => :array, :default => [], :desc => 'Space delimited list of stack names to automatically map parameter values from'
61
+ method_option :parameters, aliases: '-p', type: :hash, desc: 'Parameters supplied to the cloudformation template', default: {}
62
+ method_option :parameters_file, desc: 'Parameters supplied to the cloudformation template'
63
+ method_option :parameter_map, :aliases => '-pm', type: :hash, default: {}, desc: 'Parameter map used to direct dependent parameter values to stack template parameters'
64
+ method_option :wait, :aliases => '-w', type: :boolean, default: false, desc: 'Wait for the stack to enter STATUS_COMPLETE before returning or raise an exception if it times out'
65
+ method_option :force, :desc => 'Force a stack update on unchanged templates'
66
+ method_option :dry_run, :type => :boolean, :default => false, :desc => 'Run all code except AWS API calls'
67
+ def update
68
+ ManagedStack.new({
69
+ template: options[:template],
70
+ stack_name: options[:stack_name],
71
+ stack_policy: options[:stack_policy],
72
+ stack_policy_during_update: options[:stack_policy_during_update],
73
+ depends: options[:depends],
74
+ user_defined_parameters: options[:parameters],
75
+ parameters_file: options[:parameters_file],
76
+ parameter_map: options[:parameter_map],
77
+ wait: options[:wait],
78
+ force: options[:force],
79
+ dry_run: options[:dry_run],
80
+ debug: !!options[:debug]
81
+ }).update!
82
+ end
83
+
84
+ desc 'delete', 'Deletes a CloudFormation stack'
85
+ method_option :stack_name, aliases: '-n', desc: 'The stack name. Defaults to the camelized template file name', :required => true
86
+ method_option :retain_resources, :aliases => '-r', :type => :array, :desc => 'Space delimited list of logical resource ids to retain after the stack is deleted'
87
+ method_option :wait, :aliases => '-w', type: :boolean, default: false, desc: 'Wait for the stack to enter STATUS_COMPLETE before returning or raise an exception if it times out'
88
+ method_option :dry_run, :type => :boolean, :default => false, :desc => 'Run all code except AWS API calls'
89
+ def delete
90
+ ManagedStack.new({
91
+ stack_name: options[:stack_name],
92
+ wait: options[:wait],
93
+ dry_run: options[:dry_run],
94
+ debug: !!options[:debug]
95
+ }).delete!
96
+ end
97
+
98
+ desc 'create-keypair', 'Creates a new EC2 keypair'
99
+ method_option :name, desc: 'The name of the keypair', :required => true
100
+ def create_keypair
101
+ puts Stackit.aws.ec2.create_key_pair({
102
+ key_name: options['name']
103
+ })['key_material']
104
+ end
105
+
106
+ desc 'delete-keypair', 'Deletes an existing EC2 keypair'
107
+ method_option :name, desc: 'The name of the keypair', :required => true
108
+ def delete_keypair
109
+ Stackit.aws.ec2.delete_key_pair({
110
+ key_name: options['name']
111
+ })
112
+ end
113
+
114
+ desc 'version', 'Displays StackIT version'
115
+ def version
116
+ puts <<-LOGO
117
+ _____ _ _ _______ _______
118
+ (_____) (_)_ (_) _ (_______)(__ _ __)
119
+ (_)___ (___) ____ ___ (_)(_) (_) (_)
120
+ (___)_ (_) (____) _(___)(___) (_) (_)
121
+ ____(_)(_)_( )_( )(_)___ (_)(_) __(_)__ (_)
122
+ (_____) (__)(__)_) (____)(_) (_)(_______) (_) v#{Stackit::VERSION}
123
+
124
+ Simple, elegant CloudFormation dependency management.
125
+
126
+ LOGO
127
+ end
128
+
129
+ def self.exit_on_failure?
130
+ true
131
+ end
132
+
133
+ no_commands do
134
+
135
+ def init_cli
136
+
137
+ Stackit.aws.region = options[:region] if options[:region]
138
+ Stackit.environment = options[:environment].to_sym if options[:environment]
139
+ Stackit.debug = !!options[:debug]
140
+ if Stackit.debug
141
+ Stackit.logger.level = Logger::DEBUG
142
+ Stackit.logger.debug "Initializing CLI in debug mode."
143
+ begin
144
+ require 'pry-byebug'
145
+ rescue LoadError; end
146
+ elsif options[:verbose]
147
+ Stackit.logger.level = Logger::INFO
148
+ else
149
+ Stackit.logger.level = Logger::ERROR
150
+ end
151
+ if options[:profile]
152
+ Stackit.aws.profile = options[:profile]
153
+ elsif options[:environment]
154
+ Stackit.aws.credentials = Stackit.aws.load_credentials(
155
+ options[:environment]
156
+ )
157
+ end
158
+ if options[:assume_role] && options[:assume_role].has_key?('name')
159
+ name = options[:assume_role]['name']
160
+ duration = options[:assume_role].has_key?('duration') ? options[:assume_role]['duration'] : 3600
161
+ Stackit.aws.assume_role!(name, duration)
162
+ end
163
+ end
164
+
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,45 @@
1
+ require 'thor'
2
+
3
+ module Stackit
4
+
5
+ class ThorNotifier < Thor
6
+
7
+ include Thor::Actions
8
+
9
+ def initialize(*args)
10
+ super(*args)
11
+ end
12
+
13
+ no_commands do
14
+
15
+ def backtrace(e)
16
+ puts
17
+ puts e.backtrace
18
+ end
19
+
20
+ def error(message)
21
+ say_status 'ERROR', message, :red
22
+ end
23
+
24
+ def success(message)
25
+ say_status 'OK', message
26
+ end
27
+
28
+ def response(response, message = 'Success', respond_to_key = 'stack_id')
29
+ if response.is_a?(::Seahorse::Client::Response)
30
+ if response.respond_to?(respond_to_key)
31
+ success(response.send(respond_to_key))
32
+ else
33
+ success(message)
34
+ end
35
+ else
36
+ error(response)
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ class DefaultNotifier < ThorNotifier; end
44
+
45
+ end
@@ -0,0 +1,268 @@
1
+ module Stackit
2
+ class ManagedStack < Stack
3
+
4
+ attr_accessor :template
5
+ attr_accessor :file_parameters
6
+ attr_accessor :user_defined_parameters
7
+ attr_accessor :parameter_map
8
+ attr_accessor :dry_run
9
+ attr_accessor :depends
10
+ attr_accessor :debug
11
+ attr_accessor :force
12
+ attr_accessor :wait
13
+ attr_accessor :notifier
14
+
15
+ def initialize(options={})
16
+ super(options)
17
+ options = options.to_h.symbolize_keys!
18
+ @template = create_template(options[:template])
19
+ @user_defined_parameters = symbolized_user_defined_parameters(options[:user_defined_parameters])
20
+ @parameter_map = symbolized_parameter_map(options[:parameter_map])
21
+ @stack_name = options[:stack_name] || default_stack_name
22
+ @depends = options[:depends] || []
23
+ @debug = !!options[:debug] || Stackit.debug
24
+ @force = options[:force]
25
+ @wait = options[:wait]
26
+ @dry_run = options[:dry_run]
27
+ @notifier = options[:notifier] || Stackit::ThorNotifier.new
28
+ parse_file_parameters(options[:parameters_file]) if options[:parameters_file]
29
+ end
30
+
31
+ def symbolized_user_defined_parameters(params)
32
+ if params
33
+ params.symbolize_keys!
34
+ else
35
+ {}
36
+ end
37
+ end
38
+
39
+ def symbolized_parameter_map(param_map)
40
+ if param_map
41
+ param_map.symbolize_keys!
42
+ else
43
+ []
44
+ end
45
+ end
46
+
47
+ def default_stack_name
48
+ self.class.name.demodulize
49
+ end
50
+
51
+ def create_template(t)
52
+ template_path = t ? t : File.join(Dir.pwd, 'cloudformation', "#{stack_name.underscore.dasherize}.json")
53
+ return unless File.exist?(template_path)
54
+ template = Template.new(:template_path => template_path)
55
+ template.parse!
56
+ end
57
+
58
+ def parse_file_parameters(parameters_file)
59
+ if File.exist?(parameters_file)
60
+ @file_parameters = {}
61
+ file_params = JSON.parse(File.read(parameters_file))
62
+ file_params.inject(@file_parameters) do |hash, param|
63
+ hash.merge!(param['ParameterKey'].to_sym => param['ParameterValue'])
64
+ end
65
+ end
66
+ end
67
+
68
+ def create!
69
+ begin
70
+ response = cloudformation_request(:create_stack)
71
+ notifier.response(response)
72
+ rescue ::Aws::CloudFormation::Errors::AlreadyExistsException => e
73
+ notifier.backtrace(e) if Stackit.debug
74
+ notifier.error(e.message)
75
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
76
+ notifier.backtrace(e) if Stackit.debug
77
+ notifier.error(e.message)
78
+ end
79
+ response
80
+ end
81
+
82
+ def update!
83
+ begin
84
+ response = cloudformation_request(:update_stack)
85
+ notifier.response(response)
86
+ rescue ::Aws::CloudFormation::Errors::AlreadyExistsException => e
87
+ notifier.backtrace(e) if Stackit.debug
88
+ notifier.error(e.message)
89
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
90
+ if e.message =~ /No updates are to be performed./
91
+ notifier.success(e.message)
92
+ else
93
+ notifier.backtrace(e) if Stackit.debug
94
+ notifier.error(e.message)
95
+ end
96
+ end
97
+ response
98
+ end
99
+
100
+ def delete!
101
+ begin
102
+ response = cloudformation_request(:delete_stack)
103
+ notifier.response(response)
104
+ rescue ::Aws::CloudFormation::Errors::AlreadyExistsException => e
105
+ notifier.backtrace(e) if Stackit.debug
106
+ notifier.error(e.message)
107
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
108
+ notifier.backtrace(e) if Stackit.debug
109
+ notifier.error(e.message)
110
+ end
111
+ response
112
+ end
113
+
114
+ def deploy!
115
+ if !File.exist?(template)
116
+ delete!
117
+ wait_for_stack_to_delete
118
+ notifier.success('Delete successful')
119
+ elsif exists?
120
+ begin
121
+ update!
122
+ wait_for_stack
123
+ notifier.success('Update successful')
124
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
125
+ if e.message.include? "No updates are to be performed"
126
+ Stackit.logger.info "No updates are to be performed"
127
+ elsif e.message.include? "_FAILED state and can not be updated"
128
+ Stackit.logger.info 'Stack is in a failed state and can\'t be updated. Deleting/creating a new stack.'
129
+ delete!
130
+ wait_for_stack_to_delete
131
+ create!
132
+ wait_for_stack
133
+ notifier.success('Stack deleted and re-created')
134
+ else
135
+ raise e
136
+ end
137
+ end
138
+ else
139
+ begin
140
+ create!
141
+ wait_for_stack
142
+ notifier.success('Created successfully')
143
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
144
+ if e.message.include? "_FAILED state and can not be updated"
145
+ Stackit.logger.info 'Stack already exists, is in a failed state, and can\'t be updated. Deleting and creating a new stack.'
146
+ delete!
147
+ wait_for_stack_to_delete
148
+ create!
149
+ wait_for_stack
150
+ notifier.success('Stack deleted and re-created')
151
+ else
152
+ raise e
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def exist?
159
+ describe != nil
160
+ end
161
+
162
+ def describe
163
+ response = cloudformation_request(:describe_stacks)
164
+ if response && response[:stacks]
165
+ response[:stacks].first
166
+ else
167
+ nil
168
+ end
169
+ rescue ::Aws::CloudFormation::Errors::ValidationError
170
+ nil
171
+ end
172
+
173
+ private
174
+
175
+ DRY_RUN_RESPONSE = Struct.new(:stack_id)
176
+ REDACTED_TEXT = '****redacted****'
177
+
178
+ def capabilities
179
+ template.needs_iam_capability? ? ['CAPABILITY_IAM'] : []
180
+ end
181
+
182
+ def cloudformation_request(action)
183
+ Stackit.logger.debug "ManagedStack CloudFormation API request: #{action}"
184
+ cloudformation_options = create_cloudformation_options(action)
185
+ if debug
186
+ Stackit.logger.debug "#{action} request parameters: "
187
+ opts = cloudformation_options.clone
188
+ if opts[:parameters]
189
+ opts[:parameters].each do |param|
190
+ key = param['parameter_key']
191
+ param['parameter_value'] = REDACTED_TEXT if key =~ /username|password/i && !debug
192
+ end
193
+ end
194
+ opts[:template_body] = REDACTED_TEXT if opts[:template_body]
195
+ opts[:stack_policy_body] = JSON.parse(opts[:stack_policy_body]) if opts[:stack_policy_body]
196
+ opts[:stack_policy_during_update_body] =
197
+ JSON.parse(opts[:stack_policy_during_update_body]) if opts[:stack_policy_during_update_body]
198
+ pp opts
199
+ end
200
+ response = dry_run ? dry_run_response : cloudformation.send(action, cloudformation_options)
201
+ wait_for_stack if wait
202
+ response
203
+ end
204
+
205
+ def create_cloudformation_options(action)
206
+ case action
207
+ when :create_stack
208
+ {
209
+ parameters: to_request_parameters(merged_parameters),
210
+ capabilities: capabilities
211
+ }.reverse_merge(template.options).reverse_merge(create_stack_request_params)
212
+ when :update_stack
213
+ {
214
+ parameters: to_request_parameters(merged_parameters),
215
+ capabilities: capabilities
216
+ }.merge(template.options).merge(update_stack_request_params)
217
+ else
218
+ delete_stack_request_params
219
+ end
220
+ end
221
+
222
+ def dependent_parameters
223
+ depends.inject([]) do |arr, dep|
224
+ arr.push(Stackit::Stack.new(stack_name))
225
+ end
226
+ end
227
+
228
+ def merged_parameters
229
+
230
+ parsed_parameters = template.parsed_parameters
231
+ return parsed_parameters unless depends.length
232
+
233
+ validated_parameters = parsed_parameters.clone
234
+
235
+ # merge file parameters
236
+ validated_parameters.merge!(file_parameters) if file_parameters
237
+
238
+ # merge --depends
239
+ depends.each do |dependent_stack_name|
240
+ this_stack = Stack.new({
241
+ cloudformation: cloudformation,
242
+ stack_name: dependent_stack_name
243
+ })
244
+ validated_parameters.select { |param|
245
+ !this_stack[mapped_key(param.to_s)].nil?
246
+ }.each do | param_name, param_value |
247
+ validated_parameters.merge!(param_name => this_stack[mapped_key(param_name)])
248
+ end
249
+ end
250
+
251
+ # merge user defined parameters
252
+ validated_parameters.merge!(user_defined_parameters)
253
+ end
254
+
255
+ def mapped_key(param)
256
+ if parameter_map.has_key?(param.to_sym)
257
+ parameter_map[param.to_sym]
258
+ else
259
+ param.to_sym
260
+ end
261
+ end
262
+
263
+ def dry_run_response
264
+ DRY_RUN_RESPONSE.new('arn:stackit:dry-run:complete')
265
+ end
266
+
267
+ end
268
+ end
@@ -0,0 +1,177 @@
1
+ module Stackit
2
+ class Stack
3
+
4
+ attr_accessor :cloudformation
5
+ attr_accessor :stack_id
6
+ attr_accessor :stack_name
7
+ attr_accessor :description
8
+ attr_accessor :parameters
9
+ attr_accessor :creation_time
10
+ attr_accessor :last_updated_time
11
+ attr_accessor :stack_status
12
+ attr_accessor :stack_status_reason
13
+ attr_accessor :disable_rollback
14
+ attr_accessor :timeout_in_minutes
15
+ attr_accessor :stack_policy_body
16
+ attr_accessor :stack_policy_url
17
+ attr_accessor :stack_policy_during_update_body
18
+ attr_accessor :stack_policy_during_update_url
19
+ attr_accessor :on_failure
20
+ attr_accessor :use_previous_template
21
+ attr_accessor :retain_resources
22
+
23
+ def initialize(options = {})
24
+ options = options.to_h.symbolize_keys!
25
+ @cloudformation = options[:cloudformation] || Stackit.cloudformation
26
+ @stack_name = options[:stack_name]
27
+ @description = options[:description]
28
+ @parameters = options[:parameters]
29
+ @disable_rollback = options[:disable_rollback]
30
+ @notification_arns = options[:notification_arns]
31
+ @timeout_in_minutes = options[:timeout_in_minutes]
32
+ @capabilities = options[:capabilities]
33
+ @tags = options[:tags]
34
+ @on_failure = options[:on_failure]
35
+ @use_previous_template = options[:use_previous_template] || true
36
+ @retain_resources = options[:retain_resources]
37
+ end
38
+
39
+ def [](key)
40
+ parameters[key] || outputs[key] || resources[key]
41
+ end
42
+
43
+ def parameters
44
+ @parameters ||= begin
45
+ stack.parameters.inject({}) do |hash, param|
46
+ hash.merge(param[:parameter_key] => param[:parameter_value])
47
+ end
48
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
49
+ [] if e.message =~ /Stack with id #{stack_name} does not exist/
50
+ end
51
+ end
52
+
53
+ def outputs
54
+ @outputs ||= begin
55
+ stack.outputs.inject({}) do |hash, output|
56
+ hash.merge(output[:output_key] => output[:output_value])
57
+ end
58
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
59
+ [] if e.message =~ /Stack with id #{stack_name} does not exist/
60
+ end
61
+ end
62
+
63
+ def resources
64
+ @resources ||= list_stack_resources.inject({}) do |hash, resource|
65
+ hash.merge(resource[:logical_resource_id] => resource[:physical_resource_id])
66
+ end
67
+ end
68
+
69
+ def notification_arns
70
+ @notification_arns ||= begin
71
+ stack.notification_arns.inject([]) do |arr, arn|
72
+ arr.push(arn)
73
+ end
74
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
75
+ [] if e.message =~ /Stack with id #{stack_name} does not exist/
76
+ end
77
+ end
78
+
79
+ def capabilities
80
+ @capabilities ||= stack.capabilities.inject([]) do |arr, capability|
81
+ arr.push(capability)
82
+ end
83
+ end
84
+
85
+ def tags
86
+ @tags ||= begin
87
+ stack.tags.inject([]) do |arr, tag|
88
+ arr.push(tag)
89
+ end
90
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
91
+ [] if e.message =~ /Stack with id #{stack_name} does not exist/
92
+ end
93
+ end
94
+
95
+ def hydrate!
96
+ @stack_id = stack.stack_id
97
+ @description = stack.description
98
+ @creation_time = stack.creation_time
99
+ @last_updated_time = stack.last_updated_time
100
+ @stack_status = stack.stack_status
101
+ @stack_status_reason = stack.stack_status_reason
102
+ @disable_rollback = stack.disable_rollback
103
+ @timeout_in_minutes = stack.timeout_in_minutes
104
+ @stack_policy_body = stack.stack_policy_body if stack.respond_to?(:stack_policy_body)
105
+ @stack_policy_url = stack.stack_policy_url if stack.respond_to?(:stack_policy_url)
106
+ self
107
+ end
108
+
109
+ def create_stack_request_params
110
+ {
111
+ stack_name: stack_name,
112
+ parameters: to_request_parameters,
113
+ disable_rollback: disable_rollback,
114
+ timeout_in_minutes: timeout_in_minutes,
115
+ notification_arns: notification_arns,
116
+ on_failure: on_failure,
117
+ stack_policy_body: stack_policy_body,
118
+ stack_policy_url: stack_policy_url,
119
+ tags: to_request_tags
120
+ }
121
+ end
122
+
123
+ def update_stack_request_params
124
+ {
125
+ stack_name: stack_name,
126
+ use_previous_template: use_previous_template,
127
+ stack_policy_during_update_body: stack_policy_during_update_body,
128
+ stack_policy_during_update_url: stack_policy_during_update_url,
129
+ parameters: to_request_parameters,
130
+ stack_policy_body: stack_policy_body,
131
+ stack_policy_url: stack_policy_url,
132
+ tags: to_request_tags
133
+ }
134
+ end
135
+
136
+ def delete_stack_request_params
137
+ {
138
+ stack_name: stack_name,
139
+ retain_resources: retain_resources
140
+ }
141
+ end
142
+
143
+ private
144
+
145
+ def stack
146
+ @stack ||= cloudformation.describe_stacks(
147
+ stack_name: stack_name
148
+ )[:stacks].first
149
+ end
150
+
151
+ def list_stack_resources(next_token = nil)
152
+ result = []
153
+ response = cloudformation.list_stack_resources(
154
+ stack_name: stack.stack_id
155
+ ).inject([]) do |arr, page|
156
+ arr.concat(page[:stack_resource_summaries])
157
+ end
158
+ end
159
+
160
+ def to_request_parameters(params = parameters)
161
+ params.map do |k, v|
162
+ if v == :use_previous_value
163
+ { 'parameter_key' => k, 'use_previous_value' => true }
164
+ else
165
+ { 'parameter_key' => k, 'parameter_value' => v || '' }
166
+ end
167
+ end
168
+ end
169
+
170
+ def to_request_tags(tagz = tags)
171
+ tagz.map do |k, v|
172
+ { 'key' => k, 'value' => v || '' }
173
+ end
174
+ end
175
+
176
+ end
177
+ end
@@ -0,0 +1,51 @@
1
+ module Stackit
2
+ class Template
3
+
4
+ attr_accessor :cloudformation
5
+ attr_accessor :template_path
6
+ attr_accessor :options
7
+ attr_accessor :parsed_parameters
8
+
9
+ def initialize(options = {})
10
+ @cloudformation = options[:cloudformation] || Stackit.cloudformation
11
+ @template_path = options[:template_path]
12
+ @options = {}
13
+ end
14
+
15
+ def parse!
16
+ Stackit.logger.info "Parsing cloudformation template: #{template_path}"
17
+ if @template_path =~ /^https:\/\/s3\.amazonaws\.com/
18
+ @options[:template_url] = @template_path
19
+ else
20
+ @options[:template_body] = body
21
+ end
22
+ @options[:parameters] = validate
23
+ self
24
+ end
25
+
26
+ def parsed_parameters
27
+ @parsed_parameters ||= @options[:parameters].inject({}) do |hash, param|
28
+ hash.merge(param['parameter_key'].to_sym => param['default_value'])
29
+ end
30
+ end
31
+
32
+ def needs_iam_capability?
33
+ body =~ /AWS::IAM::AccessKey|AWS::IAM::Group|AWS::IAM::InstanceProfile|AWS::IAM::Policy|AWS::IAM::Role|AWS::IAM::User|AWS::IAM::UserToGroupAddition/ ? true : false
34
+ end
35
+
36
+ private
37
+
38
+ def body
39
+ path = File.exist?(template_path) ? template_path : File.join(Dir.pwd, template_path)
40
+ raise "Unable to stat filesystem template #{template_path}" if !File.exist?(path)
41
+ File.read(path)
42
+ end
43
+
44
+ def validate
45
+ cloudformation.validate_template(
46
+ options.slice(:template_body, :template_url)
47
+ )[:parameters]
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module Stackit
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,71 @@
1
+ module Stackit
2
+
3
+ class WaitError < StandardError; end
4
+
5
+ module Wait
6
+
7
+ def wait_until_stack_info_has_key(key)
8
+ Stackit.logger.debug "Waiting until stack #{stack_name} has key #{key}..."
9
+ wait_for(timeout: 15.minutes) do
10
+ stack = Stack.new(stack_name: stack_name).hydrate!
11
+ raise "Stack failure: #{stack.stack_status}" if stack.stack_status =~ /FAILED/
12
+ if stack[key]
13
+ yield stack if block_given?
14
+ true
15
+ end
16
+ end
17
+ end
18
+
19
+ def wait_for_stack(status_pattern = /COMPLETE/)
20
+ Stackit.logger.debug "Waiting for stack #{stack_name} to complete..."
21
+ wait_for(timeout: 15.minutes) do
22
+ stack = Stack.new(stack_name: stack_name).hydrate!
23
+ case stack.stack_status
24
+ when /FAILED/
25
+ raise WaitError, "Stack failed during wait: #{stack_name}"
26
+ when status_pattern
27
+ yield stack if block_given?
28
+ true
29
+ else
30
+ false
31
+ end
32
+ end
33
+ end
34
+
35
+ def wait_for_stack_to_delete
36
+ Stackit.logger.debug "Waiting for stack #{stack_name} to delete..."
37
+ wait_for(timeout: 15.minutes) do
38
+ begin
39
+ stack = Stack.new(stack_name: stack_name).hydrate!
40
+ if stack.nil?
41
+ yield stack if block_given?
42
+ true
43
+ else
44
+ false
45
+ end
46
+ rescue Aws::CloudFormation::Errors::ValidationError => e
47
+ if e.message.include?("does not exist")
48
+ true
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def wait_for(opts={})
57
+ raise ArgumentError, 'block expected' unless block_given?
58
+ timeout = opts[:timeout] || 600
59
+ interval = opts[:interval] || 10
60
+ attempts = timeout / interval
61
+ Stackit.logger.debug "Timeout: #{timeout}"
62
+ Stackit.logger.debug "Attempts: #{attempts}"
63
+ actual_attempts = attempts.times do
64
+ break if yield
65
+ sleep interval
66
+ end
67
+ actual_attempts != attempts
68
+ end
69
+
70
+ end
71
+ end
data/lib/stackit.rb ADDED
@@ -0,0 +1,43 @@
1
+ require "logger"
2
+ require "pp"
3
+ require "stackit/version"
4
+ require 'json'
5
+ require "active_support"
6
+ require "active_support/all"
7
+ require "awsclient"
8
+ require "stackit/aws"
9
+ require "stackit/template"
10
+ require "stackit/stack/default_notifier"
11
+ require "stackit/stack"
12
+ require "stackit/stack/managed_stack"
13
+
14
+ module Stackit
15
+ class << self
16
+
17
+ attr_accessor :aws
18
+ attr_accessor :cloudformation
19
+ attr_accessor :environment
20
+ attr_accessor :debug
21
+
22
+ def aws
23
+ @aws ||= Stackit::Aws.new
24
+ end
25
+
26
+ def cloudformation
27
+ @cloudformation ||= Stackit::Aws.new.cloudformation
28
+ end
29
+
30
+ def logger
31
+ @logger ||= Logger.new(STDOUT)
32
+ end
33
+
34
+ def environment
35
+ @environment ||= :default
36
+ end
37
+
38
+ def debug
39
+ @debug ||= false
40
+ end
41
+
42
+ end
43
+ end
data/stackit.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'stackit/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "stackit"
7
+ spec.version = Stackit::VERSION
8
+ spec.authors = ["Jeremy Hahn"]
9
+ spec.email = ["mail@jeremyhahn.com"]
10
+
11
+ spec.summary = %q{Simple, elegant CloudFormation dependency management.}
12
+ spec.description = %q{Use existing stack values (output, resource, or parameters) as input parmeters to templates.}
13
+ spec.homepage = "https://github.com/jeremyhahn/stackit"
14
+ spec.license = "GPLv3"
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ else
21
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.11"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency 'pry-byebug', '~> 2.0'
33
+
34
+ spec.add_runtime_dependency 'awsclient'
35
+ spec.add_runtime_dependency 'aws-sdk', '~> 2'
36
+ spec.add_runtime_dependency 'thor', '~> 0.19'
37
+ spec.add_runtime_dependency 'activesupport', '~> 4.2'
38
+
39
+ end
40
+
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stackit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Hahn
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-31 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.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
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: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: awsclient
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: aws-sdk
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2'
90
+ type: :runtime
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: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.19'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.19'
111
+ - !ruby/object:Gem::Dependency
112
+ name: activesupport
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '4.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '4.2'
125
+ description: Use existing stack values (output, resource, or parameters) as input
126
+ parmeters to templates.
127
+ email:
128
+ - mail@jeremyhahn.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - ".travis.yml"
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/setup
142
+ - bin/stackit
143
+ - lib/stackit.rb
144
+ - lib/stackit/aws.rb
145
+ - lib/stackit/cli.rb
146
+ - lib/stackit/stack.rb
147
+ - lib/stackit/stack/default_notifier.rb
148
+ - lib/stackit/stack/managed_stack.rb
149
+ - lib/stackit/template.rb
150
+ - lib/stackit/version.rb
151
+ - lib/stackit/wait.rb
152
+ - stackit.gemspec
153
+ homepage: https://github.com/jeremyhahn/stackit
154
+ licenses:
155
+ - GPLv3
156
+ metadata:
157
+ allowed_push_host: https://rubygems.org
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.4.8
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Simple, elegant CloudFormation dependency management.
178
+ test_files: []