stackup 0.1.0 → 0.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +12 -12
  3. data/README.md +38 -15
  4. data/bin/stackup +76 -34
  5. data/doc/Stackup/ErrorMappingProxy.html +378 -0
  6. data/doc/Stackup/InvalidStateError.html +142 -0
  7. data/doc/Stackup/NoSuchStack.html +142 -0
  8. data/doc/Stackup/NoUpdateRequired.html +134 -0
  9. data/doc/Stackup/Service.html +399 -0
  10. data/doc/Stackup/ServiceError.html +138 -0
  11. data/doc/Stackup/Stack.html +1269 -0
  12. data/doc/Stackup/StackUpdateError.html +142 -0
  13. data/doc/Stackup/StackWatcher.html +455 -0
  14. data/doc/Stackup.html +202 -0
  15. data/doc/_index.html +199 -0
  16. data/doc/class_list.html +58 -0
  17. data/doc/css/common.css +1 -0
  18. data/doc/css/full_list.css +57 -0
  19. data/doc/css/style.css +339 -0
  20. data/doc/file.README.html +143 -0
  21. data/doc/file_list.html +60 -0
  22. data/doc/frames.html +26 -0
  23. data/doc/index.html +143 -0
  24. data/doc/js/app.js +219 -0
  25. data/doc/js/full_list.js +181 -0
  26. data/doc/js/jquery.js +4 -0
  27. data/doc/method_list.html +195 -0
  28. data/doc/top-level-namespace.html +194 -0
  29. data/lib/stackup/error_mapping_proxy.rb +37 -0
  30. data/lib/stackup/errors.rb +9 -6
  31. data/lib/stackup/service.rb +39 -0
  32. data/lib/stackup/stack.rb +52 -59
  33. data/lib/stackup/stack_watcher.rb +16 -10
  34. data/lib/stackup.rb +26 -0
  35. data/spec/spec_helper.rb +15 -1
  36. data/spec/stackup/stack_spec.rb +114 -48
  37. data/spec/stackup/stack_watcher_spec.rb +20 -15
  38. data/stackup.gemspec +1 -1
  39. data/woollyams/template.json +44 -0
  40. metadata +29 -6
  41. data/pkg/stackup-0.0.1.gem +0 -0
  42. data/pkg/stackup-0.0.8.gem +0 -0
  43. data/pkg/stackup-0.0.9.gem +0 -0
  44. data/woollyams/sample-template.json +0 -79
@@ -0,0 +1,37 @@
1
+ require "stackup/errors"
2
+
3
+ module Stackup
4
+
5
+ # An error-mapping proxy for Aws::CloudFormation models.
6
+ #
7
+ # It exists to convert certain types of `ValidationError`, where useful
8
+ # information is hidden inside the "message", to Stackup exceptions.
9
+ #
10
+ class ErrorMappingProxy
11
+
12
+ def initialize(delegate)
13
+ @delegate = delegate
14
+ end
15
+
16
+ def method_missing(*args)
17
+ @delegate.send(*args)
18
+ rescue Aws::CloudFormation::Errors::ValidationError => e
19
+ case e.message
20
+ when "No updates are to be performed."
21
+ raise NoUpdateRequired, "no updates are required"
22
+ when /Stack .* does not exist$/
23
+ raise NoSuchStack, "no such stack"
24
+ when / can ?not be /
25
+ raise InvalidStateError, e.message
26
+ else
27
+ raise e
28
+ end
29
+ end
30
+
31
+ def respond_to?(method)
32
+ @delegate.respond_to?(method)
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -1,15 +1,18 @@
1
1
  module Stackup
2
2
 
3
3
  # Base Stackup Exception class
4
- class ServiceError < StandardError
5
- end
4
+ class ServiceError < StandardError; end
6
5
 
7
6
  # Raised when the specified stack does not exist
8
- class NoSuchStack < ServiceError
9
- end
7
+ class NoSuchStack < ServiceError; end
10
8
 
11
9
  # Raised to indicate a problem updating a stack
