stackup 1.1.3 → 1.2.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/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
|