kumo_keisei 2.2.2 → 3.0.1
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 +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
|