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.
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"