Skip to content

Commit 1d33c5c

Browse files
committed
Robust error handling on CloudFormationProvisioner using event polling
1 parent 84b57e7 commit 1d33c5c

9 files changed

+140
-33
lines changed

lib/eb_deployer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
require 'eb_deployer/default_component'
2222
require 'eb_deployer/component'
2323
require 'eb_deployer/eb_event_source'
24+
require 'eb_deployer/cf_event_source'
2425
require 'eb_deployer/event_poller'
2526
require 'eb_deployer/package'
2627
require 'eb_deployer/config_loader'

lib/eb_deployer/aws_driver/cloud_formation_driver.rb

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,21 @@ def create_stack(name, template, opts)
2020
end
2121

2222
def update_stack(name, template, opts)
23-
begin
24-
@client.update_stack(opts.merge(:stack_name => name,
25-
:template_body => template,
26-
:parameters => convert_parameters(opts[:parameters])))
27-
rescue Aws::CloudFormation::Errors::ValidationError => e
28-
if e.message =~ /No updates are to be performed/
29-
log(e.message)
30-
else
31-
raise
32-
end
33-
end
34-
end
35-
36-
def stack_status(name)
37-
describe_stack(name)[:stack_status].downcase.to_sym
23+
@client.update_stack(opts.merge(:stack_name => name,
24+
:template_body => template,
25+
:parameters => convert_parameters(opts[:parameters])))
3826
end
3927

4028
def query_output(name, key)
4129
output = describe_stack(name)[:outputs].find { |o| o[:output_key] == key }
4230
output && output[:output_value]
4331
end
4432

33+
def fetch_events(name, options={})
34+
response = @client.describe_stack_events(options.merge(:stack_name => name))
35+
return response.stack_events, response.next_token
36+
end
37+
4538
private
4639

4740
def describe_stack(name)

lib/eb_deployer/cf_event_source.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module EbDeployer
2+
class CfEventSource
3+
def initialize(stack_name, cf_driver)
4+
@stack_name = stack_name
5+
@cf_driver = cf_driver
6+
end
7+
8+
def get_anchor
9+
events, _ = @cf_driver.fetch_events(@stack_name)
10+
events.first
11+
end
12+
13+
def fetch_events(from_anchor, &block)
14+
events, next_token = @cf_driver.fetch_events(@stack_name)
15+
should_continue = yield(events)
16+
fetch_next(next_token, &block) if next_token && should_continue
17+
end
18+
19+
private
20+
def fetch_next(next_token, &block)
21+
events, next_token = @cf_driver.fetch_events(@stack_name, :next_token => next_token)
22+
should_continue = yield(events)
23+
fetch_next(next_token, &block) if next_token && should_continue
24+
end
25+
end
26+
end

lib/eb_deployer/cloud_formation_provisioner.rb

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,38 @@ class ResourceNotInReadyState < StandardError
33
end
44

55
class CloudFormationProvisioner
6-
SUCCESS_STATS = [:create_complete, :update_complete, :update_rollback_complete]
7-
FAILED_STATS = [:create_failed, :update_failed]
6+
SUCCESS_STATS = ["CREATE_COMPLETE", "UPDATE_COMPLETE"]
7+
FAILED_STATS = ["CREATE_FAILED", "UPDATE_FAILED"]
88

99
def initialize(stack_name, cf_driver)
1010
@stack_name = stack_name
1111
@cf_driver = cf_driver
12+
@poller = EventPoller.new(CfEventSource.new(@stack_name, @cf_driver))
1213
end
1314

1415
def provision(resources)
1516
resources = symbolize_keys(resources)
1617
template = File.read(resources[:template])
1718
capabilities = resources[:capabilities] || []
1819
params = resources[:inputs] || resources[:parameters] || {}
19-
stack_exists? ? update_stack(template, params, capabilities) : create_stack(template, params, capabilities)
20-
wait_for_stack_op_terminate
20+
anchor = nil
21+
begin
22+
if stack_exists?
23+
anchor = @poller.get_anchor
24+
update_stack(template, params, capabilities)
25+
else
26+
create_stack(template, params, capabilities)
27+
end
28+
rescue Aws::CloudFormation::Errors::ValidationError => e
29+
if e.message =~ /No updates are to be performed/
30+
log(e.message)
31+
return
32+
else
33+
raise
34+
end
35+
end
36+
wait_for_stack_op_terminate(anchor)
37+
log("Resource stack provisioned successfully")
2138
end
2239

2340
def transform_outputs(resources)
@@ -65,10 +82,6 @@ def create_stack(template, params, capabilities)
6582
:parameters => params)
6683
end
6784

68-
def stack_status
69-
@cf_driver.stack_status(@stack_name)
70-
end
71-
7285
def transform_output_to_settings(transforms)
7386
(transforms || []).inject([]) do |settings, pair|
7487
key, transform = pair
@@ -77,16 +90,22 @@ def transform_output_to_settings(transforms)
7790
end.flatten
7891
end
7992

