cfn-flow 0.7.0 → 0.8.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 +192 -90
- data/lib/cfn-flow.rb +1 -139
- data/lib/cfn_flow.rb +138 -0
- data/lib/{cfn-flow → cfn_flow}/cli.rb +13 -6
- data/lib/{cfn-flow → cfn_flow}/event_presenter.rb +0 -0
- data/lib/{cfn-flow → cfn_flow}/git.rb +0 -0
- data/lib/{cfn-flow → cfn_flow}/template.rb +0 -0
- data/lib/cfn_flow/version.rb +3 -0
- data/spec/{cfn-flow → cfn_flow}/cli_spec.rb +33 -7
- data/spec/{cfn-flow → cfn_flow}/event_presenter_spec.rb +0 -0
- data/spec/{cfn-flow → cfn_flow}/template_spec.rb +0 -0
- data/spec/{cfn-flow_spec.rb → cfn_flow_spec.rb} +2 -2
- data/spec/data/cfn-flow.yml +0 -1
- data/spec/helper.rb +5 -1
- metadata +16 -15
- data/lib/cfn-flow/version.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96e7a912fc803e4d51267f48e138ae273455d424
|
4
|
+
data.tar.gz: 0827e20cf4a86e42063c55648d50531522bdc769
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e20d070594f969140f946c1f2cc37ae777bbc795ef846a375d9e064c5078912810d8200018bca251ad0a73bbd6c9437f453fe71915c3875a12a15526ef6ac9b6
|
7
|
+
data.tar.gz: f0eb7fb958048dc13915255067efdb45e7d06941a50e291a0949aa90f6627f616bc0bc8b3de31ed0236224def48651a974130b68917e58cc992260af74bebdaf
|
data/README.md
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
# cfn-flow
|
2
|
-
|
2
|
+
`cfn-flow` is an command-line tool for developing [AWS CloudFormation](https://aws.amazon.com/cloudformation/) templates and deploying stacks.
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
Deploy stacks using a standard, reliable process with extensible
|
7
|
-
configuration in git.
|
4
|
+
It provides a *simple*, *standard*, and *flexible* process for using CloudFormation, ideal for DevOps-style organizations.
|
8
5
|
|
9
6
|
#### Opinions
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
`cfn-flow` introduces a consist, convenient workflow that encourages good template organization
|
9
|
+
and deploy practices.
|
10
|
+
|
11
|
+
1. *Optimize for happiness.* The workflow should be easy and enjoyable to use.
|
12
|
+
2. *Optimize for onboarding.* The workflow should be simple to learn & understand.
|
13
|
+
3. *Auditable changes.* Know who changed what when. Leverage git history.
|
14
|
+
4. *Immutable releases.* The code in a release never changes. To make a change,
|
15
|
+
launch a new stack.
|
16
|
+
|
17
|
+
The features & implementation of `cfn-flow` itself must also be simple. This follows the Unix philosophy of "[worse is
|
18
|
+
better](http://www.jwz.org/doc/worse-is-better.html)". `cfn-flow` values a simple design and implementation, and being composable with other workflows over handling every edge case out of the box.
|
15
19
|
|
16
20
|
## Installation
|
17
21
|
|
@@ -24,8 +28,8 @@ The `git` command is also needed.
|
|
24
28
|
|
25
29
|
## Usage
|
26
30
|
|
27
|
-
Poke around:
|
28
31
|
```
|
32
|
+
# Get help
|
29
33
|
cfn-flow help
|
30
34
|
|
31
35
|
cfn-flow help COMMAND
|
@@ -33,11 +37,74 @@ cfn-flow help COMMAND
|
|
33
37
|
cfn-flow help deploy
|
34
38
|
```
|
35
39
|
|
36
|
-
|
40
|
+
Launch a CloudFormation stack:
|
37
41
|
```
|
38
42
|
cfn-flow deploy production
|
39
43
|
```
|
40
44
|
|
45
|
+
## How it works
|
46
|
+
|
47
|
+
`cfn-flow` works from a directory containing a `cfn-flow.yml` config file, and a CloudFormation template.
|
48
|
+
Presumably your app code is in the same directory, but it doesn't have to be.
|
49
|
+
|
50
|
+
There are two key concepts for `cfn-flow`: **services** and **environments**.
|
51
|
+
|
52
|
+
#### Services
|
53
|
+
|
54
|
+
A service is a name for your project and comprises a set of resources that
|
55
|
+
change together. Each service has it's own `cfn-flow.yml` config file. A service
|
56
|
+
can be instantiated as several distinct environments.
|
57
|
+
|
58
|
+
For example, a `WebApp` service could have a CloudFormation template that
|
59
|
+
creates an ELB, LaunchConfig, and AutoScalingGroup resources.
|
60
|
+
|
61
|
+
All the resources in a service change together. Deploying the `WebApp`
|
62
|
+
service to an environment will create a new ELB, LaunchConfig, and AutoScalingGroup.
|
63
|
+
|
64
|
+
Resources that *do not* change across deploys are not part of the service (from
|
65
|
+
`cfn-flow`'s perspective).
|
66
|
+
Say all `WebApp` EC2 servers connect to a long-running RDS database. That
|
67
|
+
database is not part of the cfn-flow service because it should re-used across
|
68
|
+
deploys. The database is a *backing resource* the service uses; not part
|
69
|
+
of the service itself.
|
70
|
+
|
71
|
+
#### Environments
|
72
|
+
|
73
|
+
An environment is an particular instantiation of a service. For example, you
|
74
|
+
could deploy your `WebApp` service to both a `development` and `production` environment.
|
75
|
+
|
76
|
+
`cfn-flow` is designed to support arbitrary environments like git supports
|
77
|
+
arbitrary branches.
|
78
|
+
|
79
|
+
Then `CFN_FLOW_ENVIRONMENT` environment variable can be used in
|
80
|
+
`cfn-flow.yml` to use the environment in your template parameters.
|
81
|
+
|
82
|
+
#### Deploying
|
83
|
+
|
84
|
+
Deployments consist of launching a *new stack* in a particular environment, then
|
85
|
+
shutting down the old stack. For example:
|
86
|
+
|
87
|
+
```
|
88
|
+
cfn-flow deploy ENVIRONMENT --cleanup
|
89
|
+
```
|
90
|
+
|
91
|
+
This follows the [red/black](http://techblog.netflix.com/2013/08/deploying-netflix-api.html)
|
92
|
+
or [blue/green](http://martinfowler.com/bliki/BlueGreenDeployment.html)
|
93
|
+
deployment pattern.
|
94
|
+
|
95
|
+
After verifying the new stack is working correctly, the deployer is expected to
|
96
|
+
delete the old stack.
|
97
|
+
|
98
|
+
To roll back a bad deploy, simply delete the *new* stack, while the *old*
|
99
|
+
stack is running.
|
100
|
+
|
101
|
+
Although CloudFormation supports updating existing stacks, `cfn-flow` prefers
|
102
|
+
launching immutable stacks. Stack updates are more difficult to test than new stacks; and there's less chance of a deployment error disrupting or breaking important resources.
|
103
|
+
|
104
|
+
#### AWS credentials
|
105
|
+
|
106
|
+
Set your AWS credentials so they can be found by the AWS SDK for Ruby ([details here](http://docs.aws.amazon.com/AWSSdkDocsRuby/latest/DeveloperGuide/set-up-creds.html)), e.g. using the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables.
|
107
|
+
|
41
108
|
## Configuration
|
42
109
|
|
43
110
|
`cfn-flow` looks for `./cfn-flow.yml` for stack and template configuration.
|
@@ -55,6 +122,7 @@ stack:
|
|
55
122
|
# Stack name uses embedded ruby to support dynamic values
|
56
123
|
stack_name: MyService-<%= Time.now.to_i %>
|
57
124
|
# Required: *either* template_url or template_body
|
125
|
+
# NB: template_body is a local path to the template
|
58
126
|
template_body: path/to/template.json
|
59
127
|
# Alternatively:
|
60
128
|
# template_url: https://MyS3Bucket.s3.amazonaws.com/MyPrefix/release/abc123/template.json
|
@@ -81,8 +149,16 @@ templates:
|
|
81
149
|
bucket: MyS3Bucket
|
82
150
|
s3_prefix: 'My/S3/Prefix'
|
83
151
|
|
152
|
+
##
|
153
|
+
# Stacks
|
154
|
+
#
|
155
|
+
# These are the arguments passed when launching a new stack.
|
156
|
+
# It's nearly identical to the create_stack args in the ruby sdk, except
|
157
|
+
# parameters and tags are hashes. See http://amzn.to/1M0nBuq
|
158
|
+
|
84
159
|
stack:
|
85
160
|
stack_name: MyService-<%= Time.now.to_i %>
|
161
|
+
# NB: template_body is a local path to the template
|
86
162
|
template_body: path/to/template.yml
|
87
163
|
template_url: http://...
|
88
164
|
parameters:
|
@@ -97,6 +173,8 @@ stack:
|
|
97
173
|
stack_policy_body: "StackPolicyBody",
|
98
174
|
stack_policy_url: "StackPolicyURL",
|
99
175
|
tags:
|
176
|
+
# Whatever you want.
|
177
|
+
# Note that `cfn-flow` automatically adds two tags: `CfnFlowService` and `CfnFlowEnvironment`
|
100
178
|
TagKey: TagValue
|
101
179
|
# Who launched this stack
|
102
180
|
Deployer: <%= ENV['USER'] %>
|
@@ -104,121 +182,145 @@ stack:
|
|
104
182
|
BillingType: <%= ENV['CFN_FLOW_ENVIRONMENT'] == 'production' ? 'production' : 'development' %>
|
105
183
|
```
|
106
184
|
|
107
|
-
|
185
|
+
### UX improvements:
|
108
186
|
|
109
|
-
|
110
|
-
`cfn-flow` validates all templates and uploads them to your personal prefix, overwriting existing templates.
|
187
|
+
`cfn-flow` includes a few developer-friendly features:
|
111
188
|
|
112
|
-
|
113
|
-
committed to git (as opposed to release mode).
|
189
|
+
#### YAML > JSON
|
114
190
|
|
115
|
-
|
191
|
+
`cfn-flow` lets you write templates in either JSON or
|
192
|
+
[YAML](http://www.yaml.org). YAML is a superset of JSON that allows a terser,
|
193
|
+
less cluttered syntax, inline comments, and code re-use with anchors (like
|
194
|
+
variables). YAML templates are transparently converted to JSON when uploaded to
|
195
|
+
S3 for use in CloudFormation stacks.
|
116
196
|
|
117
|
-
|
118
|
-
|
119
|
-
|
197
|
+
Note that you can use JSON snippets inside YAML templates. JSON is always valid
|
198
|
+
YAML.
|
199
|
+
|
200
|
+
#### Embedded ruby in `cfn-flow.yml`
|
120
201
|
|
121
|
-
|
122
|
-
|
123
|
-
# NB that this overwrites existing templates in your CFN_FLOW_DEV_NAME
|
124
|
-
namespace.
|
202
|
+
To allow dynamic/programatic attributes, use
|
203
|
+
[ERB](https://en.wikipedia.org/wiki/ERuby) in `cfn-flow.yml`. For example:
|
125
204
|
|
126
|
-
|
205
|
+
```yaml
|
206
|
+
stack:
|
207
|
+
name: my-stack-<%= Time.now.to_i %>
|
208
|
+
...
|
209
|
+
parameters:
|
210
|
+
git_sha: <%= `git rev-parse --verify HEAD`.chomp %>
|
127
211
|
```
|
128
212
|
|
129
|
-
|
130
|
-
|
213
|
+
## Usage
|
214
|
+
|
215
|
+
### Working with stacks
|
216
|
+
|
217
|
+
`cfn-flow` automatically sets two tags on any stack it launches:
|
218
|
+
|
219
|
+
Name | Example value
|
220
|
+
--- | ---
|
221
|
+
CfnFlowService | `myapp`
|
222
|
+
CfnFlowEnvironment | `production`
|
131
223
|
|
132
|
-
|
224
|
+
These tags let `cfn-flow` associate stacks back to services & environments.
|
133
225
|
|
134
|
-
|
135
|
-
|
226
|
+
#### `cfn-flow deploy ENVIRONMENT`
|
227
|
+
|
228
|
+
Launches a stack in ENVIRONMENT. E.g. `cfn-flow deploy production`
|
229
|
+
|
230
|
+
Add the `--cleanup` option to be prompted to shut down other stacks in the environment.
|
231
|
+
|
232
|
+
#### `cfn-flow list ENVIRONMENT`
|
233
|
+
|
234
|
+
Show running stacks for ENVIRONMENT.
|
136
235
|
|
137
236
|
```
|
138
|
-
|
139
|
-
|
140
|
-
|
237
|
+
$ cfn-flow list production
|
238
|
+
|
239
|
+
myapp-production-aaa (CREATE_COMPLETE)
|
240
|
+
myapp-production-bbb (CREATE_FAILED)
|
141
241
|
```
|
142
242
|
|
143
|
-
|
144
|
-
directory.
|
243
|
+
#### `cfn-flow delete STACK`
|
145
244
|
|
146
|
-
|
147
|
-
diff`.
|
245
|
+
Deletes a stack.
|
148
246
|
|
149
|
-
|
247
|
+
```
|
248
|
+
$ cfn-flow delete myapp-production-aaa
|
249
|
+
```
|
150
250
|
|
151
|
-
|
152
|
-
directory you run `cfn-flow` (presumably the root of your project).
|
251
|
+
#### `cfn-flow show STACK`
|
153
252
|
|
154
|
-
|
155
|
-
environment variables are overridden by command line arguments.
|
253
|
+
Show the status of STACK.
|
156
254
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
# variable.
|
161
|
-
#
|
162
|
-
# All options in this config can be overridden with command line arguments
|
163
|
-
---
|
164
|
-
# S3 bucket where templates are uploaded. No default.
|
165
|
-
# Override with CFN_FLOW_BUCKET env var
|
166
|
-
bucket: 'my-s3-bucket'
|
255
|
+
#### `cfn-flow events STACK`
|
256
|
+
|
257
|
+
List events for STACK
|
167
258
|
|
168
|
-
|
169
|
-
|
170
|
-
to: my/project/prefix
|
259
|
+
Use the `--tail` option to poll for new events until the stack status is no
|
260
|
+
longer `*_IN_PROGRESS`
|
171
261
|
|
172
|
-
|
173
|
-
|
174
|
-
|
262
|
+
### Common workflows
|
263
|
+
|
264
|
+
#### Deploying to production
|
175
265
|
|
176
|
-
# AWS Region
|
177
|
-
# Override with AWS_REGION env var
|
178
|
-
region: us-east-1 # AWS region
|
179
266
|
```
|
267
|
+
# Launch a new stack for the current git commit
|
268
|
+
$ cfn-flow deploy production
|
269
|
+
Launching stack myapp-production-abc123
|
270
|
+
# ... wait for it to be ready
|
180
271
|
|
181
|
-
|
272
|
+
# See the other stacks
|
273
|
+
$ cfn-deploy list production
|
182
274
|
|
183
|
-
|
184
|
-
|
185
|
-
using an EC2 instance's IAM role.
|
275
|
+
myapp-production-abc123 CREATE_COMPLETE
|
276
|
+
myapp-production-xyz987 CREATE_COMPLETE
|
186
277
|
|
278
|
+
# Shut down the old stack
|
279
|
+
$ cfn-flow delete myapp-production-xyz987
|
280
|
+
```
|
187
281
|
|
188
|
-
|
282
|
+
### Launching a development environment
|
189
283
|
|
190
|
-
|
284
|
+
Launch a new stack for `myenv` environment
|
191
285
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
templates are transparently converted to JSON when uploaded to S3 for use in
|
196
|
-
CloudFormation stacks.
|
286
|
+
```
|
287
|
+
cfn-flow deploy myenv
|
288
|
+
```
|
197
289
|
|
198
|
-
|
290
|
+
### Working with templates
|
199
291
|
|
200
|
-
`cfn-flow`
|
201
|
-
resources](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stack.html). Use [Fn::Join](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html) to construct the `TemplateURL` from a parameter:
|
292
|
+
#### `cfn-flow validate`
|
202
293
|
|
203
294
|
```
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
[ "https://s3.amazonaws.com/my-bucket", {"Ref": "prefix"}, "my-template.json" ]
|
210
|
-
]
|
211
|
-
}
|
212
|
-
}
|
213
|
-
}
|
295
|
+
# Runs validate-template on all templates.
|
296
|
+
# returns an error on any failure.
|
297
|
+
# does not persist to S3
|
298
|
+
|
299
|
+
$ cfn-flow validate path/to/template.yml
|
214
300
|
```
|
215
301
|
|
216
|
-
|
302
|
+
#### `cfn-flow publish`
|
303
|
+
|
304
|
+
Publish templates to S3 with immutable release names, or overwrite "dev names"
|
305
|
+
for quicker testing. *This is only needed if you want to use nested stack resources.*
|
217
306
|
|
218
|
-
|
307
|
+
```
|
308
|
+
$ cfn-flow publish path/to/template.yml
|
309
|
+
# validates & uploads templates to dev path
|
310
|
+
# Env var CFN_FLOW_DEV_NAME=aaron
|
311
|
+
# E.g. https://mybucket.s3.amazonaws.com/myprefix/dev/aaron/mytemplate.yml
|
312
|
+
|
313
|
+
$ cfn-flow upload --release
|
314
|
+
# validates & uploads templates for current git sha
|
315
|
+
# E.g. https://mybucket.s3.amazonaws.com/myprefix/deadbeef/mytemplate.yml
|
316
|
+
|
317
|
+
$ cfn-flow upload --release=v1.0.0
|
318
|
+
# Upload templates for an arbitrary release name
|
319
|
+
# E.g. https://mybucket.s3.amazonaws.com/myprefix/v1.0.0/mytemplate.yml
|
320
|
+
```
|
219
321
|
|
220
|
-
|
322
|
+
## License
|
221
323
|
|
222
|
-
|
324
|
+
Copyright Kickstarter, Inc.
|
223
325
|
|
224
|
-
|
326
|
+
Released under an [MIT License](http://opensource.org/licenses/MIT).
|
data/lib/cfn-flow.rb
CHANGED
@@ -1,139 +1 @@
|
|
1
|
-
require '
|
2
|
-
require 'aws-sdk'
|
3
|
-
require 'multi_json'
|
4
|
-
require 'yaml'
|
5
|
-
require 'erb'
|
6
|
-
|
7
|
-
module CfnFlow
|
8
|
-
class << self
|
9
|
-
|
10
|
-
##
|
11
|
-
# Configuration
|
12
|
-
def config_path
|
13
|
-
ENV['CFN_FLOW_CONFIG_PATH'] || 'cfn-flow.yml'
|
14
|
-
end
|
15
|
-
|
16
|
-
def load_config
|
17
|
-
@config = YAML.load(
|
18
|
-
ERB.new( File.read(config_path) ).result(binding)
|
19
|
-
)
|
20
|
-
# TODO: Validate config?
|
21
|
-
end
|
22
|
-
|
23
|
-
def config_loaded?
|
24
|
-
@config.is_a? Hash
|
25
|
-
end
|
26
|
-
|
27
|
-
def config
|
28
|
-
load_config unless config_loaded?
|
29
|
-
@config
|
30
|
-
end
|
31
|
-
|
32
|
-
def service
|
33
|
-
unless config.key?('service')
|
34
|
-
raise Thor::Error.new("No service name in #{config_path}. Add 'service: my_app_name'.")
|
35
|
-
end
|
36
|
-
config['service']
|
37
|
-
end
|
38
|
-
|
39
|
-
def stack_params(environment)
|
40
|
-
unless config['stack'].is_a? Hash
|
41
|
-
raise Thor::Error.new("No stack defined in #{config_path}. Add 'stack: ...'.")
|
42
|
-
end
|
43
|
-
|
44
|
-
# Dup & symbolize keys
|
45
|
-
params = config['stack'].map{|k,v| [k.to_sym, v]}.to_h
|
46
|
-
|
47
|
-
# Expand params
|
48
|
-
if params[:parameters].is_a? Hash
|
49
|
-
expanded_params = params[:parameters].map do |key,value|
|
50
|
-
{ parameter_key: key, parameter_value: value }
|
51
|
-
end
|
52
|
-
params[:parameters] = expanded_params
|
53
|
-
end
|
54
|
-
|
55
|
-
# Expand tags
|
56
|
-
if params[:tags].is_a? Hash
|
57
|
-
tags = params[:tags].map do |key, value|
|
58
|
-
{key: key, value: value}
|
59
|
-
end
|
60
|
-
|
61
|
-
params[:tags] = tags
|
62
|
-
end
|
63
|
-
|
64
|
-
# Append CfnFlow tags
|
65
|
-
params[:tags] ||= []
|
66
|
-
params[:tags] << { key: 'CfnFlowService', value: service }
|
67
|
-
params[:tags] << { key: 'CfnFlowEnvironment', value: environment }
|
68
|
-
|
69
|
-
# Expand template body
|
70
|
-
if params[:template_body].is_a? String
|
71
|
-
begin
|
72
|
-
body = CfnFlow::Template.new(params[:template_body]).to_json
|
73
|
-
params[:template_body] = body
|
74
|
-
rescue CfnFlow::Template::Error
|
75
|
-
# Do nothing
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
params
|
80
|
-
end
|
81
|
-
|
82
|
-
def template_s3_bucket
|
83
|
-
unless config['templates'].is_a?(Hash) && config['templates']['s3_bucket']
|
84
|
-
raise Thor::Error.new("No s3_bucket defined for templates in #{config_path}. Add 'templates: { s3_bucket: ... }'.")
|
85
|
-
end
|
86
|
-
|
87
|
-
config['templates']['s3_bucket']
|
88
|
-
end
|
89
|
-
|
90
|
-
def template_s3_prefix
|
91
|
-
unless config['templates'].is_a?(Hash)
|
92
|
-
raise Thor::Error.new("No templates defined in #{config_path}. Add 'templates: ... '.")
|
93
|
-
end
|
94
|
-
|
95
|
-
# Ok for this to be ''
|
96
|
-
config['templates']['s3_prefix']
|
97
|
-
end
|
98
|
-
|
99
|
-
##
|
100
|
-
# Aws Clients
|
101
|
-
def cfn_client
|
102
|
-
@cfn_client ||= Aws::CloudFormation::Client.new(region: config[:region] || ENV['AWS_REGION'])
|
103
|
-
end
|
104
|
-
|
105
|
-
def cfn_resource
|
106
|
-
# NB: increase default retry limit to avoid throttling errors iterating over stacks.
|
107
|
-
# See https://github.com/aws/aws-sdk-ruby/issues/705
|
108
|
-
@cfn_resource ||= Aws::CloudFormation::Resource.new(
|
109
|
-
region: config[:region] || ENV['AWS_REGION'],
|
110
|
-
retry_limit: 10
|
111
|
-
)
|
112
|
-
end
|
113
|
-
|
114
|
-
# Clear aws sdk clients & config (for tests)
|
115
|
-
def clear!
|
116
|
-
@config = @cfn_client = @cfn_resource = nil
|
117
|
-
end
|
118
|
-
|
119
|
-
# Exit with status code = 1 when raising a Thor::Error
|
120
|
-
# Override thor default
|
121
|
-
def exit_on_failure?
|
122
|
-
if instance_variable_defined?(:@exit_on_failure)
|
123
|
-
@exit_on_failure
|
124
|
-
else
|
125
|
-
true
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def exit_on_failure=(value)
|
130
|
-
@exit_on_failure = value
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
require 'cfn-flow/template'
|
136
|
-
require 'cfn-flow/git'
|
137
|
-
require 'cfn-flow/event_presenter'
|
138
|
-
require 'cfn-flow/cli'
|
139
|
-
require 'cfn-flow/version'
|
1
|
+
require 'cfn_flow'
|
data/lib/cfn_flow.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'aws-sdk'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'yaml'
|
5
|
+
require 'erb'
|
6
|
+
|
7
|
+
module CfnFlow
|
8
|
+
class << self
|
9
|
+
|
10
|
+
##
|
11
|
+
# Configuration
|
12
|
+
def config_path
|
13
|
+
ENV['CFN_FLOW_CONFIG_PATH'] || 'cfn-flow.yml'
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_config
|
17
|
+
@config = YAML.load(
|
18
|
+
ERB.new( File.read(config_path) ).result(binding)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def config_loaded?
|
23
|
+
@config.is_a? Hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def config
|
27
|
+
load_config unless config_loaded?
|
28
|
+
@config
|
29
|
+
end
|
30
|
+
|
31
|
+
def service
|
32
|
+
unless config.key?('service')
|
33
|
+
raise Thor::Error.new("No service name in #{config_path}. Add 'service: my_app_name'.")
|
34
|
+
end
|
35
|
+
config['service']
|
36
|
+
end
|
37
|
+
|
38
|
+
def stack_params(environment)
|
39
|
+
unless config['stack'].is_a? Hash
|
40
|
+
raise Thor::Error.new("No stack defined in #{config_path}. Add 'stack: ...'.")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Dup & symbolize keys
|
44
|
+
params = config['stack'].map{|k,v| [k.to_sym, v]}.to_h
|
45
|
+
|
46
|
+
# Expand params
|
47
|
+
if params[:parameters].is_a? Hash
|
48
|
+
expanded_params = params[:parameters].map do |key,value|
|
49
|
+
{ parameter_key: key, parameter_value: value }
|
50
|
+
end
|
51
|
+
params[:parameters] = expanded_params
|
52
|
+
end
|
53
|
+
|
54
|
+
# Expand tags
|
55
|
+
if params[:tags].is_a? Hash
|
56
|
+
tags = params[:tags].map do |key, value|
|
57
|
+
{key: key, value: value}
|
58
|
+
end
|
59
|
+
|
60
|
+
params[:tags] = tags
|
61
|
+
end
|
62
|
+
|
63
|
+
# Append CfnFlow tags
|
64
|
+
params[:tags] ||= []
|
65
|
+
params[:tags] << { key: 'CfnFlowService', value: service }
|
66
|
+
params[:tags] << { key: 'CfnFlowEnvironment', value: environment }
|
67
|
+
|
68
|
+
# Expand template body
|
69
|
+
if params[:template_body].is_a? String
|
70
|
+
begin
|
71
|
+
body = CfnFlow::Template.new(params[:template_body]).to_json
|
72
|
+
params[:template_body] = body
|
73
|
+
rescue CfnFlow::Template::Error
|
74
|
+
# Do nothing
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
params
|
79
|
+
end
|
80
|
+
|
81
|
+
def template_s3_bucket
|
82
|
+
unless config['templates'].is_a?(Hash) && config['templates']['s3_bucket']
|
83
|
+
raise Thor::Error.new("No s3_bucket defined for templates in #{config_path}. Add 'templates: { s3_bucket: ... }'.")
|
84
|
+
end
|
85
|
+
|
86
|
+
config['templates']['s3_bucket']
|
87
|
+
end
|
88
|
+
|
89
|
+
def template_s3_prefix
|
90
|
+
unless config['templates'].is_a?(Hash)
|
91
|
+
raise Thor::Error.new("No templates defined in #{config_path}. Add 'templates: ... '.")
|
92
|
+
end
|
93
|
+
|
94
|
+
# Ok for this to be ''
|
95
|
+
config['templates']['s3_prefix']
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Aws Clients
|
100
|
+
def cfn_client
|
101
|
+
@cfn_client ||= Aws::CloudFormation::Client.new(region: config['region'] || ENV['AWS_REGION'])
|
102
|
+
end
|
103
|
+
|
104
|
+
def cfn_resource
|
105
|
+
# NB: increase default retry limit to avoid throttling errors iterating over stacks.
|
106
|
+
# See https://github.com/aws/aws-sdk-ruby/issues/705
|
107
|
+
@cfn_resource ||= Aws::CloudFormation::Resource.new(
|
108
|
+
region: config['region'] || ENV['AWS_REGION'],
|
109
|
+
retry_limit: 10
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Clear aws sdk clients & config (for tests)
|
114
|
+
def clear!
|
115
|
+
@config = @cfn_client = @cfn_resource = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
# Exit with status code = 1 when raising a Thor::Error
|
119
|
+
# Override thor default
|
120
|
+
def exit_on_failure?
|
121
|
+
if instance_variable_defined?(:@exit_on_failure)
|
122
|
+
@exit_on_failure
|
123
|
+
else
|
124
|
+
true
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def exit_on_failure=(value)
|
129
|
+
@exit_on_failure = value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
require 'cfn_flow/template'
|
135
|
+
require 'cfn_flow/git'
|
136
|
+
require 'cfn_flow/event_presenter'
|
137
|
+
require 'cfn_flow/cli'
|
138
|
+
require 'cfn_flow/version'
|
@@ -28,7 +28,7 @@ module CfnFlow
|
|
28
28
|
|
29
29
|
desc 'publish TEMPLATE [...]', 'Validate & upload templates'
|
30
30
|
method_option 'dev-name', type: :string, desc: 'Personal development prefix'
|
31
|
-
method_option :release, type: :string, desc: 'Upload release', lazy_default:
|
31
|
+
method_option :release, type: :string, desc: 'Upload release', lazy_default: true
|
32
32
|
method_option :verbose, type: :boolean, desc: 'Verbose output', default: false
|
33
33
|
def publish(*templates)
|
34
34
|
if templates.empty?
|
@@ -36,8 +36,6 @@ module CfnFlow
|
|
36
36
|
end
|
37
37
|
|
38
38
|
validate(*templates)
|
39
|
-
# TODO: check git is clean before releasing
|
40
|
-
#CfnFlow::Git.check_status if options['release']
|
41
39
|
|
42
40
|
release = publish_release
|
43
41
|
templates.each do |path|
|
@@ -95,12 +93,12 @@ module CfnFlow
|
|
95
93
|
|
96
94
|
return if stacks.empty?
|
97
95
|
|
98
|
-
table_header = options['no-header'] ? [] : [['NAME', 'ENVIRONMENT', 'STATUS']]
|
96
|
+
table_header = options['no-header'] ? [] : [['NAME', 'ENVIRONMENT', 'STATUS', 'CREATED']]
|
99
97
|
table_data = stacks.map do |s|
|
100
98
|
env_tag = s.tags.detect {|tag| tag.key == 'CfnFlowEnvironment'}
|
101
99
|
env = env_tag ? env_tag.value : 'NONE'
|
102
100
|
|
103
|
-
[ s.name, env, s.stack_status ]
|
101
|
+
[ s.name, env, s.stack_status, s.creation_time ]
|
104
102
|
end
|
105
103
|
|
106
104
|
print_table(table_header + table_data)
|
@@ -143,6 +141,14 @@ module CfnFlow
|
|
143
141
|
end
|
144
142
|
end
|
145
143
|
|
144
|
+
##
|
145
|
+
# Version command
|
146
|
+
desc "version", "Prints the version information"
|
147
|
+
def version
|
148
|
+
say CfnFlow::VERSION
|
149
|
+
end
|
150
|
+
map %w(-v --version) => :version
|
151
|
+
|
146
152
|
private
|
147
153
|
def find_stack_in_service(name)
|
148
154
|
stack = CfnFlow.cfn_resource.stack(name).load
|
@@ -164,7 +170,8 @@ module CfnFlow
|
|
164
170
|
def publish_release
|
165
171
|
# Add the release or dev name to the prefix
|
166
172
|
if options[:release]
|
167
|
-
|
173
|
+
release = options[:release] == true ? CfnFlow::Git.sha : options[:release]
|
174
|
+
'release/' + release
|
168
175
|
elsif options['dev-name']
|
169
176
|
'dev/' + options['dev-name']
|
170
177
|
elsif ENV['CFN_FLOW_DEV_NAME']
|
File without changes
|
File without changes
|
File without changes
|
@@ -68,10 +68,18 @@ describe 'CfnFlow::CLI' do
|
|
68
68
|
out.must_match("dev/#{name}")
|
69
69
|
end
|
70
70
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
71
|
+
describe 'with --release' do
|
72
|
+
it 'defaults to git sha' do
|
73
|
+
sha = CfnFlow::Git.sha
|
74
|
+
out, _ = capture_io { cli.start [:publish, template, '--release'] }
|
75
|
+
out.must_match CfnFlow::Template.new(template).url("release/#{sha}")
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'can take a value' do
|
79
|
+
release = 'v2.0'
|
80
|
+
out, _ = capture_io { cli.start [:publish, template, '--release', release] }
|
81
|
+
out.must_match CfnFlow::Template.new(template).url("release/#{release}")
|
82
|
+
end
|
75
83
|
end
|
76
84
|
|
77
85
|
it 'can fail with malformed templates' do
|
@@ -182,13 +190,13 @@ describe 'CfnFlow::CLI' do
|
|
182
190
|
end
|
183
191
|
it 'should print the stack' do
|
184
192
|
out, err = capture_io { cli.start [:list] }
|
185
|
-
out.must_match(/mystack\s+production\s+CREATE_COMPLETE/)
|
193
|
+
out.must_match(/mystack\s+production\s+CREATE_COMPLETE\s+#{memo_now.utc}/)
|
186
194
|
err.must_equal ''
|
187
195
|
end
|
188
196
|
|
189
197
|
it 'should print the header' do
|
190
198
|
out, _ = capture_io { cli.start [:list] }
|
191
|
-
out.must_match(/NAME\s+ENVIRONMENT\s+STATUS/)
|
199
|
+
out.must_match(/NAME\s+ENVIRONMENT\s+STATUS\s+CREATED/)
|
192
200
|
end
|
193
201
|
|
194
202
|
it 'should print stacks when passed an environment' do
|
@@ -213,7 +221,7 @@ describe 'CfnFlow::CLI' do
|
|
213
221
|
stacks: [
|
214
222
|
{ stack_name: "mystack",
|
215
223
|
stack_status: 'CREATE_COMPLETE',
|
216
|
-
creation_time:
|
224
|
+
creation_time: memo_now,
|
217
225
|
tags: [
|
218
226
|
{key: 'CfnFlowService', value: 'none-such-service'},
|
219
227
|
{key: 'CfnFlowEnvironment', value: 'production'}
|
@@ -380,4 +388,22 @@ describe 'CfnFlow::CLI' do
|
|
380
388
|
end
|
381
389
|
end
|
382
390
|
|
391
|
+
describe '#version' do
|
392
|
+
let(:version) { CfnFlow::VERSION + "\n" }
|
393
|
+
it 'prints the version' do
|
394
|
+
out, _ = capture_io { cli.start [:version] }
|
395
|
+
out.must_equal version
|
396
|
+
end
|
397
|
+
|
398
|
+
it 'handles -v argument' do
|
399
|
+
out, _ = capture_io { cli.start ['-v'] }
|
400
|
+
out.must_equal version
|
401
|
+
end
|
402
|
+
|
403
|
+
it 'handles --version argument' do
|
404
|
+
out, _ = capture_io { cli.start ['--version'] }
|
405
|
+
out.must_equal version
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|
383
409
|
end
|
File without changes
|
File without changes
|
@@ -135,7 +135,7 @@ describe 'CfnFlow' do
|
|
135
135
|
|
136
136
|
it 'can be overridden with config' do
|
137
137
|
ENV['AWS_REGION'] = 'env-region'
|
138
|
-
subject.instance_variable_set(:@config, {region
|
138
|
+
subject.instance_variable_set(:@config, {'region' => 'config-region' })
|
139
139
|
subject.cfn_client.config.region.must_equal 'config-region'
|
140
140
|
end
|
141
141
|
end
|
@@ -159,7 +159,7 @@ describe 'CfnFlow' do
|
|
159
159
|
|
160
160
|
it 'can be overridden with config' do
|
161
161
|
ENV['AWS_REGION'] = 'env-region'
|
162
|
-
subject.instance_variable_set(:@config, {region
|
162
|
+
subject.instance_variable_set(:@config, {'region' => 'config-region' })
|
163
163
|
subject.cfn_client.config.region.must_equal 'config-region'
|
164
164
|
end
|
165
165
|
end
|
data/spec/data/cfn-flow.yml
CHANGED
data/spec/helper.rb
CHANGED
@@ -38,11 +38,15 @@ class Minitest::Spec
|
|
38
38
|
Aws.config.delete(:cloudformation)
|
39
39
|
end
|
40
40
|
|
41
|
+
def memo_now
|
42
|
+
@now = Time.now
|
43
|
+
end
|
44
|
+
|
41
45
|
def stub_stack_data(attrs = {})
|
42
46
|
{
|
43
47
|
stack_name: "mystack",
|
44
48
|
stack_status: 'CREATE_COMPLETE',
|
45
|
-
creation_time:
|
49
|
+
creation_time: memo_now,
|
46
50
|
tags: [
|
47
51
|
{key: 'CfnFlowService', value: CfnFlow.service},
|
48
52
|
{key: 'CfnFlowEnvironment', value: 'production'}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cfn-flow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Suggs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -92,15 +92,16 @@ files:
|
|
92
92
|
- bin/cfn-flow
|
93
93
|
- bin/rake
|
94
94
|
- lib/cfn-flow.rb
|
95
|
-
- lib/
|
96
|
-
- lib/
|
97
|
-
- lib/
|
98
|
-
- lib/
|
99
|
-
- lib/
|
100
|
-
-
|
101
|
-
- spec/
|
102
|
-
- spec/
|
103
|
-
- spec/
|
95
|
+
- lib/cfn_flow.rb
|
96
|
+
- lib/cfn_flow/cli.rb
|
97
|
+
- lib/cfn_flow/event_presenter.rb
|
98
|
+
- lib/cfn_flow/git.rb
|
99
|
+
- lib/cfn_flow/template.rb
|
100
|
+
- lib/cfn_flow/version.rb
|
101
|
+
- spec/cfn_flow/cli_spec.rb
|
102
|
+
- spec/cfn_flow/event_presenter_spec.rb
|
103
|
+
- spec/cfn_flow/template_spec.rb
|
104
|
+
- spec/cfn_flow_spec.rb
|
104
105
|
- spec/data/cfn-flow.yml
|
105
106
|
- spec/data/invalid.json
|
106
107
|
- spec/data/invalid.yml
|
@@ -133,10 +134,10 @@ signing_key:
|
|
133
134
|
specification_version: 4
|
134
135
|
summary: A CLI for CloudFormation templates
|
135
136
|
test_files:
|
136
|
-
- spec/
|
137
|
-
- spec/
|
138
|
-
- spec/
|
139
|
-
- spec/
|
137
|
+
- spec/cfn_flow/cli_spec.rb
|
138
|
+
- spec/cfn_flow/event_presenter_spec.rb
|
139
|
+
- spec/cfn_flow/template_spec.rb
|
140
|
+
- spec/cfn_flow_spec.rb
|
140
141
|
- spec/data/cfn-flow.yml
|
141
142
|
- spec/data/invalid.json
|
142
143
|
- spec/data/invalid.yml
|
data/lib/cfn-flow/version.rb
DELETED