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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +52 -7
  3. data/Rakefile +2 -0
  4. data/bin/stack_master +2 -1
  5. data/features/apply.feature +63 -26
  6. data/features/delete.feature +5 -18
  7. data/features/diff.feature +6 -18
  8. data/features/events.feature +1 -5
  9. data/features/outputs.feature +1 -5
  10. data/features/region_aliases.feature +0 -4
  11. data/features/resources.feature +1 -4
  12. data/features/stack_defaults.feature +1 -7
  13. data/features/status.feature +2 -8
  14. data/features/step_definitions/stack_steps.rb +16 -0
  15. data/features/support/env.rb +1 -0
  16. data/features/validate.feature +46 -0
  17. data/lib/stack_master.rb +21 -0
  18. data/lib/stack_master/aws_driver/cloud_formation.rb +20 -1
  19. data/lib/stack_master/cli.rb +54 -27
  20. data/lib/stack_master/command.rb +9 -1
  21. data/lib/stack_master/commands/apply.rb +2 -0
  22. data/lib/stack_master/commands/list_stacks.rb +2 -0
  23. data/lib/stack_master/commands/outputs.rb +2 -1
  24. data/lib/stack_master/commands/status.rb +2 -0
  25. data/lib/stack_master/commands/terminal_helper.rb +15 -0
  26. data/lib/stack_master/commands/validate.rb +1 -1
  27. data/lib/stack_master/config.rb +9 -5
  28. data/lib/stack_master/parameter_resolver.rb +11 -5
  29. data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +3 -1
  30. data/lib/stack_master/parameter_resolvers/secret.rb +3 -1
  31. data/lib/stack_master/parameter_resolvers/security_group.rb +4 -2
  32. data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +3 -1
  33. data/lib/stack_master/parameter_resolvers/stack_output.rb +3 -1
  34. data/lib/stack_master/prompter.rb +10 -3
  35. data/lib/stack_master/resolver_array.rb +35 -0
  36. data/lib/stack_master/testing.rb +1 -0
  37. data/lib/stack_master/validator.rb +8 -4
  38. data/lib/stack_master/version.rb +1 -1
  39. data/spec/fixtures/stack_master.yml +4 -1
  40. data/spec/stack_master/command_spec.rb +28 -0
  41. data/spec/stack_master/commands/apply_spec.rb +12 -2
  42. data/spec/stack_master/commands/status_spec.rb +25 -2
  43. data/spec/stack_master/commands/validate_spec.rb +1 -1
  44. data/spec/stack_master/config_spec.rb +42 -8
  45. data/spec/stack_master/parameter_resolver_spec.rb +6 -2
  46. data/spec/stack_master/parameter_resolvers/security_group_spec.rb +5 -3
  47. data/spec/stack_master/parameter_resolvers/security_groups_spec.rb +32 -0
  48. data/spec/stack_master/prompter_spec.rb +23 -0
  49. data/spec/stack_master/resolver_array_spec.rb +42 -0
  50. data/spec/stack_master/validator_spec.rb +2 -2
  51. metadata +15 -3
@@ -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 set the environment variables to:
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` interactively
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
@@ -3,6 +3,7 @@ require 'stack_master'
3
3
  require 'stack_master/testing'
4
4
  require 'aruba/in_process'
5
5
  require 'pry'
6
+ require 'cucumber/rspec/doubles'
6
7
 
7
8
  Aruba.configure do |config|
8
9
  config.command_launcher = :in_process
@@ -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
- cf.describe_stacks(options)
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
@@ -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
- execute_stack_command(StackMaster::Commands::Apply, args, options)
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
- execute_stack_command(StackMaster::Commands::Outputs, args, options)
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
- execute_stack_command(StackMaster::Commands::Diff, args, options)
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
- execute_stack_command(StackMaster::Commands::Events, args, options)
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
- execute_stack_command(StackMaster::Commands::Resources, args, options)
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
- execute_stack_command(StackMaster::Commands::Validate, args, options)
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
- # Because delete can work without a stack_master.yml
131
- if options.config and File.file?(options.config)
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
- else
135
- region = args[0]
136
- end
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 || 'stack_master.yml'
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 execute_stack_command(command, args, options)
154
- unless args.size == 2
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
- aliased_region, stack_name = args
160
- region = Utils.underscore_to_hyphen(config.unalias_region(aliased_region))
161
- stack_name = Utils.underscore_to_hyphen(stack_name)
162
- StackMaster.cloud_formation_driver.set_region(region)
163
- stack_definition ||= config.find_stack(region, stack_name)
164
- if stack_definition.nil?
165
- say "Could not find stack definition #{stack_name} in region #{region}"
166
- return
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
- command.perform(config, stack_definition, options)
193
+
194
+ # Return success/failure
195
+ command_results.all?
169
196
  end
170
197
  end
171
198
  end
@@ -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
@@ -25,6 +25,8 @@ module StackMaster
25
25
  rescue StackMaster::CtrlC
26
26
  cancel
27
27
  end
28
+ rescue Aws::CloudFormation::Errors::ServiceError => e
29
+ StackMaster.stdout.puts "#{e.class} #{e.message}"
28
30
  end
29
31
 
30
32
  private
@@ -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, 80
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"