80-
def wait_for_stack_op_terminate
81-
stats = stack_status
82-
while !SUCCESS_STATS.include?(stats)
83-
sleep 15
84-
stats = stack_status
85-
raise "Resource stack update failed!" if FAILED_STATS.include?(stats)
86-
log "current status: #{stack_status}"
93+
def wait_for_stack_op_terminate(anchor)
94+
@poller.poll(anchor) do |event|
95+
log_event(event)
96+
if FAILED_STATS.include?(event.resource_status)
97+
raise "Resource stack update failed!"
98+
end
99+
100+
break if event.logical_resource_id == @stack_name && SUCCESS_STATS.include?(event.resource_status)
87101
end
88102
end
89103

104+
def log_event(event)
105+
puts "[#{event.timestamp}][cloud_formation_provisioner] #{event.resource_type}(#{event.logical_resource_id}) #{event.resource_status} \"#{event.resource_status_reason}\""
106+
end
107+
108+
90109
def log(msg)
91110
puts "[#{Time.now.utc}][cloud_formation_provisioner] #{msg}"
92111
end

lib/eb_deployer/event_poller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module EbDeployer
22
class EventPoller
33
include Utils
4+
POLL_INTERVAL = 15
45

56
def initialize(event_source)
67
@event_source = event_source
@@ -14,7 +15,7 @@ def poll(from_anchor, &block)
1415
handled = Set.new
1516
loop do
1617
@event_source.fetch_events(from_anchor) do |events|
17-
# events from api is latest first order
18+
# events from event source is latest first order
1819
to_be_handled = []
1920
reached_anchor = false
2021

@@ -35,7 +36,7 @@ def poll(from_anchor, &block)
3536

3637
!reached_anchor
3738
end
38-
sleep 15
39+
sleep POLL_INTERVAL
3940
end
4041
end
4142

test/aws_driver_stubs.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ def upload_file(bucket_name, obj_name, file)
271271
class CFStub
272272
def initialize
273273
@stacks = {}
274+
@events = {}
275+
@event_fetch_counter = 0;
274276
end
275277

276278
def create_stack(name, template, opts)
@@ -297,4 +299,36 @@ def query_output(name, key)
297299
def stack_config(name)
298300
@stacks[name][:opts]
299301
end
302+
303+
def set_events(name, *messages)
304+
events_seq = []
305+
messages.each do |messages_for_call_seq|
306+
if old_events = events_seq.last
307+
events_seq << generate_event_from_messages(name, messages_for_call_seq) + old_events
308+
else
309+
events_seq << generate_event_from_messages(name, messages_for_call_seq)
310+
end
311+
end
312+
@events[name] = events_seq
313+
end
314+
315+
def fetch_events(name, opts={})
316+
@event_fetch_counter += 1
317+
if es = @events[name]
318+
return es[@event_fetch_counter - 1], nil
319+
else
320+
return generate_event_from_messages(name, ["UPDATE_COMPLETE"]), nil
321+
end
322+
end
323+
324+
private
325+
326+
def generate_event_from_messages(stack, messages)
327+
messages.map do |message|
328+
event = OpenStruct.new(timestamp: Time.now,
329+
resource_type: 'AWS::CloudFormation::Stack',
330+
logical_resource_id: stack,
331+
resource_status: message)
332+
end.reverse
333+
end
300334
end

test/cf_event_poller_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
require 'test_helper'
2+
3+
class CfEventPollerTest < Test::Unit::TestCase
4+
def setup
5+
@cf = CFStub.new
6+
@poller = EbDeployer::EventPoller.new(EbDeployer::CfEventSource.new("mystack", @cf))
7+
end
8+
9+
def test_run_handle_block_through_all_events_when_there_is_no_from_anchor
10+
messages_handled = []
11+
@cf.set_events('mystack', ['a', 'b', nil])
12+
@poller.poll(nil) do |event|
13+
break if event.resource_status.nil?
14+
messages_handled << event.resource_status
15+
end
16+
17+
assert_equal ['a', 'b'], messages_handled
18+
end
19+
20+
21+
def test_can_poll_all_events_after_an_anchor
22+
@cf.set_events('mystack', ['a', 'b'], ['c', 'd', nil])
23+
anchor = @poller.get_anchor
24+
messages_handled = []
25+
@poller.poll(anchor) do |event|
26+
break if event.resource_status.nil?
27+
messages_handled << event.resource_status
28+
end
29+
30+
assert_equal ['c', 'd'], messages_handled
31+
end
32+
end

test/event_poller_test.rb renamed to test/eb_event_poller_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
require 'test_helper'
22

3-
class EventPollerTest < Test::Unit::TestCase
3+
class EbEventPollerTest < Test::Unit::TestCase
44
def setup
55
@eb = EBStub.new
66
@poller = EbDeployer::EventPoller.new(EbDeployer::EbEventSource.new("myapp", "test", @eb))

test/test_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def silence_warnings(&block)
1313
end
1414

1515
silence_warnings { EbDeployer::Utils::BACKOFF_INITIAL_SLEEP = 0 }
16+
silence_warnings { EbDeployer::EventPoller::POLL_INTERVAL = 0 }
1617

1718
class ErrorRaisingWrapper < SimpleDelegator
1819
def initialize(stub)

0 commit comments

Comments
 (0)