stackup 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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