kumo_keisei 2.2.2 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/VERSION +1 -1
- data/lib/kumo_keisei/cloud_formation_stack.rb +1 -0
- data/lib/kumo_keisei/environment_config.rb +9 -14
- data/lib/kumo_keisei/stack.rb +202 -0
- data/lib/kumo_keisei.rb +1 -0
- data/spec/lib/kumo_keisei/environment_config_spec.rb +4 -9
- data/spec/lib/kumo_keisei/stack_spec.rb +256 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 315a98c6f6409683fbcefc7524da69603174329d
|
4
|
+
data.tar.gz: 6a29b5767d2cf57cc51010febadea53f50276da1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc9a9d975f1937830c862105103f038315aef461f027d7081f6eb2709d04dda7f3945cf1a2b7cd0a10acedafae923fdc942881a6457cab6deb6f0119d3b6874d
|
7
|
+
data.tar.gz: 072aa7298226aa5aad8ec2a4de8b5adfea6101fa100ae4de0e1d2467db551859fe2d05c8b4022051ba0cec3190392983fa224e4b464a7b5f3b011654cf13a96a
|
data/README.md
CHANGED
@@ -54,8 +54,8 @@ This gem is tested with Ruby (MRI) versions 1.9.3 and 2.2.3.
|
|
54
54
|
Changes to the gem can be manually tested end to end in a project that uses the gem (i.e. http-wala).
|
55
55
|
|
56
56
|
1. First start the dev-tools container: `kumo tools debug non-production`
|
57
|
+
1. gem install specific_install
|
57
58
|
1. Re-install the gem: `gem specific_install https://github.com/redbubble/kumo_keisei_gem.git -b <your_branch>`
|
58
59
|
1. Fire up a console: `irb`
|
59
60
|
1. Require the gem: `require "kumo_keisei"`
|
60
|
-
1. Interact with the gem's classes. `KumoKeisei::
|
61
|
-
|
61
|
+
1. Interact with the gem's classes. `KumoKeisei::Stack.new(...).apply!`
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.1
|
@@ -31,6 +31,7 @@ module KumoKeisei
|
|
31
31
|
self.new(stack_name, nil).exists?
|
32
32
|
end
|
33
33
|
|
34
|
+
# <b>DEPRECATED:</b> Please use <tt>Stack</tt> class instead of the <tt>CloudFormationStack</tt> class.
|
34
35
|
def initialize(stack_name, stack_template, stack_params_filepath = nil, confirmation_timeout=30)
|
35
36
|
@stack_name = stack_name
|
36
37
|
@stack_template = stack_template
|
@@ -13,12 +13,11 @@ module KumoKeisei
|
|
13
13
|
attr_reader :app_name, :env_name
|
14
14
|
|
15
15
|
def initialize(options, logger = LOGGER)
|
16
|
-
@app_name = options[:app_name]
|
17
16
|
@env_name = options[:env_name]
|
18
|
-
@config_dir_path = options[:config_dir_path]
|
19
17
|
@params_template_file_path = options[:params_template_file_path]
|
20
18
|
@injected_config = options[:injected_config] || {}
|
21
|
-
@
|
19
|
+
@config_file_loader = KumoKeisei::FileLoader.new(config_dir_path: options[:config_path])
|
20
|
+
@template_file_loader = KumoKeisei::FileLoader.new(config_dir_path: File.dirname(options[:params_template_file_path]))
|
22
21
|
|
23
22
|
@log = logger
|
24
23
|
end
|
@@ -50,10 +49,6 @@ module KumoKeisei
|
|
50
49
|
KumoKeisei::ParameterBuilder.new(stack_params).params
|
51
50
|
end
|
52
51
|
|
53
|
-
def get_binding
|
54
|
-
binding
|
55
|
-
end
|
56
|
-
|
57
52
|
private
|
58
53
|
|
59
54
|
def kms
|
@@ -62,7 +57,7 @@ module KumoKeisei
|
|
62
57
|
|
63
58
|
def params
|
64
59
|
return nil unless @params_template_file_path
|
65
|
-
@
|
60
|
+
@template_file_loader.load_erb(File.basename(@params_template_file_path))
|
66
61
|
end
|
67
62
|
|
68
63
|
def decrypt_secrets(secrets)
|
@@ -100,29 +95,29 @@ module KumoKeisei
|
|
100
95
|
end
|
101
96
|
|
102
97
|
def encrypted_common_secrets
|
103
|
-
@
|
98
|
+
@config_file_loader.load_hash('common_secrets.yml')
|
104
99
|
end
|
105
100
|
|
106
101
|
def encrypted_env_secrets
|
107
|
-
secrets = @
|
102
|
+
secrets = @config_file_loader.load_hash(env_secrets_file_name)
|
108
103
|
|
109
104
|
if !secrets.empty?
|
110
105
|
secrets
|
111
106
|
else
|
112
|
-
@
|
107
|
+
@config_file_loader.load_hash('development_secrets.yml')
|
113
108
|
end
|
114
109
|
end
|
115
110
|
|
116
111
|
def common_config
|
117
|
-
@
|
112
|
+
@config_file_loader.load_hash('common.yml')
|
118
113
|
end
|
119
114
|
|
120
115
|
def env_config
|
121
|
-
config = @
|
116
|
+
config = @config_file_loader.load_hash(env_config_file_name)
|
122
117
|
if !config.empty?
|
123
118
|
config
|
124
119
|
else
|
125
|
-
@
|
120
|
+
@config_file_loader.load_hash('development.yml')
|
126
121
|
end
|
127
122
|
end
|
128
123
|
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module KumoKeisei
|
4
|
+
class Stack
|
5
|
+
class CreateError < StandardError; end
|
6
|
+
class UpdateError < StandardError; end
|
7
|
+
|
8
|
+
UPDATEABLE_STATUSES = [
|
9
|
+
'UPDATE_ROLLBACK_COMPLETE',
|
10
|
+
'CREATE_COMPLETE',
|
11
|
+
'UPDATE_COMPLETE'
|
12
|
+
]
|
13
|
+
|
14
|
+
RECOVERABLE_STATUSES = [
|
15
|
+
'DELETE_COMPLETE',
|
16
|
+
'ROLLBACK_COMPLETE',
|
17
|
+
'ROLLBACK_FAILED'
|
18
|
+
]
|
19
|
+
|
20
|
+
UNRECOVERABLE_STATUSES = [
|
21
|
+
'UPDATE_ROLLBACK_FAILED'
|
22
|
+
]
|
23
|
+
|
24
|
+
attr_reader :stack_name, :env_name
|
25
|
+
|
26
|
+
def self.exists?(app_name, environment_name)
|
27
|
+
self.new(app_name, environment_name).exists?
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(app_name, environment_name, options = { confirmation_timeout: 30, waiter_delay: 20, waiter_attempts: 90} )
|
31
|
+
@app_name = app_name
|
32
|
+
@env_name = environment_name
|
33
|
+
@stack_name = "#{app_name}-#{ environment_name }"
|
34
|
+
@confirmation_timeout = options[:confirmation_timeout]
|
35
|
+
@waiter_delay = options[:waiter_delay]
|
36
|
+
@waiter_attempts = options[:waiter_attempts]
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply!(stack_config)
|
40
|
+
stack_config.merge!(env_name: @env_name)
|
41
|
+
if updatable?
|
42
|
+
update!(stack_config)
|
43
|
+
else
|
44
|
+
ensure_deleted!
|
45
|
+
ConsoleJockey.write_line "Creating your new stack #{@stack_name}"
|
46
|
+
create!(stack_config)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy!
|
51
|
+
return if get_stack.nil?
|
52
|
+
|
53
|
+
flash_message "Warning! You are about to delete the CloudFormation Stack #{@stack_name}, enter 'yes' to continue."
|
54
|
+
return unless ConsoleJockey.get_confirmation(@confirmation_timeout)
|
55
|
+
|
56
|
+
wait_until_ready(false)
|
57
|
+
ensure_deleted!
|
58
|
+
end
|
59
|
+
|
60
|
+
def outputs(output)
|
61
|
+
stack = get_stack
|
62
|
+
return nil if stack.nil?
|
63
|
+
outputs_hash = stack.outputs.reduce({}) { |acc, o| acc.merge(o.output_key.to_s => o.output_value) }
|
64
|
+
|
65
|
+
outputs_hash[output]
|
66
|
+
end
|
67
|
+
|
68
|
+
def logical_resource(resource_name)
|
69
|
+
response = cloudformation.describe_stack_resource(stack_name: @stack_name, logical_resource_id: resource_name)
|
70
|
+
stack_resource = response.stack_resource_detail
|
71
|
+
stack_resource.each_pair.reduce({}) {|acc, (k, v)| acc.merge(transform_logical_resource_id(k) => v) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def exists?
|
75
|
+
!get_stack.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def transform_logical_resource_id(id)
|
81
|
+
id.to_s.split('_').map {|w| w.capitalize }.join
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_stack(options={})
|
85
|
+
@stack = nil if options[:dump_cache]
|
86
|
+
|
87
|
+
@stack ||= cloudformation.describe_stacks(stack_name: @stack_name).stacks.find { |stack| stack.stack_name == @stack_name }
|
88
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def cloudformation
|
93
|
+
@cloudformation ||= Aws::CloudFormation::Client.new
|
94
|
+
end
|
95
|
+
|
96
|
+
def ensure_deleted!
|
97
|
+
stack = get_stack
|
98
|
+
return if stack.nil?
|
99
|
+
return if stack.stack_status == 'DELETE_COMPLETE'
|
100
|
+
|
101
|
+
ConsoleJockey.write_line "There's a previous stack called #{@stack_name} that didn't create properly, I'll clean it up for you..."
|
102
|
+
cloudformation.delete_stack(stack_name: @stack_name)
|
103
|
+
cloudformation.wait_until(:stack_delete_complete, stack_name: @stack_name) { |waiter| waiter.delay = @waiter_delay; waiter.max_attempts = @waiter_attempts }
|
104
|
+
end
|
105
|
+
|
106
|
+
def updatable?
|
107
|
+
stack = get_stack
|
108
|
+
return false if stack.nil?
|
109
|
+
|
110
|
+
return true if UPDATEABLE_STATUSES.include? stack.stack_status
|
111
|
+
return false if RECOVERABLE_STATUSES.include? stack.stack_status
|
112
|
+
raise UpdateError.new("Stack is in an unrecoverable state") if UNRECOVERABLE_STATUSES.include? stack.stack_status
|
113
|
+
raise UpdateError.new("Stack is busy, try again soon")
|
114
|
+
end
|
115
|
+
|
116
|
+
def create!(stack_config)
|
117
|
+
raise StackValidationError.new("The stack name needs to be 32 characters or shorter") if @stack_name.length > 32
|
118
|
+
|
119
|
+
params_template_path = File.absolute_path(File.join(File.dirname(stack_config[:template_path]), "#{@app_name}.yml.erb"))
|
120
|
+
config = EnvironmentConfig.new(stack_config.merge(params_template_file_path: params_template_path))
|
121
|
+
|
122
|
+
cloudformation.create_stack(
|
123
|
+
stack_name: @stack_name,
|
124
|
+
template_body: File.read(stack_config[:template_path]),
|
125
|
+
parameters: config.cf_params,
|
126
|
+
capabilities: ["CAPABILITY_IAM"],
|
127
|
+
on_failure: "DELETE"
|
128
|
+
)
|
129
|
+
|
130
|
+
begin
|
131
|
+
cloudformation.wait_until(:stack_create_complete, stack_name: @stack_name) { |waiter| waiter.delay = @waiter_delay; waiter.max_attempts = @waiter_attempts }
|
132
|
+
rescue Aws::Waiters::Errors::UnexpectedError => ex
|
133
|
+
handle_unexpected_error(ex)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def update!(stack_config)
|
138
|
+
params_template_path = File.absolute_path(File.join(File.dirname(stack_config[:template_path]), "#{@app_name}.yml.erb"))
|
139
|
+
config = EnvironmentConfig.new(stack_config.merge(params_template_file_path: params_template_path))
|
140
|
+
wait_until_ready(false)
|
141
|
+
|
142
|
+
cloudformation.update_stack(
|
143
|
+
stack_name: @stack_name,
|
144
|
+
template_body: File.read(stack_config[:template_path]),
|
145
|
+
parameters: config.cf_params,
|
146
|
+
capabilities: ["CAPABILITY_IAM"]
|
147
|
+
)
|
148
|
+
|
149
|
+
cloudformation.wait_until(:stack_update_complete, stack_name: @stack_name) { |waiter| waiter.delay = @waiter_delay; waiter.max_attempts = @waiter_attempts }
|
150
|
+
rescue Aws::CloudFormation::Errors::ValidationError => ex
|
151
|
+
raise ex unless ex.message == "No updates are to be performed."
|
152
|
+
ConsoleJockey.write_line "No changes need to be applied for #{@stack_name}."
|
153
|
+
rescue Aws::Waiters::Errors::FailureStateError
|
154
|
+
ConsoleJockey.write_line "Failed to apply the environment update. The stack has been rolled back. It is still safe to apply updates."
|
155
|
+
ConsoleJockey.write_line "Find error details in the AWS CloudFormation console: #{stack_events_url}"
|
156
|
+
raise UpdateError.new("Stack update failed for #{@stack_name}.")
|
157
|
+
end
|
158
|
+
|
159
|
+
def stack_events_url
|
160
|
+
"https://console.aws.amazon.com/cloudformation/home?region=#{ENV['AWS_DEFAULT_REGION']}#/stacks?filter=active&tab=events&stackId=#{get_stack.stack_id}"
|
161
|
+
end
|
162
|
+
|
163
|
+
def wait_until_ready(raise_on_error=true)
|
164
|
+
loop do
|
165
|
+
stack = get_stack(dump_cache: true)
|
166
|
+
|
167
|
+
if stack_ready?(stack.stack_status)
|
168
|
+
if raise_on_error && stack_operation_failed?(stack.stack_status)
|
169
|
+
raise stack.stack_status
|
170
|
+
end
|
171
|
+
|
172
|
+
break
|
173
|
+
end
|
174
|
+
puts "waiting for #{@stack_name} to be READY, current: #{last_event_status}"
|
175
|
+
sleep 10
|
176
|
+
end
|
177
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
|
181
|
+
def stack_ready?(last_event_status)
|
182
|
+
last_event_status =~ /COMPLETE/ || last_event_status =~ /ROLLBACK_FAILED/
|
183
|
+
end
|
184
|
+
|
185
|
+
def stack_operation_failed?(last_event_status)
|
186
|
+
last_event_status =~ /ROLLBACK/
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_unexpected_error(error)
|
190
|
+
if error.message =~ /does not exist/
|
191
|
+
ConsoleJockey.write_line "There was an error during stack creation for #{@stack_name}, and the stack has been cleaned up."
|
192
|
+
raise CreateError.new("There was an error during stack creation. The stack has been deleted.")
|
193
|
+
else
|
194
|
+
raise error
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def flash_message(message)
|
199
|
+
ConsoleJockey.flash_message(message)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
data/lib/kumo_keisei.rb
CHANGED
@@ -11,8 +11,9 @@ describe KumoKeisei::EnvironmentConfig do
|
|
11
11
|
}
|
12
12
|
end
|
13
13
|
let(:file_loader) { instance_double(KumoKeisei::FileLoader) }
|
14
|
+
let(:file_loader_cloudformation) { instance_double(KumoKeisei::FileLoader) }
|
14
15
|
let(:parameters) { ERB.new("") }
|
15
|
-
let(:params_template_file_path) { '
|
16
|
+
let(:params_template_file_path) { 'junk.txt' }
|
16
17
|
let(:environment_config_file_name) { "#{env_name}.yml" }
|
17
18
|
let(:kms) { instance_double(KumoKi::KMS) }
|
18
19
|
let(:logger) { double(:test_logger, debug: nil) }
|
@@ -22,19 +23,12 @@ describe KumoKeisei::EnvironmentConfig do
|
|
22
23
|
allow(KumoKeisei::FileLoader).to receive(:new).and_return(file_loader)
|
23
24
|
allow(KumoKi::KMS).to receive(:new).and_return(kms)
|
24
25
|
allow(file_loader).to receive(:load_erb).with(params_template_file_path).and_return(parameters)
|
26
|
+
allow(File).to receive(:dirname).and_return('/tmp')
|
25
27
|
end
|
26
28
|
|
27
29
|
context 'unit tests' do
|
28
30
|
let(:fake_environment_binding) { binding }
|
29
31
|
|
30
|
-
describe '#get_binding' do
|
31
|
-
subject { environment_config.get_binding }
|
32
|
-
|
33
|
-
it 'returns a binding' do
|
34
|
-
expect(subject).to be_a(Binding)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
32
|
describe '#cf_params' do
|
39
33
|
subject { environment_config.cf_params }
|
40
34
|
|
@@ -207,6 +201,7 @@ describe KumoKeisei::EnvironmentConfig do
|
|
207
201
|
end
|
208
202
|
|
209
203
|
context 'integration tests' do
|
204
|
+
|
210
205
|
describe '#cf_params' do
|
211
206
|
subject { environment_config.cf_params }
|
212
207
|
|
@@ -0,0 +1,256 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
describe KumoKeisei::Stack do
|
5
|
+
|
6
|
+
def stack_result_list_with_status(status, stack_name)
|
7
|
+
stack = OpenStruct.new(stack_status: status, stack_name: stack_name)
|
8
|
+
OpenStruct.new(stacks: [stack])
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:app_name) { "my-stack" }
|
12
|
+
let(:environment_name) { 'non-production' }
|
13
|
+
let(:stack_name) { "#{app_name}-#{environment_name}" }
|
14
|
+
let(:stack_template_path) { "#{app_name}-#{environment_name}.json" }
|
15
|
+
let(:file_params_path) { nil }
|
16
|
+
let(:cloudformation) { instance_double(Aws::CloudFormation::Client) }
|
17
|
+
let(:happy_stack_status) { "CREATE_COMPLETE" }
|
18
|
+
let(:cf_stack) { stack_result_list_with_status(happy_stack_status, stack_name) }
|
19
|
+
let(:parameter_builder) { instance_double(KumoKeisei::ParameterBuilder, params: {}) }
|
20
|
+
let(:stack_template_body) { double(:stack_template_body) }
|
21
|
+
let(:cf_stack_update_params) do
|
22
|
+
{
|
23
|
+
stack_name: stack_name,
|
24
|
+
template_body: stack_template_body,
|
25
|
+
parameters: {},
|
26
|
+
capabilities: ["CAPABILITY_IAM"]
|
27
|
+
}
|
28
|
+
end
|
29
|
+
let(:cf_stack_create_params) do
|
30
|
+
cf_stack_update_params.merge(on_failure: "DELETE")
|
31
|
+
end
|
32
|
+
let(:confirmation_timeout) { 30 }
|
33
|
+
subject(:instance) { KumoKeisei::Stack.new(app_name, environment_name) }
|
34
|
+
let(:stack_config) {
|
35
|
+
{
|
36
|
+
config_path: 'config-path',
|
37
|
+
template_path: stack_template_path,
|
38
|
+
injected_config: { 'VpcId' => 'vpc-id' },
|
39
|
+
env_name: 'non-production'
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
before do
|
44
|
+
allow(KumoKeisei::ConsoleJockey).to receive(:flash_message)
|
45
|
+
allow(KumoKeisei::ConsoleJockey).to receive(:write_line).and_return(nil)
|
46
|
+
allow(KumoKeisei::ConsoleJockey).to receive(:get_confirmation).with(confirmation_timeout).and_return(false)
|
47
|
+
allow(Aws::CloudFormation::Client).to receive(:new).and_return(cloudformation)
|
48
|
+
allow(cloudformation).to receive(:describe_stacks).with({stack_name: stack_name}).and_return(cf_stack)
|
49
|
+
allow(KumoKeisei::ParameterBuilder).to receive(:new).and_return(parameter_builder)
|
50
|
+
allow(File).to receive(:read).with(stack_template_path).and_return(stack_template_body)
|
51
|
+
allow(KumoKeisei::EnvironmentConfig).to receive(:new).with(stack_config.merge(params_template_file_path: "#{app_name}.yml.erb")).and_return(double(:environment_config, cf_params: {}))
|
52
|
+
allow(File).to receive(:absolute_path).and_return("#{app_name}.yml.erb")
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#destroy!" do
|
56
|
+
it "notifies the user of what it is about to delete" do
|
57
|
+
expect(KumoKeisei::ConsoleJockey).to receive(:flash_message).with("Warning! You are about to delete the CloudFormation Stack #{stack_name}, enter 'yes' to continue.")
|
58
|
+
subject.destroy!
|
59
|
+
end
|
60
|
+
|
61
|
+
it "does delete the stack if the user confirms" do
|
62
|
+
expect(KumoKeisei::ConsoleJockey).to receive(:get_confirmation).with(confirmation_timeout).and_return(true)
|
63
|
+
expect(cloudformation).to receive(:delete_stack).with({stack_name: stack_name}).and_return(cf_stack)
|
64
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_delete_complete, stack_name: stack_name).and_return(nil)
|
65
|
+
subject.destroy!
|
66
|
+
end
|
67
|
+
|
68
|
+
it "does not delete the stack if the the user refuses confirmation" do
|
69
|
+
expect(KumoKeisei::ConsoleJockey).to receive(:get_confirmation).with(confirmation_timeout).and_return(false)
|
70
|
+
subject.destroy!
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#apply!" do
|
76
|
+
context "when the stack is updatable" do
|
77
|
+
UPDATEABLE_STATUSES = ['UPDATE_ROLLBACK_COMPLETE', 'CREATE_COMPLETE', 'UPDATE_COMPLETE']
|
78
|
+
|
79
|
+
context "when the stack has changed" do
|
80
|
+
before do
|
81
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_update_complete, stack_name: stack_name).and_return(nil)
|
82
|
+
end
|
83
|
+
|
84
|
+
UPDATEABLE_STATUSES.each do |stack_status|
|
85
|
+
it "updates the stack when in #{stack_status} status" do
|
86
|
+
allow(cloudformation).to receive(:describe_stacks).with({stack_name: stack_name}).and_return(
|
87
|
+
stack_result_list_with_status(stack_status, stack_name),
|
88
|
+
stack_result_list_with_status("UPDATE_COMPLETE", stack_name)
|
89
|
+
)
|
90
|
+
expect(cloudformation).to receive(:update_stack).with(cf_stack_update_params).and_return("stack_id")
|
91
|
+
subject.apply!(stack_config)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it "politely informs the user of any failures" do
|
96
|
+
allow(cloudformation).to receive(:wait_until)
|
97
|
+
.with(:stack_update_complete, stack_name: stack_name)
|
98
|
+
.and_raise(Aws::Waiters::Errors::FailureStateError.new(""))
|
99
|
+
|
100
|
+
allow(cloudformation).to receive(:update_stack).with(cf_stack_update_params).and_return("stack_id")
|
101
|
+
expect(KumoKeisei::ConsoleJockey).to receive(:write_line).with("Failed to apply the environment update. The stack has been rolled back. It is still safe to apply updates.")
|
102
|
+
|
103
|
+
expect { subject.apply!(stack_config) }.to raise_error(KumoKeisei::Stack::UpdateError)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "when the stack has not changed" do
|
108
|
+
let(:error) { Aws::CloudFormation::Errors::ValidationError.new('', 'No updates are to be performed.') }
|
109
|
+
|
110
|
+
UPDATEABLE_STATUSES.each do |stack_status|
|
111
|
+
it "reports that nothing has changed when in #{stack_status} status" do
|
112
|
+
allow(cloudformation).to receive(:describe_stacks)
|
113
|
+
.with({stack_name: stack_name})
|
114
|
+
.and_return(stack_result_list_with_status(stack_status, stack_name))
|
115
|
+
|
116
|
+
expect(cloudformation).to receive(:update_stack).with(cf_stack_update_params).and_raise(error)
|
117
|
+
|
118
|
+
subject.apply!(stack_config)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context "when the stack is not updatable" do
|
125
|
+
before do
|
126
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_delete_complete, stack_name: stack_name).and_return(nil)
|
127
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_create_complete, stack_name: stack_name).and_return(nil)
|
128
|
+
allow(cloudformation).to receive(:delete_stack).with(stack_name: stack_name)
|
129
|
+
end
|
130
|
+
|
131
|
+
context "and the stack has status DELETE_COMPLETE" do
|
132
|
+
|
133
|
+
it "creates the stack and does not attempt to delete the stack" do
|
134
|
+
expect(cloudformation).not_to receive(:delete_stack)
|
135
|
+
allow(cloudformation).to receive(:describe_stacks).with(stack_name: stack_name).and_return(stack_result_list_with_status('DELETE_COMPLETE', stack_name))
|
136
|
+
expect(cloudformation).to receive(:create_stack).with(cf_stack_create_params)
|
137
|
+
subject.apply!(stack_config)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "and the stack does not exist" do
|
142
|
+
it "creates the stack" do
|
143
|
+
allow(cloudformation).to receive(:delete_stack)
|
144
|
+
allow(cloudformation).to receive(:describe_stacks).with(stack_name: stack_name).and_raise(Aws::CloudFormation::Errors::ValidationError.new('',''))
|
145
|
+
expect(cloudformation).to receive(:create_stack).with(cf_stack_create_params)
|
146
|
+
subject.apply!(stack_config)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "shows a friendly error message if the stack had issues during creation" do
|
150
|
+
@call_count = 0
|
151
|
+
|
152
|
+
allow(cloudformation).to receive(:delete_stack)
|
153
|
+
allow(cloudformation).to receive(:describe_stacks).with(stack_name: stack_name) do
|
154
|
+
@call_count += 1
|
155
|
+
|
156
|
+
raise Aws::CloudFormation::Errors::ValidationError.new('','') if @call_count > 1
|
157
|
+
|
158
|
+
OpenStruct.new(stacks: [])
|
159
|
+
end
|
160
|
+
allow(cloudformation).to receive(:create_stack).with(cf_stack_create_params)
|
161
|
+
|
162
|
+
error = Aws::Waiters::Errors::UnexpectedError.new(RuntimeError.new("Stack with id #{stack_name} does not exist"))
|
163
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_create_complete, stack_name: stack_name).and_raise(error)
|
164
|
+
|
165
|
+
expect(KumoKeisei::ConsoleJockey).to receive(:write_line).with(/There was an error during stack creation for #{stack_name}, and the stack has been cleaned up./).and_return nil
|
166
|
+
expect { subject.apply!(stack_config) }.to raise_error(KumoKeisei::Stack::CreateError)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "and the stack is in ROLLBACK_COMPLETE, or ROLLBACK_FAILED" do
|
171
|
+
it "deletes the dead stack and creates a new one" do
|
172
|
+
allow(cloudformation).to receive(:describe_stacks).and_return(stack_result_list_with_status("ROLLBACK_COMPLETE", stack_name))
|
173
|
+
|
174
|
+
expect(cloudformation).to receive(:delete_stack).with(stack_name: stack_name)
|
175
|
+
expect(cloudformation).to receive(:create_stack)
|
176
|
+
|
177
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_create_complete, stack_name: stack_name).and_return(nil)
|
178
|
+
|
179
|
+
subject.apply!(stack_config)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context "and the stack in in UPDATE_ROLLBACK_FAILED" do
|
184
|
+
it "should blow up" do
|
185
|
+
allow(cloudformation).to receive(:describe_stacks).and_return(stack_result_list_with_status("UPDATE_ROLLBACK_FAILED", stack_name))
|
186
|
+
|
187
|
+
expect { subject.apply!(stack_config) }.to raise_error("Stack is in an unrecoverable state")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context "and the stack is busy" do
|
192
|
+
it "should blow up" do
|
193
|
+
allow(cloudformation).to receive(:describe_stacks).and_return(stack_result_list_with_status("UPDATE_IN_PROGRESS", stack_name))
|
194
|
+
|
195
|
+
expect { subject.apply!(stack_config) }.to raise_error("Stack is busy, try again soon")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
it "accepts short stack names" do
|
200
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_update_complete, stack_name: stack_name)
|
201
|
+
allow(cloudformation).to receive(:update_stack)
|
202
|
+
|
203
|
+
subject.apply!(stack_config)
|
204
|
+
end
|
205
|
+
|
206
|
+
context "a stack name that is too long" do
|
207
|
+
let(:app_name) { "this-will-create-a-very-long-stack-name-that-will-break-aws" }
|
208
|
+
|
209
|
+
it "blows up since the ELB names have to be 32 or shorter" do
|
210
|
+
allow(cloudformation).to receive(:wait_until).with(:stack_update_complete, stack_name: stack_name)
|
211
|
+
allow(cloudformation).to receive(:update_stack)
|
212
|
+
allow(subject).to receive(:updatable?).and_return(false)
|
213
|
+
|
214
|
+
expect { subject.apply!(stack_config) }.to raise_error(KumoKeisei::StackValidationError, "The stack name needs to be 32 characters or shorter")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe "#outputs" do
|
220
|
+
context 'when the stack exists' do
|
221
|
+
let(:output) { double(:output, output_key: "Key", output_value: "Value") }
|
222
|
+
let(:stack) { double(:stack, stack_name: stack_name, outputs: [output])}
|
223
|
+
let(:stack_result) { double(:stack_result, stacks: [stack]) }
|
224
|
+
|
225
|
+
it "returns the outputs given by CloudFormation" do
|
226
|
+
allow(cloudformation).to receive(:describe_stacks).and_return(stack_result)
|
227
|
+
expect(subject.outputs("Key")).to eq("Value")
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context 'when the stack does not exist' do
|
232
|
+
it 'returns nil' do
|
233
|
+
allow(subject).to receive(:get_stack).and_return(nil)
|
234
|
+
expect(subject.outputs('Key')).to be_nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe "#logical_resource" do
|
240
|
+
let(:stack_resource_detail) { OpenStruct.new(logical_resource_id: "with-a-fox", physical_resource_id: "i-am-sam", resource_type: "green-eggs-and-ham")}
|
241
|
+
let(:response) { double(:response, stack_resource_detail: stack_resource_detail) }
|
242
|
+
let(:stack_resource_name) { "with-a-fox" }
|
243
|
+
|
244
|
+
it "returns a hash of the stack resource detail params" do
|
245
|
+
allow(cloudformation).to receive(:describe_stack_resource)
|
246
|
+
.with(stack_name: stack_name, logical_resource_id: stack_resource_name)
|
247
|
+
.and_return(response)
|
248
|
+
|
249
|
+
expect(subject.logical_resource(stack_resource_name)).to include(
|
250
|
+
"PhysicalResourceId" => "i-am-sam",
|
251
|
+
"ResourceType" => "green-eggs-and-ham"
|
252
|
+
)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumo_keisei
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Redbubble
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -104,6 +104,7 @@ files:
|
|
104
104
|
- lib/kumo_keisei/errors.rb
|
105
105
|
- lib/kumo_keisei/file_loader.rb
|
106
106
|
- lib/kumo_keisei/parameter_builder.rb
|
107
|
+
- lib/kumo_keisei/stack.rb
|
107
108
|
- script/build.sh
|
108
109
|
- script/release-gem
|
109
110
|
- script/unit_test.sh
|
@@ -112,6 +113,7 @@ files:
|
|
112
113
|
- spec/lib/kumo_keisei/environment_config_spec.rb
|
113
114
|
- spec/lib/kumo_keisei/file_loader_spec.rb
|
114
115
|
- spec/lib/kumo_keisei/parameter_builder_spec.rb
|
116
|
+
- spec/lib/kumo_keisei/stack_spec.rb
|
115
117
|
- spec/spec_helper.rb
|
116
118
|
homepage: http://redbubble.com
|
117
119
|
licenses:
|
@@ -143,4 +145,5 @@ test_files:
|
|
143
145
|
- spec/lib/kumo_keisei/environment_config_spec.rb
|
144
146
|
- spec/lib/kumo_keisei/file_loader_spec.rb
|
145
147
|
- spec/lib/kumo_keisei/parameter_builder_spec.rb
|
148
|
+
- spec/lib/kumo_keisei/stack_spec.rb
|
146
149
|
- spec/spec_helper.rb
|