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 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