stack_master 0.2.0 → 0.3.0

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