12
- class StackUpdateError < ServiceError
13
- end
10
+ class StackUpdateError < ServiceError; end
11
+
12
+ # Raised if we can't perform that operation now
13
+ class InvalidStateError < ServiceError; end
14
+
15
+ # Raised when a stack is already up-to-date
16
+ class NoUpdateRequired < StandardError; end
14
17
 
15
18
  end
@@ -0,0 +1,39 @@
1
+ require "aws-sdk-core"
2
+ require "stackup/stack"
3
+
4
+ module Stackup
5
+
6
+ # A handle to CloudFormation.
7
+ #
8
+ class Service
9
+
10
+ def initialize(cf_client = {})
11
+ cf_client = Aws::CloudFormation::Client.new(cf_client) if cf_client.is_a?(Hash)
12
+ @cf_client = cf_client
13
+ end
14
+
15
+ # @return [Stackup::Stack] the named stack
16
+ #
17
+ def stack(name, options = {})
18
+ Stack.new(name, cf_client, options)
19
+ end
20
+
21
+ # @return [Enumeration<String>] names of existing stacks
22
+ #
23
+ def stack_names
24
+ Enumerator.new do |y|
25
+ cf_client.describe_stacks.each do |response|
26
+ response.stacks.each do |stack|
27
+ y << stack.stack_name
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :cf_client
36
+
37
+ end
38
+
39
+ end
data/lib/stackup/stack.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "aws-sdk-resources"
2
2
  require "logger"
3
- require "stackup/errors"
3
+ require "stackup/error_mapping_proxy"
4
4
  require "stackup/stack_watcher"
5
5
 
6
6
  module Stackup
@@ -13,7 +13,6 @@ module Stackup
13
13
  client = Aws::CloudFormation::Client.new(client) if client.is_a?(Hash)
14
14
  @name = name
15
15
  @cf_client = client
16
- @watcher = Stackup::StackWatcher.new(cf_stack)
17
16
  options.each do |key, value|
18
17
  public_send("#{key}=", value)
19
18
  end
@@ -35,8 +34,6 @@ module Stackup
35
34
  #
36
35
  def status
37
36
  cf_stack.stack_status
38
- rescue Aws::CloudFormation::Errors::ValidationError => e
39
- handle_validation_error(e)
40
37
  end
41
38
 
42
39
  # @return [boolean] true iff the stack exists
@@ -50,16 +47,20 @@ module Stackup
50
47
 
51
48
  # Create or update the stack.
52
49
  #
53
- # @param [String] template template JSON
54
- # @param [Array<Hash>] parameters template parameters
50
+ # @param [Hash] options create/update options
51
+ # accepts a superset of the options supported by
52
+ # +Aws::CloudFormation::Stack#update+
53
+ # (see http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Stack.html#update-instance_method)
55
54
  # @return [Symbol] `:created` or `:updated` if successful
56
55
  # @raise [Stackup::StackUpdateError] if operation fails
57
56
  #
58
- def create_or_update(template, parameters = [])
57
+ def create_or_update(options)
58
+ options = options.dup
59
+ options[:capabilities] ||= ["CAPABILITY_IAM"]
59
60
  delete if ALMOST_DEAD_STATUSES.include?(status)
60
- update(template, parameters)
61
+ update(options)
61
62
  rescue NoSuchStack
62
- create(template, parameters)
63
+ create(options)
63
64
  end
64
65
 
65
66
  alias_method :up, :create_or_update
@@ -74,17 +75,37 @@ module Stackup
74
75
  # @raise [Stackup::StackUpdateError] if operation fails
75
76
  #
76
77
  def delete
77
- return nil unless exists?
78
+ begin
79
+ @stack_id = cf_stack.stack_id
80
+ rescue NoSuchStack
81
+ return nil
82
+ end
78
83
  status = modify_stack do
79
- cf_client.delete_stack(:stack_name => name)
84
+ cf_stack.delete
80
85
  end
81
- fail StackUpdateError, "stack delete failed" unless status.nil?
82
- rescue NoSuchStack
86
+ fail StackUpdateError, "stack delete failed" unless status == "DELETE_COMPLETE"
83
87
  :deleted
88
+ ensure
89
+ @stack_id = nil
84
90
  end
85
91
 
86
92
  alias_method :down, :delete
