stackup 1.1.3 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +4 -0
- data/README.md +13 -1
- data/bin/stackup +147 -27
- data/lib/stackup/change_set.rb +153 -0
- data/lib/stackup/error_handling.rb +6 -5
- data/lib/stackup/errors.rb +7 -1
- data/lib/stackup/parameters.rb +12 -3
- data/lib/stackup/stack.rb +30 -9
- data/lib/stackup/version.rb +1 -1
- data/lib/stackup/yaml.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/stackup/stack_spec.rb +210 -2
- data/spec/stackup/yaml_spec.rb +24 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0711fc64ee45d78fe5e7d71ab1afff01906ceb1
|
4
|
+
data.tar.gz: b7374819ee928e7c3147980b88a2ef5b70c84c67
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4574add771970c33b04d0b1cd60007b1e72f03863241e69912b880ec6b77d5f55149cfaafffe823a88a5c017097df05b2047981ed05c2bf4abdb93707e827bb
|
7
|
+
data.tar.gz: e1ba38d93f94fd8081884c2d01d9568c9572eba7ea38b0be5213607709b123ac2008aa91cbfe9aff8a6ece7978c20636ad2519a819a543dbcf87f1e90de6558e
|
data/CHANGES.md
CHANGED
data/README.md
CHANGED
@@ -18,6 +18,7 @@ AWS CloudFormation stacks.
|
|
18
18
|
- [Using URLs as inputs](#using-urls-as-inputs)
|
19
19
|
- [Stack deletion](#stack-deletion)
|
20
20
|
- [Stack inspection](#stack-inspection)
|
21
|
+
- [Change-set support](#change-set-support)
|
21
22
|
- [Programmatic usage](#programmatic-usage)
|
22
23
|
- [Rake integration](#rake-integration)
|
23
24
|
- [Docker image](#docker-image)
|
@@ -150,6 +151,17 @@ Inspect details of a stack with:
|
|
150
151
|
$ stackup myapp-test resources
|
151
152
|
$ stackup myapp-test outputs
|
152
153
|
|
154
|
+
### Change-set support
|
155
|
+
|
156
|
+
You can also create, list, inspect, apply and delete [change sets](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html) using `stackup`.
|
157
|
+
|
158
|
+
$ stackup myapp-test change-sets
|
159
|
+
$ stackup myapp-test change-set create -t template.json
|
160
|
+
$ stackup myapp-test change-set inspect
|
161
|
+
$ stackup myapp-test change-set apply
|
162
|
+
|
163
|
+
The change-set name defaults to "pending", but can be overridden using `--name`.
|
164
|
+
|
153
165
|
## Programmatic usage
|
154
166
|
|
155
167
|
Get a handle to a `Stack` object as follows:
|
@@ -195,7 +207,7 @@ Parameters and tags may be specified via files, or as a Hash, e.g.
|
|
195
207
|
Stackup is also published as a Docker image. Basic usage is:
|
196
208
|
|
197
209
|
docker run --rm \
|
198
|
-
-v `pwd`:/cwd \
|
210
|
+
-v "`pwd`:/cwd" \
|
199
211
|
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
|
200
212
|
-e AWS_DEFAULT_REGION \
|
201
213
|
realestate/stackup:latest ...
|
data/bin/stackup
CHANGED
@@ -129,12 +129,6 @@ Clamp do
|
|
129
129
|
puts final_status unless final_status.nil?
|
130
130
|
end
|
131
131
|
|
132
|
-
def parameters_from_files
|
133
|
-
parameter_sources.map { |src|
|
134
|
-
Stackup::Parameters.new(src.data).to_hash
|
135
|
-
}.inject({}, :merge)
|
136
|
-
end
|
137
|
-
|
138
132
|
subcommand "status", "Print stack status." do
|
139
133
|
|
140
134
|
def execute
|
@@ -143,14 +137,9 @@ Clamp do
|
|
143
137
|
|
144
138
|
end
|
145
139
|
|
146
|
-
|
140
|
+
module HasParameters
|
147
141
|
|
148
|
-
|
149
|
-
:attribute_name => :template_source,
|
150
|
-
&Stackup::Source.method(:new)
|
151
|
-
|
152
|
-
option ["-T", "--use-previous-template"], :flag,
|
153
|
-
"reuse the existing template"
|
142
|
+
extend Clamp::Option::Declaration
|
154
143
|
|
155
144
|
option ["-p", "--parameters"], "FILE", "parameters file (last wins)",
|
156
145
|
:multivalued => true,
|
@@ -161,6 +150,40 @@ Clamp do
|
|
161
150
|
:multivalued => true,
|
162
151
|
:attribute_name => :override_list
|
163
152
|
|
153
|
+
private
|
154
|
+
|
155
|
+
def parameters
|
156
|
+
parameters_from_files.merge(parameter_overrides)
|
157
|
+
end
|
158
|
+
|
159
|
+
def parameters_from_files
|
160
|
+
parameter_sources.map { |src|
|
161
|
+
Stackup::Parameters.new(src.data).to_hash
|
162
|
+
}.inject({}, :merge)
|
163
|
+
end
|
164
|
+
|
165
|
+
def parameter_overrides
|
166
|
+
{}.tap do |result|
|
167
|
+
override_list.each do |override|
|
168
|
+
key, value = override.split("=", 2)
|
169
|
+
result[key] = value
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
subcommand "up", "Create/update the stack." do
|
177
|
+
|
178
|
+
option ["-t", "--template"], "FILE", "template source",
|
179
|
+
:attribute_name => :template_source,
|
180
|
+
&Stackup::Source.method(:new)
|
181
|
+
|
182
|
+
option ["-T", "--use-previous-template"], :flag,
|
183
|
+
"reuse the existing template"
|
184
|
+
|
185
|
+
include HasParameters
|
186
|
+
|
164
187
|
option "--tags", "FILE", "stack tags file",
|
165
188
|
:attribute_name => :tag_source,
|
166
189
|
&Stackup::Source.method(:new)
|
@@ -201,40 +224,137 @@ Clamp do
|
|
201
224
|
end
|
202
225
|
end
|
203
226
|
|
227
|
+
end
|
228
|
+
|
229
|
+
subcommand ["change-sets"], "List change-sets." do
|
230
|
+
|
231
|
+
def execute
|
232
|
+
stack.change_set_summaries.each do |change_set|
|
233
|
+
puts [
|
234
|
+
pad(change_set.change_set_name, 36),
|
235
|
+
pad(change_set.status, 20),
|
236
|
+
pad(change_set.execution_status, 24)
|
237
|
+
].join(" ")
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
204
241
|
private
|
205
242
|
|
206
|
-
def
|
207
|
-
|
243
|
+
def pad(s, width)
|
244
|
+
(s || "").ljust(width)
|
208
245
|
end
|
209
246
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
247
|
+
end
|
248
|
+
|
249
|
+
subcommand ["change-set"], "Change-set operations." do
|
250
|
+
|
251
|
+
option "--name", "NAME", "Name of change-set",
|
252
|
+
:attribute_name => :change_set_name,
|
253
|
+
:default => "pending"
|
254
|
+
|
255
|
+
subcommand "create", "Create a change-set." do
|
256
|
+
|
257
|
+
option ["-d", "--description"], "DESC",
|
258
|
+
"Change-set description"
|
259
|
+
|
260
|
+
option ["-t", "--template"], "FILE", "template source",
|
261
|
+
:attribute_name => :template_source,
|
262
|
+
&Stackup::Source.method(:new)
|
263
|
+
|
264
|
+
option ["-T", "--use-previous-template"], :flag,
|
265
|
+
"reuse the existing template"
|
266
|
+
|
267
|
+
option ["--force"], :flag,
|
268
|
+
"replace existing change-set of the same name"
|
269
|
+
|
270
|
+
include HasParameters
|
271
|
+
|
272
|
+
option "--tags", "FILE", "stack tags file",
|
273
|
+
:attribute_name => :tag_source,
|
274
|
+
&Stackup::Source.method(:new)
|
275
|
+
|
276
|
+
def execute
|
277
|
+
unless template_source || use_previous_template?
|
278
|
+
signal_usage_error "Specify either --template or --use-previous-template"
|
279
|
+
end
|
280
|
+
options = {}
|
281
|
+
if template_source
|
282
|
+
if template_source.s3?
|
283
|
+
options[:template_url] = template_source.location
|
284
|
+
else
|
285
|
+
options[:template] = template_source.data
|
286
|
+
end
|
287
|
+
end
|
288
|
+
options[:parameters] = parameters
|
289
|
+
options[:description] = description if description
|
290
|
+
options[:tags] = tag_source.data if tag_source
|
291
|
+
options[:use_previous_template] = use_previous_template?
|
292
|
+
options[:force] = force?
|
293
|
+
report_change do
|
294
|
+
change_set.create(options)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
299
|
+
|
300
|
+
subcommand "changes", "Describe the change-set." do
|
301
|
+
|
302
|
+
def execute
|
303
|
+
display_data(change_set.describe.changes.map(&:to_h))
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
|
308
|
+
subcommand "inspect", "Show full change-set details." do
|
309
|
+
|
310
|
+
def execute
|
311
|
+
display_data(change_set.describe.to_h)
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
subcommand ["apply", "execute"], "Apply the change-set." do
|
317
|
+
|
318
|
+
def execute
|
319
|
+
report_change do
|
320
|
+
change_set.execute
|
215
321
|
end
|
216
322
|
end
|
323
|
+
|
324
|
+
end
|
325
|
+
|
326
|
+
subcommand "delete", "Delete the change-set." do
|
327
|
+
|
328
|
+
def execute
|
329
|
+
report_change do
|
330
|
+
change_set.delete
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
end
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
def change_set
|
339
|
+
stack.change_set(change_set_name)
|
217
340
|
end
|
218
341
|
|
219
342
|
end
|
220
343
|
|
221
344
|
subcommand "diff", "Compare template/params to current stack." do
|
222
345
|
|
346
|
+
option "--diff-format", "FORMAT", "'text', 'color', or 'html'", :default => "color"
|
347
|
+
|
223
348
|
option ["-t", "--template"], "FILE", "template source",
|
224
349
|
:attribute_name => :template_source,
|
225
350
|
&Stackup::Source.method(:new)
|
226
351
|
|
227
|
-
|
228
|
-
:multivalued => true,
|
229
|
-
:attribute_name => :parameter_sources,
|
230
|
-
&Stackup::Source.method(:new)
|
352
|
+
include HasParameters
|
231
353
|
|
232
354
|
option "--tags", "FILE", "stack tags file",
|
233
355
|
:attribute_name => :tag_source,
|
234
356
|
&Stackup::Source.method(:new)
|
235
357
|
|
236
|
-
option "--diff-format", "FORMAT", "'text', 'color', or 'html'", :default => "color"
|
237
|
-
|
238
358
|
def execute
|
239
359
|
current = {}
|
240
360
|
planned = {}
|
@@ -265,7 +385,7 @@ Clamp do
|
|
265
385
|
end
|
266
386
|
|
267
387
|
def new_parameters
|
268
|
-
existing_parameters.merge(
|
388
|
+
existing_parameters.merge(parameters)
|
269
389
|
end
|
270
390
|
|
271
391
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require "stackup/error_handling"
|
2
|
+
|
3
|
+
module Stackup
|
4
|
+
|
5
|
+
# An abstraction of a CloudFormation change-set.
|
6
|
+
#
|
7
|
+
class ChangeSet
|
8
|
+
|
9
|
+
def initialize(name, stack)
|
10
|
+
@name = name
|
11
|
+
@stack = stack
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name
|
15
|
+
attr_reader :stack
|
16
|
+
|
17
|
+
include ErrorHandling
|
18
|
+
|
19
|
+
# Create the change-set.
|
20
|
+
#
|
21
|
+
# Refer +Aws::CloudFormation::Client#create_change_set+
|
22
|
+
# (see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html#create_change_set-instance_method)
|
23
|
+
#
|
24
|
+
# @param [Hash] options change-set options
|
25
|
+
# @option options [Array<String>] :capabilities (CAPABILITY_NAMED_IAM)
|
26
|
+
# list of capabilities required for stack template
|
27
|
+
# @option options [String] :description
|
28
|
+
# change-set description
|
29
|
+
# @option options [String] :notification_arns
|
30
|
+
# ARNs for the Amazon SNS topics associated with this stack
|
31
|
+
# @option options [Hash, Array<Hash>] :parameters
|
32
|
+
# stack parameters, either as a Hash, or an Array of
|
33
|
+
# +Aws::CloudFormation::Types::Parameter+ structures
|
34
|
+
# @option options [Hash, Array<Hash>] :tags
|
35
|
+
# stack tags, either as a Hash, or an Array of
|
36
|
+
# +Aws::CloudFormation::Types::Tag+ structures
|
37
|
+
# @option options [Array<String>] :resource_types
|
38
|
+
# resource types that you have permissions to work with
|
39
|
+
# @option options [Hash] :template
|
40
|
+
# stack template, as Ruby data
|
41
|
+
# @option options [String] :template_body
|
42
|
+
# stack template, as JSON or YAML
|
43
|
+
# @option options [String] :template_url
|
44
|
+
# location of stack template
|
45
|
+
# @option options [boolean] :use_previous_template
|
46
|
+
# if true, reuse the existing template
|
47
|
+
# @option options [boolean] :force
|
48
|
+
# if true, delete any existing change-set of the same name
|
49
|
+
#
|
50
|
+
# @return [String] change-set id
|
51
|
+
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
52
|
+
#
|
53
|
+
def create(options = {})
|
54
|
+
options = options.dup
|
55
|
+
options[:stack_name] = stack.name
|
56
|
+
options[:change_set_name] = name
|
57
|
+
options[:change_set_type] = stack.exists? ? "UPDATE" : "CREATE"
|
58
|
+
force = options.delete(:force)
|
59
|
+
if (template_data = options.delete(:template))
|
60
|
+
options[:template_body] = MultiJson.dump(template_data)
|
61
|
+
end
|
62
|
+
if (parameters = options[:parameters])
|
63
|
+
options[:parameters] = Parameters.new(parameters).to_a
|
64
|
+
end
|
65
|
+
if (tags = options[:tags])
|
66
|
+
options[:tags] = normalize_tags(tags)
|
67
|
+
end
|
68
|
+
options[:capabilities] ||= ["CAPABILITY_NAMED_IAM"]
|
69
|
+
delete if force
|
70
|
+
handling_cf_errors do
|
71
|
+
cf_client.create_change_set(options)
|
72
|
+
loop do
|
73
|
+
current = describe
|
74
|
+
logger.debug("change_set_status=#{current.status}")
|
75
|
+
case current.status
|
76
|
+
when /COMPLETE/
|
77
|
+
return current.status
|
78
|
+
when "FAILED"
|
79
|
+
logger.error(current.status_reason)
|
80
|
+
fail StackUpdateError, "change-set creation failed" if status == "FAILED"
|
81
|
+
end
|
82
|
+
sleep(wait_poll_interval)
|
83
|
+
end
|
84
|
+
status
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Execute the change-set.
|
89
|
+
#
|
90
|
+
# @return [String] resulting stack status
|
91
|
+
# @raise [Stackup::NoSuchChangeSet] if the change-set doesn't exist
|
92
|
+
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
93
|
+
# @raise [Stackup::StackUpdateError] if operation fails
|
94
|
+
#
|
95
|
+
def execute
|
96
|
+
modify_stack("UPDATE_COMPLETE", "update failed") do
|
97
|
+
cf_client.execute_change_set(:stack_name => stack.name, :change_set_name => name)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Delete a change-set.
|
102
|
+
#
|
103
|
+
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
104
|
+
#
|
105
|
+
def delete
|
106
|
+
handling_cf_errors do
|
107
|
+
cf_client.delete_change_set(:stack_name => stack.name, :change_set_name => name)
|
108
|
+
end
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
|
112
|
+
def status
|
113
|
+
describe.status
|
114
|
+
end
|
115
|
+
|
116
|
+
# Describe the change-set.
|
117
|
+
#
|
118
|
+
# Refer +Aws::CloudFormation::Client#describe_change_set+
|
119
|
+
# (http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html#describe_change_set-instance_method)
|
120
|
+
#
|
121
|
+
# @return change-set state, as data
|
122
|
+
#
|
123
|
+
def describe
|
124
|
+
handling_cf_errors do
|
125
|
+
cf_client.describe_change_set(:stack_name => stack.name, :change_set_name => name)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def cf_client
|
132
|
+
stack.send(:cf_client)
|
133
|
+
end
|
134
|
+
|
135
|
+
def modify_stack(*args, &block)
|
136
|
+
stack.send(:modify_stack, *args, &block)
|
137
|
+
end
|
138
|
+
|
139
|
+
def normalize_tags(tags)
|
140
|
+
stack.send(:normalize_tags, tags)
|
141
|
+
end
|
142
|
+
|
143
|
+
def logger
|
144
|
+
stack.send(:logger)
|
145
|
+
end
|
146
|
+
|
147
|
+
def wait_poll_interval
|
148
|
+
stack.send(:wait_poll_interval)
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -6,14 +6,15 @@ module Stackup
|
|
6
6
|
#
|
7
7
|
module ErrorHandling
|
8
8
|
|
9
|
-
# Invoke an Aws::CloudFormation operation
|
9
|
+
# Invoke an Aws::CloudFormation operation.
|
10
10
|
#
|
11
|
-
# If
|
12
|
-
#
|
13
|
-
# an appropriate Stackup exception.
|
11
|
+
# If an exception is raised, convert it to a Stackup exception,
|
12
|
+
# if appropriate.
|
14
13
|
#
|
15
|
-
def
|
14
|
+
def handling_cf_errors
|
16
15
|
yield
|
16
|
+
rescue Aws::CloudFormation::Errors::ChangeSetNotFound => e
|
17
|
+
raise NoSuchChangeSet, "no such change-set"
|
17
18
|
rescue Aws::CloudFormation::Errors::ValidationError => e
|
18
19
|
case e.message
|
19
20
|
when /Stack .* does not exist/
|
data/lib/stackup/errors.rb
CHANGED
@@ -6,8 +6,14 @@ module Stackup
|
|
6
6
|
# Raised when something else dodgy happened
|
7
7
|
class ValidationError < ServiceError; end
|
8
8
|
|
9
|
+
# Raised when the specified thing does not exist
|
10
|
+
class NoSuchThing < ValidationError; end
|
11
|
+
|
9
12
|
# Raised when the specified stack does not exist
|
10
|
-
class NoSuchStack <
|
13
|
+
class NoSuchStack < NoSuchThing; end
|
14
|
+
|
15
|
+
# Raised when the specified change-set does not exist
|
16
|
+
class NoSuchChangeSet < NoSuchThing; end
|
11
17
|
|
12
18
|
# Raised if we can't perform that operation now
|
13
19
|
class InvalidStateError < ValidationError; end
|
data/lib/stackup/parameters.rb
CHANGED
@@ -16,8 +16,12 @@ module Stackup
|
|
16
16
|
def hashify(parameters)
|
17
17
|
{}.tap do |result|
|
18
18
|
parameters.each do |p|
|
19
|
-
|
20
|
-
|
19
|
+
begin
|
20
|
+
p_struct = ParameterStruct.new(p)
|
21
|
+
result[p_struct.key] = p_struct.value
|
22
|
+
rescue ArgumentError
|
23
|
+
raise ArgumentError, "invalid parameter record: #{p.inspect}"
|
24
|
+
end
|
21
25
|
end
|
22
26
|
end
|
23
27
|
end
|
@@ -53,7 +57,12 @@ module Stackup
|
|
53
57
|
if name.respond_to?(:gsub)
|
54
58
|
name = name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
55
59
|
end
|
56
|
-
|
60
|
+
writer = "#{name}="
|
61
|
+
if respond_to?(writer)
|
62
|
+
public_send(writer, value)
|
63
|
+
else
|
64
|
+
raise ArgumentError, "invalid attribute: #{name.inspect}"
|
65
|
+
end
|
57
66
|
end
|
58
67
|
end
|
59
68
|
|
data/lib/stackup/stack.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "aws-sdk-resources"
|
2
2
|
require "logger"
|
3
3
|
require "multi_json"
|
4
|
+
require "stackup/change_set"
|
4
5
|
require "stackup/error_handling"
|
5
6
|
require "stackup/parameters"
|
6
7
|
require "stackup/stack_watcher"
|
@@ -49,7 +50,7 @@ module Stackup
|
|
49
50
|
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
50
51
|
#
|
51
52
|
def status
|
52
|
-
|
53
|
+
handling_cf_errors do
|
53
54
|
cf_stack.stack_status
|
54
55
|
end
|
55
56
|
end
|
@@ -107,7 +108,7 @@ module Stackup
|
|
107
108
|
# stack creation timeout
|
108
109
|
# @option options [boolean] :use_previous_template
|
109
110
|
# if true, reuse the existing template
|
110
|
-
# @return [
|
111
|
+
# @return [String] resulting stack status
|
111
112
|
# @raise [Stackup::StackUpdateError] if operation fails
|
112
113
|
#
|
113
114
|
def create_or_update(options)
|
@@ -140,12 +141,12 @@ module Stackup
|
|
140
141
|
|
141
142
|
# Delete the stack.
|
142
143
|
#
|
143
|
-
# @return [
|
144
|
+
# @return [String] "DELETE_COMPLETE"
|
144
145
|
# @raise [Stackup::StackUpdateError] if operation fails
|
145
146
|
#
|
146
147
|
def delete
|
147
148
|
begin
|
148
|
-
@stack_id =
|
149
|
+
@stack_id = handling_cf_errors do
|
149
150
|
cf_stack.stack_id
|
150
151
|
end
|
151
152
|
rescue NoSuchStack
|
@@ -162,7 +163,7 @@ module Stackup
|
|
162
163
|
|
163
164
|
# Cancel update in-progress.
|
164
165
|
#
|
165
|
-
# @return [
|
166
|
+
# @return [String] resulting stack status
|
166
167
|
# @raise [Stackup::StackUpdateError] if operation fails
|
167
168
|
#
|
168
169
|
def cancel_update
|
@@ -189,7 +190,7 @@ module Stackup
|
|
189
190
|
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
190
191
|
#
|
191
192
|
def template_body
|
192
|
-
|
193
|
+
handling_cf_errors do
|
193
194
|
cf_client.get_template(:stack_name => name).template_body
|
194
195
|
end
|
195
196
|
end
|
@@ -240,6 +241,26 @@ module Stackup
|
|
240
241
|
extract_hash(:resource_summaries, :logical_resource_id, :physical_resource_id)
|
241
242
|
end
|
242
243
|
|
244
|
+
# List change-sets.
|
245
|
+
#
|
246
|
+
# @return [Array<ChangeSetSummary>]
|
247
|
+
# @raise [Stackup::NoSuchStack] if the stack doesn't exist
|
248
|
+
#
|
249
|
+
def change_set_summaries
|
250
|
+
handling_cf_errors do
|
251
|
+
cf_client.list_change_sets(:stack_name => name).summaries
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# An abstraction of a CloudFormation change-set.
|
256
|
+
#
|
257
|
+
# @param [String] name change-set name
|
258
|
+
# @return [ChangeSet] an handle for change-set operations
|
259
|
+
#
|
260
|
+
def change_set(name)
|
261
|
+
ChangeSet.new(name, self)
|
262
|
+
end
|
263
|
+
|
243
264
|
def watch(zero = true)
|
244
265
|
watcher = Stackup::StackWatcher.new(cf_stack)
|
245
266
|
watcher.zero if zero
|
@@ -313,7 +334,7 @@ module Stackup
|
|
313
334
|
#
|
314
335
|
def modify_stack_synchronously
|
315
336
|
watch do |watcher|
|
316
|
-
|
337
|
+
handling_cf_errors do
|
317
338
|
yield
|
318
339
|
end
|
319
340
|
loop do
|
@@ -331,7 +352,7 @@ module Stackup
|
|
331
352
|
# @return the stack status
|
332
353
|
#
|
333
354
|
def modify_stack_asynchronously
|
334
|
-
|
355
|
+
handling_cf_errors do
|
335
356
|
yield
|
336
357
|
end
|
337
358
|
self.status
|
@@ -355,7 +376,7 @@ module Stackup
|
|
355
376
|
# @return [Hash<String, String>] mapping of collection
|
356
377
|
#
|
357
378
|
def extract_hash(collection_name, key_name, value_name)
|
358
|
-
|
379
|
+
handling_cf_errors do
|
359
380
|
{}.tap do |result|
|
360
381
|
cf_stack.public_send(collection_name).each do |item|
|
361
382
|
key = item.public_send(key_name)
|
data/lib/stackup/version.rb
CHANGED
data/lib/stackup/yaml.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
data/spec/stackup/stack_spec.rb
CHANGED
@@ -10,30 +10,42 @@ describe Stackup::Stack do
|
|
10
10
|
let(:unique_stack_id) { "ID:#{stack_name}" }
|
11
11
|
let(:stack_options) { {} }
|
12
12
|
|
13
|
+
let(:change_set_name) { "ch-ch-changes" }
|
14
|
+
|
13
15
|
subject(:stack) { described_class.new(stack_name, cf_client, stack_options) }
|
14
16
|
|
15
17
|
before do
|
16
18
|
cf_client.stub_responses(:describe_stacks, *describe_stacks_responses)
|
19
|
+
cf_client.stub_responses(:describe_change_set, *describe_change_set_responses)
|
17
20
|
allow(stack).to receive(:sleep).at_most(5).times
|
18
21
|
# partial stubbing, to support spying
|
19
22
|
allow(cf_client).to receive(:create_stack).and_call_original
|
20
23
|
allow(cf_client).to receive(:delete_stack).and_call_original
|
21
24
|
allow(cf_client).to receive(:update_stack).and_call_original
|
22
25
|
allow(cf_client).to receive(:cancel_update_stack).and_call_original
|
26
|
+
allow(cf_client).to receive(:list_change_sets).and_call_original
|
27
|
+
allow(cf_client).to receive(:create_change_set).and_call_original
|
28
|
+
allow(cf_client).to receive(:execute_change_set).and_call_original
|
29
|
+
allow(cf_client).to receive(:delete_change_set).and_call_original
|
30
|
+
allow(cf_client).to receive(:describe_change_set).and_call_original
|
23
31
|
end
|
24
32
|
|
25
|
-
def
|
33
|
+
def error(type, message)
|
26
34
|
{
|
27
35
|
:status_code => 400,
|
28
36
|
:headers => {},
|
29
37
|
:body => [
|
30
|
-
"<ErrorResponse><Error><Code
|
38
|
+
"<ErrorResponse><Error><Code>#{type}</Code><Message>",
|
31
39
|
message,
|
32
40
|
"</Message></Error></ErrorResponse>"
|
33
41
|
].join
|
34
42
|
}
|
35
43
|
end
|
36
44
|
|
45
|
+
def validation_error(message)
|
46
|
+
error("ValidationError", message)
|
47
|
+
end
|
48
|
+
|
37
49
|
def stack_description(stack_status, details = {})
|
38
50
|
{
|
39
51
|
:stacks => [
|
@@ -47,6 +59,14 @@ describe Stackup::Stack do
|
|
47
59
|
}
|
48
60
|
end
|
49
61
|
|
62
|
+
let(:describe_change_set_responses) do
|
63
|
+
[
|
64
|
+
{
|
65
|
+
:status => "CREATE_COMPLETE"
|
66
|
+
}
|
67
|
+
]
|
68
|
+
end
|
69
|
+
|
50
70
|
context "before stack exists" do
|
51
71
|
|
52
72
|
let(:describe_stacks_responses) do
|
@@ -262,6 +282,86 @@ describe Stackup::Stack do
|
|
262
282
|
|
263
283
|
end
|
264
284
|
|
285
|
+
describe "#change_set#create" do
|
286
|
+
|
287
|
+
let(:template) { "stack template" }
|
288
|
+
|
289
|
+
let(:options) do
|
290
|
+
{ :template_body => template }
|
291
|
+
end
|
292
|
+
|
293
|
+
def create_change_set
|
294
|
+
stack.change_set(change_set_name).create(options)
|
295
|
+
end
|
296
|
+
|
297
|
+
it "calls :create_change_set" do
|
298
|
+
expected_args = {
|
299
|
+
:stack_name => stack_name,
|
300
|
+
:change_set_name => change_set_name,
|
301
|
+
:change_set_type => "CREATE",
|
302
|
+
:template_body => template
|
303
|
+
}
|
304
|
+
create_change_set
|
305
|
+
expect(cf_client).to have_received(:create_change_set)
|
306
|
+
.with(hash_including(expected_args))
|
307
|
+
end
|
308
|
+
|
309
|
+
context "with :template as data" do
|
310
|
+
|
311
|
+
let(:options) do
|
312
|
+
{ :template => { "foo" => "bar" } }
|
313
|
+
end
|
314
|
+
|
315
|
+
it "converts the template to JSON" do
|
316
|
+
create_change_set
|
317
|
+
expect(cf_client).to have_received(:create_change_set)
|
318
|
+
.with(hash_including(:template_body))
|
319
|
+
end
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
context "with :parameters as Hash" do
|
324
|
+
|
325
|
+
before do
|
326
|
+
options[:parameters] = { "foo" => "bar" }
|
327
|
+
end
|
328
|
+
|
329
|
+
it "converts them to an Array" do
|
330
|
+
expected_parameters = [
|
331
|
+
{
|
332
|
+
:parameter_key => "foo",
|
333
|
+
:parameter_value => "bar"
|
334
|
+
}
|
335
|
+
]
|
336
|
+
create_change_set
|
337
|
+
expect(cf_client).to have_received(:create_change_set) do |options|
|
338
|
+
expect(options[:parameters]).to eq(expected_parameters)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
end
|
343
|
+
|
344
|
+
context "with :tags as Hash" do
|
345
|
+
|
346
|
+
before do
|
347
|
+
options[:tags] = { "foo" => "bar", "code" => 123 }
|
348
|
+
end
|
349
|
+
|
350
|
+
it "converts them to an Array" do
|
351
|
+
expected_tags = [
|
352
|
+
{ :key => "foo", :value => "bar" },
|
353
|
+
{ :key => "code", :value => "123" }
|
354
|
+
]
|
355
|
+
create_change_set
|
356
|
+
expect(cf_client).to have_received(:create_change_set) do |options|
|
357
|
+
expect(options[:tags]).to eq(expected_tags)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|
362
|
+
|
363
|
+
end
|
364
|
+
|
265
365
|
end
|
266
366
|
|
267
367
|
context "with existing stack" do
|
@@ -426,6 +526,114 @@ describe Stackup::Stack do
|
|
426
526
|
|
427
527
|
end
|
428
528
|
|
529
|
+
describe "#change_set_summaries" do
|
530
|
+
|
531
|
+
it "calls :list_change_sets" do
|
532
|
+
cf_client.stub_responses(:list_change_sets, :summaries => [
|
533
|
+
{ :change_set_name => "take1" },
|
534
|
+
{ :change_set_name => "take2" },
|
535
|
+
])
|
536
|
+
summaries = stack.change_set_summaries
|
537
|
+
expect(summaries.map(&:change_set_name)).to eql(%w(take1 take2))
|
538
|
+
expect(cf_client).to have_received(:list_change_sets)
|
539
|
+
.with(:stack_name => stack_name)
|
540
|
+
end
|
541
|
+
|
542
|
+
end
|
543
|
+
|
544
|
+
describe "#change_set#create" do
|
545
|
+
|
546
|
+
let(:template) { "stack template" }
|
547
|
+
|
548
|
+
let(:options) do
|
549
|
+
{ :template_body => template }
|
550
|
+
end
|
551
|
+
|
552
|
+
def create_change_set
|
553
|
+
stack.change_set(change_set_name).create(options)
|
554
|
+
end
|
555
|
+
|
556
|
+
it "calls :create_change_set" do
|
557
|
+
expected_args = {
|
558
|
+
:stack_name => stack_name,
|
559
|
+
:change_set_name => change_set_name,
|
560
|
+
:change_set_type => "UPDATE",
|
561
|
+
:template_body => template
|
562
|
+
}
|
563
|
+
create_change_set
|
564
|
+
expect(cf_client).to have_received(:create_change_set)
|
565
|
+
.with(hash_including(expected_args))
|
566
|
+
end
|
567
|
+
|
568
|
+
end
|
569
|
+
|
570
|
+
describe "#change_set#execute" do
|
571
|
+
|
572
|
+
let(:change_set_name) { "change-it" }
|
573
|
+
|
574
|
+
let(:describe_stacks_responses) do
|
575
|
+
[
|
576
|
+
stack_description("UPDATE_IN_PROGRESS"),
|
577
|
+
stack_description("UPDATE_COMPLETE")
|
578
|
+
]
|
579
|
+
end
|
580
|
+
|
581
|
+
def execute_change_set
|
582
|
+
stack.change_set(change_set_name).execute
|
583
|
+
end
|
584
|
+
|
585
|
+
it "calls :execute_change_set" do
|
586
|
+
execute_change_set
|
587
|
+
expected_args = {
|
588
|
+
:stack_name => stack_name,
|
589
|
+
:change_set_name => change_set_name
|
590
|
+
}
|
591
|
+
expect(cf_client).to have_received(:execute_change_set)
|
592
|
+
.with(hash_including(expected_args))
|
593
|
+
end
|
594
|
+
|
595
|
+
it "it sleeps" do
|
596
|
+
execute_change_set
|
597
|
+
expect(stack).to have_received(:sleep).with(5)
|
598
|
+
end
|
599
|
+
|
600
|
+
it "returns status" do
|
601
|
+
expect(execute_change_set).to eq("UPDATE_COMPLETE")
|
602
|
+
end
|
603
|
+
|
604
|
+
context "if change-set doesn't exist" do
|
605
|
+
|
606
|
+
before do
|
607
|
+
cf_client.stub_responses(
|
608
|
+
:execute_change_set,
|
609
|
+
error("ChangeSetNotFound", "ChangeSet [foo] does not exist")
|
610
|
+
)
|
611
|
+
end
|
612
|
+
|
613
|
+
it "raises a NoSuchChangeSet error" do
|
614
|
+
expect { execute_change_set }.to raise_error(Stackup::NoSuchChangeSet)
|
615
|
+
end
|
616
|
+
|
617
|
+
end
|
618
|
+
|
619
|
+
end
|
620
|
+
|
621
|
+
describe "#change_set#delete" do
|
622
|
+
|
623
|
+
let(:change_set_name) { "change-it" }
|
624
|
+
|
625
|
+
it "calls :delete_change_set" do
|
626
|
+
stack.change_set(change_set_name).delete
|
627
|
+
expected_args = {
|
628
|
+
:stack_name => stack_name,
|
629
|
+
:change_set_name => change_set_name
|
630
|
+
}
|
631
|
+
expect(cf_client).to have_received(:delete_change_set)
|
632
|
+
.with(hash_including(expected_args))
|
633
|
+
end
|
634
|
+
|
635
|
+
end
|
636
|
+
|
429
637
|
%w(CREATE_FAILED ROLLBACK_COMPLETE).each do |initial_status|
|
430
638
|
context "when status is #{initial_status}" do
|
431
639
|
|
data/spec/stackup/yaml_spec.rb
CHANGED
@@ -100,6 +100,30 @@ describe Stackup::YAML do
|
|
100
100
|
|
101
101
|
end
|
102
102
|
|
103
|
+
context "with a string with multiple dots" do
|
104
|
+
|
105
|
+
let(:input) do
|
106
|
+
<<-YAML
|
107
|
+
Outputs:
|
108
|
+
Foo: !GetAtt Bar.Baz.Some.More.Things
|
109
|
+
YAML
|
110
|
+
end
|
111
|
+
|
112
|
+
it "splits on the first dot" do
|
113
|
+
expect(data).to eql(
|
114
|
+
"Outputs" => {
|
115
|
+
"Foo" => {
|
116
|
+
"Fn::GetAtt" => [
|
117
|
+
"Bar",
|
118
|
+
"Baz.Some.More.Things"
|
119
|
+
]
|
120
|
+
}
|
121
|
+
}
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
103
127
|
end
|
104
128
|
|
105
129
|
describe "!GetAZs" do
|
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: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Williams
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-10-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk-resources
|
@@ -94,6 +94,7 @@ files:
|
|
94
94
|
- README.md
|
95
95
|
- bin/stackup
|
96
96
|
- lib/stackup.rb
|
97
|
+
- lib/stackup/change_set.rb
|
97
98
|
- lib/stackup/differ.rb
|
98
99
|
- lib/stackup/error_handling.rb
|
99
100
|
- lib/stackup/errors.rb
|
@@ -133,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
134
|
version: '0'
|
134
135
|
requirements: []
|
135
136
|
rubyforge_project:
|
136
|
-
rubygems_version: 2.
|
137
|
+
rubygems_version: 2.5.1
|
137
138
|
signing_key:
|
138
139
|
specification_version: 4
|
139
140
|
summary: Manage CloudFormation stacks
|