stack_master 0.2.0 → 0.3.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/features/apply.feature +9 -2
  3. data/lib/stack_master.rb +20 -1
  4. data/lib/stack_master/aws_driver/cloud_formation.rb +15 -35
  5. data/lib/stack_master/change_set.rb +104 -0
  6. data/lib/stack_master/cli.rb +3 -0
  7. data/lib/stack_master/command.rb +29 -3
  8. data/lib/stack_master/commands/apply.rb +54 -38
  9. data/lib/stack_master/commands/validate.rb +1 -1
  10. data/lib/stack_master/config.rb +10 -1
  11. data/lib/stack_master/paged_response_accumulator.rb +29 -0
  12. data/lib/stack_master/parameter_loader.rb +3 -0
  13. data/lib/stack_master/stack.rb +7 -1
  14. data/lib/stack_master/stack_events/fetcher.rb +1 -8
  15. data/lib/stack_master/template_compiler.rb +18 -14
  16. data/lib/stack_master/template_compilers/json.rb +18 -0
  17. data/lib/stack_master/template_compilers/sparkle_formation.rb +10 -0
  18. data/lib/stack_master/test_driver/cloud_formation.rb +33 -0
  19. data/lib/stack_master/validator.rb +5 -4
  20. data/lib/stack_master/version.rb +1 -1
  21. data/spec/stack_master/change_set_spec.rb +62 -0
  22. data/spec/stack_master/command_spec.rb +21 -1
  23. data/spec/stack_master/commands/apply_spec.rb +53 -9
  24. data/spec/stack_master/commands/validate_spec.rb +1 -1
  25. data/spec/stack_master/paged_response_accumulator_spec.rb +39 -0
  26. data/spec/stack_master/stack_events/fetcher_spec.rb +8 -33
  27. data/spec/stack_master/stack_spec.rb +19 -1
  28. data/spec/stack_master/template_compiler_spec.rb +12 -39
  29. data/spec/stack_master/template_compilers/json_spec.rb +29 -0
  30. data/spec/stack_master/template_compilers/sparkle_formation_spec.rb +22 -0
  31. data/spec/stack_master/test_driver/cloud_formation_spec.rb +39 -12
  32. data/spec/stack_master/validator_spec.rb +2 -1
  33. data/spec/stack_master_spec.rb +81 -0
  34. metadata +17 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 106f5899a9d5076230bdf26135167adf3d658182
4
- data.tar.gz: 8fd1dea5cd5026d8c316b641928ba14457c5fa8c
3
+ metadata.gz: 2a13283a8d2b9fc7717a9f49f38919761f818db3
4
+ data.tar.gz: 973ef37d67af339860b676584b92646c2c667b1e
5
5
  SHA512:
6
- metadata.gz: 6db154802da177625841f36cbc9924140990a92133c57cbd080643849b2d58cc1b2a6e7ed0bdc80c4fca5d2df2e0391a5d1e077a361914cfb6cc533f18e9e20d
7
- data.tar.gz: 6d657581c952323eb3fe878094fd3f86847f01525f65bddd21cdb5be473a47d9770e095b5cbd51d8abfe9ff373e8f3649f27917cbac8ef6803defbb7819c4265
6
+ metadata.gz: 0a34296f6a8fcf99c0c87ac6f5c3cdd3630de041ea3ee420c5a3a4df6741559caf49e1fcc41c1ea9c4d55ec979edbb1625018c7937b1a2ac7ee3a1c5faa5f703
7
+ data.tar.gz: 47902c47d322c770f65a1547a0bf6bdd5997191989f86b58b8bdd2d89714d52c6446b625fac94a4fca7a13d4a29dcf0f2d28dcf2fdd9691c5ab322beff58a5bd
@@ -15,6 +15,10 @@ Feature: Apply command
15
15
  """
16
16
  KeyName: my-key
17
17
  """
18
+ And a file named "parameters/myapp_web.yml" with:
19
+ """
20
+ VpcId: vpc-blah
21
+ """
18
22
  And a directory named "templates"