87
93
 
94
+ # Cancel update in-progress.
95
+ #
96
+ # @return [Symbol] `:update_cancelled` if successful
97
+ # @raise [Stackup::StackUpdateError] if operation fails
98
+ #
99
+ def cancel_update
100
+ status = modify_stack do
101
+ cf_stack.cancel_update
102
+ end
103
+ fail StackUpdateError, "update cancel failed" unless status =~ /_COMPLETE$/
104
+ :update_cancelled
105
+ rescue InvalidStateError
106
+ nil
107
+ end
108
+
88
109
  # Get stack outputs.
89
110
  #
90
111
  # @return [Hash<String, String>]
@@ -97,28 +118,23 @@ module Stackup
97
118
  h[output.output_key] = output.output_value
98
119
  end
99
120
  end
100
- rescue Aws::CloudFormation::Errors::ValidationError => e
101
- handle_validation_error(e)
102
121
  end
103
122
 
104
123
  private
105
124
 
106
- def create(template, parameters)
125
+ def create(options)
126
+ options[:stack_name] = name
107
127
  status = modify_stack do
108
- cf_client.create_stack(
109
- :stack_name => name,
110
- :template_body => template,
111
- :capabilities => ["CAPABILITY_IAM"],
112
- :parameters => parameters
113
- )
128
+ ErrorMappingProxy.new(cf).create_stack(options)
114
129
  end
115
130
  fail StackUpdateError, "stack creation failed" unless status == "CREATE_COMPLETE"
116
131
  :created
117
132
  end
118
133
 
119
- def update(template, parameters)
134
+ def update(options)
135
+ options.delete(:disable_rollback)
120
136
  status = modify_stack do
121
- cf_client.update_stack(:stack_name => name, :template_body => template, :parameters => parameters, :capabilities => ["CAPABILITY_IAM"])
137
+ cf_stack.update(options)
122
138
  end
123
139
  fail StackUpdateError, "stack update failed" unless status == "UPDATE_COMPLETE"
124
140
  :updated
@@ -127,11 +143,17 @@ module Stackup
127
143
  end
128
144
 
129
145
  def logger
130
- @logger ||= (cf_client.config[:logger] || Logger.new($stdout))
146
+ @logger ||= cf_client.config[:logger]
147
+ @logger ||= Logger.new($stdout).tap { |l| l.level = Logger::INFO }
148
+ end
149
+
150
+ def cf
151
+ Aws::CloudFormation::Resource.new(:client => cf_client)
131
152
  end
132
153
 
133
154
  def cf_stack
134
- Aws::CloudFormation::Stack.new(:name => name, :client => cf_client)
155
+ id_or_name = @stack_id || name
156
+ ErrorMappingProxy.new(cf.stack(id_or_name))
135
157
  end
136
158
 
137
159
  def event_handler
@@ -146,47 +168,18 @@ module Stackup
146
168
  # @return the final stack status
147
169
  #
148
170
  def modify_stack
171
+ watcher = Stackup::StackWatcher.new(cf_stack)
149
172
  watcher.zero
150
173
  yield
151
- wait_until_stable
152
- rescue Aws::CloudFormation::Errors::ValidationError => e
153
- handle_validation_error(e)
154
- end
155
-
156
- # Wait (displaying stack events) until the stack reaches a stable state.
157
- #
158
- # @return the final stack status
159
- #
160
- def wait_until_stable
161
174
  loop do
162
- report_new_events
163
- cf_stack.reload
175
+ watcher.each_new_event(&event_handler)
176
+ status = self.status
177
+ logger.debug("stack_status=#{status}")
164
178
  return status if status.nil? || status =~ /_(COMPLETE|FAILED)$/
165
179
  sleep(5)
166
180
  end
167
181
  end
168
182
 
169
- def report_new_events
170
- watcher.new_events.each do |e|
171
- event_handler.call(e)
172
- end
173
- end
174
-
175
- def handle_validation_error(e)
176
- case e.message
177
- when "No updates are to be performed."
178
- fail NoUpdateRequired, "no updates are required"
179
- when / does not exist$/
180
- fail NoSuchStack, "no such stack: #{name}"
181
- else
182
- raise e
183
- end
184
- end
185
-
186
- # Raised when a stack is already up-to-date
187
- class NoUpdateRequired < StandardError
188
- end
189
-
190
183
  end
