stack_master 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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