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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c7bca8cc5ad9464ac48f4584a1d85cd14bb8e0b
4
- data.tar.gz: c39f36d3f6455097c66a0e1284eab2c5d7402b64
3
+ metadata.gz: eb2d6fdde6c2c8a5c212ceaedc25d27c25fbc825
4
+ data.tar.gz: ceb28eba091a63fb75505bc39d4409280145618c
5
5
  SHA512:
6
- metadata.gz: cf9a371a2ed906ce11a8fc77b307786ba0156fb96042d18c49a73747c52399fdfb774e0b6177df376566dec989abf32b162697675c80a14915a8db754eee368d
7
- data.tar.gz: 740933a0d714f31265014462db257c930c65c3bfdf9e9b8dcfdf63b6379f7e362b55d2c121de2d0babd031cd8c2cffb1634a63bce426d0065b567d84f672f275
6
+ metadata.gz: 9ee79f7e351f949f06afc53253f8e6e6760abec82d83dee94071c3d2a9d003030978a9d71dc7a4da4c89f307380fab15151cd1c1d6eed8a3cc42014bd09998c0
7
+ data.tar.gz: 06276a434b95691c632bd99688b18f83b14d3dc0e8f389da6593740809cd164c5737ad16197577a65453029d8c22d0ed1fbb275fc6ddc38283c4196a07ee2c8f
@@ -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
- cf.create_stack(stack_options.merge(tags: proposed_stack.aws_tags))
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
- raise ResolverNotFound.new(file_name)
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)
@@ -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
- if template_body
22
- @template_hash ||= JSON.parse(template_body)
23
- end
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 template_body.size > MAX_TEMPLATE_SIZE
28
- @compressed_template_body ||= JSON.dump(template_hash)
29
- else
30
- template_body
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
@@ -60,6 +60,7 @@ module StackMaster
60
60
  end
61
61
 
62
62
  def s3_template_file_name
63
+ return template if ['.json', '.yaml', '.yml'].include?(File.extname(template))
63
64
  Utils.change_extension(template, 'json')
64
65
  end
65
66
 
@@ -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
- if @current_stack
16
- JSON.pretty_generate(@current_stack.template_hash)
17
- else
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
- template_body = File.read(template_file_path)
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
@@ -1,3 +1,3 @@
1
1
  module StackMaster
2
- VERSION = "0.7.2"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -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 JSON' do
11
- valid_myapp_vpc_as_json = File.read('spec/fixtures/templates/json/valid_myapp_vpc.json')
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(JSON.parse(compile)).to eq(valid_myapp_vpc_as_hash)
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.7.2
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-08-18 00:00:00.000000000 Z
12
+ date: 2016-10-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler