stack_master 0.0.4 → 0.1.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/README.md +52 -7
- data/Rakefile +2 -0
- data/bin/stack_master +2 -1
- data/features/apply.feature +63 -26
- data/features/delete.feature +5 -18
- data/features/diff.feature +6 -18
- data/features/events.feature +1 -5
- data/features/outputs.feature +1 -5
- data/features/region_aliases.feature +0 -4
- data/features/resources.feature +1 -4
- data/features/stack_defaults.feature +1 -7
- data/features/status.feature +2 -8
- data/features/step_definitions/stack_steps.rb +16 -0
- data/features/support/env.rb +1 -0
- data/features/validate.feature +46 -0
- data/lib/stack_master.rb +21 -0
- data/lib/stack_master/aws_driver/cloud_formation.rb +20 -1
- data/lib/stack_master/cli.rb +54 -27
- data/lib/stack_master/command.rb +9 -1
- data/lib/stack_master/commands/apply.rb +2 -0
- data/lib/stack_master/commands/list_stacks.rb +2 -0
- data/lib/stack_master/commands/outputs.rb +2 -1
- data/lib/stack_master/commands/status.rb +2 -0
- data/lib/stack_master/commands/terminal_helper.rb +15 -0
- data/lib/stack_master/commands/validate.rb +1 -1
- data/lib/stack_master/config.rb +9 -5
- data/lib/stack_master/parameter_resolver.rb +11 -5
- data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +3 -1
- data/lib/stack_master/parameter_resolvers/secret.rb +3 -1
- data/lib/stack_master/parameter_resolvers/security_group.rb +4 -2
- data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +3 -1
- data/lib/stack_master/parameter_resolvers/stack_output.rb +3 -1
- data/lib/stack_master/prompter.rb +10 -3
- data/lib/stack_master/resolver_array.rb +35 -0
- data/lib/stack_master/testing.rb +1 -0
- data/lib/stack_master/validator.rb +8 -4
- data/lib/stack_master/version.rb +1 -1
- data/spec/fixtures/stack_master.yml +4 -1
- data/spec/stack_master/command_spec.rb +28 -0
- data/spec/stack_master/commands/apply_spec.rb +12 -2
- data/spec/stack_master/commands/status_spec.rb +25 -2
- data/spec/stack_master/commands/validate_spec.rb +1 -1
- data/spec/stack_master/config_spec.rb +42 -8
- data/spec/stack_master/parameter_resolver_spec.rb +6 -2
- data/spec/stack_master/parameter_resolvers/security_group_spec.rb +5 -3
- data/spec/stack_master/parameter_resolvers/security_groups_spec.rb +32 -0
- data/spec/stack_master/prompter_spec.rb +23 -0
- data/spec/stack_master/resolver_array_spec.rb +42 -0
- data/spec/stack_master/validator_spec.rb +2 -2
- metadata +15 -3
data/features/status.feature
CHANGED
@@ -61,15 +61,9 @@ Feature: Status command
|
|
61
61
|
{
|
62
62
|
}
|
63
63
|
"""
|
64
|
-
And I set the environment variables to:
|
65
|
-
| variable | value |
|
66
|
-
| STUB_AWS | true |
|
67
64
|
|
68
65
|
Scenario: Run status command and get a list of stack statuii
|
69
|
-
Given I
|
70
|
-
| variable | value |
|
71
|
-
| ANSWER | y |
|
72
|
-
And I stub the following stacks:
|
66
|
+
Given I stub the following stacks:
|
73
67
|
| stack_id | stack_name | parameters | region | stack_status |
|
74
68
|
| 1 | stack1 | KeyName=my-key | us-east-1 | CREATE_COMPLETE |
|
75
69
|
| 2 | stack2 | | us-east-1 | UPDATE_COMPLETE |
|
@@ -112,7 +106,7 @@ Feature: Status command
|
|
112
106
|
}
|
113
107
|
"""
|
114
108
|
|
115
|
-
When I run `stack_master status --trace`
|
109
|
+
When I run `stack_master status --trace`
|
116
110
|
And the output should contain all of these lines:
|
117
111
|
| REGION \| STACK_NAME \| STACK_STATUS \| DIFFERENT |
|
118
112
|
| ----------\|------------\|-----------------\|---------- |
|
@@ -1,3 +1,11 @@
|
|
1
|
+
Before do
|
2
|
+
StackMaster.non_interactive_answer = 'y'
|
3
|
+
end
|
4
|
+
|
5
|
+
Given(/^I will answer prompts with "([^"]*)"$/) do |answer|
|
6
|
+
StackMaster.non_interactive_answer = answer
|
7
|
+
end
|
8
|
+
|
1
9
|
Given(/^I stub the following stack events:$/) do |table|
|
2
10
|
table.hashes.each do |row|
|
3
11
|
row.symbolize_keys!
|
@@ -48,3 +56,11 @@ Then(/^the stack "([^"]*)" should contain this notification ARN "([^"]*)"$/) do
|
|
48
56
|
expect(stack).to be
|
49
57
|
expect(stack.notification_arns).to include notification_arn
|
50
58
|
end
|
59
|
+
|
60
|
+
Given(/^I stub CloudFormation validate calls to pass validation$/) do
|
61
|
+
allow(StackMaster.cloud_formation_driver).to receive(:validate_template).and_return(true)
|
62
|
+
end
|
63
|
+
|
64
|
+
Given(/^I stub CloudFormation validate calls to fail validation with message "([^"]*)"$/) do |message|
|
65
|
+
allow(StackMaster.cloud_formation_driver).to receive(:validate_template).and_raise(Aws::CloudFormation::Errors::ValidationError.new('', message))
|
66
|
+
end
|
data/features/support/env.rb
CHANGED
@@ -0,0 +1,46 @@
|
|
1
|
+
Feature: Validate command
|
2
|
+
|
3
|
+
Background:
|
4
|
+
Given a file named "stack_master.yml" with:
|
5
|
+
"""
|
6
|
+
stacks:
|
7
|
+
us_east_1:
|
8
|
+
stack1:
|
9
|
+
template: stack1.json
|
10
|
+
"""
|
11
|
+
And a directory named "parameters"
|
12
|
+
And a file named "parameters/stack1.yml" with:
|
13
|
+
"""
|
14
|
+
InstanceTypeParameter: my-type
|
15
|
+
"""
|
16
|
+
And a directory named "templates"
|
17
|
+
And a file named "templates/stack1.json" with:
|
18
|
+
"""
|
19
|
+
{
|
20
|
+
"AWSTemplateFormatVersion": "2010-09-09",
|
21
|
+
"Description": "Test template",
|
22
|
+
"Parameters": {
|
23
|
+
"InstanceTypeParameter" : { "Type" : "String" }
|
24
|
+
},
|
25
|
+
"Mappings": {},
|
26
|
+
"Resources": {
|
27
|
+
"MyAwesomeQueue" : {
|
28
|
+
"Type" : "AWS::SQS::Queue",
|
29
|
+
"Properties" : {
|
30
|
+
"VisibilityTimeout" : "1"
|
31
|
+
}
|
32
|
+
}
|
33
|
+
},
|
34
|
+
"Outputs": {}
|
35
|
+
}
|
36
|
+
"""
|
37
|
+
|
38
|
+
Scenario: Validate successfully
|
39
|
+
Given I stub CloudFormation validate calls to pass validation
|
40
|
+
And I run `stack_master validate us-east-1 stack1`
|
41
|
+
Then the output should contain "stack1: valid"
|
42
|
+
|
43
|
+
Scenario: Validate unsuccessfully
|
44
|
+
Given I stub CloudFormation validate calls to fail validation with message "Blah"
|
45
|
+
And I run `stack_master validate us-east-1 stack1`
|
46
|
+
Then the output should contain "stack1: invalid. Blah"
|
data/lib/stack_master.rb
CHANGED
@@ -26,6 +26,7 @@ require "stack_master/sns_topic_finder"
|
|
26
26
|
require "stack_master/security_group_finder"
|
27
27
|
require "stack_master/parameter_loader"
|
28
28
|
require "stack_master/parameter_resolver"
|
29
|
+
require "stack_master/resolver_array"
|
29
30
|
require "stack_master/parameter_resolvers/stack_output"
|
30
31
|
require "stack_master/parameter_resolvers/secret"
|
31
32
|
require "stack_master/parameter_resolvers/sns_topic_name"
|
@@ -35,6 +36,7 @@ require "stack_master/utils"
|
|
35
36
|
require "stack_master/config"
|
36
37
|
require "stack_master/stack_definition"
|
37
38
|
require "stack_master/template_compiler"
|
39
|
+
require "stack_master/commands/terminal_helper"
|
38
40
|
require "stack_master/commands/apply"
|
39
41
|
require "stack_master/commands/events"
|
40
42
|
require "stack_master/commands/outputs"
|
@@ -52,6 +54,21 @@ require "stack_master/cli"
|
|
52
54
|
module StackMaster
|
53
55
|
extend self
|
54
56
|
|
57
|
+
def interactive?
|
58
|
+
!non_interactive?
|
59
|
+
end
|
60
|
+
|
61
|
+
def non_interactive?
|
62
|
+
@non_interactive || false
|
63
|
+
end
|
64
|
+
|
65
|
+
def non_interactive!
|
66
|
+
@non_interactive = true
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_accessor :non_interactive_answer
|
70
|
+
@non_interactive_answer = 'y'
|
71
|
+
|
55
72
|
def base_dir
|
56
73
|
File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
57
74
|
end
|
@@ -72,6 +89,10 @@ module StackMaster
|
|
72
89
|
@stdout = io
|
73
90
|
end
|
74
91
|
|
92
|
+
def stdin
|
93
|
+
$stdin
|
94
|
+
end
|
95
|
+
|
75
96
|
def stderr
|
76
97
|
@stderr || $stderr
|
77
98
|
end
|
@@ -11,7 +11,9 @@ module StackMaster
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def describe_stacks(options)
|
14
|
-
|
14
|
+
retry_with_backoff do
|
15
|
+
cf.describe_stacks(options)
|
16
|
+
end
|
15
17
|
end
|
16
18
|
|
17
19
|
def cancel_update_stack(options)
|
@@ -51,6 +53,23 @@ module StackMaster
|
|
51
53
|
def cf
|
52
54
|
@cf ||= Aws::CloudFormation::Client.new(region: @region)
|
53
55
|
end
|
56
|
+
|
57
|
+
def retry_with_backoff
|
58
|
+
delay = 1
|
59
|
+
max_delay = 30
|
60
|
+
begin
|
61
|
+
yield
|
62
|
+
rescue Aws::CloudFormation::Errors::Throttling => e
|
63
|
+
if e.message =~ /Rate exceeded/
|
64
|
+
sleep delay
|
65
|
+
delay *= 2
|
66
|
+
if delay > max_delay
|
67
|
+
delay = max_delay
|
68
|
+
end
|
69
|
+
retry
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
54
73
|
end
|
55
74
|
end
|
56
75
|
end
|
data/lib/stack_master/cli.rb
CHANGED
@@ -12,12 +12,24 @@ module StackMaster
|
|
12
12
|
TablePrint::Config.io = StackMaster.stdout
|
13
13
|
end
|
14
14
|
|
15
|
+
def default_config_file
|
16
|
+
"stack_master.yml"
|
17
|
+
end
|
18
|
+
|
15
19
|
def execute!
|
16
20
|
program :name, 'StackMaster'
|
17
21
|
program :version, '0.0.1'
|
18
22
|
program :description, 'AWS Stack Management'
|
19
23
|
|
20
|
-
global_option '-c', '--config FILE', 'Config file to use'
|
24
|
+
global_option '-c', '--config FILE', String, 'Config file to use'
|
25
|
+
global_option '-y', '--yes', 'Run in non-interactive mode answering yes to any prompts' do
|
26
|
+
StackMaster.non_interactive!
|
27
|
+
StackMaster.non_interactive_answer = 'y'
|
28
|
+
end
|
29
|
+
global_option '-n', '--no', 'Run in non-interactive mode answering no to any prompts' do
|
30
|
+
StackMaster.non_interactive!
|
31
|
+
StackMaster.non_interactive_answer = 'n'
|
32
|
+
end
|
21
33
|
|
22
34
|
command :apply do |c|
|
23
35
|
c.syntax = 'stack_master apply [region_or_alias] [stack_name]'
|
@@ -25,7 +37,8 @@ module StackMaster
|
|
25
37
|
c.description = "Creates or updates a stack. Shows a diff of the proposed stack's template and parameters. Tails stack events until CloudFormation has completed."
|
26
38
|
c.example 'update a stack named myapp-vpc in us-east-1', 'stack_master apply us-east-1 myapp-vpc'
|
27
39
|
c.action do |args, options|
|
28
|
-
|
40
|
+
options.defaults config: default_config_file
|
41
|
+
execute_stacks_command(StackMaster::Commands::Apply, args, options)
|
29
42
|
end
|
30
43
|
end
|
31
44
|
|
@@ -34,7 +47,8 @@ module StackMaster
|
|
34
47
|
c.summary = 'Displays outputs for a stack'
|
35
48
|
c.description = "Displays outputs for a stack"
|
36
49
|
c.action do |args, options|
|
37
|
-
|
50
|
+
options.defaults config: default_config_file
|
51
|
+
execute_stacks_command(StackMaster::Commands::Outputs, args, options)
|
38
52
|
end
|
39
53
|
end
|
40
54
|
|
@@ -44,6 +58,7 @@ module StackMaster
|
|
44
58
|
c.description = 'Initialises the expected directory structure and stack_master.yml file'
|
45
59
|
c.option('--overwrite', 'Overwrite existing files')
|
46
60
|
c.action do |args, options|
|
61
|
+
options.defaults config: default_config_file
|
47
62
|
unless args.size == 2
|
48
63
|
say "Invalid arguments. stack_master init [region] [stack_name]"
|
49
64
|
else
|
@@ -58,7 +73,8 @@ module StackMaster
|
|
58
73
|
c.description = "Shows a diff of the proposed stack's template and parameters"
|
59
74
|
c.example 'diff a stack named myapp-vpc in us-east-1', 'stack_master diff us-east-1 myapp-vpc'
|
60
75
|
c.action do |args, options|
|
61
|
-
|
76
|
+
options.defaults config: default_config_file
|
77
|
+
execute_stacks_command(StackMaster::Commands::Diff, args, options)
|
62
78
|
end
|
63
79
|
end
|
64
80
|
|
@@ -71,7 +87,8 @@ module StackMaster
|
|
71
87
|
c.option '--all', 'Show all events'
|
72
88
|
c.option '--tail', 'Tail events'
|
73
89
|
c.action do |args, options|
|
74
|
-
|
90
|
+
options.defaults config: default_config_file
|
91
|
+
execute_stacks_command(StackMaster::Commands::Events, args, options)
|
75
92
|
end
|
76
93
|
end
|
77
94
|
|
@@ -80,7 +97,8 @@ module StackMaster
|
|
80
97
|
c.summary = "Shows stack resources"
|
81
98
|
c.description = "Shows stack resources"
|
82
99
|
c.action do |args, options|
|
83
|
-
|
100
|
+
options.defaults config: default_config_file
|
101
|
+
execute_stacks_command(StackMaster::Commands::Resources, args, options)
|
84
102
|
end
|
85
103
|
end
|
86
104
|
|
@@ -89,6 +107,7 @@ module StackMaster
|
|
89
107
|
c.summary = 'List stack definitions'
|
90
108
|
c.description = 'List stack definitions'
|
91
109
|
c.action do |args, options|
|
110
|
+
options.defaults config: default_config_file
|
92
111
|
say "Invalid arguments." if args.size > 0
|
93
112
|
config = load_config(options.config)
|
94
113
|
StackMaster::Commands::ListStacks.perform(config)
|
@@ -101,7 +120,8 @@ module StackMaster
|
|
101
120
|
c.description = 'Validate a template'
|
102
121
|
c.example 'validate a stack named myapp-vpc in us-east-1', 'stack_master validate us-east-1 myapp-vpc'
|
103
122
|
c.action do |args, options|
|
104
|
-
|
123
|
+
options.defaults config: default_config_file
|
124
|
+
execute_stacks_command(StackMaster::Commands::Validate, args, options)
|
105
125
|
end
|
106
126
|
end
|
107
127
|
|
@@ -111,6 +131,7 @@ module StackMaster
|
|
111
131
|
c.description = 'Checks the status of all stacks defined in the stack_master.yml file. Warning this operation can be somewhat slow.'
|
112
132
|
c.example 'description', 'Check the status of all stack definitions'
|
113
133
|
c.action do |args, options|
|
134
|
+
options.defaults config: default_config_file
|
114
135
|
say "Invalid arguments. stack_master status" and return unless args.size == 0
|
115
136
|
config = load_config(options.config)
|
116
137
|
StackMaster::Commands::Status.perform(config)
|
@@ -123,17 +144,20 @@ module StackMaster
|
|
123
144
|
c.description = 'Deletes a stack. The stack does not necessarily have to appear in the stack_master.yml file.'
|
124
145
|
c.example 'description', 'Delete a stack'
|
125
146
|
c.action do |args, options|
|
147
|
+
options.default config: default_config_file
|
126
148
|
unless args.size == 2
|
127
149
|
say "Invalid arguments. stack_master delete [region] [stack_name]"
|
128
150
|
return
|
129
151
|
end
|
130
|
-
|
131
|
-
|
152
|
+
|
153
|
+
# Because delete can work without a stack_master.yml
|
154
|
+
if options.config and File.file?(options.config)
|
132
155
|
config = load_config(options.config)
|
133
156
|
region = Utils.underscore_to_hyphen(config.unalias_region(args[0]))
|
134
|
-
|
135
|
-
|
136
|
-
|
157
|
+
else
|
158
|
+
region = args[0]
|
159
|
+
end
|
160
|
+
|
137
161
|
StackMaster.cloud_formation_driver.set_region(region)
|
138
162
|
StackMaster::Commands::Delete.perform(region, args[1])
|
139
163
|
end
|
@@ -143,29 +167,32 @@ module StackMaster
|
|
143
167
|
end
|
144
168
|
|
145
169
|
def load_config(file)
|
146
|
-
stack_file = file ||
|
170
|
+
stack_file = file || default_config_file
|
147
171
|
StackMaster::Config.load!(stack_file)
|
148
172
|
rescue Errno::ENOENT => e
|
149
173
|
say "Failed to load config file #{stack_file}"
|
150
174
|
exit 1
|
151
175
|
end
|
152
176
|
|
153
|
-
def
|
154
|
-
|
155
|
-
say "Invalid arguments. stack_master #{command.name.split('::').last.downcase} [region] [stack_name]"
|
156
|
-
return
|
157
|
-
end
|
177
|
+
def execute_stacks_command(command, args, options)
|
178
|
+
command_results = []
|
158
179
|
config = load_config(options.config)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
180
|
+
args = [nil, nil] if args.size == 0
|
181
|
+
args.each_slice(2) do |aliased_region, stack_name|
|
182
|
+
region = Utils.underscore_to_hyphen(config.unalias_region(aliased_region))
|
183
|
+
stack_name = Utils.underscore_to_hyphen(stack_name)
|
184
|
+
stack_definitions = config.filter(region, stack_name)
|
185
|
+
if stack_definitions.empty?
|
186
|
+
say "Could not find stack definition #{stack_name} in region #{region}"
|
187
|
+
end
|
188
|
+
stack_definitions.each do |stack_definition|
|
189
|
+
StackMaster.cloud_formation_driver.set_region(stack_definition.region)
|
190
|
+
command_results.push command.perform(config, stack_definition, options).success?
|
191
|
+
end
|
167
192
|
end
|
168
|
-
|
193
|
+
|
194
|
+
# Return success/failure
|
195
|
+
command_results.all?
|
169
196
|
end
|
170
197
|
end
|
171
198
|
end
|
data/lib/stack_master/command.rb
CHANGED
@@ -6,8 +6,16 @@ module StackMaster
|
|
6
6
|
|
7
7
|
module ClassMethods
|
8
8
|
def perform(*args)
|
9
|
-
new(*args).perform
|
9
|
+
new(*args).tap { |command| command.perform }
|
10
10
|
end
|
11
11
|
end
|
12
|
+
|
13
|
+
def failed
|
14
|
+
@failed = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def success?
|
18
|
+
@failed != true
|
19
|
+
end
|
12
20
|
end
|
13
21
|
end
|
@@ -3,12 +3,14 @@ module StackMaster
|
|
3
3
|
class ListStacks
|
4
4
|
include Command
|
5
5
|
include Commander::UI
|
6
|
+
include StackMaster::Commands::TerminalHelper
|
6
7
|
|
7
8
|
def initialize(config)
|
8
9
|
@config = config
|
9
10
|
end
|
10
11
|
|
11
12
|
def perform
|
13
|
+
tp.set :max_width, self.window_size
|
12
14
|
tp @config.stacks, :region, :stack_name
|
13
15
|
end
|
14
16
|
end
|
@@ -3,6 +3,7 @@ module StackMaster
|
|
3
3
|
class Outputs
|
4
4
|
include Command
|
5
5
|
include Commander::UI
|
6
|
+
include StackMaster::Commands::TerminalHelper
|
6
7
|
|
7
8
|
def initialize(config, stack_definition, options = {})
|
8
9
|
@config = config
|
@@ -11,7 +12,7 @@ module StackMaster
|
|
11
12
|
|
12
13
|
def perform
|
13
14
|
if stack
|
14
|
-
tp.set :max_width,
|
15
|
+
tp.set :max_width, self.window_size
|
15
16
|
tp stack.outputs, :output_key, :output_value, :description
|
16
17
|
else
|
17
18
|
StackMaster.stdout.puts "Stack doesn't exist"
|
@@ -2,6 +2,7 @@ module StackMaster
|
|
2
2
|
module Commands
|
3
3
|
class Status
|
4
4
|
include Command
|
5
|
+
include StackMaster::Commands::TerminalHelper
|
5
6
|
|
6
7
|
def initialize(config, show_progress = true)
|
7
8
|
@config = config
|
@@ -15,6 +16,7 @@ module StackMaster
|
|
15
16
|
progress.increment if @show_progress
|
16
17
|
status
|
17
18
|
end
|
19
|
+
tp.set :max_width, self.window_size
|
18
20
|
tp.set :io, StackMaster.stdout
|
19
21
|
tp status
|
20
22
|
StackMaster.stdout.puts " * No echo parameters can't be diffed"
|