cfn-flow 0.2.1 → 0.5.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/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
|