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.
- checksums.yaml +4 -4
- data/features/apply.feature +9 -2
- data/lib/stack_master.rb +20 -1
- data/lib/stack_master/aws_driver/cloud_formation.rb +15 -35
- data/lib/stack_master/change_set.rb +104 -0
- data/lib/stack_master/cli.rb +3 -0
- data/lib/stack_master/command.rb +29 -3
- data/lib/stack_master/commands/apply.rb +54 -38
- data/lib/stack_master/commands/validate.rb +1 -1
- data/lib/stack_master/config.rb +10 -1
- data/lib/stack_master/paged_response_accumulator.rb +29 -0
- data/lib/stack_master/parameter_loader.rb +3 -0
- data/lib/stack_master/stack.rb +7 -1
- data/lib/stack_master/stack_events/fetcher.rb +1 -8
- data/lib/stack_master/template_compiler.rb +18 -14
- data/lib/stack_master/template_compilers/json.rb +18 -0
- data/lib/stack_master/template_compilers/sparkle_formation.rb +10 -0
- data/lib/stack_master/test_driver/cloud_formation.rb +33 -0
- data/lib/stack_master/validator.rb +5 -4
- data/lib/stack_master/version.rb +1 -1
- data/spec/stack_master/change_set_spec.rb +62 -0
- data/spec/stack_master/command_spec.rb +21 -1
- data/spec/stack_master/commands/apply_spec.rb +53 -9
- data/spec/stack_master/commands/validate_spec.rb +1 -1
- data/spec/stack_master/paged_response_accumulator_spec.rb +39 -0
- data/spec/stack_master/stack_events/fetcher_spec.rb +8 -33
- data/spec/stack_master/stack_spec.rb +19 -1
- data/spec/stack_master/template_compiler_spec.rb +12 -39
- data/spec/stack_master/template_compilers/json_spec.rb +29 -0
- data/spec/stack_master/template_compilers/sparkle_formation_spec.rb +22 -0
- data/spec/stack_master/test_driver/cloud_formation_spec.rb +39 -12
- data/spec/stack_master/validator_spec.rb +2 -1
- data/spec/stack_master_spec.rb +81 -0
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a13283a8d2b9fc7717a9f49f38919761f818db3
|
4
|
+
data.tar.gz: 973ef37d67af339860b676584b92646c2c667b1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a34296f6a8fcf99c0c87ac6f5c3cdd3630de041ea3ee420c5a3a4df6741559caf49e1fcc41c1ea9c4d55ec979edbb1625018c7937b1a2ac7ee3a1c5faa5f703
|
7
|
+
data.tar.gz: 47902c47d322c770f65a1547a0bf6bdd5997191989f86b58b8bdd2d89714d52c6446b625fac94a4fca7a13d4a29dcf0f2d28dcf2fdd9691c5ab322beff58a5bd
|
data/features/apply.feature
CHANGED
@@ -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:
|
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
|
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
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
data/lib/stack_master/cli.rb
CHANGED
@@ -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]'
|
data/lib/stack_master/command.rb
CHANGED
@@ -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
|
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
|
-
|
18
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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(
|
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
|
73
|
-
|
74
|
-
|
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
|
82
|
-
|
83
|
-
|
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
|
87
|
-
|
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:
|
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(
|
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
|