stackup 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
|