19
23
  And a file named "templates/myapp_vpc.rb" with:
20
24
  """
@@ -157,7 +161,7 @@ Feature: Apply command
157
161
  And the output should match /2020-10-29 00:00:00 \+[0-9]{4} myapp-web AWS::CloudFormation::Stack CREATE_COMPLETE/
158
162
  Then the exit status should be 0
159
163
 
160
- Scenario: Run apply on an existing stack
164
+ Scenario: Update a stack
161
165
  Given I stub the following stack events:
162
166
  | stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp |
163
167
  | 1 | 1 | myapp-vpc | TestSg | CREATE_COMPLETE | AWS::EC2::SecurityGroup | 2020-10-29 00:00:00 |
@@ -203,9 +207,12 @@ Feature: Apply command
203
207
  | Stack diff: |
204
208
  | - "TestSg2": { |
205
209
  | Parameters diff: No changes |
210
+ | Proposed change set: |
211
+ | Replace |
212
+ | Apply change set (y/n)? |
206
213
  Then the exit status should be 0
207
214
 
208
- Scenario: Update an existing stack that has changed with --changed
215
+ Scenario: Update a stack that has changed with --changed
209
216
  Given I stub the following stack events:
210
217
  | stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp |
211
218
  | 1 | 1 | myapp-vpc | TestSg | CREATE_COMPLETE | AWS::EC2::SecurityGroup | 2020-10-29 00:00:00 |
data/lib/stack_master.rb CHANGED
@@ -35,10 +35,14 @@ require "stack_master/parameter_resolvers/security_group"
35
35
  require "stack_master/parameter_resolvers/latest_ami_by_tags"
36
36
  require "stack_master/utils"
37
37
  require "stack_master/config"
38
+ require "stack_master/paged_response_accumulator"
38
39
  require "stack_master/stack_definition"
39
40
  require "stack_master/template_compiler"
41
+ require "stack_master/template_compilers/sparkle_formation"
42
+ require "stack_master/template_compilers/json"
40
43
  require "stack_master/commands/terminal_helper"
41
44
  require "stack_master/commands/apply"
45
+ require "stack_master/change_set"
42
46
  require "stack_master/commands/events"
43
47
  require "stack_master/commands/outputs"
44
48
  require "stack_master/commands/init"
@@ -60,13 +64,28 @@ module StackMaster
60
64
  end
61
65
 
62
66
  def non_interactive?
63
- @non_interactive || false
67
+ @non_interactive
64
68
  end
69
+ @non_interactive = false
65
70
 
66
71
  def non_interactive!
67
72
  @non_interactive = true
68
73
  end
69
74
 
75
+ def debug!
76
+ @debug = true
77
+ end
78
+ @debug = false
79
+
80
+ def debug?
81
+ @debug
82
+ end
83
+
84
+ def debug(message)
85
+ return unless debug?
86
+ stderr.puts "[DEBUG] #{message}".colorize(:green)
87
+ end
88
+
70
89
  attr_accessor :non_interactive_answer
71
90
  @non_interactive_answer = 'y'
72
91
 
@@ -1,14 +1,26 @@
1
1
  module StackMaster
2
2
  module AwsDriver
3
3
  class CloudFormation
4
+ extend Forwardable
5
+
4
6
  def set_region(region)
5
7
  @region = region
6
8
  @cf = nil
7
9
  end
8
10
 
9
- def delete_stack(options)
10
- cf.delete_stack(options)
11
- end
11
+ def_delegators :cf, :create_change_set,
12
+ :describe_change_set,
13
+ :execute_change_set,
14
+ :delete_change_set,
15
+ :delete_stack,
16
+ :cancel_update_stack,
17
+ :describe_stack_resources,
18
+ :get_template,
19
+ :get_stack_policy,
20
+ :describe_stack_events,
21
+ :update_stack,
22
+ :create_stack,
23
+ :validate_template
12
24
 
13
25
  def describe_stacks(options)
14
26
  retry_with_backoff do
@@ -16,38 +28,6 @@ module StackMaster
16
28
  end
17
29
  end
18
30
 
19
- def cancel_update_stack(options)
20
- cf.cancel_update_stack(options)
21
- end
22
-
23
- def describe_stack_resources(options)
24
- cf.describe_stack_resources(options)
25
- end
26
-
27
- def get_template(options)
28
- cf.get_template(options)
29
- end
30
-
31
- def get_stack_policy(options)
32
- cf.get_stack_policy(options)
33
- end
34
-
35
- def describe_stack_events(options)
36
- cf.describe_stack_events(options)
37
- end
38
-
39
- def update_stack(options)
40
- cf.update_stack(options)
41
- end
42
-
43
- def create_stack(options)
44
- cf.create_stack(options)
45
- end
46
-
47
- def validate_template(options)
48
- cf.validate_template(options)
49
- end
50
-
51
31
  private
52
32
 
53
33
  def cf
@@ -0,0 +1,104 @@
1
+ module StackMaster
2
+ class ChangeSet
3
+ END_STATES = [
4
+ 'CREATE_COMPLETE',
5
+ 'DELETE_COMPLETE',
6
+ 'FAILED'
7
+ ]
8
+
9
+ def self.generate_change_set_name
10
+ 'StackMaster' + Time.now.strftime('%Y-%m-%e-%H%M-%s')
11
+ end
12
+
13
+ def self.create(create_options)
14
+ cf = StackMaster.cloud_formation_driver
15
+ change_set_name = generate_change_set_name
16
+ change_set_id = cf.create_change_set(create_options.merge(change_set_name: change_set_name)).id
17
+ find(change_set_id)
18
+ end
19
+
20
+ def self.find(id)
21
+ begin
22
+ response = PagedResponseAccumulator.call(cf, :describe_change_set, { change_set_name: id }, :changes)
23
+ end while !END_STATES.include?(response.status)
24
+ new(response)
25
+ end
26
+
27
+ def self.delete(id)
28
+ cf.delete_change_set(change_set_name: id)
29
+ end
30
+
31
+ def self.execute(id, stack_name)
32
+ cf.execute_change_set(change_set_name: id,
33
+ stack_name: stack_name)
34
+ end
35
+
36
+ def self.cf
37
+ StackMaster.cloud_formation_driver
38
+ end
39
+
40
+ def initialize(describe_change_set_response)
41
+ @response = describe_change_set_response
42
+ end
43
+
44
+ def display(io)
45
+ io.puts "Proposed change set:"
46
+ @response.changes.each do |change|
47
+ display_resource_change(io, change.resource_change)
48
+ end
49
+ end
50
+
51
+ def failed?
52
+ @response.status == 'FAILED'
53
+ end
54
+
55
+ def status_reason
56
+ @response.status_reason
57
+ end
58
+
59
+ def id
60
+ @response.change_set_id
61
+ end
62
+
63
+ private
64
+
65
+ def display_resource_change(io, resource_change)
66
+ action_name = if resource_change.replacement == 'True'
67
+ 'Replace'
68
+ else
69
+ resource_change.action
70
+ end
71
+ message = "#{action_name} #{resource_change.resource_type} #{resource_change.logical_resource_id}"
72
+ color = action_color(action_name)
73
+ io.puts message.colorize(color)
74
+ resource_change.details.each do |detail|
75
+ display_resource_change_detail(io, action_name, color, detail)
76
+ end
77
+ end
78
+
79
+ def display_resource_change_detail(io, action_name, color, detail)
80
+ target_name = [detail.target.attribute, detail.target.name].compact.join('.')
81
+ detail_messages = [target_name]
82
+ if action_name == 'Replace'
83
+ detail_messages << "#{detail.target.requires_recreation} requires recreation"
84
+ end
85
+ triggered_by = [detail.change_source, detail.causing_entity].compact.join('.')
86
+ if detail.evaluation != 'Static'
87
+ triggered_by << "(#{detail.evaluation})"
88
+ end
89
+ detail_messages << "Triggered by: #{triggered_by}"
90
+ io.puts "- #{detail_messages.join('. ')}. ".colorize(color)
91
+ end
92
+
93
+ def action_color(action_name)
94
+ case action_name
95
+ when 'Add'
96
+ :green
97
+ when 'Modify'
98
+ :yellow
99
+ when 'Remove', 'Replace'
100
+ :red
101
+ end
102
+ end
103
+ end
104
+ end
@@ -31,6 +31,9 @@ module StackMaster
31
31
  StackMaster.non_interactive!
32
32
  StackMaster.non_interactive_answer = 'n'
33
33
  end
34
+ global_option '-d', '--debug', 'Run in debug mode' do
35
+ StackMaster.debug!
36
+ end
34
37
 
35
38
  command :apply do |c|
36
39
  c.syntax = 'stack_master apply [region_or_alias] [stack_name]'
@@ -2,11 +2,14 @@ module StackMaster
2
2
  module Command
3
3
  def self.included(base)
4
4
  base.extend ClassMethods
5
+ base.prepend Perform
5
6
  end
6
7
 
7
8
  module ClassMethods
8
9
  def perform(*args)
9
- new(*args).tap { |command| command.perform }
10
+ new(*args).tap do |command|
11
+ command.perform
12
+ end
10
13
  end
11
14
 
12
15
  def command_name
@@ -14,12 +17,35 @@ module StackMaster
14
17
  end
15
18
  end
16
19
 
17
- def failed
18
- @failed = true
20
+ module Perform
21
+ def perform
22
+ catch(:halt) do
23
+ super
24
+ end
25
+ rescue Aws::CloudFormation::Errors::ServiceError => e
26
+ failed "#{e.class} #{e.message}"
27
+ end
19
28
  end
20
29
 
21
30
  def success?
22
31
  @failed != true
23
32
  end
33
+
34
+ private
35
+
36
+ def failed(message = nil)
37
+ StackMaster.stderr.puts(message) if message
38
+ @failed = true
39
+ end
40
+
41
+ def failed!(message = nil)
42
+ failed(message)
43
+ halt!
44
+ end
45
+
46
+ def halt!(message = nil)
47
+ StackMaster.stdout.puts(message) if message
48
+ throw :halt
49
+ end
24
50
  end
25
51
  end
@@ -4,29 +4,20 @@ module StackMaster
4
4
  include Command
5
5
  include Commander::UI
6
6
  include StackMaster::Prompter
7
+ TEMPLATE_TOO_LARGE_ERROR_MESSAGE = 'The (space compressed) stack is larger than the limit set by AWS. See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html'
7
8
 
8
9
  def initialize(config, stack_definition, options = {})
9
10
  @config = config
10
11
  @stack_definition = stack_definition
11
12
  @from_time = Time.now
12
- @updating = false
13
13
  end
14
14
 
15
15
  def perform
16
16
  diff_stacks
17
- unless ask?("Continue and apply the stack (y/n)? ")
18
- StackMaster.stdout.puts "Stack update aborted"
19
- return
20
- end
21
- begin
22
- return if stack_too_big
23
- create_or_update_stack
24
- tail_stack_events
25
- rescue StackMaster::CtrlC
26
- cancel
27
- end
28
- rescue Aws::CloudFormation::Errors::ServiceError => e
29
- StackMaster.stdout.puts "#{e.class} #{e.message}"
17
+ ensure_valid_parameters!
18
+ ensure_valid_template_body_size!
19
+ create_or_update_stack
20
+ tail_stack_events
30
21
  end
31
22
 
32
23
  private
@@ -36,7 +27,7 @@ module StackMaster
36
27
  end
37
28
 
38
29
  def stack
39
- @stack ||= Stack.find(@stack_definition.region, @stack_definition.stack_name)
30
+ @stack ||= Stack.find(region, stack_name)
40
31
  end
41
32
 
42
33
  def proposed_stack
@@ -51,16 +42,6 @@ module StackMaster
51
42
  StackDiffer.new(proposed_stack, stack).output_diff
52
43
  end
53
44
 
54
- def cancel
55
- if @updating
56
- if ask?("Cancel stack update?")
57
- StackMaster.stdout.puts "Attempting to cancel stack update"
58
- cf.cancel_update_stack({stack_name: @stack_definition.stack_name})
59
- tail_stack_events
60
- end
61
- end
62
- end
63
-
64
45
  def create_or_update_stack
65
46
  if stack_exists?
66
47
  update_stack
@@ -69,27 +50,35 @@ module StackMaster
69
50
  end
70
51
  end
71
52
 
72
- def stack_too_big
73
- if proposed_stack.too_big?
74
- StackMaster.stdout.puts 'The (space compressed) stack is larger than the limit set by AWS. See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html'
75
- true
76
- else
77
- false
53
+ def create_stack
54
+ unless ask?("Create stack (y/n)? ")
55
+ failed!("Stack creation aborted")
78
56
  end
57
+ cf.create_stack(stack_options.merge(tags: proposed_stack.aws_tags))
79
58
  end
80
59
 
81
- def update_stack
82
- @updating = true
83
- cf.update_stack(stack_options)
60
+ def ask_to_cancel_stack_update
61
+ if ask?("Cancel stack update?")
62
+ StackMaster.stdout.puts "Attempting to cancel stack update"
63
+ cf.cancel_update_stack(stack_name: stack_name)
64
+ tail_stack_events
65
+ end
84
66
  end
85
67
 
86
- def create_stack
87
- cf.create_stack(stack_options.merge(tags: proposed_stack.aws_tags))
68
+ def update_stack
69
+ @change_set = ChangeSet.create(stack_options)
70
+ halt!(@change_set.status_reason) if @change_set.failed?
71
+ @change_set.display(StackMaster.stdout)
72
+ unless ask?("Apply change set (y/n)? ")
73
+ ChangeSet.delete(@change_set.id)
74
+ halt! "Stack update aborted"
75
+ end
76
+ execute_change_set
88
77
  end
89
78
 
90
79
  def stack_options
91
80
  {
92
- stack_name: @stack_definition.stack_name,
81
+ stack_name: stack_name,
93
82
  template_body: proposed_stack.maybe_compressed_template_body,
94
83
  parameters: proposed_stack.aws_parameters,
95
84
  capabilities: ['CAPABILITY_IAM'],
@@ -99,8 +88,35 @@ module StackMaster
99
88
  end
100
89
 
101
90
  def tail_stack_events
102
- StackEvents::Streamer.stream(@stack_definition.stack_name, @stack_definition.region, io: StackMaster.stdout, from: @from_time)
91
+ StackEvents::Streamer.stream(stack_name, region, io: StackMaster.stdout, from: @from_time)
92
+ rescue StackMaster::CtrlC
93
+ ask_to_cancel_stack_update
94
+ end
95
+
96
+ def execute_change_set
97
+ ChangeSet.execute(@change_set.id, stack_name)
98
+ rescue StackMaster::CtrlC
99
+ ask_to_cancel_stack_update
100
+ end
101
+
102
+ def ensure_valid_parameters!
103
+ if @proposed_stack.missing_parameters?
104
+ StackMaster.stderr.puts "Empty/blank parameters detected, ensure values exist for those parameters. Parameters will be read from the following locations:"
105
+ @stack_definition.parameter_files.each do |parameter_file|
106
+ StackMaster.stderr.puts " - #{parameter_file}"
107
+ end
108
+ halt!
109
+ end
103
110
  end
111
+
112
+ def ensure_valid_template_body_size!
113
+ if proposed_stack.too_big?
114
+ failed! TEMPLATE_TOO_LARGE_ERROR_MESSAGE
115
+ end
116
+ end
117
+
118
+ extend Forwardable
119
+ def_delegators :@stack_definition, :stack_name, :region
104
120
  end
105
121
  end
106
122
  end