191
184
 
192
185
  end
@@ -2,6 +2,10 @@ require "aws-sdk-core"
2
2
 
3
3
  module Stackup
4
4
 
5
+ # A stack event observer.
6
+ #
7
+ # Keeps track of previously processed events, and yields the new ones.
8
+ #
5
9
  class StackWatcher
6
10
 
7
11
  def initialize(stack)
@@ -13,22 +17,24 @@ module Stackup
13
17
 
14
18
  # Yield all events since the last call
15
19
  #
16
- def new_events
17
- [].tap do |events|
18
- stack.events.each do |event|
19
- break if @processed_event_ids.include?(event.event_id)
20
- events.unshift(event)
21
- @processed_event_ids.add(event.event_id)
22
- end
20
+ def each_new_event
21
+ buffer = []
22
+ stack.events.each do |event|
23
+ break if @processed_event_ids.include?(event.event_id)
24
+ buffer.unshift(event)
23
25
  end
24
- rescue ::Aws::CloudFormation::Errors::ValidationError
25
- []
26
+ buffer.each do |event|
27
+ yield event if block_given?
28
+ @processed_event_ids.add(event.event_id)
29
+ end
30
+ rescue Aws::CloudFormation::Errors::ValidationError
31
+ # okay
26
32
  end
27
33
 
28
34
  # Consume all new events
29
35
  #
30
36
  def zero
31
- new_events
37
+ each_new_event
32
38
  nil
33
39
  end
34
40
 
data/lib/stackup.rb CHANGED
@@ -1 +1,27 @@
1
+ require "forwardable"
2
+ require "stackup/service"
1
3
  require "stackup/stack"
4
+
5
+ # Allow use of `Stackup.stacks` rather than `Stackup().stacks`
6
+ #
7
+ module Stackup
8
+
9
+ class << self
10
+
11
+ def service(client = {})
12
+ Stackup::Service.new(client)
13
+ end
14
+
15
+ extend Forwardable
16
+
17
+ def_delegators :service, :stack, :stack_names
18
+
19
+ end
20
+
21
+ end
22
+
23
+ # rubocop:disable Style/MethodName
24
+
25
+ def Stackup(*args)
26
+ Stackup.service(*args)
27
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,19 @@
1
- require File.expand_path("../../lib/stackup", __FILE__)
1
+ require "console_logger"
2
+
3
+ module CfStubbing
4
+
5
+ def stub_cf_client
6
+ client_options = { :stub_responses => true }
7
+ if ENV.key?("AWS_DEBUG")
8
+ client_options[:logger] = ConsoleLogger.new(STDOUT, true)
9
+ client_options[:log_level] = :debug
10
+ end
11
+ Aws::CloudFormation::Client.new(client_options)
12
+ end
13
+
14
+ end
2
15
 
3
16
  RSpec.configure do |c|
4
17
  c.mock_with :rspec
18
+ c.include CfStubbing
5
19
  end
@@ -1,15 +1,10 @@
1
1
  require "spec_helper"
2
2
 
3
+ require "stackup/stack"
4
+
3
5
  describe Stackup::Stack do
4
6
 
5
- let(:cf_client) do
6
- client_options = { :stub_responses => true }
7
- if ENV.key?("AWS_DEBUG")
8
- client_options[:logger] = Logger.new(STDOUT)
9
- client_options[:log_level] = :debug
10
- end
11
- Aws::CloudFormation::Client.new(client_options)
12
- end
7
+ let(:cf_client) { stub_cf_client }
13
8
 
14
9
  let(:stack_name) { "stack_name" }
15
10
  let(:unique_stack_id) { "ID:#{stack_name}" }
@@ -23,24 +18,21 @@ describe Stackup::Stack do
23
18
  allow(cf_client).to receive(:create_stack).and_call_original
24
19
  allow(cf_client).to receive(:delete_stack).and_call_original
25
20
  allow(cf_client).to receive(:update_stack).and_call_original
