cfn-flow 0.7.0 → 0.8.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 +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