stack_master 0.0.1

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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +208 -0
  7. data/Rakefile +11 -0
  8. data/apply_demo.gif +0 -0
  9. data/bin/stack_master +16 -0
  10. data/example/simple/Gemfile +3 -0
  11. data/example/simple/parameters/myapp_vpc.yml +1 -0
  12. data/example/simple/parameters/myapp_web.yml +2 -0
  13. data/example/simple/stack_master.yml +13 -0
  14. data/example/simple/templates/myapp_vpc.rb +39 -0
  15. data/example/simple/templates/myapp_web.rb +16 -0
  16. data/features/apply.feature +241 -0
  17. data/features/delete.feature +43 -0
  18. data/features/diff.feature +191 -0
  19. data/features/events.feature +38 -0
  20. data/features/init.feature +6 -0
  21. data/features/outputs.feature +49 -0
  22. data/features/region_aliases.feature +66 -0
  23. data/features/resources.feature +45 -0
  24. data/features/stack_defaults.feature +88 -0
  25. data/features/status.feature +124 -0
  26. data/features/step_definitions/stack_steps.rb +50 -0
  27. data/features/support/env.rb +14 -0
  28. data/lib/stack_master.rb +81 -0
  29. data/lib/stack_master/aws_driver/cloud_formation.rb +56 -0
  30. data/lib/stack_master/cli.rb +164 -0
  31. data/lib/stack_master/command.rb +13 -0
  32. data/lib/stack_master/commands/apply.rb +104 -0
  33. data/lib/stack_master/commands/delete.rb +53 -0
  34. data/lib/stack_master/commands/diff.rb +31 -0
  35. data/lib/stack_master/commands/events.rb +39 -0
  36. data/lib/stack_master/commands/init.rb +109 -0
  37. data/lib/stack_master/commands/list_stacks.rb +16 -0
  38. data/lib/stack_master/commands/outputs.rb +27 -0
  39. data/lib/stack_master/commands/resources.rb +33 -0
  40. data/lib/stack_master/commands/status.rb +47 -0
  41. data/lib/stack_master/commands/validate.rb +17 -0
  42. data/lib/stack_master/config.rb +86 -0
  43. data/lib/stack_master/ctrl_c.rb +4 -0
  44. data/lib/stack_master/parameter_loader.rb +17 -0
  45. data/lib/stack_master/parameter_resolver.rb +45 -0
  46. data/lib/stack_master/parameter_resolvers/secret.rb +42 -0
  47. data/lib/stack_master/parameter_resolvers/security_group.rb +20 -0
  48. data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +29 -0
  49. data/lib/stack_master/parameter_resolvers/stack_output.rb +53 -0
  50. data/lib/stack_master/prompter.rb +14 -0
  51. data/lib/stack_master/security_group_finder.rb +29 -0
  52. data/lib/stack_master/sns_topic_finder.rb +27 -0
  53. data/lib/stack_master/stack.rb +96 -0
  54. data/lib/stack_master/stack_definition.rb +49 -0
  55. data/lib/stack_master/stack_differ.rb +80 -0
  56. data/lib/stack_master/stack_events/fetcher.rb +45 -0
  57. data/lib/stack_master/stack_events/presenter.rb +27 -0
  58. data/lib/stack_master/stack_events/streamer.rb +55 -0
  59. data/lib/stack_master/stack_states.rb +34 -0
  60. data/lib/stack_master/template_compiler.rb +21 -0
  61. data/lib/stack_master/test_driver/cloud_formation.rb +139 -0
  62. data/lib/stack_master/testing.rb +7 -0
  63. data/lib/stack_master/utils.rb +31 -0
  64. data/lib/stack_master/validator.rb +25 -0
  65. data/lib/stack_master/version.rb +3 -0
  66. data/logo.png +0 -0
  67. data/script/buildkite/bundle.sh +5 -0
  68. data/script/buildkite/clean.sh +3 -0
  69. data/script/buildkite_rspec.sh +27 -0
  70. data/spec/fixtures/parameters/myapp_vpc.yml +1 -0
  71. data/spec/fixtures/stack_master.yml +35 -0
  72. data/spec/fixtures/templates/myapp_vpc.json +1 -0
  73. data/spec/spec_helper.rb +99 -0
  74. data/spec/stack_master/commands/apply_spec.rb +92 -0
  75. data/spec/stack_master/commands/delete_spec.rb +40 -0
  76. data/spec/stack_master/commands/init_spec.rb +17 -0
  77. data/spec/stack_master/commands/status_spec.rb +38 -0
  78. data/spec/stack_master/commands/validate_spec.rb +26 -0
  79. data/spec/stack_master/config_spec.rb +81 -0
  80. data/spec/stack_master/parameter_loader_spec.rb +81 -0
  81. data/spec/stack_master/parameter_resolver_spec.rb +58 -0
  82. data/spec/stack_master/parameter_resolvers/secret_spec.rb +66 -0
  83. data/spec/stack_master/parameter_resolvers/security_group_spec.rb +17 -0
  84. data/spec/stack_master/parameter_resolvers/sns_topic_name_spec.rb +43 -0
  85. data/spec/stack_master/parameter_resolvers/stack_output_spec.rb +77 -0
  86. data/spec/stack_master/security_group_finder_spec.rb +49 -0
  87. data/spec/stack_master/sns_topic_finder_spec.rb +25 -0
  88. data/spec/stack_master/stack_definition_spec.rb +37 -0
  89. data/spec/stack_master/stack_differ_spec.rb +34 -0
  90. data/spec/stack_master/stack_events/fetcher_spec.rb +65 -0
  91. data/spec/stack_master/stack_events/presenter_spec.rb +18 -0
  92. data/spec/stack_master/stack_events/streamer_spec.rb +33 -0
  93. data/spec/stack_master/stack_spec.rb +157 -0
  94. data/spec/stack_master/template_compiler_spec.rb +48 -0
  95. data/spec/stack_master/test_driver/cloud_formation_spec.rb +24 -0
  96. data/spec/stack_master/utils_spec.rb +30 -0
  97. data/spec/stack_master/validator_spec.rb +38 -0
  98. data/stack_master.gemspec +38 -0
  99. data/stacktemplates/parameter_region.yml +3 -0
  100. data/stacktemplates/parameter_stack_name.yml +3 -0
  101. data/stacktemplates/stack.json.erb +20 -0
  102. data/stacktemplates/stack_master.yml.erb +6 -0
  103. metadata +427 -0