21
+ allow(cf_client).to receive(:cancel_update_stack).and_call_original
26
22
  end
27
23
 
28
- def service_error(code, message)
24
+ def validation_error(message)
29
25
  {
30
26
  :status_code => 400,
31
27
  :headers => {},
32
- :body => "<ErrorResponse><Error><Code>#{code}</Code><Message>#{message}</Message></Error></ErrorResponse>"
28
+ :body => [
29
+ "<ErrorResponse><Error><Code>ValidationError</Code><Message>",
30
+ message,
31
+ "</Message></Error></ErrorResponse>"
32
+ ].join
33
33
  }
34
34
  end
35
35
 
36
- def stack_does_not_exist
37
- service_error("ValidationError", "Stack with id #{stack_name} does not exist")
38
- end
39
-
40
- def no_update_required
41
- service_error("ValidationError", "No updates are to be performed.")
42
- end
43
-
44
36
  def stack_description(stack_status)
45
37
  {
46
38
  :stacks => [
@@ -58,7 +50,7 @@ describe Stackup::Stack do
58
50
 
59
51
  let(:describe_stacks_responses) do
60
52
  [
61
- stack_does_not_exist
53
+ validation_error("Stack with id #{stack_name} does not exist")
62
54
  ]
63
55
  end
64
56
 
@@ -85,7 +77,7 @@ describe Stackup::Stack do
85
77
  let(:template) { "stack template" }
86
78
 
87
79
  def create_or_update
88
- stack.create_or_update(template)
80
+ stack.create_or_update(:template_body => template)
89
81
  end
90
82
 
91
83
  context "successful" do
@@ -162,14 +154,14 @@ describe Stackup::Stack do
162
154
  let(:describe_stacks_responses) do
163
155
  super() + [
164
156
  stack_description("DELETE_IN_PROGRESS"),
165
- stack_does_not_exist
157
+ stack_description("DELETE_COMPLETE")
166
158
  ]
167
159
  end
168
160
 
169
161
  it "calls delete_stack" do
170
162
  stack.delete
171
163
  expect(cf_client).to have_received(:delete_stack)
172
- .with(hash_including(:stack_name => stack_name))
164
+ .with(hash_including(:stack_name => unique_stack_id))
173
165
  end
174
166
 
175
167
  it "returns :deleted" do
@@ -199,28 +191,36 @@ describe Stackup::Stack do
199
191
 
200
192
  let(:template) { "stack template" }
201
193
 
194
+ let(:options) do
195
+ { :template_body => template }
196
+ end
197
+
202
198
  def create_or_update
203
- stack.create_or_update(template)
199
+ stack.create_or_update(options)
204
200
  end
205
201
 
206
- context "successful" do
202
+ let(:describe_stacks_responses) do
203
+ super() + [
204
+ stack_description("UPDATE_IN_PROGRESS"),
205
+ final_describe_stacks_response
206
+ ]
207
+ end
207
208
 
208
- let(:describe_stacks_responses) do
209
- super() + [
210
- stack_description("UPDATE_IN_PROGRESS"),
211
- stack_description("UPDATE_COMPLETE")
212
- ]
213
- end
209
+ let(:final_describe_stacks_response) do
210
+ stack_description("UPDATE_COMPLETE")
211
+ end
214
212
 
215
- it "calls :update_stack" do
216
- expected_args = {
217
- :stack_name => stack_name,
218
- :template_body => template
219
- }
220
- create_or_update
221
- expect(cf_client).to have_received(:update_stack)
222
- .with(hash_including(expected_args))
223
- end
213
+ it "calls :update_stack" do
214
+ expected_args = {
215
+ :stack_name => stack_name,
216
+ :template_body => template
217
+ }
218
+ create_or_update
219
+ expect(cf_client).to have_received(:update_stack)
220
+ .with(hash_including(expected_args))
221
+ end
222
+
223
+ context "successful" do
224
224
 
225
225
  it "returns :updated" do
226
226
  expect(create_or_update).to eq(:updated)
@@ -229,7 +229,10 @@ describe Stackup::Stack do
229
229
  context "if no updates are required" do
230
230
 
231
231
  before do
232
- cf_client.stub_responses(:update_stack, no_update_required)
232
+ cf_client.stub_responses(
233
+ :update_stack,
234
+ validation_error("No updates are to be performed.")
235
+ )
233
236
  end
234
237
 
235
238
  it "returns nil" do
@@ -242,11 +245,8 @@ describe Stackup::Stack do
242
245
 
243
246
  context "unsuccessful" do
244
247
 
245
- let(:describe_stacks_responses) do
246
- super() + [
247
- stack_description("UPDATE_IN_PROGRESS"),
248
- stack_description("UPDATE_ROLLBACK_COMPLETE")
249
- ]
248
+ let(:final_describe_stacks_response) do
249
+ stack_description("UPDATE_ROLLBACK_COMPLETE")
250
250
  end
251
251
 
252
252
  it "raises a StackUpdateError" do
@@ -255,6 +255,20 @@ describe Stackup::Stack do
255
255
 
256
256
  end
257
257
 
258
+ context "with :disable_rollback" do
259
+
260
+ before do
261
+ options[:disable_rollback] = true
262
+ end
263
+
264
+ it "calls :update_stack" do
265
+ create_or_update
266
+ expect(cf_client).to have_received(:update_stack)
267
+ .with(hash_not_including(:disable_rollback))
268
+ end
269
+
270
+ end
271
+
258
272
  end
259
273
 
260
274
  %w(CREATE_FAILED ROLLBACK_COMPLETE).each do |initial_status|
@@ -267,20 +281,23 @@ describe Stackup::Stack do
267
281
  let(:template) { "stack template" }
268
282
 
269
283
  def create_or_update
270
- stack.create_or_update(template)
284
+ stack.create_or_update(:template_body => template)
271
285
  end
272
286
 
273
287
  let(:describe_stacks_responses) do
274
288
  super() + [
275
289
  stack_description("DELETE_IN_PROGRESS"),
276
- stack_does_not_exist,
290
+ validation_error("Stack with id #{stack_name} does not exist"),
277
291
  stack_description("CREATE_IN_PROGRESS"),
278
292
  stack_description("CREATE_COMPLETE")
279
293
  ]
280
294
  end
281
295
 
282
296
  before do
283
- cf_client.stub_responses(:update_stack, stack_does_not_exist)
297
+ cf_client.stub_responses(
298
+ :update_stack,
299
+ validation_error("Stack [woollyams-test] does not exist")
300
+ )
284
301
  end
285
302
 
286
303
  it "calls :delete_stack, then :create_stack first" do
@@ -294,6 +311,55 @@ describe Stackup::Stack do
294
311
  end
295
312
  end
296
313
 
314
+ context "when status is stable" do
315
+
316
+ before do
317
+ cf_client.stub_responses(
318
+ :cancel_update_stack,
319
+ validation_error("that cannot be called from current stack status")
320
+ )
321
+ end
322
+
323
+ describe "#cancel_update" do
324
+
325
+ it "returns nil" do
326
+ expect(stack.cancel_update).to be_nil
327
+ end
328
+
329
+ end
330
+
331
+ end
332
+
333
+ context "when status is UPDATE_IN_PROGRESS" do
334
+
335
+ let(:stack_status) { "UPDATE_IN_PROGRESS" }
336
+
337
+ describe "#cancel_update" do
338
+
339
+ let(:describe_stacks_responses) do
340
+ super() + [
341
+ stack_description("UPDATE_ROLLBACK_IN_PROGRESS"),
342
+ stack_description("UPDATE_ROLLBACK_COMPLETE")
343
+ ]
344
+ end
345
+
346
+ it "calls :cancel_update_stack" do
347
+ expected_args = {
348
+ :stack_name => stack_name
349
+ }
350
+ stack.cancel_update
351
+ expect(cf_client).to have_received(:cancel_update_stack)
352
+ .with(hash_including(expected_args))
353
+ end
354
+
355
+ it "returns :update_cancelled" do
356
+ expect(stack.cancel_update).to eq(:update_cancelled)
357
+ end
358
+
359
+ end
360
+
361
+ end
362
+
297
363
  end
298
364
 
299
365
  end