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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +208 -0
- data/Rakefile +11 -0
- data/apply_demo.gif +0 -0
- data/bin/stack_master +16 -0
- data/example/simple/Gemfile +3 -0
- data/example/simple/parameters/myapp_vpc.yml +1 -0
- data/example/simple/parameters/myapp_web.yml +2 -0
- data/example/simple/stack_master.yml +13 -0
- data/example/simple/templates/myapp_vpc.rb +39 -0
- data/example/simple/templates/myapp_web.rb +16 -0
- data/features/apply.feature +241 -0
- data/features/delete.feature +43 -0
- data/features/diff.feature +191 -0
- data/features/events.feature +38 -0
- data/features/init.feature +6 -0
- data/features/outputs.feature +49 -0
- data/features/region_aliases.feature +66 -0
- data/features/resources.feature +45 -0
- data/features/stack_defaults.feature +88 -0
- data/features/status.feature +124 -0
- data/features/step_definitions/stack_steps.rb +50 -0
- data/features/support/env.rb +14 -0
- data/lib/stack_master.rb +81 -0
- data/lib/stack_master/aws_driver/cloud_formation.rb +56 -0
- data/lib/stack_master/cli.rb +164 -0
- data/lib/stack_master/command.rb +13 -0
- data/lib/stack_master/commands/apply.rb +104 -0
- data/lib/stack_master/commands/delete.rb +53 -0
- data/lib/stack_master/commands/diff.rb +31 -0
- data/lib/stack_master/commands/events.rb +39 -0
- data/lib/stack_master/commands/init.rb +109 -0
- data/lib/stack_master/commands/list_stacks.rb +16 -0
- data/lib/stack_master/commands/outputs.rb +27 -0
- data/lib/stack_master/commands/resources.rb +33 -0
- data/lib/stack_master/commands/status.rb +47 -0
- data/lib/stack_master/commands/validate.rb +17 -0
- data/lib/stack_master/config.rb +86 -0
- data/lib/stack_master/ctrl_c.rb +4 -0
- data/lib/stack_master/parameter_loader.rb +17 -0
- data/lib/stack_master/parameter_resolver.rb +45 -0
- data/lib/stack_master/parameter_resolvers/secret.rb +42 -0
- data/lib/stack_master/parameter_resolvers/security_group.rb +20 -0
- data/lib/stack_master/parameter_resolvers/sns_topic_name.rb +29 -0
- data/lib/stack_master/parameter_resolvers/stack_output.rb +53 -0
- data/lib/stack_master/prompter.rb +14 -0
- data/lib/stack_master/security_group_finder.rb +29 -0
- data/lib/stack_master/sns_topic_finder.rb +27 -0
- data/lib/stack_master/stack.rb +96 -0
- data/lib/stack_master/stack_definition.rb +49 -0
- data/lib/stack_master/stack_differ.rb +80 -0
- data/lib/stack_master/stack_events/fetcher.rb +45 -0
- data/lib/stack_master/stack_events/presenter.rb +27 -0
- data/lib/stack_master/stack_events/streamer.rb +55 -0
- data/lib/stack_master/stack_states.rb +34 -0
- data/lib/stack_master/template_compiler.rb +21 -0
- data/lib/stack_master/test_driver/cloud_formation.rb +139 -0
- data/lib/stack_master/testing.rb +7 -0
- data/lib/stack_master/utils.rb +31 -0
- data/lib/stack_master/validator.rb +25 -0
- data/lib/stack_master/version.rb +3 -0
- data/logo.png +0 -0
- data/script/buildkite/bundle.sh +5 -0
- data/script/buildkite/clean.sh +3 -0
- data/script/buildkite_rspec.sh +27 -0
- data/spec/fixtures/parameters/myapp_vpc.yml +1 -0
- data/spec/fixtures/stack_master.yml +35 -0
- data/spec/fixtures/templates/myapp_vpc.json +1 -0
- data/spec/spec_helper.rb +99 -0
- data/spec/stack_master/commands/apply_spec.rb +92 -0
- data/spec/stack_master/commands/delete_spec.rb +40 -0
- data/spec/stack_master/commands/init_spec.rb +17 -0
- data/spec/stack_master/commands/status_spec.rb +38 -0
- data/spec/stack_master/commands/validate_spec.rb +26 -0
- data/spec/stack_master/config_spec.rb +81 -0
- data/spec/stack_master/parameter_loader_spec.rb +81 -0
- data/spec/stack_master/parameter_resolver_spec.rb +58 -0
- data/spec/stack_master/parameter_resolvers/secret_spec.rb +66 -0
- data/spec/stack_master/parameter_resolvers/security_group_spec.rb +17 -0
- data/spec/stack_master/parameter_resolvers/sns_topic_name_spec.rb +43 -0
- data/spec/stack_master/parameter_resolvers/stack_output_spec.rb +77 -0
- data/spec/stack_master/security_group_finder_spec.rb +49 -0
- data/spec/stack_master/sns_topic_finder_spec.rb +25 -0
- data/spec/stack_master/stack_definition_spec.rb +37 -0
- data/spec/stack_master/stack_differ_spec.rb +34 -0
- data/spec/stack_master/stack_events/fetcher_spec.rb +65 -0
- data/spec/stack_master/stack_events/presenter_spec.rb +18 -0
- data/spec/stack_master/stack_events/streamer_spec.rb +33 -0
- data/spec/stack_master/stack_spec.rb +157 -0
- data/spec/stack_master/template_compiler_spec.rb +48 -0
- data/spec/stack_master/test_driver/cloud_formation_spec.rb +24 -0
- data/spec/stack_master/utils_spec.rb +30 -0
- data/spec/stack_master/validator_spec.rb +38 -0
- data/stack_master.gemspec +38 -0
- data/stacktemplates/parameter_region.yml +3 -0
- data/stacktemplates/parameter_stack_name.yml +3 -0
- data/stacktemplates/stack.json.erb +20 -0
- data/stacktemplates/stack_master.yml.erb +6 -0
- metadata +427 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
RSpec.describe StackMaster::SecurityGroupFinder do
|
|
2
|
+
subject(:finder) { described_class.new(region) }
|
|
3
|
+
let(:region) { 'us-east-1' }
|
|
4
|
+
let(:group_name) { "our-api-BeanstalkSg-T4RKD99YOY2F" }
|
|
5
|
+
let(:filter) do
|
|
6
|
+
{
|
|
7
|
+
filters: [
|
|
8
|
+
{
|
|
9
|
+
name: "group-name",
|
|
10
|
+
values: [group_name],
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe "#find" do
|
|
17
|
+
before do
|
|
18
|
+
allow_any_instance_of(Aws::EC2::Resource).to receive(:security_groups).with(filter).and_return(security_groups)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context "one sg match" do
|
|
22
|
+
let(:security_groups) { [
|
|
23
|
+
double(id: 'sg-a7d2ccc0')
|
|
24
|
+
] }
|
|
25
|
+
it "returns the id" do
|
|
26
|
+
expect(finder.find(group_name)).to eq 'sg-a7d2ccc0'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "more than one sg matches" do
|
|
31
|
+
let(:security_groups) { [
|
|
32
|
+
double(id: 'sg-a7d2ccc0'),
|
|
33
|
+
double(id: 'sg-a7d2ccc2'),
|
|
34
|
+
] }
|
|
35
|
+
it "returns the id" do
|
|
36
|
+
err = StackMaster::SecurityGroupFinder::MultipleSecurityGroupsFound
|
|
37
|
+
expect { finder.find(group_name) }.to raise_error(err)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context "no matches" do
|
|
42
|
+
let(:security_groups) { [] }
|
|
43
|
+
it "returns the id" do
|
|
44
|
+
err = StackMaster::SecurityGroupFinder::SecurityGroupNotFound
|
|
45
|
+
expect { finder.find(group_name) }.to raise_error(err)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
RSpec.describe StackMaster::SnsTopicFinder do
|
|
2
|
+
|
|
3
|
+
subject(:finder) { described_class.new(region) }
|
|
4
|
+
let(:region) { 'us-east-1' }
|
|
5
|
+
let(:topics) do
|
|
6
|
+
[
|
|
7
|
+
double(arn: 'arn:aws:sns:us-east-1:581634149801:topic1name'),
|
|
8
|
+
double(arn: 'arn:aws:sns:us-east-1:581634149801:topic2name'),
|
|
9
|
+
]
|
|
10
|
+
end
|
|
11
|
+
before do
|
|
12
|
+
allow_any_instance_of(Aws::SNS::Resource).to receive(:topics).and_return topics
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '#find' do
|
|
16
|
+
it 'finds the topics that exist' do
|
|
17
|
+
expect(finder.find('topic1name')).to eq 'arn:aws:sns:us-east-1:581634149801:topic1name'
|
|
18
|
+
expect(finder.find('topic2name')).to eq 'arn:aws:sns:us-east-1:581634149801:topic2name'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'raises an exception for topics that do not exist' do
|
|
22
|
+
expect { finder.find('unknowntopics') }.to raise_error StackMaster::SnsTopicFinder::TopicNotFound
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
RSpec.describe StackMaster::StackDefinition do
|
|
2
|
+
subject(:stack_definition) do
|
|
3
|
+
StackMaster::StackDefinition.new(
|
|
4
|
+
region: region,
|
|
5
|
+
stack_name: stack_name,
|
|
6
|
+
template: template,
|
|
7
|
+
tags: tags,
|
|
8
|
+
base_dir: base_dir)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:region) { 'us-east-1' }
|
|
12
|
+
let(:stack_name) { 'stack_name' }
|
|
13
|
+
let(:template) { 'template.json' }
|
|
14
|
+
let(:tags) { {'environment' => 'production'} }
|
|
15
|
+
let(:base_dir) { '/base_dir' }
|
|
16
|
+
|
|
17
|
+
it 'has default and region specific parameter file locations' do
|
|
18
|
+
expect(stack_definition.parameter_files).to eq([
|
|
19
|
+
"/base_dir/parameters/#{stack_name}.yml",
|
|
20
|
+
"/base_dir/parameters/#{region}/#{stack_name}.yml"
|
|
21
|
+
])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context 'with additional parameter lookup dirs' do
|
|
25
|
+
before do
|
|
26
|
+
stack_definition.send(:additional_parameter_lookup_dirs=, ['production'])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'includes a parameter lookup dir for it' do
|
|
30
|
+
expect(stack_definition.parameter_files).to eq([
|
|
31
|
+
"/base_dir/parameters/#{stack_name}.yml",
|
|
32
|
+
"/base_dir/parameters/#{region}/#{stack_name}.yml",
|
|
33
|
+
"/base_dir/parameters/production/#{stack_name}.yml"
|
|
34
|
+
])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
RSpec.describe StackMaster::StackDiffer do
|
|
2
|
+
subject(:differ) { described_class.new(proposed_stack, stack) }
|
|
3
|
+
let(:stack) { StackMaster::Stack.new(stack_name: stack_name,
|
|
4
|
+
region: region,
|
|
5
|
+
stack_id: 123,
|
|
6
|
+
template_body: '{}',
|
|
7
|
+
parameters: {}) }
|
|
8
|
+
let(:proposed_stack) { StackMaster::Stack.new(stack_name: stack_name,
|
|
9
|
+
region: region,
|
|
10
|
+
parameters: { 'param1' => 'hello'},
|
|
11
|
+
template_body: "{\"a\": 1}") }
|
|
12
|
+
let(:stack_name) { 'myapp-vpc' }
|
|
13
|
+
let(:region) { 'us-east-1' }
|
|
14
|
+
|
|
15
|
+
describe "#output_diff" do
|
|
16
|
+
context "entirely new stack" do
|
|
17
|
+
let(:stack) { nil }
|
|
18
|
+
|
|
19
|
+
it "outputs the entire stack" do
|
|
20
|
+
expect { differ.output_diff }.to output(/\+ \"a\"\: 1/).to_stdout
|
|
21
|
+
expect { differ.output_diff }.to output(/param1\: hello/).to_stdout
|
|
22
|
+
expect { differ.output_diff }.to output(/No stack found/).to_stdout
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context "stack update" do
|
|
27
|
+
it "outputs the stack diff" do
|
|
28
|
+
expect { differ.output_diff }.to output(/\+ \"a\"\: 1/).to_stdout
|
|
29
|
+
expect { differ.output_diff }.to output(/param1\: hello/).to_stdout
|
|
30
|
+
expect { differ.output_diff }.to_not output(/No stack found/).to_stdout
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
RSpec.describe StackMaster::StackEvents::Fetcher do
|
|
2
|
+
let(:cf) { Aws::CloudFormation::Client.new }
|
|
3
|
+
|
|
4
|
+
before do
|
|
5
|
+
allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf)
|
|
6
|
+
allow(StackMaster::StackEvents::Streamer).to receive(:stream)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
context 'with 2 stack events' do
|
|
10
|
+
let(:events) { [
|
|
11
|
+
{ event_id: '1', stack_id: '1', stack_name: 'blah', timestamp: Time.now},
|
|
12
|
+
{ event_id: '2', stack_id: '1', stack_name: 'blah', timestamp: Time.now}
|
|
13
|
+
] }
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
cf.stub_responses(:describe_stack_events, stack_events: events)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'returns stack events' do
|
|
20
|
+
events = StackMaster::StackEvents::Fetcher.fetch('blah', 'us-east-1')
|
|
21
|
+
expect(events.count).to eq 2
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context 'when more than one page is present' do
|
|
26
|
+
let(:page_one_events) { [
|
|
27
|
+
{ event_id: '1', stack_id: '1', stack_name: 'blah', timestamp: Time.now},
|
|
28
|
+
{ event_id: '2', stack_id: '1', stack_name: 'blah', timestamp: Time.now}
|
|
29
|
+
] }
|
|
30
|
+
let(:page_two_events) { [
|
|
31
|
+
{ event_id: '3', stack_id: '1', stack_name: 'blah', timestamp: Time.now}
|
|
32
|
+
] }
|
|
33
|
+
|
|
34
|
+
before do
|
|
35
|
+
cf.stub_responses(:describe_stack_events, { stack_events: page_one_events, next_token: 'blah' }, { stack_events: page_two_events } )
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns all the stack events combined' do
|
|
39
|
+
events = StackMaster::StackEvents::Fetcher.fetch('blah', 'us-east-1')
|
|
40
|
+
expect(events.count).to eq 3
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'filtering with a from timestamp' do
|
|
45
|
+
let(:two_pm) { Time.parse('2015-10-27 14:00') }
|
|
46
|
+
let(:three_pm) { Time.parse('2015-10-27 15:00') }
|
|
47
|
+
let(:four_pm) { Time.parse('2015-10-27 16:00') }
|
|
48
|
+
|
|
49
|
+
let(:events) {
|
|
50
|
+
[
|
|
51
|
+
{ event_id: '1', stack_id: '1', stack_name: 'blah', timestamp: two_pm },
|
|
52
|
+
{ event_id: '2', stack_id: '1', stack_name: 'blah', timestamp: four_pm },
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
before do
|
|
57
|
+
cf.stub_responses(:describe_stack_events, stack_events: events)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'only returns events after the timestamp' do
|
|
61
|
+
events = StackMaster::StackEvents::Fetcher.fetch('blah', 'us-east-1', from: three_pm)
|
|
62
|
+
expect(events.map(&:event_id)).to eq ['2']
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
RSpec.describe StackMaster::StackEvents::Presenter do
|
|
2
|
+
describe "#print_event" do
|
|
3
|
+
let(:time) { Time.new(2001,1,1,2,2,2) }
|
|
4
|
+
let(:event) do
|
|
5
|
+
double(:event,
|
|
6
|
+
timestamp: time,
|
|
7
|
+
logical_resource_id: 'MyAwesomeQueue',
|
|
8
|
+
resource_type: 'AWS::SQS::Queue',
|
|
9
|
+
resource_status: 'CREATE_IN_PROGRESS',
|
|
10
|
+
resource_status_reason: 'Resource creation Initiated')
|
|
11
|
+
end
|
|
12
|
+
subject(:print_event) { described_class.print_event($stdout, event) }
|
|
13
|
+
|
|
14
|
+
it "nicely presents event data" do
|
|
15
|
+
expect { print_event }.to output("\e[0;33;49m2001-01-01 02:02:02 +1100 MyAwesomeQueue AWS::SQS::Queue CREATE_IN_PROGRESS Resource creation Initiated\e[0m\n").to_stdout
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
RSpec.describe StackMaster::StackEvents::Streamer do
|
|
2
|
+
let(:events_first_call) { [
|
|
3
|
+
OpenStruct.new(event_id: '1', resource_status: 'BLAH', timestamp: Time.now),
|
|
4
|
+
OpenStruct.new(event_id: '2', resource_status: 'BLAH', timestamp: Time.now),
|
|
5
|
+
OpenStruct.new(event_id: '3', resource_status: 'BLAH', timestamp: Time.now),
|
|
6
|
+
] }
|
|
7
|
+
let(:events_second_call) {
|
|
8
|
+
events_first_call + [
|
|
9
|
+
OpenStruct.new(event_id: '4', resource_status: 'UPDATE_COMPLETE', resource_type: 'AWS::CloudFormation::Stack', logical_resource_id: stack_name, timestamp: Time.now)
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
let(:stack_name) { 'stack-name' }
|
|
13
|
+
let(:region) { 'us-east-1' }
|
|
14
|
+
let(:now) { Time.now }
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
allow(StackMaster::StackEvents::Fetcher).to receive(:fetch).with(stack_name, region, from: now).and_return(events_first_call, events_second_call)
|
|
18
|
+
allow(Time).to receive(:now).and_return(now)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns after seeing a finish state' do
|
|
22
|
+
events = []
|
|
23
|
+
StackMaster::StackEvents::Streamer.stream(stack_name, region, sleep_between_fetches: 0) do |event|
|
|
24
|
+
events << event
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'streams events to an io object' do
|
|
29
|
+
io = StringIO.new
|
|
30
|
+
StackMaster::StackEvents::Streamer.stream(stack_name, region, sleep_between_fetches: 0, io: io)
|
|
31
|
+
expect(io.string).to include('UPDATE_COMPLETE')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
RSpec.describe StackMaster::Stack do
|
|
2
|
+
let(:region) { 'us-east-1' }
|
|
3
|
+
let(:stack_name) { 'myapp_vpc' }
|
|
4
|
+
let(:stack_id) { '1' }
|
|
5
|
+
let(:stack_policy_body) { '{}' }
|
|
6
|
+
let(:cf) { Aws::CloudFormation::Client.new }
|
|
7
|
+
subject(:stack) { StackMaster::Stack.find(region, stack_name) }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
allow(Aws::CloudFormation::Client).to receive(:new).and_return(cf)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '.find' do
|
|
14
|
+
context 'when the stack exists in AWS' do
|
|
15
|
+
let(:parameters) {
|
|
16
|
+
[
|
|
17
|
+
{parameter_key: 'param1', parameter_value: 'value1'},
|
|
18
|
+
{parameter_key: 'param2', parameter_value: 'value2'}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
before do
|
|
22
|
+
cf.stub_responses(:describe_stacks, stacks: [{ stack_id: stack_id, stack_name: stack_name, creation_time: Time.now, stack_status: 'UPDATE_COMPLETE', parameters: parameters, notification_arns: ['test_arn']}])
|
|
23
|
+
cf.stub_responses(:get_template, template_body: "{}")
|
|
24
|
+
cf.stub_responses(:get_stack_policy, stack_policy_body: stack_policy_body)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns a stack object with a stack_id' do
|
|
28
|
+
expect(stack.stack_id).to eq stack_id
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "returns a template body" do
|
|
32
|
+
expect(stack.template_body).to eq "{}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'parses parameters into a hash' do
|
|
36
|
+
expect(stack.parameters).to eq({'param1' => 'value1', 'param2' => 'value2'})
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'sets notification_arns' do
|
|
40
|
+
expect(stack.notification_arns).to eq(['test_arn'])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'sets the stack policy' do
|
|
44
|
+
expect(stack.stack_policy_body).to eq stack_policy_body
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context 'when the stack does not exist in AWS' do
|
|
49
|
+
before do
|
|
50
|
+
cf.stub_responses(:describe_stacks, Aws::CloudFormation::Errors::ValidationError.new('a', 'b'))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'returns nil' do
|
|
54
|
+
stack = StackMaster::Stack.find(region, stack_name)
|
|
55
|
+
expect(stack).to be_nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context 'when CF returns no stacks' do
|
|
60
|
+
before do
|
|
61
|
+
cf.stub_responses(:describe_stacks, stacks: [])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'returns nil' do
|
|
65
|
+
stack = StackMaster::Stack.find(region, stack_name)
|
|
66
|
+
expect(stack).to be_nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '.generate' do
|
|
72
|
+
let(:tags) { { 'tag1' => 'value1' } }
|
|
73
|
+
let(:stack_definition) { StackMaster::StackDefinition.new(region: region, stack_name: stack_name, tags: tags, base_dir: '/base_dir', template: template_file_name, notification_arns: ['test_arn'], stack_policy_file: 'no_replace_rds.json') }
|
|
74
|
+
let(:config) { StackMaster::Config.new({'stacks' => {}}, '/base_dir') }
|
|
75
|
+
subject(:stack) { StackMaster::Stack.generate(stack_definition, config) }
|
|
76
|
+
let(:parameter_hash) { { 'DbPassword' => { 'secret' => 'db_password' } } }
|
|
77
|
+
let(:resolved_parameters) { { 'DbPassword' => 'sdfgjkdhlfjkghdflkjghdflkjg', 'InstanceType' => 't2.medium' } }
|
|
78
|
+
let(:template_file_name) { 'template.rb' }
|
|
79
|
+
let(:template_body) { '{"Parameters": { "VpcId": { "Description": "VPC ID" }, "InstanceType": { "Description": "Instance Type", "Default": "t2.micro" }} }' }
|
|
80
|
+
let(:stack_policy_body) { '{}' }
|
|
81
|
+
|
|
82
|
+
before do
|
|
83
|
+
allow(StackMaster::ParameterLoader).to receive(:load).and_return(parameter_hash)
|
|
84
|
+
allow(StackMaster::ParameterResolver).to receive(:resolve).and_return(resolved_parameters)
|
|
85
|
+
allow(StackMaster::TemplateCompiler).to receive(:compile).with(stack_definition.template_file_path).and_return(template_body)
|
|
86
|
+
allow(File).to receive(:read).with(stack_definition.stack_policy_file_path).and_return(stack_policy_body)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'has the stack definitions region' do
|
|
90
|
+
expect(stack.region).to eq region
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'has the stack definitions name' do
|
|
94
|
+
expect(stack.stack_name).to eq stack_name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'has the stack definitions tags' do
|
|
98
|
+
expect(stack.tags).to eq tags
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'resolves the parameters' do
|
|
102
|
+
expect(stack.parameters).to eq resolved_parameters
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'compiles the template body' do
|
|
106
|
+
expect(stack.template_body).to eq template_body
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'has notification_arns' do
|
|
110
|
+
expect(stack.notification_arns).to eq ['test_arn']
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'has the stack policy' do
|
|
114
|
+
expect(stack.stack_policy_body).to eq stack_policy_body
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'extracts default template parameters' do
|
|
118
|
+
expect(stack.template_default_parameters).to eq('VpcId' => nil, 'InstanceType' => 't2.micro')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'exposes parameters with defaults taken into account' do
|
|
122
|
+
expect(stack.parameters_with_defaults).to eq('DbPassword' => 'sdfgjkdhlfjkghdflkjghdflkjg', 'InstanceType' => 't2.medium', 'VpcId' => nil)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe "#maybe_compressed_template_body" do
|
|
127
|
+
subject(:maybe_compressed_template_body) do
|
|
128
|
+
stack.maybe_compressed_template_body
|
|
129
|
+
end
|
|
130
|
+
context "undersized json" do
|
|
131
|
+
let(:stack) { described_class.new(template_body: '{ }' ) }
|
|
132
|
+
|
|
133
|
+
it "leaves the json alone if it's not too large" do
|
|
134
|
+
expect(maybe_compressed_template_body).to eq('{ }')
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
context "oversized json" do
|
|
139
|
+
let(:stack) { described_class.new(template_body: "{#{' ' * 60000}}" ) }
|
|
140
|
+
it "compresses the json when it's overly bulbous" do
|
|
141
|
+
expect(maybe_compressed_template_body).to eq('{}')
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
describe '#too_big?' do
|
|
147
|
+
let(:big_stack) { described_class.new(template_body: "{\"a\":\"#{'x' * 60000}\"}") }
|
|
148
|
+
let(:little_stack) { described_class.new(template_body: "{\"a\":\"#{'x' * 1000}\"}") }
|
|
149
|
+
|
|
150
|
+
it 'returns true for big stacks' do
|
|
151
|
+
expect(big_stack.too_big?).to be_truthy
|
|
152
|
+
end
|
|
153
|
+
it 'returns false for small stacks' do
|
|
154
|
+
expect(little_stack.too_big?).to be_falsey
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
RSpec.describe StackMaster::TemplateCompiler do
|
|
2
|
+
describe ".compile" do
|
|
3
|
+
def compile
|
|
4
|
+
StackMaster::TemplateCompiler.compile(template_file_path)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
context 'json template' do
|
|
8
|
+
let(:template_file_path) { '/base_dir/templates/template.json' }
|
|
9
|
+
|
|
10
|
+
context "small json template" do
|
|
11
|
+
before do
|
|
12
|
+
allow(File).to receive(:read).with(template_file_path).and_return('{ }')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "reads from the template file path" do
|
|
16
|
+
expect(compile).to eq('{ }')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context 'extra big json template' do
|
|
21
|
+
before do
|
|
22
|
+
allow(File).to receive(:read).with(template_file_path).and_return("{ #{' ' * 60000} }")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "reads from the template file path" do
|
|
26
|
+
expect(compile).to eq('{}')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'sparkleformation template' do
|
|
32
|
+
let(:template_file_path) { '/base_dir/templates/template.rb' }
|
|
33
|
+
|
|
34
|
+
before do
|
|
35
|
+
allow(SparkleFormation).to receive(:compile).with(template_file_path).and_return({})
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'compiles with sparkleformation' do
|
|
39
|
+
expect(compile).to eq("{\n}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'sets the appropriate sparkle_path' do
|
|
43
|
+
compile
|
|
44
|
+
expect(SparkleFormation.sparkle_path).to eq File.dirname(template_file_path)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|