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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8d9136bd380d1644176d3b65c7ee93315016d455
4
- data.tar.gz: ea8bdd9c6766e6f581557073a2afb5134efd3902
3
+ metadata.gz: f0711fc64ee45d78fe5e7d71ab1afff01906ceb1
4
+ data.tar.gz: b7374819ee928e7c3147980b88a2ef5b70c84c67
5
5
  SHA512:
6
- metadata.gz: 3f9a13acc03300f01506f109ae336c794e49753ef592e630462faa89fa39f6e858e43fe56c33c3c03c59ded1132e063a1eb7c2431005b674ee6f43d39a6749d4
7
- data.tar.gz: 48b38607b8e0ee0015ab1fd81e049e62c9b53ab7338b005be51e15a9fcf8e573bbde69cd9352fec2688111ae2c1b60492234dacaed56d25cfdab9bfaa2610dde
6
+ metadata.gz: a4574add771970c33b04d0b1cd60007b1e72f03863241e69912b880ec6b77d5f55149cfaafffe823a88a5c017097df05b2047981ed05c2bf4abdb93707e827bb
7
+ data.tar.gz: e1ba38d93f94fd8081884c2d01d9568c9572eba7ea38b0be5213607709b123ac2008aa91cbfe9aff8a6ece7978c20636ad2519a819a543dbcf87f1e90de6558e
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.2.0 (2017-10-24)
2
+
3
+ * Add support for change-sets.
4
+
1
5
  ## 1.1.3 (2017-05-01)
2
6
 
3
7
  * Fix #39: parse error when JSON-like text is embedded in YAML.
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
- subcommand "up", "Create/update the stack." do
140
+ module HasParameters
147
141
 
148
- option ["-t", "--template"], "FILE", "template source",
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 parameters
207
- parameters_from_files.merge(parameter_overrides)
243
+ def pad(s, width)
244
+ (s || "").ljust(width)
208
245
  end
209
246
 
210
- def parameter_overrides
211
- {}.tap do |result|
212
- override_list.each do |override|
213
- key, value = override.split("=", 2)
214
- result[key] = value
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
- option ["-p", "--parameters"], "FILE", "parameters file (last wins)",
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(parameters_from_files)
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 a +ValidationError+ is raised, check the message; there's often
12
- # useful information is hidden inside. If that's the case, convert it to
13
- # an appropriate Stackup exception.
11
+ # If an exception is raised, convert it to a Stackup exception,
12
+ # if appropriate.
14
13
  #
15
- def handling_validation_error
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/
@@ -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 < ValidationError; end
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
@@ -16,8 +16,12 @@ module Stackup
16
16
  def hashify(parameters)
17
17
  {}.tap do |result|
18
18
  parameters.each do |p|
19
- p_struct = ParameterStruct.new(p)
20
- result[p_struct.key] = p_struct.value
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
- public_send("#{name}=", value)
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
- handling_validation_error do
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 [Symbol] +:created+ or +:updated+ if successful
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 [Symbol] +:deleted+ if successful
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 = handling_validation_error do
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 [Symbol] +:update_cancelled+ if successful
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
- handling_validation_error do
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
- handling_validation_error do
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
- handling_validation_error do
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
- handling_validation_error do
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)
@@ -1,5 +1,5 @@
1
1
  module Stackup
2
2
 
3
- VERSION = "1.1.3"
3
+ VERSION = "1.2.0"
4
4
 
5
5
  end
data/lib/stackup/yaml.rb CHANGED
@@ -64,7 +64,7 @@ module Stackup
64
64
 
65
65
  def array_or_dotted_string(arg)
66
66
  if arg.respond_to?(:split)
67
- arg.split(".")
67
+ arg.split(".", 2)
68
68
  else
69
69
  arg
70
70
  end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require "byebug"
1
2
  require "console_logger"
2
3
 
3
4
  module CfStubbing
@@ -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 validation_error(message)
33
+ def error(type, message)
26
34
  {
27
35
  :status_code => 400,
28
36
  :headers => {},
29
37
  :body => [
30
- "<ErrorResponse><Error><Code>ValidationError</Code><Message>",
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
 
@@ -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.1.3
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-05-01 00:00:00.000000000 Z
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.6.10
137
+ rubygems_version: 2.5.1
137
138
  signing_key:
138
139
  specification_version: 4
139
140
  summary: Manage CloudFormation stacks