stack_master 0.7.2 → 0.8.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/lib/stack_master/cli.rb +1 -0
- data/lib/stack_master/commands/apply.rb +5 -2
- data/lib/stack_master/parameter_resolver.rb +7 -1
- data/lib/stack_master/stack.rb +22 -8
- data/lib/stack_master/stack_definition.rb +1 -0
- data/lib/stack_master/stack_differ.rb +4 -5
- data/lib/stack_master/template_compilers/yaml.rb +2 -3
- data/lib/stack_master/version.rb +1 -1
- data/spec/stack_master/commands/apply_spec.rb +21 -2
- data/spec/stack_master/commands/status_spec.rb +8 -8
- data/spec/stack_master/parameter_resolver_spec.rb +35 -0
- data/spec/stack_master/stack_differ_spec.rb +3 -1
- data/spec/stack_master/stack_spec.rb +3 -2
- data/spec/stack_master/template_compilers/yaml_spec.rb +4 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb2d6fdde6c2c8a5c212ceaedc25d27c25fbc825
|
4
|
+
data.tar.gz: ceb28eba091a63fb75505bc39d4409280145618c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ee79f7e351f949f06afc53253f8e6e6760abec82d83dee94071c3d2a9d003030978a9d71dc7a4da4c89f307380fab15151cd1c1d6eed8a3cc42014bd09998c0
|
7
|
+
data.tar.gz: 06276a434b95691c632bd99688b18f83b14d3dc0e8f389da6593740809cd164c5737ad16197577a65453029d8c22d0ed1fbb275fc6ddc38283c4196a07ee2c8f
|
data/lib/stack_master/cli.rb
CHANGED
@@ -41,6 +41,7 @@ module StackMaster
|
|
41
41
|
c.summary = 'Creates or updates a stack'
|
42
42
|
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."
|
43
43
|
c.example 'update a stack named myapp-vpc in us-east-1', 'stack_master apply us-east-1 myapp-vpc'
|
44
|
+
c.option '--on-failure ACTION', String, 'Action to take on CREATE_FAILURE. Valid Values: [ DO_NOTHING | ROLLBACK | DELETE ]. Default: ROLLBACK'
|
44
45
|
c.action do |args, options|
|
45
46
|
options.defaults config: default_config_file
|
46
47
|
execute_stacks_command(StackMaster::Commands::Apply, args, options)
|
@@ -6,11 +6,13 @@ module StackMaster
|
|
6
6
|
include StackMaster::Prompter
|
7
7
|
TEMPLATE_TOO_LARGE_ERROR_MESSAGE = 'The (space compressed) stack is larger than the limit set by AWS. See http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html'.freeze
|
8
8
|
|
9
|
-
def initialize(config, stack_definition, options =
|
9
|
+
def initialize(config, stack_definition, options = Commander::Command::Options.new)
|
10
10
|
@config = config
|
11
11
|
@s3_config = stack_definition.s3
|
12
12
|
@stack_definition = stack_definition
|
13
13
|
@from_time = Time.now
|
14
|
+
@options = options
|
15
|
+
@options.on_failure ||= "ROLLBACK"
|
14
16
|
end
|
15
17
|
|
16
18
|
def perform
|
@@ -64,7 +66,8 @@ module StackMaster
|
|
64
66
|
failed!("Stack creation aborted")
|
65
67
|
end
|
66
68
|
upload_files
|
67
|
-
|
69
|
+
on_failure = @config.stack_defaults['on_failure'] || @options.on_failure
|
70
|
+
cf.create_stack(stack_options.merge({tags: proposed_stack.aws_tags, on_failure: on_failure}))
|
68
71
|
end
|
69
72
|
|
70
73
|
def ask_to_cancel_stack_update
|
@@ -30,7 +30,11 @@ module StackMaster
|
|
30
30
|
def require_parameter_resolver(file_name)
|
31
31
|
require "stack_master/parameter_resolvers/#{file_name}"
|
32
32
|
rescue LoadError
|
33
|
-
|
33
|
+
if file_name == file_name.singularize
|
34
|
+
raise ResolverNotFound.new(file_name)
|
35
|
+
else
|
36
|
+
require_parameter_resolver(file_name.singularize)
|
37
|
+
end
|
34
38
|
end
|
35
39
|
|
36
40
|
def load_parameter_resolver(class_name)
|
@@ -53,6 +57,8 @@ module StackMaster
|
|
53
57
|
value = parameter_value.values.first
|
54
58
|
resolver_class_name = resolver_name.camelize
|
55
59
|
call_resolver(resolver_class_name, value)
|
60
|
+
rescue Aws::CloudFormation::Errors::ValidationError
|
61
|
+
raise InvalidParameter, $!.message
|
56
62
|
end
|
57
63
|
|
58
64
|
def call_resolver(class_name, value)
|
data/lib/stack_master/stack.rb
CHANGED
@@ -9,6 +9,7 @@ module StackMaster
|
|
9
9
|
:stack_status,
|
10
10
|
:parameters,
|
11
11
|
:template_body,
|
12
|
+
:template_format,
|
12
13
|
:notification_arns,
|
13
14
|
:outputs,
|
14
15
|
:stack_policy_body,
|
@@ -18,17 +19,20 @@ module StackMaster
|
|
18
19
|
include Utils::Initializable
|
19
20
|
|
20
21
|
def template_hash
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
return unless template_body
|
23
|
+
@template_hash ||= case template_format
|
24
|
+
when :json
|
25
|
+
JSON.parse(template_body)
|
26
|
+
when :yaml
|
27
|
+
YAML.load(template_body)
|
28
|
+
end
|
24
29
|
end
|
25
30
|
|
26
31
|
def maybe_compressed_template_body
|
27
|
-
if
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
32
|
+
# Do not compress the template if it's not JSON because parsing YAML as a hash ignores
|
33
|
+
# CloudFormation-specific tags such as !Ref
|
34
|
+
return template_body if template_body.size <= MAX_TEMPLATE_SIZE || template_format != :json
|
35
|
+
@compressed_template_body ||= JSON.dump(template_hash)
|
32
36
|
end
|
33
37
|
|
34
38
|
def template_default_parameters
|
@@ -57,6 +61,7 @@ module StackMaster
|
|
57
61
|
params_hash
|
58
62
|
end
|
59
63
|
template_body ||= cf.get_template(stack_name: stack_name).template_body
|
64
|
+
template_format = identify_template_format(template_body)
|
60
65
|
stack_policy_body ||= cf.get_stack_policy(stack_name: stack_name).stack_policy_body
|
61
66
|
outputs = cf_stack.outputs
|
62
67
|
|
@@ -65,6 +70,7 @@ module StackMaster
|
|
65
70
|
stack_id: cf_stack.stack_id,
|
66
71
|
parameters: parameters,
|
67
72
|
template_body: template_body,
|
73
|
+
template_format: template_format,
|
68
74
|
outputs: outputs,
|
69
75
|
notification_arns: cf_stack.notification_arns,
|
70
76
|
stack_policy_body: stack_policy_body,
|
@@ -76,6 +82,7 @@ module StackMaster
|
|
76
82
|
def self.generate(stack_definition, config)
|
77
83
|
parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
|
78
84
|
template_body = TemplateCompiler.compile(config, stack_definition.template_file_path)
|
85
|
+
template_format = identify_template_format(template_body)
|
79
86
|
parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash)
|
80
87
|
stack_policy_body = if stack_definition.stack_policy_file_path
|
81
88
|
File.read(stack_definition.stack_policy_file_path)
|
@@ -85,10 +92,17 @@ module StackMaster
|
|
85
92
|
tags: stack_definition.tags,
|
86
93
|
parameters: parameters,
|
87
94
|
template_body: template_body,
|
95
|
+
template_format: template_format,
|
88
96
|
notification_arns: stack_definition.notification_arns,
|
89
97
|
stack_policy_body: stack_policy_body)
|
90
98
|
end
|
91
99
|
|
100
|
+
def self.identify_template_format(template_body)
|
101
|
+
return :json if template_body =~ /^{/x # ignore leading whitespaces
|
102
|
+
:yaml
|
103
|
+
end
|
104
|
+
private_class_method :identify_template_format
|
105
|
+
|
92
106
|
def max_template_size(use_s3)
|
93
107
|
return MAX_S3_TEMPLATE_SIZE if use_s3
|
94
108
|
MAX_TEMPLATE_SIZE
|
@@ -8,15 +8,14 @@ module StackMaster
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def proposed_template
|
11
|
+
return @proposed_stack.template_body unless @proposed_stack.template_format == :json
|
11
12
|
JSON.pretty_generate(JSON.parse(@proposed_stack.template_body))
|
12
13
|
end
|
13
14
|
|
14
15
|
def current_template
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
''
|
19
|
-
end
|
16
|
+
return '' unless @current_stack
|
17
|
+
return @current_stack.template_body unless @current_stack.template_format == :json
|
18
|
+
JSON.pretty_generate(@current_stack.template_hash)
|
20
19
|
end
|
21
20
|
|
22
21
|
def current_parameters
|
@@ -6,10 +6,9 @@ module StackMaster::TemplateCompilers
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def self.compile(template_file_path)
|
9
|
-
|
10
|
-
JSON.dump(YAML.load(template_body))
|
9
|
+
File.read(template_file_path)
|
11
10
|
end
|
12
11
|
|
13
12
|
StackMaster::TemplateCompiler.register(:yaml, self)
|
14
13
|
end
|
15
|
-
end
|
14
|
+
end
|
data/lib/stack_master/version.rb
CHANGED
@@ -7,8 +7,9 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
7
7
|
let(:notification_arn) { 'test_arn' }
|
8
8
|
let(:stack_definition) { StackMaster::StackDefinition.new(base_dir: '/base_dir', region: region, stack_name: stack_name) }
|
9
9
|
let(:template_body) { '{}' }
|
10
|
+
let(:template_format) { :json }
|
10
11
|
let(:parameters) { { 'param_1' => 'hello' } }
|
11
|
-
let(:proposed_stack) { StackMaster::Stack.new(template_body: template_body, tags: { 'environment' => 'production' } , parameters: parameters, notification_arns: [notification_arn], stack_policy_body: stack_policy_body ) }
|
12
|
+
let(:proposed_stack) { StackMaster::Stack.new(template_body: template_body, template_format: template_format, tags: { 'environment' => 'production' } , parameters: parameters, notification_arns: [notification_arn], stack_policy_body: stack_policy_body ) }
|
12
13
|
let(:stack_policy_body) { '{}' }
|
13
14
|
|
14
15
|
before do
|
@@ -134,7 +135,8 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
134
135
|
}],
|
135
136
|
capabilities: ['CAPABILITY_IAM'],
|
136
137
|
notification_arns: [notification_arn],
|
137
|
-
stack_policy_body: stack_policy_body
|
138
|
+
stack_policy_body: stack_policy_body,
|
139
|
+
on_failure: 'ROLLBACK'
|
138
140
|
)
|
139
141
|
end
|
140
142
|
|
@@ -155,6 +157,23 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
155
157
|
expect(StackMaster::StackEvents::Streamer).to have_received(:stream).with(stack_name, region, io: STDOUT, from: Time.now)
|
156
158
|
end
|
157
159
|
end
|
160
|
+
|
161
|
+
it 'on_failure can be set to a custom value' do
|
162
|
+
config.stack_defaults['on_failure'] = 'DELETE'
|
163
|
+
apply
|
164
|
+
expect(cf).to have_received(:create_stack).with(
|
165
|
+
hash_including(on_failure: 'DELETE')
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'on_failure can be passed in options' do
|
170
|
+
options = Commander::Command::Options.new
|
171
|
+
options.on_failure = 'DELETE'
|
172
|
+
StackMaster::Commands::Apply.perform(config, stack_definition, options)
|
173
|
+
expect(cf).to have_received(:create_stack).with(
|
174
|
+
hash_including(on_failure: 'DELETE')
|
175
|
+
)
|
176
|
+
end
|
158
177
|
end
|
159
178
|
|
160
179
|
context 'one or more parameters are empty' do
|
@@ -17,10 +17,10 @@ RSpec.describe StackMaster::Commands::Status do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
context "some parameters are different" do
|
20
|
-
let(:stack1) { double(:stack1, template_hash: {}, parameters_with_defaults: {a: 1}, stack_status: 'UPDATE_COMPLETE') }
|
21
|
-
let(:stack2) { double(:stack2, template_hash: {}, parameters_with_defaults: {a: 2}, stack_status: 'CREATE_COMPLETE') }
|
22
|
-
let(:proposed_stack1) { double(:proposed_stack1, template_body: "{}", parameters_with_defaults: {a: 1}) }
|
23
|
-
let(:proposed_stack2) { double(:proposed_stack2, template_body: "{}", parameters_with_defaults: {a: 1}) }
|
20
|
+
let(:stack1) { double(:stack1, template_hash: {}, template_format: :json, parameters_with_defaults: {a: 1}, stack_status: 'UPDATE_COMPLETE') }
|
21
|
+
let(:stack2) { double(:stack2, template_hash: {}, template_format: :json, parameters_with_defaults: {a: 2}, stack_status: 'CREATE_COMPLETE') }
|
22
|
+
let(:proposed_stack1) { double(:proposed_stack1, template_body: "{}", template_format: :json, parameters_with_defaults: {a: 1}) }
|
23
|
+
let(:proposed_stack2) { double(:proposed_stack2, template_body: "{}", template_format: :json, parameters_with_defaults: {a: 1}) }
|
24
24
|
|
25
25
|
it "returns the status of call stacks" do
|
26
26
|
out = "REGION | STACK_NAME | STACK_STATUS | DIFFERENT\n----------|------------|-----------------|----------\nus-east-1 | stack1 | UPDATE_COMPLETE | No \nus-east-1 | stack2 | CREATE_COMPLETE | Yes \n * No echo parameters can't be diffed\n"
|
@@ -29,10 +29,10 @@ RSpec.describe StackMaster::Commands::Status do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
context "some templates are different" do
|
32
|
-
let(:stack1) { double(:stack1, template_hash: {foo: 'bar'}, parameters_with_defaults: {a: 1}, stack_status: 'UPDATE_COMPLETE') }
|
33
|
-
let(:stack2) { double(:stack2, template_hash: {}, parameters_with_defaults: {a: 1}, stack_status: 'CREATE_COMPLETE') }
|
34
|
-
let(:proposed_stack1) { double(:proposed_stack1, template_body: "{}", parameters_with_defaults: {a: 1}) }
|
35
|
-
let(:proposed_stack2) { double(:proposed_stack2, template_body: "{}", parameters_with_defaults: {a: 1}) }
|
32
|
+
let(:stack1) { double(:stack1, template_hash: {foo: 'bar'}, template_format: :json, parameters_with_defaults: {a: 1}, stack_status: 'UPDATE_COMPLETE') }
|
33
|
+
let(:stack2) { double(:stack2, template_hash: {}, template_format: :json, parameters_with_defaults: {a: 1}, stack_status: 'CREATE_COMPLETE') }
|
34
|
+
let(:proposed_stack1) { double(:proposed_stack1, template_body: "{}", template_format: :json, parameters_with_defaults: {a: 1}) }
|
35
|
+
let(:proposed_stack2) { double(:proposed_stack2, template_body: "{}", template_format: :json, parameters_with_defaults: {a: 1}) }
|
36
36
|
|
37
37
|
it "returns the status of call stacks" do
|
38
38
|
out = "REGION | STACK_NAME | STACK_STATUS | DIFFERENT\n----------|------------|-----------------|----------\nus-east-1 | stack1 | UPDATE_COMPLETE | Yes \nus-east-1 | stack2 | CREATE_COMPLETE | No \n * No echo parameters can't be diffed\n"
|
@@ -17,9 +17,20 @@ RSpec.describe StackMaster::ParameterResolver do
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
}
|
20
|
+
let(:bad_resolver) {
|
21
|
+
Class.new do
|
22
|
+
def initialize(config, region)
|
23
|
+
end
|
24
|
+
|
25
|
+
def resolve(value)
|
26
|
+
raise Aws::CloudFormation::Errors::ValidationError.new(nil, "Can't find stack")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
}
|
20
30
|
|
21
31
|
before do
|
22
32
|
stub_const('StackMaster::ParameterResolvers::MyResolver', my_resolver)
|
33
|
+
stub_const('StackMaster::ParameterResolvers::BadResolver', bad_resolver)
|
23
34
|
end
|
24
35
|
|
25
36
|
def resolve(params)
|
@@ -69,6 +80,14 @@ RSpec.describe StackMaster::ParameterResolver do
|
|
69
80
|
end
|
70
81
|
end
|
71
82
|
|
83
|
+
context 'when the resolver throws a ValidationError' do
|
84
|
+
it 'throws a invalid parameter error' do
|
85
|
+
expect {
|
86
|
+
resolve(param: { bad_resolver: 2 })
|
87
|
+
}.to raise_error StackMaster::ParameterResolver::InvalidParameter
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
72
91
|
context 'resolver class caching' do
|
73
92
|
it "uses the same instance of the resolver for the duration of the resolve run" do
|
74
93
|
expect(my_resolver).to receive(:new).once.and_call_original
|
@@ -99,5 +118,21 @@ RSpec.describe StackMaster::ParameterResolver do
|
|
99
118
|
|
100
119
|
parameter_resolver.resolve
|
101
120
|
end
|
121
|
+
|
122
|
+
context "using an array resolver" do
|
123
|
+
let(:params) do
|
124
|
+
{
|
125
|
+
param: { other_dummy_resolvers: [1, 2] }
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
it "tries to load the plural and singular forms" do
|
130
|
+
expect(parameter_resolver).to receive(:require_parameter_resolver).with("other_dummy_resolvers").once.and_call_original.ordered
|
131
|
+
expect(parameter_resolver).to receive(:require_parameter_resolver).with("other_dummy_resolver").once.ordered
|
132
|
+
expect(parameter_resolver).to receive(:call_resolver).and_return nil
|
133
|
+
|
134
|
+
parameter_resolver.resolve
|
135
|
+
end
|
136
|
+
end
|
102
137
|
end
|
103
138
|
end
|
@@ -6,11 +6,13 @@ RSpec.describe StackMaster::StackDiffer do
|
|
6
6
|
region: region,
|
7
7
|
stack_id: 123,
|
8
8
|
template_body: '{}',
|
9
|
+
template_format: :json,
|
9
10
|
parameters: current_params) }
|
10
11
|
let(:proposed_stack) { StackMaster::Stack.new(stack_name: stack_name,
|
11
12
|
region: region,
|
12
13
|
parameters: proposed_params,
|
13
|
-
template_body: "{\"a\": 1}"
|
14
|
+
template_body: "{\"a\": 1}",
|
15
|
+
template_format: :json) }
|
14
16
|
let(:stack_name) { 'myapp-vpc' }
|
15
17
|
let(:region) { 'us-east-1' }
|
16
18
|
|
@@ -77,6 +77,7 @@ RSpec.describe StackMaster::Stack do
|
|
77
77
|
let(:resolved_parameters) { { 'DbPassword' => 'sdfgjkdhlfjkghdflkjghdflkjg', 'InstanceType' => 't2.medium' } }
|
78
78
|
let(:template_file_name) { 'template.rb' }
|
79
79
|
let(:template_body) { '{"Parameters": { "VpcId": { "Description": "VPC ID" }, "InstanceType": { "Description": "Instance Type", "Default": "t2.micro" }} }' }
|
80
|
+
let(:template_format) { :json }
|
80
81
|
let(:stack_policy_body) { '{}' }
|
81
82
|
|
82
83
|
before do
|
@@ -136,7 +137,7 @@ RSpec.describe StackMaster::Stack do
|
|
136
137
|
end
|
137
138
|
|
138
139
|
context "oversized json" do
|
139
|
-
let(:stack) { described_class.new(template_body: "{#{' ' * 60000}}" ) }
|
140
|
+
let(:stack) { described_class.new(template_body: "{#{' ' * 60000}}", template_format: :json) }
|
140
141
|
it "compresses the json when it's overly bulbous" do
|
141
142
|
expect(maybe_compressed_template_body).to eq('{}')
|
142
143
|
end
|
@@ -176,7 +177,7 @@ RSpec.describe StackMaster::Stack do
|
|
176
177
|
describe '#missing_parameters?' do
|
177
178
|
subject { stack.missing_parameters? }
|
178
179
|
|
179
|
-
let(:stack) { StackMaster::Stack.new(parameters: parameters, template_body: '{}') }
|
180
|
+
let(:stack) { StackMaster::Stack.new(parameters: parameters, template_body: '{}', template_format: :json) }
|
180
181
|
|
181
182
|
context 'when a parameter has a nil value' do
|
182
183
|
let(:parameters) { { 'my_param' => nil } }
|
@@ -7,12 +7,11 @@ RSpec.describe StackMaster::TemplateCompilers::Yaml do
|
|
7
7
|
context 'valid YAML template' do
|
8
8
|
let(:template_file_path) { 'spec/fixtures/templates/yml/valid_myapp_vpc.yml' }
|
9
9
|
|
10
|
-
it 'produces valid
|
11
|
-
|
12
|
-
valid_myapp_vpc_as_hash = JSON.parse(valid_myapp_vpc_as_json)
|
10
|
+
it 'produces valid YAML' do
|
11
|
+
valid_myapp_vpc_yaml = File.read('spec/fixtures/templates/yml/valid_myapp_vpc.yml')
|
13
12
|
|
14
|
-
expect(
|
13
|
+
expect(compile).to eq(valid_myapp_vpc_yaml)
|
15
14
|
end
|
16
15
|
end
|
17
16
|
end
|
18
|
-
end
|
17
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stack_master
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steve Hodgkiss
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-
|
12
|
+
date: 2016-10-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|