stackup 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +10 -8
- data/bin/stackup +91 -2
- data/lib/stackup/errors.rb +15 -0
- data/lib/stackup/stack.rb +146 -80
- data/lib/stackup/stack_watcher.rb +41 -0
- data/pkg/stackup-0.0.9.gem +0 -0
- data/spec/stackup/stack_spec.rb +219 -155
- data/spec/stackup/stack_watcher_spec.rb +91 -0
- data/stackup.gemspec +4 -2
- data/woollyams/sample-template.json +40 -0
- metadata +35 -6
- data/lib/stackup/cli.rb +0 -54
- data/lib/stackup/monitor.rb +0 -33
- data/spec/stackup/monitor_spec.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 018537d107f8cc0049863b56dd604f8c881217c9
|
4
|
+
data.tar.gz: b62b34f99958d38df0d44d4a46af577832eb4174
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe03ddbff07fbb42c2781268e131b16dfc08942bdb5b2079d6905f58b720b091d8b7c461b6413c8b1cc42e7c080d7d560a8d2d369b0a9536b033fd9ba609304d
|
7
|
+
data.tar.gz: 49a82e6317772a3e32322d65bc438bdcc550b0d6f61cfdf2cc06fcfa0b258135be7247fbe6f31665a6d7b6a33b5608a54a64bcc7d7f49a479a489adf75348ad6
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
stackup (0.0
|
4
|
+
stackup (0.1.0)
|
5
5
|
aws-sdk (~> 2.0)
|
6
6
|
clamp (~> 1.0)
|
7
|
+
console_logger
|
8
|
+
multi_json
|
7
9
|
|
8
10
|
GEM
|
9
11
|
remote: https://rubygems.org/
|
@@ -11,17 +13,17 @@ GEM
|
|
11
13
|
ast (2.0.0)
|
12
14
|
astrolabe (1.3.0)
|
13
15
|
parser (>= 2.2.0.pre.3, < 3.0)
|
14
|
-
aws-sdk (2.1.
|
15
|
-
aws-sdk-resources (= 2.1.
|
16
|
-
aws-sdk-core (2.1.
|
16
|
+
aws-sdk (2.1.26)
|
17
|
+
aws-sdk-resources (= 2.1.26)
|
18
|
+
aws-sdk-core (2.1.26)
|
17
19
|
jmespath (~> 1.0)
|
18
|
-
aws-sdk-resources (2.1.
|
19
|
-
aws-sdk-core (= 2.1.
|
20
|
+
aws-sdk-resources (2.1.26)
|
21
|
+
aws-sdk-core (= 2.1.26)
|
20
22
|
byebug (6.0.2)
|
21
23
|
clamp (1.0.0)
|
24
|
+
console_logger (1.0.0)
|
22
25
|
diff-lcs (1.2.5)
|
23
|
-
jmespath (1.
|
24
|
-
multi_json (~> 1.0)
|
26
|
+
jmespath (1.1.3)
|
25
27
|
multi_json (1.11.2)
|
26
28
|
parser (2.2.2.5)
|
27
29
|
ast (>= 1.1, < 3.0)
|
data/bin/stackup
CHANGED
@@ -2,6 +2,95 @@
|
|
2
2
|
|
3
3
|
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
4
4
|
|
5
|
-
require "
|
5
|
+
require "clamp"
|
6
|
+
require "console_logger"
|
7
|
+
require "stackup/stack"
|
8
|
+
require "multi_json"
|
9
|
+
require "yaml"
|
6
10
|
|
7
|
-
|
11
|
+
Clamp do
|
12
|
+
|
13
|
+
option "--debug", :flag, "enable debugging"
|
14
|
+
|
15
|
+
option ["-f", "--format"], "FORMAT", "output format", :default => "yaml"
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def logger
|
20
|
+
@logger ||= ConsoleLogger.new($stdout, debug?)
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_data(data)
|
24
|
+
case format.downcase
|
25
|
+
when "json"
|
26
|
+
MultiJson.dump(data, :pretty => true)
|
27
|
+
when "yaml"
|
28
|
+
YAML.dump(data)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def display_data(data)
|
33
|
+
puts format_data(data)
|
34
|
+
end
|
35
|
+
|
36
|
+
subcommand "stack", "Manage a stack." do
|
37
|
+
|
38
|
+
parameter "NAME", "Name of stack", :attribute_name => :stack_name
|
39
|
+
|
40
|
+
def run(*args)
|
41
|
+
super(*args)
|
42
|
+
rescue Stackup::NoSuchStack => e
|
43
|
+
signal_error "stack '#{stack_name}' does not exist"
|
44
|
+
rescue Stackup::StackUpdateError => e
|
45
|
+
signal_error e.message
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def stack
|
51
|
+
Stackup::Stack.new(stack_name, :logger => logger, :log_level => :debug)
|
52
|
+
end
|
53
|
+
|
54
|
+
def report_change
|
55
|
+
change = yield
|
56
|
+
puts "Stack #{change}" unless change.nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
subcommand "status", "Print stack status." do
|
60
|
+
|
61
|
+
def execute
|
62
|
+
puts stack.status
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
subcommand "up", "Create/update the stack" do
|
68
|
+
|
69
|
+
parameter "TEMPLATE", "CloudFormation template (.json)", :attribute_name => :template_file
|
70
|
+
|
71
|
+
def execute
|
72
|
+
template = File.read(template_file)
|
73
|
+
report_change { stack.create_or_update(template) }
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
subcommand ["down", "delete"], "Remove the stack." do
|
79
|
+
|
80
|
+
def execute
|
81
|
+
report_change { stack.delete }
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
subcommand "outputs", "Stack outputs." do
|
87
|
+
|
88
|
+
def execute
|
89
|
+
display_data(stack.outputs)
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Stackup
|
2
|
+
|
3
|
+
# Base Stackup Exception class
|
4
|
+
class ServiceError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Raised when the specified stack does not exist
|
8
|
+
class NoSuchStack < ServiceError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Raised to indicate a problem updating a stack
|
12
|
+
class StackUpdateError < ServiceError
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/lib/stackup/stack.rb
CHANGED
@@ -1,126 +1,192 @@
|
|
1
1
|
require "aws-sdk-resources"
|
2
|
-
require "
|
2
|
+
require "logger"
|
3
|
+
require "stackup/errors"
|
4
|
+
require "stackup/stack_watcher"
|
3
5
|
|
4
6
|
module Stackup
|
5
|
-
class Stack
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
# An abstraction of a CloudFormation stack.
|
9
|
+
#
|
10
|
+
class Stack
|
10
11
|
|
11
|
-
def initialize(name,
|
12
|
-
|
13
|
-
@stack = Aws::CloudFormation::Stack.new(:name => name, :client => cf)
|
14
|
-
@monitor = Stackup::Monitor.new(@stack)
|
15
|
-
@monitor.new_events # drain previous events
|
12
|
+
def initialize(name, client = {}, options = {})
|
13
|
+
client = Aws::CloudFormation::Client.new(client) if client.is_a?(Hash)
|
16
14
|
@name = name
|
15
|
+
@cf_client = client
|
16
|
+
@watcher = Stackup::StackWatcher.new(cf_stack)
|
17
|
+
options.each do |key, value|
|
18
|
+
public_send("#{key}=", value)
|
19
|
+
end
|
17
20
|
end
|
18
21
|
|
19
|
-
attr_reader :
|
22
|
+
attr_reader :name, :cf_client, :watcher
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
# Register a handler for reporting of stack events
|
25
|
+
# @param [Proc] event_handler
|
26
|
+
#
|
27
|
+
def on_event(event_handler = nil, &block)
|
28
|
+
event_handler ||= block
|
29
|
+
fail ArgumentError, "no event_handler provided" if event_handler.nil?
|
30
|
+
@event_handler = event_handler
|
25
31
|
end
|
26
32
|
|
27
|
-
|
28
|
-
|
33
|
+
# @return [String] the current stack status
|
34
|
+
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
35
|
+
#
|
36
|
+
def status
|
37
|
+
cf_stack.stack_status
|
38
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
39
|
+
handle_validation_error(e)
|
29
40
|
end
|
30
41
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
:capabilities => ["CAPABILITY_IAM"],
|
36
|
-
:parameters => parameters)
|
37
|
-
status = wait_for_events
|
38
|
-
|
39
|
-
fail CreateError, "stack creation failed" unless status == "CREATE_COMPLETE"
|
42
|
+
# @return [boolean] true iff the stack exists
|
43
|
+
#
|
44
|
+
def exists?
|
45
|
+
status
|
40
46
|
true
|
41
|
-
|
42
|
-
|
43
|
-
return false
|
47
|
+
rescue NoSuchStack
|
48
|
+
false
|
44
49
|
end
|
45
50
|
|
46
|
-
|
51
|
+
# Create or update the stack.
|
52
|
+
#
|
53
|
+
# @param [String] template template JSON
|
54
|
+
# @param [Array<Hash>] parameters template parameters
|
55
|
+
# @return [Symbol] `:created` or `:updated` if successful
|
56
|
+
# @raise [Stackup::StackUpdateError] if operation fails
|
57
|
+
#
|
58
|
+
def create_or_update(template, parameters = [])
|
59
|
+
delete if ALMOST_DEAD_STATUSES.include?(status)
|
60
|
+
update(template, parameters)
|
61
|
+
rescue NoSuchStack
|
62
|
+
create(template, parameters)
|
47
63
|
end
|
48
64
|
|
49
|
-
|
50
|
-
return false unless exists?
|
51
|
-
if stack.stack_status == "CREATE_FAILED"
|
52
|
-
puts "Stack is in CREATE_FAILED state so must be manually deleted before it can be updated"
|
53
|
-
return false
|
54
|
-
end
|
55
|
-
if stack.stack_status == "ROLLBACK_COMPLETE"
|
56
|
-
deleted = delete
|
57
|
-
return false if !deleted
|
58
|
-
end
|
59
|
-
cf.update_stack(:stack_name => name, :template_body => template, :parameters => parameters, :capabilities => ["CAPABILITY_IAM"])
|
65
|
+
alias_method :up, :create_or_update
|
60
66
|
|
61
|
-
|
62
|
-
fail UpdateError, "stack update failed" unless status == "UPDATE_COMPLETE"
|
63
|
-
true
|
67
|
+
ALMOST_DEAD_STATUSES = %w(CREATE_FAILED ROLLBACK_COMPLETE)
|
64
68
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
+
# Delete the stack.
|
70
|
+
#
|
71
|
+
# @param [String] template template JSON
|
72
|
+
# @param [Array<Hash>] parameters template parameters
|
73
|
+
# @return [Symbol] `:deleted` if successful
|
74
|
+
# @raise [Stackup::StackUpdateError] if operation fails
|
75
|
+
#
|
76
|
+
def delete
|
77
|
+
return nil unless exists?
|
78
|
+
status = modify_stack do
|
79
|
+
cf_client.delete_stack(:stack_name => name)
|
69
80
|
end
|
70
|
-
|
81
|
+
fail StackUpdateError, "stack delete failed" unless status.nil?
|
82
|
+
rescue NoSuchStack
|
83
|
+
:deleted
|
71
84
|
end
|
72
85
|
|
73
|
-
|
86
|
+
alias_method :down, :delete
|
87
|
+
|
88
|
+
# Get stack outputs.
|
89
|
+
#
|
90
|
+
# @return [Hash<String, String>]
|
91
|
+
# mapping of logical resource-name to physical resource-name
|
92
|
+
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
93
|
+
#
|
94
|
+
def outputs
|
95
|
+
{}.tap do |h|
|
96
|
+
cf_stack.outputs.each do |output|
|
97
|
+
h[output.output_key] = output.output_value
|
98
|
+
end
|
99
|
+
end
|
100
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
101
|
+
handle_validation_error(e)
|
74
102
|
end
|
75
103
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
status =
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
104
|
+
private
|
105
|
+
|
106
|
+
def create(template, parameters)
|
107
|
+
status = modify_stack do
|
108
|
+
cf_client.create_stack(
|
109
|
+
:stack_name => name,
|
110
|
+
:template_body => template,
|
111
|
+
:capabilities => ["CAPABILITY_IAM"],
|
112
|
+
:parameters => parameters
|
113
|
+
)
|
114
|
+
end
|
115
|
+
fail StackUpdateError, "stack creation failed" unless status == "CREATE_COMPLETE"
|
116
|
+
:created
|
84
117
|
end
|
85
118
|
|
86
|
-
def
|
87
|
-
|
88
|
-
|
89
|
-
else
|
90
|
-
create(template, parameters)
|
119
|
+
def update(template, parameters)
|
120
|
+
status = modify_stack do
|
121
|
+
cf_client.update_stack(:stack_name => name, :template_body => template, :parameters => parameters, :capabilities => ["CAPABILITY_IAM"])
|
91
122
|
end
|
92
|
-
|
93
|
-
|
123
|
+
fail StackUpdateError, "stack update failed" unless status == "UPDATE_COMPLETE"
|
124
|
+
:updated
|
125
|
+
rescue NoUpdateRequired
|
126
|
+
nil
|
94
127
|
end
|
95
128
|
|
96
|
-
def
|
97
|
-
|
129
|
+
def logger
|
130
|
+
@logger ||= (cf_client.config[:logger] || Logger.new($stdout))
|
98
131
|
end
|
99
132
|
|
100
|
-
def
|
101
|
-
|
102
|
-
response[:code].nil?
|
133
|
+
def cf_stack
|
134
|
+
Aws::CloudFormation::Stack.new(:name => name, :client => cf_client)
|
103
135
|
end
|
104
136
|
|
105
|
-
|
137
|
+
def event_handler
|
138
|
+
@event_handler ||= lambda do |e|
|
139
|
+
fields = [e.logical_resource_id, e.resource_status, e.resource_status_reason]
|
140
|
+
logger.info(fields.compact.join(" - "))
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Execute a block, reporting stack events, until the stack is stable.
|
145
|
+
#
|
146
|
+
# @return the final stack status
|
147
|
+
#
|
148
|
+
def modify_stack
|
149
|
+
watcher.zero
|
150
|
+
yield
|
151
|
+
wait_until_stable
|
152
|
+
rescue Aws::CloudFormation::Errors::ValidationError => e
|
153
|
+
handle_validation_error(e)
|
154
|
+
end
|
106
155
|
|
107
156
|
# Wait (displaying stack events) until the stack reaches a stable state.
|
108
157
|
#
|
109
|
-
|
158
|
+
# @return the final stack status
|
159
|
+
#
|
160
|
+
def wait_until_stable
|
110
161
|
loop do
|
111
|
-
|
112
|
-
|
162
|
+
report_new_events
|
163
|
+
cf_stack.reload
|
113
164
|
return status if status.nil? || status =~ /_(COMPLETE|FAILED)$/
|
114
|
-
sleep(
|
165
|
+
sleep(5)
|
115
166
|
end
|
116
167
|
end
|
117
168
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
169
|
+
def report_new_events
|
170
|
+
watcher.new_events.each do |e|
|
171
|
+
event_handler.call(e)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def handle_validation_error(e)
|
176
|
+
case e.message
|
177
|
+
when "No updates are to be performed."
|
178
|
+
fail NoUpdateRequired, "no updates are required"
|
179
|
+
when / does not exist$/
|
180
|
+
fail NoSuchStack, "no such stack: #{name}"
|
181
|
+
else
|
182
|
+
raise e
|
123
183
|
end
|
124
184
|
end
|
185
|
+
|
186
|
+
# Raised when a stack is already up-to-date
|
187
|
+
class NoUpdateRequired < StandardError
|
188
|
+
end
|
189
|
+
|
125
190
|
end
|
191
|
+
|
126
192
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "aws-sdk-core"
|
2
|
+
|
3
|
+
module Stackup
|
4
|
+
|
5
|
+
class StackWatcher
|
6
|
+
|
7
|
+
def initialize(stack)
|
8
|
+
@stack = stack
|
9
|
+
@processed_event_ids = Set.new
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :stack
|
13
|
+
|
14
|
+
# Yield all events since the last call
|
15
|
+
#
|
16
|
+
def new_events
|
17
|
+
[].tap do |events|
|
18
|
+
stack.events.each do |event|
|
19
|
+
break if @processed_event_ids.include?(event.event_id)
|
20
|
+
events.unshift(event)
|
21
|
+
@processed_event_ids.add(event.event_id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError
|
25
|
+
[]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Consume all new events
|
29
|
+
#
|
30
|
+
def zero
|
31
|
+
new_events
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :processed_event_ids
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
Binary file
|
data/spec/stackup/stack_spec.rb
CHANGED
@@ -2,234 +2,298 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Stackup::Stack do
|
4
4
|
|
5
|
-
let(:
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
let(:cf_client) do
|
6
|
+
client_options = { :stub_responses => true }
|
7
|
+
if ENV.key?("AWS_DEBUG")
|
8
|
+
client_options[:logger] = Logger.new(STDOUT)
|
9
|
+
client_options[:log_level] = :debug
|
10
|
+
end
|
11
|
+
Aws::CloudFormation::Client.new(client_options)
|
12
|
+
end
|
10
13
|
|
11
|
-
let(:
|
12
|
-
let(:
|
13
|
-
let(:stack_status) { nil }
|
14
|
+
let(:stack_name) { "stack_name" }
|
15
|
+
let(:unique_stack_id) { "ID:#{stack_name}" }
|
14
16
|
|
15
|
-
|
17
|
+
subject(:stack) { described_class.new(stack_name, cf_client) }
|
16
18
|
|
17
19
|
before do
|
18
|
-
|
19
|
-
allow(
|
20
|
-
|
20
|
+
cf_client.stub_responses(:describe_stacks, *describe_stacks_responses)
|
21
|
+
allow(stack).to receive(:sleep).at_most(5).times
|
22
|
+
# partial stubbing, to support spying
|
23
|
+
allow(cf_client).to receive(:create_stack).and_call_original
|
24
|
+
allow(cf_client).to receive(:delete_stack).and_call_original
|
25
|
+
allow(cf_client).to receive(:update_stack).and_call_original
|
21
26
|
end
|
22
27
|
|
23
|
-
|
28
|
+
def service_error(code, message)
|
29
|
+
{
|
30
|
+
:status_code => 400,
|
31
|
+
:headers => {},
|
32
|
+
:body => "<ErrorResponse><Error><Code>#{code}</Code><Message>#{message}</Message></Error></ErrorResponse>"
|
33
|
+
}
|
34
|
+
end
|
24
35
|
|
25
|
-
|
36
|
+
def stack_does_not_exist
|
37
|
+
service_error("ValidationError", "Stack with id #{stack_name} does not exist")
|
38
|
+
end
|
26
39
|
|
27
|
-
|
28
|
-
|
40
|
+
def no_update_required
|
41
|
+
service_error("ValidationError", "No updates are to be performed.")
|
42
|
+
end
|
43
|
+
|
44
|
+
def stack_description(stack_status)
|
45
|
+
{
|
46
|
+
:stacks => [
|
47
|
+
{
|
48
|
+
:creation_time => Time.now - 100,
|
49
|
+
:stack_id => unique_stack_id,
|
50
|
+
:stack_name => stack_name,
|
51
|
+
:stack_status => stack_status
|
52
|
+
}
|
53
|
+
]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
context "before stack exists" do
|
58
|
+
|
59
|
+
let(:describe_stacks_responses) do
|
60
|
+
[
|
61
|
+
stack_does_not_exist
|
62
|
+
]
|
29
63
|
end
|
30
64
|
|
31
|
-
|
32
|
-
|
33
|
-
|
65
|
+
describe "#exists?" do
|
66
|
+
it "is false" do
|
67
|
+
expect(stack.exists?).to be false
|
34
68
|
end
|
35
|
-
it { expect(created).to be true }
|
36
69
|
end
|
37
70
|
|
38
|
-
|
39
|
-
|
40
|
-
|
71
|
+
describe "#status" do
|
72
|
+
it "raises a NoSuchStack error" do
|
73
|
+
expect { stack.status }.to raise_error(Stackup::NoSuchStack)
|
41
74
|
end
|
42
|
-
it { expect{ created }.to raise_error Stackup::Stack::CreateError }
|
43
75
|
end
|
44
76
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
subject(:updated) { stack.update(template, parameters) }
|
49
|
-
|
50
|
-
context "when there is no existing stack" do
|
51
|
-
before do
|
52
|
-
allow(stack).to receive(:exists?).and_return(false)
|
77
|
+
describe "#delete" do
|
78
|
+
it "returns nil" do
|
79
|
+
expect(stack.delete).to be_nil
|
53
80
|
end
|
54
|
-
it { expect(updated).to be false }
|
55
81
|
end
|
56
82
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
83
|
+
describe "#create_or_update" do
|
84
|
+
|
85
|
+
let(:template) { "stack template" }
|
86
|
+
|
87
|
+
def create_or_update
|
88
|
+
stack.create_or_update(template)
|
62
89
|
end
|
63
90
|
|
64
|
-
context "
|
65
|
-
|
66
|
-
|
91
|
+
context "successful" do
|
92
|
+
|
93
|
+
let(:describe_stacks_responses) do
|
94
|
+
super() + [
|
95
|
+
stack_description("CREATE_IN_PROGRESS"),
|
96
|
+
stack_description("CREATE_COMPLETE")
|
97
|
+
]
|
67
98
|
end
|
68
99
|
|
69
|
-
|
70
|
-
|
100
|
+
it "calls :create_stack" do
|
101
|
+
expected_args = {
|
102
|
+
:stack_name => stack_name,
|
103
|
+
:template_body => template
|
104
|
+
}
|
105
|
+
create_or_update
|
106
|
+
expect(cf_client).to have_received(:create_stack)
|
107
|
+
.with(hash_including(expected_args))
|
71
108
|
end
|
72
109
|
|
73
|
-
|
74
|
-
|
75
|
-
allow(stack).to receive(:wait_for_events).and_return("UPDATE_FAILED")
|
76
|
-
end
|
77
|
-
it { expect{ updated }.to raise_error Stackup::Stack::UpdateError }
|
110
|
+
it "returns :created" do
|
111
|
+
expect(create_or_update).to eq(:created)
|
78
112
|
end
|
113
|
+
|
79
114
|
end
|
80
115
|
|
81
|
-
context "
|
82
|
-
|
83
|
-
|
116
|
+
context "unsuccessful" do
|
117
|
+
|
118
|
+
let(:describe_stacks_responses) do
|
119
|
+
super() + [
|
120
|
+
stack_description("CREATE_IN_PROGRESS"),
|
121
|
+
stack_description("CREATE_FAILED")
|
122
|
+
]
|
123
|
+
end
|
124
|
+
|
125
|
+
it "raises a StackUpdateError" do
|
126
|
+
expect { create_or_update }
|
127
|
+
.to raise_error(Stackup::StackUpdateError)
|
84
128
|
end
|
85
129
|
|
86
|
-
|
130
|
+
end
|
87
131
|
|
88
|
-
|
89
|
-
allow(response).to receive(:[]).with(:stack_id).and_return("1")
|
90
|
-
expect(stack).to receive(:delete).and_return(true)
|
91
|
-
stack.update(template, parameters)
|
92
|
-
end
|
132
|
+
end
|
93
133
|
|
94
|
-
|
95
|
-
before do
|
96
|
-
allow(response).to receive(:[]).with(:stack_id).and_return("1")
|
97
|
-
allow(stack).to receive(:delete).and_return(true)
|
98
|
-
end
|
99
|
-
it { expect(updated).to be true }
|
100
|
-
end
|
134
|
+
end
|
101
135
|
|
102
|
-
|
103
|
-
before do
|
104
|
-
allow(response).to receive(:[]).with(:stack_id).and_return("1")
|
105
|
-
allow(stack).to receive(:delete).and_return(false)
|
106
|
-
end
|
107
|
-
it { expect(updated).to be false }
|
108
|
-
end
|
109
|
-
end
|
136
|
+
context "with existing stack" do
|
110
137
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
138
|
+
let(:stack_status) { "CREATE_COMPLETE" }
|
139
|
+
|
140
|
+
let(:describe_stacks_responses) do
|
141
|
+
[
|
142
|
+
stack_description(stack_status)
|
143
|
+
]
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#exists?" do
|
147
|
+
it "is true" do
|
148
|
+
expect(stack.exists?).to be true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "#status" do
|
153
|
+
it "returns the stack status" do
|
154
|
+
expect(stack.status).to eq(stack_status)
|
117
155
|
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "#delete" do
|
118
159
|
|
119
|
-
context "
|
120
|
-
|
121
|
-
|
160
|
+
context "if successful" do
|
161
|
+
|
162
|
+
let(:describe_stacks_responses) do
|
163
|
+
super() + [
|
164
|
+
stack_description("DELETE_IN_PROGRESS"),
|
165
|
+
stack_does_not_exist
|
166
|
+
]
|
122
167
|
end
|
123
168
|
|
124
|
-
it "
|
125
|
-
|
126
|
-
expect(
|
127
|
-
|
169
|
+
it "calls delete_stack" do
|
170
|
+
stack.delete
|
171
|
+
expect(cf_client).to have_received(:delete_stack)
|
172
|
+
.with(hash_including(:stack_name => stack_name))
|
128
173
|
end
|
129
174
|
|
130
|
-
it "
|
131
|
-
|
132
|
-
expect(cf_client).not_to receive(:delete_stack)
|
133
|
-
expect(updated).to be false
|
175
|
+
it "returns :deleted" do
|
176
|
+
expect(stack.delete).to eq(:deleted)
|
134
177
|
end
|
178
|
+
|
135
179
|
end
|
136
|
-
end
|
137
|
-
end
|
138
180
|
|
139
|
-
|
181
|
+
context "if unsuccessful" do
|
140
182
|
|
141
|
-
|
183
|
+
let(:describe_stacks_responses) do
|
184
|
+
super() + [
|
185
|
+
stack_description("DELETE_IN_PROGRESS"),
|
186
|
+
stack_description("DELETE_FAILED")
|
187
|
+
]
|
188
|
+
end
|
142
189
|
|
143
|
-
|
190
|
+
it "raises a StackUpdateError" do
|
191
|
+
expect { stack.delete }.to raise_error(Stackup::StackUpdateError)
|
192
|
+
end
|
144
193
|
|
145
|
-
before do
|
146
|
-
allow(stack).to receive(:exists?).and_return(true)
|
147
|
-
allow(cf_stack).to receive(:stack_status).and_return("CREATE_COMPLETE")
|
148
|
-
allow(cf_client).to receive(:update_stack).and_return({ stack_id: "stack-name" })
|
149
194
|
end
|
150
195
|
|
151
|
-
it "updates the stack" do
|
152
|
-
expect(stack).to receive(:update)
|
153
|
-
deploy
|
154
|
-
end
|
155
196
|
end
|
156
197
|
|
157
|
-
|
198
|
+
describe "#create_or_update" do
|
158
199
|
|
159
|
-
|
160
|
-
allow(stack).to receive(:exists?).and_return(false)
|
161
|
-
allow(cf_client).to receive(:create_stack).and_return({ stack_id: "stack-name" })
|
162
|
-
end
|
200
|
+
let(:template) { "stack template" }
|
163
201
|
|
164
|
-
|
165
|
-
|
166
|
-
deploy
|
202
|
+
def create_or_update
|
203
|
+
stack.create_or_update(template)
|
167
204
|
end
|
168
|
-
end
|
169
|
-
end
|
170
205
|
|
206
|
+
context "successful" do
|
171
207
|
|
172
|
-
|
208
|
+
let(:describe_stacks_responses) do
|
209
|
+
super() + [
|
210
|
+
stack_description("UPDATE_IN_PROGRESS"),
|
211
|
+
stack_description("UPDATE_COMPLETE")
|
212
|
+
]
|
213
|
+
end
|
173
214
|
|
174
|
-
|
215
|
+
it "calls :update_stack" do
|
216
|
+
expected_args = {
|
217
|
+
:stack_name => stack_name,
|
218
|
+
:template_body => template
|
219
|
+
}
|
220
|
+
create_or_update
|
221
|
+
expect(cf_client).to have_received(:update_stack)
|
222
|
+
.with(hash_including(expected_args))
|
223
|
+
end
|
175
224
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
end
|
225
|
+
it "returns :updated" do
|
226
|
+
expect(create_or_update).to eq(:updated)
|
227
|
+
end
|
180
228
|
|
181
|
-
|
229
|
+
context "if no updates are required" do
|
182
230
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
end
|
231
|
+
before do
|
232
|
+
cf_client.stub_responses(:update_stack, no_update_required)
|
233
|
+
end
|
187
234
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
allow(cf_client).to receive(:delete_stack)
|
192
|
-
end
|
235
|
+
it "returns nil" do
|
236
|
+
expect(create_or_update).to be_nil
|
237
|
+
end
|
193
238
|
|
194
|
-
context "deleting the stack succeeds" do
|
195
|
-
before do
|
196
|
-
allow(stack).to receive(:wait_for_events).and_return("DELETE_COMPLETE")
|
197
239
|
end
|
198
|
-
|
240
|
+
|
199
241
|
end
|
200
242
|
|
201
|
-
context "
|
202
|
-
|
203
|
-
|
243
|
+
context "unsuccessful" do
|
244
|
+
|
245
|
+
let(:describe_stacks_responses) do
|
246
|
+
super() + [
|
247
|
+
stack_description("UPDATE_IN_PROGRESS"),
|
248
|
+
stack_description("UPDATE_ROLLBACK_COMPLETE")
|
249
|
+
]
|
250
|
+
end
|
251
|
+
|
252
|
+
it "raises a StackUpdateError" do
|
253
|
+
expect { create_or_update }.to raise_error(Stackup::StackUpdateError)
|
204
254
|
end
|
205
|
-
|
255
|
+
|
206
256
|
end
|
257
|
+
|
207
258
|
end
|
208
|
-
end
|
209
259
|
|
260
|
+
%w(CREATE_FAILED ROLLBACK_COMPLETE).each do |initial_status|
|
261
|
+
context "when status is #{initial_status}" do
|
210
262
|
|
211
|
-
|
212
|
-
it "should be valid if cf validate say so" do
|
213
|
-
allow(cf_client).to receive(:validate_template).and_return({})
|
214
|
-
expect(stack.valid?(template)).to be true
|
215
|
-
end
|
263
|
+
let(:stack_status) { initial_status }
|
216
264
|
|
217
|
-
|
218
|
-
allow(cf_client).to receive(:validate_template).and_return(:code => "404")
|
219
|
-
expect(stack.valid?(template)).to be false
|
220
|
-
end
|
265
|
+
describe "#create_or_update" do
|
221
266
|
|
222
|
-
|
267
|
+
let(:template) { "stack template" }
|
223
268
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
269
|
+
def create_or_update
|
270
|
+
stack.create_or_update(template)
|
271
|
+
end
|
272
|
+
|
273
|
+
let(:describe_stacks_responses) do
|
274
|
+
super() + [
|
275
|
+
stack_description("DELETE_IN_PROGRESS"),
|
276
|
+
stack_does_not_exist,
|
277
|
+
stack_description("CREATE_IN_PROGRESS"),
|
278
|
+
stack_description("CREATE_COMPLETE")
|
279
|
+
]
|
280
|
+
end
|
229
281
|
|
230
|
-
|
231
|
-
|
232
|
-
|
282
|
+
before do
|
283
|
+
cf_client.stub_responses(:update_stack, stack_does_not_exist)
|
284
|
+
end
|
285
|
+
|
286
|
+
it "calls :delete_stack, then :create_stack first" do
|
287
|
+
create_or_update
|
288
|
+
expect(cf_client).to have_received(:delete_stack)
|
289
|
+
expect(cf_client).to have_received(:create_stack)
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
233
295
|
end
|
296
|
+
|
234
297
|
end
|
298
|
+
|
235
299
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "stackup/stack_watcher"
|
4
|
+
|
5
|
+
describe Stackup::StackWatcher do
|
6
|
+
|
7
|
+
let(:stack) { instance_double(Aws::CloudFormation::Stack, :events => events) }
|
8
|
+
let(:events) { [] }
|
9
|
+
|
10
|
+
subject(:monitor) { described_class.new(stack) }
|
11
|
+
|
12
|
+
def add_event(description)
|
13
|
+
@event_id ||= 0
|
14
|
+
event = instance_double(
|
15
|
+
Aws::CloudFormation::Event,
|
16
|
+
:event_id => @event_id, :resource_status_reason => description
|
17
|
+
)
|
18
|
+
events.unshift(event)
|
19
|
+
@event_id += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with a empty set of events" do
|
23
|
+
|
24
|
+
describe "#new_events" do
|
25
|
+
|
26
|
+
it "is empty" do
|
27
|
+
expect(subject.new_events).to be_empty
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when the stack does not exist" do
|
35
|
+
|
36
|
+
before do
|
37
|
+
allow(stack).to receive(:events) do
|
38
|
+
fail Aws::CloudFormation::Errors::ValidationError.new("test", "no such stack")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#new_events" do
|
43
|
+
|
44
|
+
it "is empty" do
|
45
|
+
expect(subject.new_events).to be_empty
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def new_event_reasons
|
53
|
+
subject.new_events.map(&:resource_status_reason)
|
54
|
+
end
|
55
|
+
|
56
|
+
context "when the stack has existing events" do
|
57
|
+
|
58
|
+
before do
|
59
|
+
add_event("earlier")
|
60
|
+
add_event("later")
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#new_events" do
|
64
|
+
|
65
|
+
it "returns the events in the order they occurred" do
|
66
|
+
expect(new_event_reasons).to eq(["earlier", "later"])
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
context "and more events occur" do
|
72
|
+
|
73
|
+
before do
|
74
|
+
subject.new_events
|
75
|
+
add_event("even")
|
76
|
+
add_event("more")
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#new_events" do
|
80
|
+
|
81
|
+
it "returns only the new events" do
|
82
|
+
expect(new_event_reasons).to eq(["even", "more"])
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/stackup.gemspec
CHANGED
@@ -4,10 +4,10 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
Gem::Specification.new do |spec|
|
5
5
|
|
6
6
|
spec.name = "stackup"
|
7
|
-
spec.version = "0.0
|
7
|
+
spec.version = "0.1.0"
|
8
8
|
spec.authors = ["Arvind Kunday", "Mike Williams"]
|
9
9
|
spec.email = ["arvind.kunday@rea-group.com", "mike.williams@rea-group.com"]
|
10
|
-
spec.summary = "
|
10
|
+
spec.summary = "Manage CloudFormation stacks"
|
11
11
|
spec.homepage = "https://github.com/realestate-com-au/stackup"
|
12
12
|
spec.license = "MIT"
|
13
13
|
|
@@ -17,5 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
|
18
18
|
spec.add_dependency "aws-sdk", "~> 2.0"
|
19
19
|
spec.add_dependency "clamp", "~> 1.0"
|
20
|
+
spec.add_dependency "console_logger"
|
21
|
+
spec.add_dependency "multi_json"
|
20
22
|
|
21
23
|
end
|
@@ -34,6 +34,46 @@
|
|
34
34
|
}
|
35
35
|
]
|
36
36
|
}
|
37
|
+
},
|
38
|
+
"role2": {
|
39
|
+
"Type": "AWS::IAM::Role",
|
40
|
+
"Properties": {
|
41
|
+
"AssumeRolePolicyDocument": {
|
42
|
+
"Version" : "2012-10-17",
|
43
|
+
"Statement": [
|
44
|
+
{
|
45
|
+
"Effect": "Allow",
|
46
|
+
"Principal": {
|
47
|
+
"Service": [ "ec2.amazonaws.com" ]
|
48
|
+
},
|
49
|
+
"Action": [ "sts:AssumeRole" ]
|
50
|
+
}
|
51
|
+
]
|
52
|
+
},
|
53
|
+
"Path": "/blah/",
|
54
|
+
"Policies": [
|
55
|
+
{
|
56
|
+
"PolicyName": "rooty",
|
57
|
+
"PolicyDocument": {
|
58
|
+
"Version" : "2012-10-17",
|
59
|
+
"Statement": [
|
60
|
+
{
|
61
|
+
"Effect": "Allow",
|
62
|
+
"Action": "*",
|
63
|
+
"Resource": "*"
|
64
|
+
}
|
65
|
+
]
|
66
|
+
}
|
67
|
+
}
|
68
|
+
]
|
69
|
+
}
|
70
|
+
}
|
71
|
+
},
|
72
|
+
"Outputs": {
|
73
|
+
"role": {
|
74
|
+
"Value": {
|
75
|
+
"Ref": "role"
|
76
|
+
}
|
37
77
|
}
|
38
78
|
}
|
39
79
|
}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stackup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arvind Kunday
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-10-
|
12
|
+
date: 2015-10-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -39,6 +39,34 @@ dependencies:
|
|
39
39
|
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '1.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: console_logger
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: multi_json
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
42
70
|
description:
|
43
71
|
email:
|
44
72
|
- arvind.kunday@rea-group.com
|
@@ -54,14 +82,15 @@ files:
|
|
54
82
|
- Rakefile
|
55
83
|
- bin/stackup
|
56
84
|
- lib/stackup.rb
|
57
|
-
- lib/stackup/
|
58
|
-
- lib/stackup/monitor.rb
|
85
|
+
- lib/stackup/errors.rb
|
59
86
|
- lib/stackup/stack.rb
|
87
|
+
- lib/stackup/stack_watcher.rb
|
60
88
|
- pkg/stackup-0.0.1.gem
|
61
89
|
- pkg/stackup-0.0.8.gem
|
90
|
+
- pkg/stackup-0.0.9.gem
|
62
91
|
- spec/spec_helper.rb
|
63
|
-
- spec/stackup/monitor_spec.rb
|
64
92
|
- spec/stackup/stack_spec.rb
|
93
|
+
- spec/stackup/stack_watcher_spec.rb
|
65
94
|
- stackup.gemspec
|
66
95
|
- woollyams/sample-template.json
|
67
96
|
homepage: https://github.com/realestate-com-au/stackup
|
@@ -87,5 +116,5 @@ rubyforge_project:
|
|
87
116
|
rubygems_version: 2.4.8
|
88
117
|
signing_key:
|
89
118
|
specification_version: 4
|
90
|
-
summary:
|
119
|
+
summary: Manage CloudFormation stacks
|
91
120
|
test_files: []
|
data/lib/stackup/cli.rb
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
require "clamp"
|
2
|
-
require "json"
|
3
|
-
require "stackup/stack"
|
4
|
-
|
5
|
-
module Stackup
|
6
|
-
|
7
|
-
class CLI < Clamp::Command
|
8
|
-
|
9
|
-
subcommand "stack", "Manage a stack." do
|
10
|
-
|
11
|
-
parameter "NAME", "Name of stack", :attribute_name => :stack_name
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def stack
|
16
|
-
Stackup::Stack.new(stack_name)
|
17
|
-
end
|
18
|
-
|
19
|
-
subcommand "status", "Print stack status." do
|
20
|
-
|
21
|
-
def execute
|
22
|
-
puts stack.status
|
23
|
-
end
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
subcommand "deploy", "Create/update the stack" do
|
28
|
-
|
29
|
-
parameter "TEMPLATE", "CloudFormation template (.json)", :attribute_name => :template_file
|
30
|
-
|
31
|
-
def execute
|
32
|
-
template = File.read(template_file)
|
33
|
-
stack.deploy(template)
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
subcommand "delete", "Remove the stack." do
|
39
|
-
def execute
|
40
|
-
stack.delete
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
subcommand "outputs", "Stack outputs." do
|
45
|
-
def execute
|
46
|
-
stack.outputs
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
end
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
end
|
data/lib/stackup/monitor.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
require "aws-sdk-core"
|
2
|
-
|
3
|
-
module Stackup
|
4
|
-
class Monitor
|
5
|
-
|
6
|
-
attr_accessor :stack, :events
|
7
|
-
def initialize(stack)
|
8
|
-
@stack = stack
|
9
|
-
@events = Set.new
|
10
|
-
end
|
11
|
-
|
12
|
-
def new_events
|
13
|
-
stack.events.take_while do |event|
|
14
|
-
!seen?(event)
|
15
|
-
end.reverse
|
16
|
-
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
17
|
-
[]
|
18
|
-
end
|
19
|
-
|
20
|
-
private
|
21
|
-
|
22
|
-
def seen?(event)
|
23
|
-
event_id = event.id
|
24
|
-
if events.include?(event_id)
|
25
|
-
true
|
26
|
-
else
|
27
|
-
events.add(event_id)
|
28
|
-
false
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
end
|
33
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe Stackup::Monitor do
|
4
|
-
|
5
|
-
let(:stack) { instance_double(Aws::CloudFormation::Stack, :events => events) }
|
6
|
-
let(:monitor) { described_class.new(stack) }
|
7
|
-
|
8
|
-
let(:event) { instance_double(Aws::CloudFormation::Event, :id => "1") }
|
9
|
-
let(:events) { [event] }
|
10
|
-
|
11
|
-
before do
|
12
|
-
allow(event).to receive(:event_id).and_return("1")
|
13
|
-
end
|
14
|
-
|
15
|
-
it "should add the event if it is non-existent" do
|
16
|
-
expect(monitor.new_events.size).to eq(1)
|
17
|
-
end
|
18
|
-
|
19
|
-
it "should skip the event if it has been shown" do
|
20
|
-
expect(monitor.new_events.size).to eq(1)
|
21
|
-
expect(monitor.new_events.size).to eq(0)
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|