@@ -0,0 +1,124 @@
1
+ Feature: Status 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
+ stack2:
11
+ template: stack2.json
12
+ stack3:
13
+ template: stack3.json
14
+ """
15
+ And a directory named "parameters"
16
+ And a file named "parameters/stack1.yml" with:
17
+ """
18
+ KeyName: my-key
19
+ """
20
+ And a directory named "templates"
21
+ And a file named "templates/stack1.json" with:
22
+ """
23
+ {
24
+ "AWSTemplateFormatVersion": "2010-09-09",
25
+ "Description": "Test template",
26
+ "Parameters": {
27
+ "InstanceTypeParameter" : { "Type" : "String" }
28
+ },
29
+ "Mappings": {},
30
+ "Resources": {
31
+ "MyAwesomeQueue" : {
32
+ "Type" : "AWS::SQS::Queue",
33
+ "Properties" : {
34
+ "VisibilityTimeout" : "1"
35
+ }
36
+ }
37
+ },
38
+ "Outputs": {}
39
+ }
40
+ """
41
+ And a file named "templates/stack2.json" with:
42
+ """
43
+ {
44
+ "AWSTemplateFormatVersion": "2010-09-09",
45
+ "Description": "Test template",
46
+ "Parameters": {},
47
+ "Mappings": {},
48
+ "Resources": {
49
+ "MoarQueue" : {
50
+ "Type" : "AWS::SQS::Queue",
51
+ "Properties" : {
52
+ "VisibilityTimeout" : "1"
53
+ }
54
+ }
55
+ },
56
+ "Outputs": {}
57
+ }
58
+ """
59
+ And a file named "templates/stack3.json" with:
60
+ """
61
+ {
62
+ }
63
+ """
64
+ And I set the environment variables to:
65
+ | variable | value |
66
+ | STUB_AWS | true |
67
+
68
+ 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:
73
+ | stack_id | stack_name | parameters | region | stack_status |
74
+ | 1 | stack1 | KeyName=my-key | us-east-1 | CREATE_COMPLETE |
75
+ | 2 | stack2 | | us-east-1 | UPDATE_COMPLETE |
76
+ And I stub a template for the stack "stack1":
77
+ """
78
+ {
79
+ "AWSTemplateFormatVersion": "2010-09-09",
80
+ "Description": "Test template",
81
+ "Parameters": {
82
+ "InstanceTypeParameter" : { "Type" : "String" }
83
+ },
84
+ "Mappings": {},
85
+ "Resources": {
86
+ "MyAwesomeQueue" : {
87
+ "Type" : "AWS::SQS::Queue",
88
+ "Properties" : {
89
+ "VisibilityTimeout" : "7"
90
+ }
91
+ }
92
+ },
93
+ "Outputs": {}
94
+ }
95
+ """
96
+ And I stub a template for the stack "stack2":
97
+ """
98
+ {
99
+ "AWSTemplateFormatVersion": "2010-09-09",
100
+ "Description": "Test template",
101
+ "Parameters": {},
102
+ "Mappings": {},
103
+ "Resources": {
104
+ "MoarQueue" : {
105
+ "Type" : "AWS::SQS::Queue",
106
+ "Properties" : {
107
+ "VisibilityTimeout" : "1"
108
+ }
109
+ }
110
+ },
111
+ "Outputs": {}
112
+ }
113
+ """
114
+
115
+ When I run `stack_master status --trace` interactively
116
+ And the output should contain all of these lines:
117
+ | REGION \| STACK_NAME \| STACK_STATUS \| DIFFERENT |
118
+ | ----------\|------------\|-----------------\|---------- |
119
+ | us-east-1 \| stack1 \| CREATE_COMPLETE \| Yes |
120
+ | us-east-1 \| stack2 \| UPDATE_COMPLETE \| No |
121
+ | us-east-1 \| stack3 \| \| Yes |
122
+
123
+ Then the exit status should be 0
124
+
@@ -0,0 +1,50 @@
1
+ Given(/^I stub the following stack events:$/) do |table|
2
+ table.hashes.each do |row|
3
+ row.symbolize_keys!
4
+ StackMaster.cloud_formation_driver.add_stack_event(row)
5
+ end
6
+ end
7
+
8
+ Given(/^I stub the following stack resources:$/) do |table|
9
+ table.hashes.each do |row|
10
+ row.symbolize_keys!
11
+ StackMaster.cloud_formation_driver.add_stack_resource(row)
12
+ end
13
+ end
14
+
15
+ def extract_hash_from_kv_string(string)
16
+ string.to_s.split(',').inject({}) do |hash, kv|
17
+ key, value = kv.split('=')
18
+ hash[key] = value
19
+ hash
20
+ end
21
+ end
22
+
23
+
24
+ Given(/^I stub the following stacks:$/) do |table|
25
+ table.hashes.each do |row|
26
+ row.symbolize_keys!
27
+ row[:parameters] = StackMaster::Utils.hash_to_aws_parameters(extract_hash_from_kv_string(row[:parameters]))
28
+ outputs = extract_hash_from_kv_string(row[:outputs]).inject([]) do |array, (k, v)|
29
+ array << OpenStruct.new(output_key: k, output_value: v)
30
+ array
31
+ end
32
+ row[:outputs] = outputs
33
+ StackMaster.cloud_formation_driver.add_stack(row)
34
+ end
35
+ end
36
+
37
+ Given(/^I stub a template for the stack "([^"]*)":$/) do |stack_name, template_body|
38
+ StackMaster.cloud_formation_driver.set_template(stack_name, template_body)
39
+ end
40
+
41
+ Then(/^the stack "([^"]*)" should have a policy with the following:$/) do |stack_name, policy|
42
+ stack_policy_body = StackMaster.cloud_formation_driver.get_stack_policy(stack_name: stack_name).stack_policy_body
43
+ expect(stack_policy_body).to eq policy
44
+ end
45
+
46
+ Then(/^the stack "([^"]*)" should contain this notification ARN "([^"]*)"$/) do |stack_name, notification_arn|
47
+ stack = StackMaster.cloud_formation_driver.describe_stacks(stack_name: stack_name).stacks.first
48
+ expect(stack).to be
49
+ expect(stack.notification_arns).to include notification_arn
50
+ end
@@ -0,0 +1,14 @@
1
+ require 'aruba/cucumber'
2
+ require 'stack_master'
3
+ require 'stack_master/testing'
4
+ require 'aruba/in_process'
5
+ require 'pry'
6
+
7
+ Aruba.configure do |config|
8
+ config.command_launcher = :in_process
9
+ config.main_class = StackMaster::CLI
10
+ end
11
+
12
+ Before do
13
+ StackMaster.cloud_formation_driver.reset
14
+ end
@@ -0,0 +1,81 @@
1
+ require "commander"
2
+ require "yaml"
3
+ require "virtus"
4
+ require "aws-sdk"
5
+ require "diffy"
6
+ require "colorize"
7
+ require "table_print"
8
+ require 'active_support/core_ext/string'
9
+ require "erb"
10
+ require 'sparkle_formation'
11
+ require 'dotgpg'
12
+
13
+ require "stack_master/ctrl_c"
14
+ require "stack_master/command"
15
+ require "stack_master/version"
16
+ require "stack_master/stack"
17
+ require "stack_master/prompter"
18
+ require "stack_master/aws_driver/cloud_formation"
19
+ require "stack_master/test_driver/cloud_formation"
20
+ require "stack_master/stack_events/fetcher"
21
+ require "stack_master/stack_events/presenter"
22
+ require "stack_master/stack_events/streamer"
23
+ require "stack_master/stack_states"
24
+ require "stack_master/sns_topic_finder"
25
+ require "stack_master/security_group_finder"
26
+ require "stack_master/parameter_loader"
27
+ require "stack_master/parameter_resolver"
28
+ require "stack_master/parameter_resolvers/stack_output"
29
+ require "stack_master/parameter_resolvers/secret"
30
+ require "stack_master/parameter_resolvers/sns_topic_name"
31
+ require "stack_master/parameter_resolvers/security_group"
32
+ require "stack_master/utils"
33
+ require "stack_master/config"
34
+ require "stack_master/stack_definition"
35
+ require "stack_master/template_compiler"
36
+ require "stack_master/commands/apply"
37
+ require "stack_master/commands/events"
38
+ require "stack_master/commands/outputs"
39
+ require "stack_master/commands/init"
40
+ require "stack_master/commands/diff"
41
+ require "stack_master/commands/list_stacks"
42
+ require "stack_master/commands/validate"
43
+ require "stack_master/commands/resources"
44
+ require "stack_master/commands/delete"
45
+ require "stack_master/commands/status"
46
+ require "stack_master/stack_differ"
47
+ require "stack_master/validator"
48
+ require "stack_master/cli"
49
+
50
+ module StackMaster
51
+ extend self
52
+
53
+ def base_dir
54
+ File.expand_path(File.join(File.dirname(__FILE__), ".."))
55
+ end
56
+
57
+ def cloud_formation_driver
58
+ @cloud_formation_driver ||= AwsDriver::CloudFormation.new
59
+ end
60
+
61
+ def cloud_formation_driver=(value)
62
+ @cloud_formation_driver = value
63
+ end
64
+
65
+ def stdout
66
+ @stdout || $stdout
67
+ end
68
+
69
+ def stdout=(io)
70
+ @stdout = io
71
+ end
72
+
73
+ def stderr
74
+ @stderr || $stderr
75
+ end
76
+
77
+ def stderr=(io)
78
+ @stderr = io
79
+ end
80
+ end
81
+
@@ -0,0 +1,56 @@
1
+ module StackMaster
2
+ module AwsDriver
3
+ class CloudFormation
4
+ def set_region(region)
5
+ @region = region
6
+ @cf = nil
7
+ end
8
+
9
+ def delete_stack(options)
10
+ cf.delete_stack(options)
11
+ end
12
+
13
+ def describe_stacks(options)
14
+ cf.describe_stacks(options)
15
+ end
16
+
17
+ def cancel_update_stack(options)
18
+ cf.cancel_update_stack(options)
19
+ end
20
+
21
+ def describe_stack_resources(options)
22
+ cf.describe_stack_resources(options)
23
+ end
24
+
25
+ def get_template(options)
26
+ cf.get_template(options)
27
+ end
28
+
29
+ def get_stack_policy(options)
30
+ cf.get_stack_policy(options)
31
+ end
32
+
33
+ def describe_stack_events(options)
34
+ cf.describe_stack_events(options)
35
+ end
36
+
37
+ def update_stack(options)
38
+ cf.update_stack(options)
39
+ end
40
+
41
+ def create_stack(options)
42
+ cf.create_stack(options)
43
+ end
44
+
45
+ def validate_template(options)
46
+ cf.validate_template(options)
47
+ end
48
+
49
+ private
50
+
51
+ def cf
52
+ @cf ||= Aws::CloudFormation::Client.new(region: @region)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,164 @@
1
+ require 'commander'
2
+
3
+ module StackMaster
4
+ class CLI
5
+ include Commander::Methods
6
+
7
+ def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
8
+ @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
9
+ Commander::Runner.instance_variable_set('@singleton', Commander::Runner.new(argv))
10
+ StackMaster.stdout = @stdout
11
+ StackMaster.stderr = @stderr
12
+ TablePrint::Config.io = StackMaster.stdout
13
+ end
14
+
15
+ def execute!
16
+ program :name, 'StackMaster'
17
+ program :version, '0.0.1'
18
+ program :description, 'AWS Stack Management'
19
+
20
+ global_option '-c', '--config FILE', 'Config file to use'
21
+
22
+ command :apply do |c|
23
+ c.syntax = 'stack_master apply [region_or_alias] [stack_name]'
24
+ c.summary = 'Creates or updates a stack'
25
+ 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
+ c.example 'update a stack named myapp-vpc in us-east-1', 'stack_master apply us-east-1 myapp-vpc'
27
+ c.action do |args, options|
28
+ execute_stack_command(StackMaster::Commands::Apply, args, options)
29
+ end
30
+ end
31
+
32
+ command :outputs do |c|
33
+ c.syntax = 'stack_master outputs [region_or_alias] [stack_name]'
34
+ c.summary = 'Displays outputs for a stack'
35
+ c.description = "Displays outputs for a stack"
36
+ c.action do |args, options|
37
+ execute_stack_command(StackMaster::Commands::Outputs, args, options)
38
+ end
39
+ end
40
+
41
+ command :init do |c|
42
+ c.syntax = 'stack_master init [region_or_alias] [stack_name]'
43
+ c.summary = 'Initialises the expected directory structure and stack_master.yml file'
44
+ c.description = 'Initialises the expected directory structure and stack_master.yml file'
45
+ c.option('--overwrite', 'Overwrite existing files')
46
+ c.action do |args, options|
47
+ unless args.size == 2
48
+ say "Invalid arguments. stack_master init [region] [stack_name]"
49
+ else
50
+ StackMaster::Commands::Init.perform(options.overwrite, *args)
51
+ end
52
+ end
53
+ end
54
+
55
+ command :diff do |c|
56
+ c.syntax = 'stack_master diff [region_or_alias] [stack_name]'
57
+ c.summary = "Shows a diff of the proposed stack's template and parameters"
58
+ c.description = "Shows a diff of the proposed stack's template and parameters"
59
+ c.example 'diff a stack named myapp-vpc in us-east-1', 'stack_master diff us-east-1 myapp-vpc'
60
+ c.action do |args, options|
61
+ execute_stack_command(StackMaster::Commands::Diff, args, options)
62
+ end
63
+ end
64
+
65
+ command :events do |c|
66
+ c.syntax = 'stack_master events [region_or_alias] [stack_name]'
67
+ c.summary = "Shows events for a stack"
68
+ c.description = "Shows events for a stack"
69
+ c.example 'show events for myapp-vpc in us-east-1', 'stack_master events us-east-1 myapp-vpc'
70
+ c.option '--number Integer', Integer, 'Number of recent events to show'
71
+ c.option '--all', 'Show all events'
72
+ c.option '--tail', 'Tail events'
73
+ c.action do |args, options|
74
+ execute_stack_command(StackMaster::Commands::Events, args, options)
75
+ end
76
+ end
77
+
78
+ command :resources do |c|
79
+ c.syntax = 'stack_master resources [region] [stack_name]'
80
+ c.summary = "Shows stack resources"
81
+ c.description = "Shows stack resources"
82
+ c.action do |args, options|
83
+ execute_stack_command(StackMaster::Commands::Resources, args, options)
84
+ end
85
+ end
86
+
87
+ command :list do |c|
88
+ c.syntax = 'stack_master list'
89
+ c.summary = 'List stack definitions'
90
+ c.description = 'List stack definitions'
91
+ c.action do |args, options|
92
+ say "Invalid arguments." if args.size > 0
93
+ config = load_config(options.config)
94
+ StackMaster::Commands::ListStacks.perform(config)
95
+ end
96
+ end
97
+
98
+ command :validate do |c|
99
+ c.syntax = 'stack_master validate [region_or_alias] [stack_name]'
100
+ c.summary = 'Validate a template'
101
+ c.description = 'Validate a template'
102
+ c.example 'validate a stack named myapp-vpc in us-east-1', 'stack_master validate us-east-1 myapp-vpc'
103
+ c.action do |args, options|
104
+ execute_stack_command(StackMaster::Commands::Validate, args, options)
105
+ end
106
+ end
107
+
108
+ command :status do |c|
109
+ c.syntax = 'stack_master status'
110
+ c.summary = 'Check the current status stacks.'
111
+ c.description = 'Checks the status of all stacks defined in the stack_master.yml file. Warning this operation can be somewhat slow.'
112
+ c.example 'description', 'Check the status of all stack definitions'
113
+ c.action do |args, options|
114
+ say "Invalid arguments. stack_master status" and return unless args.size == 0
115
+ config = load_config(options.config)
116
+ StackMaster::Commands::Status.perform(config)
117
+ end
118
+ end
119
+
120
+ command :delete do |c|
121
+ c.syntax = 'stack_master delete [region] [stack_name]'
122
+ c.summary = 'Delete an existing stack'
123
+ c.description = 'Deletes a stack. The stack does not necessarily have to appear in the stack_master.yml file.'
124
+ c.example 'description', 'Delete a stack'
125
+ c.action do |args, options|
126
+ unless args.size == 2
127
+ say "Invalid arguments. stack_master delete [region] [stack_name]"
128
+ return
129
+ end
130
+ StackMaster.cloud_formation_driver.set_region(args[0])
131
+ StackMaster::Commands::Delete.perform(*args)
132
+ end
133
+ end
134
+
135
+ run!
136
+ end
137
+
138
+ def load_config(file)
139
+ stack_file = file || 'stack_master.yml'
140
+ StackMaster::Config.load!(stack_file)
141
+ rescue Errno::ENOENT => e
142
+ say "Failed to load config file #{stack_file}"
143
+ exit 1
144
+ end
145
+
146
+ def execute_stack_command(command, args, options)
147
+ unless args.size == 2
148
+ say "Invalid arguments. stack_master #{command.name.split('::').last.downcase} [region] [stack_name]"
149
+ return
150
+ end
151
+ config = load_config(options.config)
152
+ aliased_region, stack_name = args
153
+ region = Utils.underscore_to_hyphen(config.unalias_region(aliased_region))
154
+ stack_name = Utils.underscore_to_hyphen(stack_name)
155
+ StackMaster.cloud_formation_driver.set_region(region)
156
+ stack_definition ||= config.find_stack(region, stack_name)
157
+ if stack_definition.nil?
158
+ say "Could not find stack definition #{stack_name} in region #{region}"
159
+ return
160
+ end
161
+ command.perform(config, stack_definition, options)
162
+ end
163
+ end
164
+ end