stack_master 0.2.0 → 0.3.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/features/apply.feature +9 -2
- data/lib/stack_master.rb +20 -1
- data/lib/stack_master/aws_driver/cloud_formation.rb +15 -35
- data/lib/stack_master/change_set.rb +104 -0
- data/lib/stack_master/cli.rb +3 -0
- data/lib/stack_master/command.rb +29 -3
- data/lib/stack_master/commands/apply.rb +54 -38
- data/lib/stack_master/commands/validate.rb +1 -1
- data/lib/stack_master/config.rb +10 -1
- data/lib/stack_master/paged_response_accumulator.rb +29 -0
- data/lib/stack_master/parameter_loader.rb +3 -0
- data/lib/stack_master/stack.rb +7 -1
- data/lib/stack_master/stack_events/fetcher.rb +1 -8
- data/lib/stack_master/template_compiler.rb +18 -14
- data/lib/stack_master/template_compilers/json.rb +18 -0
- data/lib/stack_master/template_compilers/sparkle_formation.rb +10 -0
- data/lib/stack_master/test_driver/cloud_formation.rb +33 -0
- data/lib/stack_master/validator.rb +5 -4
- data/lib/stack_master/version.rb +1 -1
- data/spec/stack_master/change_set_spec.rb +62 -0
- data/spec/stack_master/command_spec.rb +21 -1
- data/spec/stack_master/commands/apply_spec.rb +53 -9
- data/spec/stack_master/commands/validate_spec.rb +1 -1
- data/spec/stack_master/paged_response_accumulator_spec.rb +39 -0
- data/spec/stack_master/stack_events/fetcher_spec.rb +8 -33
- data/spec/stack_master/stack_spec.rb +19 -1
- data/spec/stack_master/template_compiler_spec.rb +12 -39
- data/spec/stack_master/template_compilers/json_spec.rb +29 -0
- data/spec/stack_master/template_compilers/sparkle_formation_spec.rb +22 -0
- data/spec/stack_master/test_driver/cloud_formation_spec.rb +39 -12
- data/spec/stack_master/validator_spec.rb +2 -1
- data/spec/stack_master_spec.rb +81 -0
- metadata +17 -3
data/lib/stack_master/config.rb
CHANGED
@@ -14,7 +14,8 @@ module StackMaster
|
|
14
14
|
:base_dir,
|
15
15
|
:stack_defaults,
|
16
16
|
:region_defaults,
|
17
|
-
:region_aliases
|
17
|
+
:region_aliases,
|
18
|
+
:template_compilers,
|
18
19
|
|
19
20
|
def self.search_up_and_chdir(config_file)
|
20
21
|
return config_file unless File.dirname(config_file) == "."
|
@@ -41,6 +42,7 @@ module StackMaster
|
|
41
42
|
end
|
42
43
|
@region_defaults = normalise_region_defaults(config.fetch('region_defaults', {}))
|
43
44
|
@stacks = []
|
45
|
+
@template_compilers = default_template_compilers
|
44
46
|
load_config
|
45
47
|
end
|
46
48
|
|
@@ -61,6 +63,13 @@ module StackMaster
|
|
61
63
|
|
62
64
|
private
|
63
65
|
|
66
|
+
def default_template_compilers
|
67
|
+
{
|
68
|
+
rb: :sparkle_formation,
|
69
|
+
json: :json,
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
64
73
|
def load_config
|
65
74
|
unaliased_stacks = resolve_region_aliases(@config.fetch('stacks'))
|
66
75
|
load_stacks(unaliased_stacks)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module StackMaster
|
2
|
+
class PagedResponseAccumulator
|
3
|
+
def self.call(*args)
|
4
|
+
new(*args).call
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(cf, method, arguments, accumulator_method)
|
8
|
+
@cf = cf
|
9
|
+
@method = method
|
10
|
+
@arguments = arguments
|
11
|
+
@accumulator_method = accumulator_method
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
book = []
|
16
|
+
next_token = nil
|
17
|
+
first_response = nil
|
18
|
+
begin
|
19
|
+
response = @cf.public_send(@method, @arguments.merge(next_token: next_token))
|
20
|
+
first_response = response if first_response.nil?
|
21
|
+
next_token = response.next_token
|
22
|
+
book += response.public_send(@accumulator_method)
|
23
|
+
end while !next_token.nil?
|
24
|
+
first_response.send("#{@accumulator_method}=", book.reverse)
|
25
|
+
first_response.send(:next_token=, book.reverse)
|
26
|
+
first_response
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,10 +1,13 @@
|
|
1
1
|
module StackMaster
|
2
2
|
class ParameterLoader
|
3
3
|
def self.load(parameter_files)
|
4
|
+
StackMaster.debug "Searching for parameter files..."
|
4
5
|
parameter_files.reduce({}) do |hash, file_name|
|
5
6
|
parameters = if File.exists?(file_name)
|
7
|
+
StackMaster.debug " #{file_name} found"
|
6
8
|
YAML.load(File.read(file_name)) || {}
|
7
9
|
else
|
10
|
+
StackMaster.debug " #{file_name} not found"
|
8
11
|
{}
|
9
12
|
end
|
10
13
|
parameters.each do |key, value|
|
data/lib/stack_master/stack.rb
CHANGED
@@ -40,6 +40,12 @@ module StackMaster
|
|
40
40
|
template_default_parameters.merge(parameters)
|
41
41
|
end
|
42
42
|
|
43
|
+
def missing_parameters?
|
44
|
+
parameters_with_defaults.any? do |key, value|
|
45
|
+
value == nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
43
49
|
def self.find(region, stack_name)
|
44
50
|
cf = StackMaster.cloud_formation_driver
|
45
51
|
cf_stack = cf.describe_stacks(stack_name: stack_name).stacks.first
|
@@ -67,7 +73,7 @@ module StackMaster
|
|
67
73
|
|
68
74
|
def self.generate(stack_definition, config)
|
69
75
|
parameter_hash = ParameterLoader.load(stack_definition.parameter_files)
|
70
|
-
template_body = TemplateCompiler.compile(stack_definition.template_file_path)
|
76
|
+
template_body = TemplateCompiler.compile(config, stack_definition.template_file_path)
|
71
77
|
parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash)
|
72
78
|
stack_policy_body = if stack_definition.stack_policy_file_path
|
73
79
|
File.read(stack_definition.stack_policy_file_path)
|
@@ -31,14 +31,7 @@ module StackMaster
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def fetch_events
|
34
|
-
|
35
|
-
next_token = nil
|
36
|
-
begin
|
37
|
-
response = cf.describe_stack_events(stack_name: @stack_name, next_token: next_token)
|
38
|
-
next_token = response.next_token
|
39
|
-
events += response.stack_events
|
40
|
-
end while !next_token.nil?
|
41
|
-
events.reverse
|
34
|
+
PagedResponseAccumulator.call(cf, :describe_stack_events, { stack_name: @stack_name }, :stack_events).stack_events
|
42
35
|
end
|
43
36
|
end
|
44
37
|
end
|
@@ -1,21 +1,25 @@
|
|
1
1
|
module StackMaster
|
2
2
|
class TemplateCompiler
|
3
3
|
|
4
|
-
|
4
|
+
def self.compile(config, template_file_path)
|
5
|
+
template_compiler_for_file(template_file_path, config).compile(template_file_path)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.register(name, klass)
|
9
|
+
@compilers ||= {}
|
10
|
+
@compilers[name] = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
# private
|
14
|
+
def self.template_compiler_for_file(template_file_path, config)
|
15
|
+
compiler_name = config.template_compilers.fetch(file_ext(template_file_path))
|
16
|
+
@compilers.fetch(compiler_name)
|
17
|
+
end
|
18
|
+
private_class_method :template_compiler_for_file
|
5
19
|
|
6
|
-
def self.
|
7
|
-
|
8
|
-
SparkleFormation.sparkle_path = File.dirname(template_file_path)
|
9
|
-
JSON.pretty_generate(SparkleFormation.compile(template_file_path))
|
10
|
-
else
|
11
|
-
template_body = File.read(template_file_path)
|
12
|
-
if template_body.size > MAX_TEMPLATE_SIZE
|
13
|
-
# Parse the json and rewrite compressed
|
14
|
-
JSON.dump(JSON.parse(template_body))
|
15
|
-
else
|
16
|
-
template_body
|
17
|
-
end
|
18
|
-
end
|
20
|
+
def self.file_ext(template_file_path)
|
21
|
+
File.extname(template_file_path).gsub('.', '').to_sym
|
19
22
|
end
|
23
|
+
private_class_method :file_ext
|
20
24
|
end
|
21
25
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StackMaster::TemplateCompilers
|
2
|
+
class Json
|
3
|
+
MAX_TEMPLATE_SIZE = 51200
|
4
|
+
private_constant :MAX_TEMPLATE_SIZE
|
5
|
+
|
6
|
+
def self.compile(template_file_path)
|
7
|
+
template_body = File.read(template_file_path)
|
8
|
+
if template_body.size > MAX_TEMPLATE_SIZE
|
9
|
+
# Parse the json and rewrite compressed
|
10
|
+
JSON.dump(JSON.parse(template_body))
|
11
|
+
else
|
12
|
+
template_body
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
StackMaster::TemplateCompiler.register(:json, self)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module StackMaster::TemplateCompilers
|
2
|
+
class SparkleFormation
|
3
|
+
def self.compile(template_file_path)
|
4
|
+
::SparkleFormation.sparkle_path = File.dirname(template_file_path)
|
5
|
+
JSON.pretty_generate(::SparkleFormation.compile(template_file_path))
|
6
|
+
end
|
7
|
+
|
8
|
+
StackMaster::TemplateCompiler.register(:sparkle_formation, self)
|
9
|
+
end
|
10
|
+
end
|
@@ -60,6 +60,39 @@ module StackMaster
|
|
60
60
|
@stack_events = {}
|
61
61
|
@stack_resources = {}
|
62
62
|
@stack_policies = {}
|
63
|
+
@change_sets = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_change_set(options)
|
67
|
+
id = SecureRandom.uuid
|
68
|
+
options.merge!(change_set_id: id)
|
69
|
+
@change_sets[id] = options
|
70
|
+
@change_sets[options.fetch(:change_set_name)] = options
|
71
|
+
OpenStruct.new(id: id)
|
72
|
+
end
|
73
|
+
|
74
|
+
def describe_change_set(options)
|
75
|
+
change_set_id = options.fetch(:change_set_name)
|
76
|
+
change_set = @change_sets.fetch(change_set_id)
|
77
|
+
change_details = [
|
78
|
+
OpenStruct.new(evaluation: 'Static', change_source: 'ResourceReference', target: OpenStruct.new(attribute: 'Properties', requires_recreation: 'Always', name: 'blah'))
|
79
|
+
]
|
80
|
+
change = OpenStruct.new(action: 'Modify', replacement: 'True', scope: ['Properties'], details: change_details)
|
81
|
+
changes = [
|
82
|
+
OpenStruct.new(type: 'AWS::Resource', resource_change: change)
|
83
|
+
]
|
84
|
+
OpenStruct.new(change_set.merge(changes: changes, status: 'CREATE_COMPLETE'))
|
85
|
+
end
|
86
|
+
|
87
|
+
def execute_change_set(options)
|
88
|
+
change_set_id = options.fetch(:change_set_name)
|
89
|
+
change_set = @change_sets.fetch(change_set_id)
|
90
|
+
update_stack(change_set)
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete_change_set(options)
|
94
|
+
change_set_id = options.fetch(:change_set_name)
|
95
|
+
@change_sets.delete(change_set_id)
|
63
96
|
end
|
64
97
|
|
65
98
|
def describe_stacks(options = {})
|
@@ -1,16 +1,17 @@
|
|
1
1
|
module StackMaster
|
2
2
|
class Validator
|
3
|
-
def self.valid?(stack_definition)
|
4
|
-
new(stack_definition).perform
|
3
|
+
def self.valid?(stack_definition, config)
|
4
|
+
new(stack_definition, config).perform
|
5
5
|
end
|
6
6
|
|
7
|
-
def initialize(stack_definition)
|
7
|
+
def initialize(stack_definition, config)
|
8
8
|
@stack_definition = stack_definition
|
9
|
+
@config = config
|
9
10
|
end
|
10
11
|
|
11
12
|
def perform
|
12
13
|
StackMaster.stdout.print "#{@stack_definition.stack_name}: "
|
13
|
-
template_body = TemplateCompiler.compile(@stack_definition.template_file_path)
|
14
|
+
template_body = TemplateCompiler.compile(@config, @stack_definition.template_file_path)
|
14
15
|
cf.validate_template(template_body: template_body)
|
15
16
|
StackMaster.stdout.puts "valid"
|
16
17
|
true
|
data/lib/stack_master/version.rb
CHANGED
@@ -0,0 +1,62 @@
|
|
1
|
+
RSpec.describe StackMaster::ChangeSet do
|
2
|
+
let(:cf) { instance_double(Aws::CloudFormation::Client) }
|
3
|
+
let(:region) { 'us-east-1' }
|
4
|
+
let(:stack_name) { 'myapp-vpc' }
|
5
|
+
let(:change_set_name) { 'changeset-123' }
|
6
|
+
|
7
|
+
describe '.create' do
|
8
|
+
before do
|
9
|
+
allow(StackMaster::ChangeSet).to receive(:generate_change_set_name).and_return(change_set_name)
|
10
|
+
allow(StackMaster).to receive(:cloud_formation_driver).and_return(cf)
|
11
|
+
allow(cf).to receive(:create_change_set).and_return(double(id: 'id-1'))
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'successful response' do
|
15
|
+
before do
|
16
|
+
allow(cf).to receive(:describe_change_set).with(change_set_name: 'id-1', next_token: nil).and_return(double(next_token: nil, changes: [], :changes= => nil, :next_token= => nil, status: 'CREATE_COMPLETE'))
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'calls the create change set API with the addition of a name' do
|
20
|
+
change_set = StackMaster::ChangeSet.create(stack_name: '123')
|
21
|
+
expect(cf).to have_received(:create_change_set).with(
|
22
|
+
stack_name: '123',
|
23
|
+
change_set_name: change_set_name
|
24
|
+
)
|
25
|
+
expect(change_set.failed?).to eq false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'unsuccessful response' do
|
30
|
+
before do
|
31
|
+
allow(cf).to receive(:describe_change_set).with(change_set_name: 'id-1', next_token: nil).and_return(double(next_token: nil, changes: [], :changes= => nil, :next_token= => nil, status: 'FAILED', status_reason: 'No changes'))
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'is marked as failed' do
|
35
|
+
change_set = StackMaster::ChangeSet.create(stack_name: '123')
|
36
|
+
expect(change_set.failed?).to eq true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#display' do
|
42
|
+
context 'a successful response' do
|
43
|
+
let(:target) { OpenStruct.new(name: 'GroupDescription', attribute: 'Properties', requires_recreation: 'Always') }
|
44
|
+
let(:changes) { [
|
45
|
+
OpenStruct.new(resource_change: OpenStruct.new(replacement: 'True', action: 'Modify', resource_type: 'EC2::Instance', logical_resource_id: '123', details: [OpenStruct.new(target: target, change_source: 'DirectModification', evaluation: 'Static', causing_entity: 'blah')]))
|
46
|
+
] }
|
47
|
+
let(:cf_response) { double(next_token: nil, changes: changes, :changes= => nil, :next_token= => nil, status: 'FAILED', status_reason: 'No changes') }
|
48
|
+
let(:io) { StringIO.new }
|
49
|
+
subject(:change_set) { StackMaster::ChangeSet.new(cf_response) }
|
50
|
+
let(:message) { io.string }
|
51
|
+
before { change_set.display(io) }
|
52
|
+
|
53
|
+
it 'outputs key data' do
|
54
|
+
expect(message).to include 'Replace EC2::Instance 123'
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'outputs detail data' do
|
58
|
+
expect(message).to include 'Properties.GroupDescription. Always requires recreation. Triggered by: DirectModification.blah'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -3,12 +3,17 @@ RSpec.describe StackMaster::Command do
|
|
3
3
|
Class.new do
|
4
4
|
include StackMaster::Command
|
5
5
|
|
6
|
-
def initialize(callable = nil)
|
6
|
+
def initialize(callable = nil, halt = nil)
|
7
7
|
@callable = callable
|
8
|
+
@halt = halt
|
8
9
|
end
|
9
10
|
|
11
|
+
attr_reader :finished
|
12
|
+
|
10
13
|
def perform
|
11
14
|
instance_eval(&@callable) if @callable
|
15
|
+
halt! if @halt
|
16
|
+
@finished = true
|
12
17
|
false
|
13
18
|
end
|
14
19
|
end
|
@@ -25,4 +30,19 @@ RSpec.describe StackMaster::Command do
|
|
25
30
|
expect(command_class.perform(proc { failed }).success?).to eq false
|
26
31
|
end
|
27
32
|
end
|
33
|
+
|
34
|
+
describe '#halt!' do
|
35
|
+
it 'exits the command' do
|
36
|
+
expect(command_class.perform(nil, true).finished).to_not eq true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when a CF error occurs' do
|
41
|
+
it 'outputs the message' do
|
42
|
+
error_proc = proc {
|
43
|
+
raise Aws::CloudFormation::Errors::ServiceError.new('a', 'the message')
|
44
|
+
}
|
45
|
+
expect { command_class.perform(error_proc) }.to output(/the message/).to_stderr
|
46
|
+
end
|
47
|
+
end
|
28
48
|
end
|
@@ -6,14 +6,14 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
6
6
|
let(:notification_arn) { 'test_arn' }
|
7
7
|
let(:stack_definition) { StackMaster::StackDefinition.new(base_dir: '/base_dir', region: region, stack_name: stack_name) }
|
8
8
|
let(:template_body) { '{}' }
|
9
|
-
let(:
|
9
|
+
let(:parameters) { { 'param_1' => 'hello' } }
|
10
|
+
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 ) }
|
10
11
|
let(:stack_policy_body) { '{}' }
|
11
12
|
|
12
13
|
before do
|
13
14
|
allow(StackMaster::Stack).to receive(:find).with(region, stack_name).and_return(stack)
|
14
15
|
allow(StackMaster::Stack).to receive(:generate).with(stack_definition, config).and_return(proposed_stack)
|
15
16
|
allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf)
|
16
|
-
allow(cf).to receive(:update_stack)
|
17
17
|
allow(cf).to receive(:create_stack)
|
18
18
|
allow(StackMaster::StackDiffer).to receive(:new).with(proposed_stack, stack).and_return double.as_null_object
|
19
19
|
allow(StackMaster::StackEvents::Streamer).to receive(:stream)
|
@@ -26,10 +26,17 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
26
26
|
|
27
27
|
context 'the stack exist' do
|
28
28
|
let(:stack) { StackMaster::Stack.new(stack_id: '1') }
|
29
|
+
let(:change_set) { double(display: true, failed?: false, id: 'id-1') }
|
29
30
|
|
30
|
-
|
31
|
+
before do
|
32
|
+
allow(cf).to receive(:create_change_set).and_return(OpenStruct.new(id: '1'))
|
33
|
+
allow(StackMaster::ChangeSet).to receive(:create).and_return(change_set)
|
34
|
+
allow(cf).to receive(:execute_change_set).and_return(OpenStruct.new(id: '1'))
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'creates a change set' do
|
31
38
|
apply
|
32
|
-
expect(
|
39
|
+
expect(StackMaster::ChangeSet).to have_received(:create).with(
|
33
40
|
stack_name: stack_name,
|
34
41
|
template_body: proposed_stack.template_body,
|
35
42
|
parameters: [
|
@@ -48,13 +55,31 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
48
55
|
end
|
49
56
|
end
|
50
57
|
|
51
|
-
context '
|
58
|
+
context 'the changeset failed to create' do
|
59
|
+
before do
|
60
|
+
allow(change_set).to receive(:failed?).and_return(true)
|
61
|
+
allow(change_set).to receive(:status_reason).and_return('reason')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'outputs the status reason' do
|
65
|
+
expect { apply }.to output(/reason/).to_stdout
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'user decides to not apply the change set' do
|
52
70
|
before do
|
53
|
-
allow(
|
71
|
+
allow(StackMaster).to receive(:non_interactive_answer).and_return('n')
|
72
|
+
allow(StackMaster::ChangeSet).to receive(:delete)
|
73
|
+
allow(StackMaster::ChangeSet).to receive(:execute)
|
74
|
+
apply
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'deletes the change set' do
|
78
|
+
expect(StackMaster::ChangeSet).to have_received(:delete).with(change_set.id)
|
54
79
|
end
|
55
80
|
|
56
|
-
it '
|
57
|
-
expect
|
81
|
+
it "doesn't execute the change set" do
|
82
|
+
expect(StackMaster::ChangeSet).to_not have_received(:execute).with(change_set.id)
|
58
83
|
end
|
59
84
|
end
|
60
85
|
end
|
@@ -88,7 +113,7 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
88
113
|
end
|
89
114
|
|
90
115
|
it 'exits with a message' do
|
91
|
-
expect { apply }.to output(/The \(space compressed\) stack is larger than the limit set by AWS/).
|
116
|
+
expect { apply }.to output(/The \(space compressed\) stack is larger than the limit set by AWS/).to_stderr
|
92
117
|
end
|
93
118
|
end
|
94
119
|
|
@@ -99,4 +124,23 @@ RSpec.describe StackMaster::Commands::Apply do
|
|
99
124
|
end
|
100
125
|
end
|
101
126
|
end
|
127
|
+
|
128
|
+
context 'one or more parameters are empty' do
|
129
|
+
let(:stack) { StackMaster::Stack.new(stack_id: '1', parameters: parameters) }
|
130
|
+
let(:parameters) { { 'param_1' => nil } }
|
131
|
+
|
132
|
+
it "doesn't allow apply" do
|
133
|
+
expect { apply }.to_not output(/Continue and apply the stack/).to_stdout
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'outputs a description of the problem' do
|
137
|
+
expect { apply }.to output(/Empty\/blank parameters detected/).to_stderr
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'outputs where param files are loaded from' do
|
141
|
+
stack_definition.parameter_files.each do |parameter_file|
|
142
|
+
expect { apply }.to output(/#{parameter_file}/).to_stderr
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
102
146
|
end
|