cfn-flow 0.2.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -1
- data/Rakefile +1 -1
- data/lib/cfn-flow.rb +131 -1
- data/lib/cfn-flow/cli.rb +154 -98
- data/lib/cfn-flow/event_presenter.rb +39 -0
- data/lib/cfn-flow/git.rb +1 -0
- data/lib/cfn-flow/template.rb +64 -50
- data/lib/cfn-flow/version.rb +3 -0
- data/spec/cfn-flow/cli_spec.rb +372 -0
- data/spec/cfn-flow/event_presenter_spec.rb +58 -0
- data/spec/cfn-flow/template_spec.rb +140 -0
- data/spec/cfn-flow_spec.rb +179 -0
- data/spec/data/cfn-flow.yml +12 -4
- data/spec/data/invalid.json +1 -0
- data/spec/data/invalid.yml +3 -0
- data/spec/helper.rb +48 -15
- metadata +19 -23
- data/spec/cfn-flow_cli_spec.rb +0 -30
- data/spec/cfn-flow_template_spec.rb +0 -56
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
|
3
|
+
describe 'EventPresenter' do
|
4
|
+
|
5
|
+
subject { CfnFlow::EventPresenter }
|
6
|
+
after { CfnFlow::EventPresenter.seen_event_ids.clear }
|
7
|
+
|
8
|
+
let(:event) { stub_event }
|
9
|
+
let(:event_with_reason) { stub_event(resource_status_reason: 'stubbed-reason') }
|
10
|
+
|
11
|
+
describe '.seen_event_ids' do
|
12
|
+
it 'should be a set' do
|
13
|
+
subject.seen_event_ids.must_be_kind_of Set
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '.present' do
|
18
|
+
it 'should present the right number of events' do
|
19
|
+
events = [event, event_with_reason]
|
20
|
+
result = subject.present(events) {|e| e}
|
21
|
+
|
22
|
+
result.size.must_equal 2
|
23
|
+
result.each {|e| e.must_be_kind_of CfnFlow::EventPresenter }
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should omit dupe events' do
|
27
|
+
subject.present([event]) {}
|
28
|
+
subject.present([event]) {}.must_equal []
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should render the status' do
|
32
|
+
out, _ = capture_io do
|
33
|
+
subject.present([event]) { |e| puts e.to_s }
|
34
|
+
end
|
35
|
+
|
36
|
+
out.must_match event.resource_status
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#initialize' do
|
41
|
+
it 'should add to .seen_event_ids' do
|
42
|
+
subject.new(event)
|
43
|
+
subject.seen_event_ids.include?(event.id).must_equal true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#to_s' do
|
48
|
+
it 'should show the appropriate details' do
|
49
|
+
str = subject.new(event).to_s
|
50
|
+
str.must_match event.logical_resource_id
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should show a reason' do
|
54
|
+
str = subject.new(event_with_reason).to_s
|
55
|
+
str.must_match event_with_reason.resource_status_reason
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
|
3
|
+
describe 'CfnFlow::Template' do
|
4
|
+
|
5
|
+
let(:template) {
|
6
|
+
CfnFlow::Template.new('spec/data/sqs.template')
|
7
|
+
}
|
8
|
+
|
9
|
+
let(:yml_template) {
|
10
|
+
CfnFlow::Template.new('spec/data/sqs.yml')
|
11
|
+
}
|
12
|
+
|
13
|
+
let(:not_a_template) {
|
14
|
+
CfnFlow::Template.new('spec/data/cfn-flow.yml')
|
15
|
+
}
|
16
|
+
|
17
|
+
let(:release) { 'deadbeef' }
|
18
|
+
|
19
|
+
describe '#initialize' do
|
20
|
+
subject { CfnFlow::Template }
|
21
|
+
|
22
|
+
it('succeeds') do
|
23
|
+
subject.new('f').must_be_kind_of CfnFlow::Template
|
24
|
+
end
|
25
|
+
|
26
|
+
it('requires args') do
|
27
|
+
-> { subject.new }.must_raise(ArgumentError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#yaml?' do
|
32
|
+
it 'works' do
|
33
|
+
yml_template.yaml?.must_equal true
|
34
|
+
yml_template.json?.must_equal false
|
35
|
+
template.yaml?.must_equal false
|
36
|
+
template.json?.must_equal true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#is_cfn_template?' do
|
41
|
+
it 'works' do
|
42
|
+
yml_template.is_cfn_template?.must_equal true
|
43
|
+
template.is_cfn_template?.must_equal true
|
44
|
+
not_a_template.is_cfn_template?.must_equal false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#bucket' do
|
49
|
+
it 'uses CfnFlow.template_s3_bucket' do
|
50
|
+
template.bucket.must_equal CfnFlow.template_s3_bucket
|
51
|
+
end
|
52
|
+
it 'has the correct value' do
|
53
|
+
template.bucket.must_equal 'test-bucket'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#s3_prefix' do
|
58
|
+
it 'uses CfnFlow.template_s3_prefix' do
|
59
|
+
template.s3_prefix.must_equal CfnFlow.template_s3_prefix
|
60
|
+
end
|
61
|
+
it 'has the correct value' do
|
62
|
+
template.s3_prefix.must_equal 'test-prefix'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#key' do
|
67
|
+
it 'has the correct value' do
|
68
|
+
expected = File.join(template.s3_prefix, release, template.local_path)
|
69
|
+
template.key(release).must_equal expected
|
70
|
+
end
|
71
|
+
|
72
|
+
it "removes leading './'" do
|
73
|
+
CfnFlow::Template.new('./foo').key(release).must_equal "test-prefix/#{release}/foo"
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can have a empty s3_prefix" do
|
77
|
+
CfnFlow.instance_variable_set(:@config, {'templates' => {'s3_bucket' => 'foo'}})
|
78
|
+
expected = File.join(release, template.local_path)
|
79
|
+
template.key(release).must_equal expected
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#s3_object' do
|
84
|
+
it 'is an S3::Object' do
|
85
|
+
subject = template.s3_object(release)
|
86
|
+
subject.must_be_kind_of Aws::S3::Object
|
87
|
+
subject.bucket.name.must_equal template.bucket
|
88
|
+
subject.key.must_equal template.key(release)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#url' do
|
93
|
+
it 'is the correct S3 url' do
|
94
|
+
uri = URI.parse(template.url(release))
|
95
|
+
uri.scheme.must_equal 'https'
|
96
|
+
uri.host.must_match(/\A#{template.bucket}\.s3\..+\.amazonaws\.com\z/)
|
97
|
+
uri.path.must_equal('/' + template.key(release))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#upload' do
|
102
|
+
it 'succeeds' do
|
103
|
+
template.upload(release)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#local_data' do
|
108
|
+
it 'should read valid data' do
|
109
|
+
template.local_data.must_be_kind_of Hash
|
110
|
+
template.local_data.must_be_kind_of Hash
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should raise an error on invalid json data' do
|
114
|
+
-> { CfnFlow::Template.new('spec/data/invalid.json').local_data }.must_raise CfnFlow::Template::Error
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should raise an error on invalid YAML data' do
|
118
|
+
-> { CfnFlow::Template.new('spec/data/invalid.yml').local_data }.must_raise CfnFlow::Template::Error
|
119
|
+
end
|
120
|
+
it 'should raise an on a missing file' do
|
121
|
+
-> { CfnFlow::Template.new('no/such/file').local_data }.must_raise CfnFlow::Template::Error
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '#to_json' do
|
126
|
+
it 'should work' do
|
127
|
+
template.to_json.must_equal MultiJson.dump(template.local_data, pretty: true)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe '#validate!' do
|
132
|
+
it 'succeeds' do
|
133
|
+
template.validate!
|
134
|
+
end
|
135
|
+
it 'can raise an error' do
|
136
|
+
Aws.config[:cloudformation] = {stub_responses: {validate_template: 'ValidationError'}}
|
137
|
+
-> { template.validate! }.must_raise Aws::CloudFormation::Errors::ValidationError
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
describe 'CfnFlow' do
|
4
|
+
subject { CfnFlow }
|
5
|
+
|
6
|
+
describe '.config_path' do
|
7
|
+
it 'should be ./cfn-flow.yml by default' do
|
8
|
+
ENV.delete('CFN_FLOW_CONFIG_PATH')
|
9
|
+
subject.config_path.must_equal 'cfn-flow.yml'
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'can be overridden with ENV[CFN_FLOW_CONFIG_PATH]' do
|
13
|
+
ENV['CFN_FLOW_CONFIG_PATH'] = 'foo/bar'
|
14
|
+
subject.config_path.must_equal 'foo/bar'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.config_loaded?' do
|
19
|
+
it 'should be false by default' do
|
20
|
+
subject.config_loaded?.must_equal false
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should be true after loading' do
|
24
|
+
subject.load_config
|
25
|
+
subject.config_loaded?.must_equal true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.config' do
|
30
|
+
it('should be a hash') { subject.config.must_be_kind_of(Hash) }
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '.service' do
|
34
|
+
it('raises an error when missing') do
|
35
|
+
subject.instance_variable_set(:@config, {})
|
36
|
+
error = -> { subject.service }.must_raise(Thor::Error)
|
37
|
+
error.message.must_match 'No service name'
|
38
|
+
end
|
39
|
+
|
40
|
+
it('returns the service') do
|
41
|
+
subject.instance_variable_set(:@config, {'service' => 'RoflScaler'})
|
42
|
+
subject.service.must_equal 'RoflScaler'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '.stack_params' do
|
47
|
+
it('raises an error when missing') do
|
48
|
+
subject.instance_variable_set(:@config, {})
|
49
|
+
error = -> { subject.stack_params('env') }.must_raise(Thor::Error)
|
50
|
+
error.message.must_match 'No stack defined'
|
51
|
+
end
|
52
|
+
|
53
|
+
it('expands parameters') do
|
54
|
+
stack = {'parameters' => {'ami' => 'ami-12345' } }
|
55
|
+
subject.instance_variable_set(:@config, {'service' => 'myservice', 'stack' => stack})
|
56
|
+
subject.stack_params('env')[:parameters].must_equal [ { parameter_key: 'ami', parameter_value: 'ami-12345' } ]
|
57
|
+
end
|
58
|
+
|
59
|
+
it('expands tags') do
|
60
|
+
stack = {'tags' => {'Deployer' => 'Aaron' } }
|
61
|
+
subject.instance_variable_set(:@config, {'service' => 'myservice', 'stack' => stack})
|
62
|
+
expected = [
|
63
|
+
{ key: 'Deployer', value: 'Aaron' },
|
64
|
+
{ key: 'CfnFlowService', value: 'myservice' },
|
65
|
+
{ key: 'CfnFlowEnvironment', value: 'env' }
|
66
|
+
]
|
67
|
+
|
68
|
+
subject.stack_params('env')[:tags].must_equal expected
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'appends CfnFlow tags' do
|
72
|
+
subject.instance_variable_set(:@config, {'service' => 'myservice', 'stack' => {}})
|
73
|
+
expected = [
|
74
|
+
{ key: 'CfnFlowService', value: 'myservice' },
|
75
|
+
{ key: 'CfnFlowEnvironment', value: 'env' }
|
76
|
+
]
|
77
|
+
|
78
|
+
subject.stack_params('env')[:tags].must_equal expected
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'expands template body' do
|
82
|
+
template_path = 'spec/data/sqs.template'
|
83
|
+
stack = {'template_body' => template_path}
|
84
|
+
subject.instance_variable_set(:@config, {'service' => 'myservice', 'stack' => stack})
|
85
|
+
subject.stack_params('env')[:template_body].must_equal CfnFlow::Template.new(template_path).to_json
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '.template_s3_bucket' do
|
91
|
+
it('raises an error when missing') do
|
92
|
+
subject.instance_variable_set(:@config, {})
|
93
|
+
error = -> { subject.template_s3_bucket }.must_raise(Thor::Error)
|
94
|
+
error.message.must_match 'No s3_bucket defined'
|
95
|
+
|
96
|
+
subject.instance_variable_set(:@config, {'templates' => {}})
|
97
|
+
error = -> { subject.template_s3_bucket }.must_raise(Thor::Error)
|
98
|
+
error.message.must_match 'No s3_bucket defined'
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'succeeds' do
|
102
|
+
subject.instance_variable_set(:@config, {'templates' => {'s3_bucket' => 'hello'}})
|
103
|
+
subject.template_s3_bucket.must_equal 'hello'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '.template_s3_prefix' do
|
108
|
+
it('raises an error when missing') do
|
109
|
+
subject.instance_variable_set(:@config, {})
|
110
|
+
error = -> { subject.template_s3_prefix }.must_raise(Thor::Error)
|
111
|
+
error.message.must_match 'No templates defined'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'succeeds' do
|
115
|
+
subject.instance_variable_set(:@config, {'templates' => {'s3_prefix' => 'hello'}})
|
116
|
+
subject.template_s3_prefix.must_equal 'hello'
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'can be nil' do
|
120
|
+
subject.instance_variable_set(:@config, {'templates' => {}})
|
121
|
+
subject.template_s3_prefix.must_equal nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '.cfn_client' do
|
126
|
+
it 'should work' do
|
127
|
+
subject.cfn_client.must_be_kind_of Aws::CloudFormation::Client
|
128
|
+
end
|
129
|
+
|
130
|
+
describe 'aws region' do
|
131
|
+
it 'should default to the env region' do
|
132
|
+
ENV['AWS_REGION'] = 'env-region'
|
133
|
+
subject.cfn_client.config.region.must_equal 'env-region'
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'can be overridden with config' do
|
137
|
+
ENV['AWS_REGION'] = 'env-region'
|
138
|
+
subject.instance_variable_set(:@config, {region: 'config-region' })
|
139
|
+
subject.cfn_client.config.region.must_equal 'config-region'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
describe '.cfn_resource' do
|
146
|
+
it 'should work' do
|
147
|
+
subject.cfn_resource.must_be_kind_of Aws::CloudFormation::Resource
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should set a retry_limit' do
|
151
|
+
subject.cfn_resource.client.config.retry_limit.must_equal 10
|
152
|
+
end
|
153
|
+
|
154
|
+
describe 'aws region' do
|
155
|
+
it 'should default to the env region' do
|
156
|
+
ENV['AWS_REGION'] = 'env-region'
|
157
|
+
subject.cfn_client.config.region.must_equal 'env-region'
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'can be overridden with config' do
|
161
|
+
ENV['AWS_REGION'] = 'env-region'
|
162
|
+
subject.instance_variable_set(:@config, {region: 'config-region' })
|
163
|
+
subject.cfn_client.config.region.must_equal 'config-region'
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe '.exit_on_failure?' do
|
169
|
+
it 'is true by default' do
|
170
|
+
CfnFlow.remove_instance_variable(:@exit_on_failure) if CfnFlow.instance_variable_defined?(:@exit_on_failure)
|
171
|
+
CfnFlow.exit_on_failure?.must_equal true
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'can be set' do
|
175
|
+
CfnFlow.exit_on_failure = false
|
176
|
+
CfnFlow.exit_on_failure?.must_equal false
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/spec/data/cfn-flow.yml
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
prefix: test-prefix
|
4
|
-
from: spec/data
|
5
|
-
to: test-to
|
2
|
+
service: cfn-flow-specs
|
6
3
|
region: us-east-1
|
4
|
+
templates:
|
5
|
+
s3_bucket: test-bucket
|
6
|
+
s3_prefix: test-prefix
|
7
|
+
|
8
|
+
stack:
|
9
|
+
stack_name: cfn-flow-spec-stack
|
10
|
+
template_body: sqs.yml
|
11
|
+
parameters:
|
12
|
+
AlarmEMail: test@example.com
|
13
|
+
tags:
|
14
|
+
test_tag: hello world
|
@@ -0,0 +1 @@
|
|
1
|
+
{
|
data/spec/helper.rb
CHANGED
@@ -16,23 +16,56 @@ ENV['AWS_REGION'] = 'us-east-1'
|
|
16
16
|
ENV['AWS_ACCESS_KEY_ID'] = 'test-key'
|
17
17
|
ENV['AWS_SECRET_ACCESS_KEY'] = 'test-secret'
|
18
18
|
ENV['CFN_FLOW_DEV_NAME'] = 'cfn-flow-specs'
|
19
|
+
ENV['CFN_FLOW_CONFIG_PATH'] = 'spec/data/cfn-flow.yml'
|
20
|
+
ENV['CFN_FLOW_EVENT_POLL_INTERVAL'] = '0'
|
19
21
|
|
20
22
|
class Minitest::Spec
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
23
|
+
before do
|
24
|
+
# Reset env between tests:
|
25
|
+
@orig_env = ENV.to_hash
|
26
|
+
|
27
|
+
# Disable exit on failure so CLI tests don't bomb out
|
28
|
+
CfnFlow.exit_on_failure = false
|
29
|
+
end
|
30
|
+
|
31
|
+
after do
|
32
|
+
# Reset env
|
33
|
+
ENV.clear
|
34
|
+
ENV.update(@orig_env)
|
35
|
+
|
36
|
+
# Reset stubs
|
37
|
+
CfnFlow.clear!
|
38
|
+
Aws.config.delete(:cloudformation)
|
39
|
+
end
|
40
|
+
|
41
|
+
def stub_stack_data(attrs = {})
|
42
|
+
{
|
43
|
+
stack_name: "mystack",
|
44
|
+
stack_status: 'CREATE_COMPLETE',
|
45
|
+
creation_time: Time.now,
|
46
|
+
tags: [
|
47
|
+
{key: 'CfnFlowService', value: CfnFlow.service},
|
48
|
+
{key: 'CfnFlowEnvironment', value: 'production'}
|
49
|
+
]
|
50
|
+
}.merge(attrs)
|
51
|
+
end
|
52
|
+
|
53
|
+
def stub_event_data(attrs = {})
|
54
|
+
{
|
55
|
+
stack_id: 'mystack',
|
56
|
+
stack_name: 'mystack',
|
57
|
+
event_id: SecureRandom.hex,
|
58
|
+
resource_status: 'CREATE_COMPLETE',
|
59
|
+
logical_resource_id: 'stubbed-resource-id',
|
60
|
+
resource_type: 'stubbed-resource-type',
|
61
|
+
timestamp: Time.now
|
62
|
+
}.merge(attrs)
|
63
|
+
end
|
64
|
+
|
65
|
+
def stub_event(attrs = {})
|
66
|
+
data = stub_event_data(attrs)
|
67
|
+
id = data.delete(:event_id)
|
68
|
+
Aws::CloudFormation::Event.new(id: id, data: data)
|
33
69
|
end
|
34
70
|
|
35
|
-
# Reset env between tests
|
36
|
-
before { @orig_env = ENV.to_hash }
|
37
|
-
after { ENV.clear; ENV.update(@orig_env) }
|
38
71
|
end
|