stackit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []