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