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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9f8f9aab09c95d9dddb3c040f9aa3270744b5830
4
- data.tar.gz: 50f5665c307bf533cf2d610756f8ccd038e679fd
3
+ metadata.gz: 315a98c6f6409683fbcefc7524da69603174329d
4
+ data.tar.gz: 6a29b5767d2cf57cc51010febadea53f50276da1
5
5
  SHA512:
6
- metadata.gz: f3849ee9e8ac8f7dcb4a377659b86c04a15170f75adb8ca235c035d1eaf22cb654b32b2ff3bdc7ac8a766d649d1f44785ed00be8c777851b6805f9469071e82e
7
- data.tar.gz: 7c77918fc986460cba503ab03b33ca6696ac8466c0c7f0deb2340a3b9435a05a7a037164db61aaaac87fef94dd87a167d668c7b57358ae5fe5962d4050e0cfcb
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::CloudFormationStack.new(...).apply!`
61
-
61
+ 1. Interact with the gem's classes. `KumoKeisei::Stack.new(...).apply!`
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.2.2
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
- @file_loader = KumoKeisei::FileLoader.new(options)
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
- @file_loader.load_erb(@params_template_file_path)
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
- @file_loader.load_hash('common_secrets.yml')
98
+ @config_file_loader.load_hash('common_secrets.yml')
104
99
  end
105
100
 
106
101
  def encrypted_env_secrets
107
- secrets = @file_loader.load_hash(env_secrets_file_name)
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
- @file_loader.load_hash('development_secrets.yml')
107
+ @config_file_loader.load_hash('development_secrets.yml')
113
108
  end
114
109
  end
115
110
 
116
111
  def common_config
117
- @file_loader.load_hash('common.yml')
112
+ @config_file_loader.load_hash('common.yml')
118
113
  end
119
114
 
120
115
  def env_config
121
- config = @file_loader.load_hash(env_config_file_name)
116
+ config = @config_file_loader.load_hash(env_config_file_name)
122
117
  if !config.empty?
123
118
  config
124
119
  else
125
- @file_loader.load_hash('development.yml')
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
@@ -1,3 +1,4 @@
1
1
  require_relative "kumo_keisei/cloud_formation_stack"
2
+ require_relative "kumo_keisei/stack"
2
3
  require_relative "kumo_keisei/environment_config"
3
4
  require_relative "kumo_keisei/errors"
@@ -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) { '/junk.txt' }
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: 2.2.2
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-07-01 00:00:00.000000000 Z
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