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 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