bora 1.0.1 → 1.1.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 +103 -24
- data/bora.gemspec +2 -0
- data/lib/bora.rb +16 -2
- data/lib/bora/cfn/stack.rb +5 -2
- data/lib/bora/cfn/stack_status.rb +3 -1
- data/lib/bora/cli.rb +18 -5
- data/lib/bora/parameter_resolver.rb +48 -0
- data/lib/bora/parameter_resolver_loader.rb +31 -0
- data/lib/bora/resolver/cfn.rb +40 -0
- data/lib/bora/resolver/credstash.rb +41 -0
- data/lib/bora/resolver/hostedzone.rb +38 -0
- data/lib/bora/stack.rb +61 -56
- data/lib/bora/template.rb +8 -7
- data/lib/bora/version.rb +1 -1
- metadata +22 -4
- data/lib/bora/cfn_param_resolver.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97bb5e174fb739e5d972f9e5de97fb3cace2719c
|
4
|
+
data.tar.gz: 680167b8916dc55560d6ad7558529bf036dd4d06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 47ec7127e767d1c7f6a95df782b9ee8b62021e7843076852523324d1950f3cab0a60e2ea6e9eefa486dd0ed49a7376241dcba7756f1e279523a1257ff3fae6b9
|
7
|
+
data.tar.gz: e420921e96864718b6826e720fff544af5868b132032c5bc9ec74f0535f45a4e74326abcf88a12c6e950b4ced2c60889960a00c8297d0f646a78ef6abf07d847
|
data/README.md
CHANGED
@@ -15,6 +15,8 @@ Given this config, Bora then provides commands (or Rake tasks) to work with thos
|
|
15
15
|
|
16
16
|
## Installation
|
17
17
|
|
18
|
+
This gem requires Ruby 2.1 or greater.
|
19
|
+
|
18
20
|
If you're using Bundler, add this line to your application's `Gemfile`:
|
19
21
|
|
20
22
|
```ruby
|
@@ -63,6 +65,10 @@ To get a full list of available tasks run `rake -T`.
|
|
63
65
|
The example below is a `bora.yml` file showing all available options:
|
64
66
|
|
65
67
|
```yaml
|
68
|
+
# Optional. The default region for all stacks in the file.
|
69
|
+
# See below for further information.
|
70
|
+
default_region: us-east-1
|
71
|
+
|
66
72
|
# A map defining all the CloudFormation templates available.
|
67
73
|
# A "template" is effectively a single CloudFormation JSON (or cfndsl template).
|
68
74
|
templates:
|
@@ -75,6 +81,11 @@ templates:
|
|
75
81
|
# (see CloudFormation docs for more details)
|
76
82
|
capabilities: [CAPABILITY_IAM]
|
77
83
|
|
84
|
+
# Optional. The default region for all stacks in this template.
|
85
|
+
# Overrides "default_region" at the global level.
|
86
|
+
# See below for further information.
|
87
|
+
default_region: us-west-2
|
88
|
+
|
78
89
|
# A map defining all the "stacks" associated with this template
|
79
90
|
# for example, "uat" and "prod"
|
80
91
|
stacks:
|
@@ -92,6 +103,12 @@ templates:
|
|
92
103
|
# name concatenated with the stack name as defined in this file,
|
93
104
|
# eg: "app-prod".
|
94
105
|
stack_name: prod-application-stack
|
106
|
+
|
107
|
+
# Optional. Default region for this stack.
|
108
|
+
# Overrides "default_region" at the template level.
|
109
|
+
# See below for further information.
|
110
|
+
default_region: ap-southeast-2
|
111
|
+
|
95
112
|
params:
|
96
113
|
InstanceType: m4.xlarge
|
97
114
|
AMI: ami-11032472
|
@@ -120,7 +137,7 @@ templates:
|
|
120
137
|
|
121
138
|
# You can refer to outputs of other stacks using "${}" notation too.
|
122
139
|
# See below for further details.
|
123
|
-
app_url: http://${app-uat/outputs/Domain}/api
|
140
|
+
app_url: http://${cfn://app-uat/outputs/Domain}/api
|
124
141
|
|
125
142
|
# Traditional CloudFormation parameters
|
126
143
|
InstanceType: t2.micro
|
@@ -129,27 +146,6 @@ templates:
|
|
129
146
|
prod: {}
|
130
147
|
```
|
131
148
|
|
132
|
-
## Parameter Substitution
|
133
|
-
|
134
|
-
Bora supports looking up parameter values from other stacks and interpolating them into input parameters.
|
135
|
-
At present you can only look up the outputs of other stacks,
|
136
|
-
however in the future it may support looking up stack parameters or resources.
|
137
|
-
Other future possibilities include looking up values from other services,
|
138
|
-
for example AMI IDs.
|
139
|
-
|
140
|
-
The format is as follows:
|
141
|
-
|
142
|
-
`${<stack_name>/outputs/<output_name>}`
|
143
|
-
|
144
|
-
For example:
|
145
|
-
```yaml
|
146
|
-
params:
|
147
|
-
api_url: http://${api-stack/outputs/Domain}/api
|
148
|
-
```
|
149
|
-
|
150
|
-
This will look up the `Domain` output from the stack named `api-stack` and substitute it into the `api_url` parameter.
|
151
|
-
|
152
|
-
|
153
149
|
## Command Reference
|
154
150
|
|
155
151
|
The following commands are available through the command line and rake tasks.
|
@@ -167,7 +163,7 @@ The following commands are available through the command line and rake tasks.
|
|
167
163
|
* **validate** - Validates the template using the AWS CloudFormation "validate" API call
|
168
164
|
|
169
165
|
|
170
|
-
|
166
|
+
### Command Line
|
171
167
|
|
172
168
|
Run `bora help` to see all available commands.
|
173
169
|
|
@@ -175,7 +171,7 @@ Run `bora help` to see all available commands.
|
|
175
171
|
eg: `bora help apply`.
|
176
172
|
|
177
173
|
|
178
|
-
|
174
|
+
### Rake Tasks
|
179
175
|
|
180
176
|
To use the rake tasks, simply put this in your `Rakefile`:
|
181
177
|
```ruby
|
@@ -186,6 +182,88 @@ Bora.new.rake_tasks
|
|
186
182
|
To get a full list of available tasks run `rake -T`.
|
187
183
|
|
188
184
|
|
185
|
+
## Specifying Regions
|
186
|
+
You can specify the region in which to create a stack in a few ways.
|
187
|
+
The order of precedence is as follows (first non-empty value found wins):
|
188
|
+
|
189
|
+
- The `--region` parameter on the command line (only available in the CLI, not in the Rake tasks)
|
190
|
+
- The `default_region` setting within the stack section in `bora.yml`
|
191
|
+
- The `default_region` setting within the template section in `bora.yml`
|
192
|
+
- The `default_region` setting at the top level of `bora.yml`
|
193
|
+
- The [default region as determined by the AWS Ruby SDK](https://docs.aws.amazon.com/sdkforruby/api/index.html).
|
194
|
+
|
195
|
+
|
196
|
+
## Parameter Substitution
|
197
|
+
|
198
|
+
Bora supports looking up parameter values from various locations and interpolating them into stack parameters.
|
199
|
+
This is useful so that you don't have to hard-code values into your stack parameters that may change across regions or over time.
|
200
|
+
For example, you might have a VPC template that creates a subnet and returns the subnet ID as a stack output.
|
201
|
+
You could then have an application template that creates an EC2 instance in that subnet,
|
202
|
+
with the subnet ID parameter looked up dynamically from the VPC stack.
|
203
|
+
|
204
|
+
These lookup parameters are specified using `${}` syntax within the parameter value,
|
205
|
+
and the lookup target is a URI.
|
206
|
+
|
207
|
+
For example:
|
208
|
+
|
209
|
+
```yaml
|
210
|
+
params:
|
211
|
+
api_url: http://${cfn://api-stack/outputs/Domain}/api
|
212
|
+
```
|
213
|
+
|
214
|
+
This will look up the `Domain` output from the stack named `api-stack` and substitute it into the `api_url` parameter.
|
215
|
+
The URI "scheme" (`cfn` in the above example) controls which resolver will handle the lookup.
|
216
|
+
The format of the rest of the URI is dependent on the resolver.
|
217
|
+
|
218
|
+
There are a number of resolvers that come with Bora (documented below),
|
219
|
+
or you can write your own.
|
220
|
+
|
221
|
+
|
222
|
+
### Stack Output Lookup
|
223
|
+
|
224
|
+
You can look up outputs from stacks in the same region.
|
225
|
+
|
226
|
+
For example:
|
227
|
+
```bash
|
228
|
+
# Look up output "MyOutput" from stack "my-stack" in the same region as the current stack.
|
229
|
+
${cfn://my-stack/outputs/MyOutput}
|
230
|
+
|
231
|
+
# Look up an output from a stack in another region
|
232
|
+
${cfn://my-stack.ap-southeast-2/outputs/MyOutput}
|
233
|
+
```
|
234
|
+
|
235
|
+
|
236
|
+
### CredStash Key Lookup
|
237
|
+
[CredStash](https://github.com/fugue/credstash) is a utility for storing secrets using AWS KMS.
|
238
|
+
You can pass these secrets as parameters to your stack.
|
239
|
+
If you do so, you should use a CloudFormation parameter with the ["NoEcho" flag](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html) to true,
|
240
|
+
so as to not expose the secret in the template.
|
241
|
+
|
242
|
+
For example:
|
243
|
+
```bash
|
244
|
+
# Simple key lookup in same region as the stack. Note 3 slashes. Will run `credstash get mykey`.
|
245
|
+
${credstash:///mykey}
|
246
|
+
|
247
|
+
# Lookup with a key context. Will run `credstash get mykey app=webapp`.
|
248
|
+
${credstash:///mykey?app=webapp}
|
249
|
+
|
250
|
+
# Lookup a credstash in another region.
|
251
|
+
${credstash://ap-southeast-2/mykey?app=webapp}
|
252
|
+
```
|
253
|
+
|
254
|
+
|
255
|
+
### Route53 Hosted Zone ID Lookup
|
256
|
+
Looks up the Route53 hosted zone ID given a hosted zone name (eg: example.com).
|
257
|
+
Also allows you to specify if you want the private or public hosted zone for a given name,
|
258
|
+
which can be useful if you have set up split-view DNS with both public and private zones for the same name.
|
259
|
+
|
260
|
+
```bash
|
261
|
+
${hostedzone://example.com}
|
262
|
+
${hostedzone://example.com/public}
|
263
|
+
${hostedzone://example.com/private}
|
264
|
+
```
|
265
|
+
|
266
|
+
|
189
267
|
## Overriding Stack Parameters from the Command Line
|
190
268
|
|
191
269
|
Some commands accept a list of parameters that will override those defined in the YAML file.
|
@@ -205,6 +283,7 @@ $ rake web-uat:apply[instance_type=t2.micro,ami=ami-11032472]
|
|
205
283
|
## Related Projects
|
206
284
|
The following projects provided inspiration for Bora:
|
207
285
|
* [CfnDsl](https://github.com/stevenjack/cfndsl) - A Ruby DSL for CloudFormation templates
|
286
|
+
* [StackMaster](https://github.com/envato/stack_master) - Very similar in goals to Bora
|
208
287
|
* [CloudFormer](https://github.com/kunday/cloudformer) - Rake tasks for CloudFormation
|
209
288
|
* [Cumulus](https://github.com/cotdsa/cumulus) - A Python YAML based tool for working with CloudFormation
|
210
289
|
|
data/bora.gemspec
CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.bindir = "exe"
|
17
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
18
|
spec.require_paths = ["lib"]
|
19
|
+
spec.required_ruby_version = '>= 2.1.0'
|
19
20
|
|
20
21
|
spec.add_dependency "aws-sdk", "~> 2.0"
|
21
22
|
spec.add_dependency "cfndsl", "~> 0.4"
|
@@ -26,4 +27,5 @@ Gem::Specification.new do |spec|
|
|
26
27
|
|
27
28
|
spec.add_development_dependency "bundler", "~> 1.11"
|
28
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
spec.add_development_dependency "simplecov", "~> 0.12"
|
29
31
|
end
|
data/lib/bora.rb
CHANGED
@@ -6,13 +6,16 @@ require "bora/tasks"
|
|
6
6
|
|
7
7
|
class Bora
|
8
8
|
DEFAULT_CONFIG_FILE = "bora.yml"
|
9
|
+
INHERITABLE_PROPERTIES = ["default_region"]
|
9
10
|
|
10
|
-
def initialize(config_file_or_hash: DEFAULT_CONFIG_FILE, colorize: true)
|
11
|
+
def initialize(config_file_or_hash: DEFAULT_CONFIG_FILE, override_config: {}, colorize: true)
|
11
12
|
@templates = {}
|
12
13
|
config = load_config(config_file_or_hash)
|
13
14
|
String.disable_colorization = !colorize
|
15
|
+
raise "No templates defined" if !config['templates']
|
14
16
|
config['templates'].each do |template_name, template_config|
|
15
|
-
|
17
|
+
resolved_config = resolve_template_config(config, template_config, override_config)
|
18
|
+
@templates[template_name] = Template.new(template_name, resolved_config, override_config)
|
16
19
|
end
|
17
20
|
end
|
18
21
|
|
@@ -44,4 +47,15 @@ class Bora
|
|
44
47
|
end
|
45
48
|
end
|
46
49
|
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def resolve_template_config(bora_config, template_config, override_config)
|
54
|
+
inheritable_properties(bora_config).merge(template_config).merge(inheritable_properties(override_config))
|
55
|
+
end
|
56
|
+
|
57
|
+
def inheritable_properties(config)
|
58
|
+
config.select { |k| INHERITABLE_PROPERTIES.include?(k) }
|
59
|
+
end
|
60
|
+
|
47
61
|
end
|
data/lib/bora/cfn/stack.rb
CHANGED
@@ -12,8 +12,9 @@ class Bora
|
|
12
12
|
class Stack
|
13
13
|
NO_UPDATE_MESSAGE = "No updates are to be performed"
|
14
14
|
|
15
|
-
def initialize(stack_name)
|
15
|
+
def initialize(stack_name, region = nil)
|
16
16
|
@stack_name = stack_name
|
17
|
+
@region = region
|
17
18
|
@processed_events = Set.new
|
18
19
|
end
|
19
20
|
|
@@ -88,7 +89,9 @@ class Bora
|
|
88
89
|
private
|
89
90
|
|
90
91
|
def cloudformation
|
91
|
-
@cfn ||=
|
92
|
+
@cfn ||= begin
|
93
|
+
@region ? Aws::CloudFormation::Client.new(region: @region) : Aws::CloudFormation::Client.new
|
94
|
+
end
|
92
95
|
end
|
93
96
|
|
94
97
|
def method_missing(sym, *args, &block)
|
@@ -4,6 +4,8 @@ class Bora
|
|
4
4
|
module Cfn
|
5
5
|
|
6
6
|
class StackStatus
|
7
|
+
DOES_NOT_EXIST_MESSAGE = "Stack does not exist"
|
8
|
+
|
7
9
|
def initialize(underlying_stack)
|
8
10
|
@stack = underlying_stack
|
9
11
|
if @stack
|
@@ -24,7 +26,7 @@ class Bora
|
|
24
26
|
status_reason = @stack.stack_status_reason ? " - #{@stack.stack_status_reason}" : ""
|
25
27
|
"#{@stack.stack_name} - #{@status}#{status_reason}"
|
26
28
|
else
|
27
|
-
|
29
|
+
DOES_NOT_EXIST_MESSAGE
|
28
30
|
end
|
29
31
|
end
|
30
32
|
end
|
data/lib/bora/cli.rb
CHANGED
@@ -3,7 +3,17 @@ require "bora"
|
|
3
3
|
|
4
4
|
class Bora
|
5
5
|
class Cli < Thor
|
6
|
-
class_option :file,
|
6
|
+
class_option :file,
|
7
|
+
type: :string,
|
8
|
+
aliases: :f,
|
9
|
+
default: Bora::DEFAULT_CONFIG_FILE,
|
10
|
+
desc: "The Bora config file to use"
|
11
|
+
|
12
|
+
class_option :region,
|
13
|
+
type: :string,
|
14
|
+
aliases: :r,
|
15
|
+
default: nil,
|
16
|
+
desc: "The region to use for the stack operation. Overrides any regions specified in the Bora config file."
|
7
17
|
|
8
18
|
desc "list", "Lists the available stacks"
|
9
19
|
def list
|
@@ -15,8 +25,9 @@ class Bora
|
|
15
25
|
|
16
26
|
desc "apply STACK_NAME", "Creates or updates the stack"
|
17
27
|
option :params, type: :array, aliases: :p, desc: "Parameters to be passed to the template, eg: --params 'instance_type=t2.micro'"
|
28
|
+
option :pretty, type: :boolean, default: false, desc: "Send pretty (formatted) JSON to AWS (only works for cfndsl templates)"
|
18
29
|
def apply(stack_name)
|
19
|
-
stack(options.file, stack_name).apply(params)
|
30
|
+
stack(options.file, stack_name).apply(params, options.pretty)
|
20
31
|
end
|
21
32
|
|
22
33
|
desc "delete STACK_NAME", "Deletes the stack"
|
@@ -72,7 +83,9 @@ class Bora
|
|
72
83
|
private
|
73
84
|
|
74
85
|
def stack(config_file, stack_name)
|
75
|
-
|
86
|
+
region = options.region
|
87
|
+
override_config = region ? {"default_region" => region} : {}
|
88
|
+
bora = bora(config_file, override_config)
|
76
89
|
stack = bora.stack(stack_name)
|
77
90
|
if !stack
|
78
91
|
STDERR.puts "Could not find stack #{stack_name}"
|
@@ -81,8 +94,8 @@ class Bora
|
|
81
94
|
stack
|
82
95
|
end
|
83
96
|
|
84
|
-
def bora(config_file)
|
85
|
-
Bora.new(config_file_or_hash: config_file)
|
97
|
+
def bora(config_file, override_config = {})
|
98
|
+
Bora.new(config_file_or_hash: config_file, override_config: override_config)
|
86
99
|
end
|
87
100
|
|
88
101
|
def params
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'bora/parameter_resolver_loader'
|
3
|
+
|
4
|
+
class Bora
|
5
|
+
class ParameterResolver
|
6
|
+
def initialize(stack)
|
7
|
+
@stack = stack
|
8
|
+
@loader = ParameterResolverLoader.new
|
9
|
+
@resolver_cache = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def resolve(params)
|
13
|
+
params.map { |k, v| [k, process_param_substitutions(v)] }.to_h
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def process_param_substitutions(val)
|
20
|
+
return val unless val.is_a? String
|
21
|
+
old_val = nil
|
22
|
+
while old_val != val
|
23
|
+
old_val = val
|
24
|
+
val = val.sub(/\${[^}]+}/) do |m|
|
25
|
+
token = m[2..-2]
|
26
|
+
uri = parse_uri(token)
|
27
|
+
resolver_name = uri.scheme
|
28
|
+
resolver = @resolver_cache[resolver_name] || @loader.load_resolver(resolver_name).new(@stack)
|
29
|
+
resolver.resolve(uri)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
val
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_uri(s)
|
36
|
+
uri = URI(s)
|
37
|
+
|
38
|
+
# Support for legacy CFN substitutions without a scheme, eg: ${stack/outputs/foo}.
|
39
|
+
# Will be removed in next breaking version.
|
40
|
+
if !uri.scheme && uri.path && uri.path.count("/") == 2
|
41
|
+
uri = URI("cfn://#{s}")
|
42
|
+
end
|
43
|
+
|
44
|
+
uri
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Bora
|
2
|
+
class ParameterResolverLoader
|
3
|
+
ResolverNotFound = Class.new(StandardError)
|
4
|
+
|
5
|
+
def load_resolver(name)
|
6
|
+
resolver_class = name.split("_").reject(&:empty?).map { |s| s.capitalize }.join
|
7
|
+
class_name = "Bora::Resolver::#{resolver_class}"
|
8
|
+
begin
|
9
|
+
resolver_class = Kernel.const_get(class_name)
|
10
|
+
rescue NameError
|
11
|
+
require_resolver_file(name)
|
12
|
+
resolver_class = Kernel.const_get(class_name)
|
13
|
+
end
|
14
|
+
resolver_class
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def require_resolver_file(name)
|
21
|
+
require_path = "bora/resolver/#{name}"
|
22
|
+
begin
|
23
|
+
require require_path
|
24
|
+
rescue LoadError
|
25
|
+
raise ResolverNotFound, "Could not find resolver for '#{name}'. Expected to find it at '#{require_path}'"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'bora/cfn/stack'
|
2
|
+
|
3
|
+
class Bora
|
4
|
+
module Resolver
|
5
|
+
class Cfn
|
6
|
+
StackDoesNotExist = Class.new(StandardError)
|
7
|
+
ValueNotFound = Class.new(StandardError)
|
8
|
+
InvalidParameter = Class.new(StandardError)
|
9
|
+
|
10
|
+
def initialize(stack)
|
11
|
+
@stack = stack
|
12
|
+
@stack_cache = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve(uri)
|
16
|
+
stack_name = uri.host
|
17
|
+
section, name = uri.path.split("/").reject(&:empty?)
|
18
|
+
if !stack_name || !section || !name || section != 'outputs'
|
19
|
+
raise InvalidParameter, "Invalid parameter substitution: #{uri}"
|
20
|
+
end
|
21
|
+
|
22
|
+
stack_name, uri_region = stack_name.split(".")
|
23
|
+
region = uri_region || @stack.region
|
24
|
+
|
25
|
+
param_stack = @stack_cache[stack_name] || Bora::Cfn::Stack.new(stack_name, region)
|
26
|
+
if !param_stack.exists?
|
27
|
+
raise StackDoesNotExist, "Output #{name} not found in stack #{stack_name} as the stack does not exist"
|
28
|
+
end
|
29
|
+
|
30
|
+
outputs = param_stack.outputs || []
|
31
|
+
matching_output = outputs.find { |output| output.key == name }
|
32
|
+
if !matching_output
|
33
|
+
raise ValueNotFound, "Output #{name} not found in stack #{stack_name}"
|
34
|
+
end
|
35
|
+
|
36
|
+
matching_output.value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'bora/cfn/stack'
|
3
|
+
|
4
|
+
class Bora
|
5
|
+
module Resolver
|
6
|
+
class Credstash
|
7
|
+
InvalidParameter = Class.new(StandardError)
|
8
|
+
|
9
|
+
def initialize(stack)
|
10
|
+
@stack = stack
|
11
|
+
end
|
12
|
+
|
13
|
+
def resolve(uri)
|
14
|
+
raise InvalidParameter, "Invalid credstash parameter #{uri}: no credstash key" if !uri.path
|
15
|
+
key = uri.path[1..-1]
|
16
|
+
raise InvalidParameter, "Invalid credstash parameter #{uri}: no credstash key" if !key || key.empty?
|
17
|
+
region = resolve_region(uri, @stack)
|
18
|
+
context = parse_key_context(uri)
|
19
|
+
output = `credstash --region #{region} get #{key}#{context}`
|
20
|
+
exit_code = $?
|
21
|
+
raise NotFound, output if exit_code.exitstatus != 0
|
22
|
+
output.rstrip
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def resolve_region(uri, stack)
|
29
|
+
region = uri.host || stack.region || Aws::CloudFormation::Client.new.config[:region]
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_key_context(uri)
|
33
|
+
return "" if !uri.query
|
34
|
+
query = URI::decode_www_form(uri.query).to_h
|
35
|
+
context_params = query.map { |k,v| "#{k}=#{v}" }.join(" ")
|
36
|
+
" #{context_params}"
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'bora/cfn/stack'
|
3
|
+
|
4
|
+
class Bora
|
5
|
+
module Resolver
|
6
|
+
class Hostedzone
|
7
|
+
InvalidParameterError = Class.new(StandardError)
|
8
|
+
NotFoundError = Class.new(StandardError)
|
9
|
+
MultipleMatchesError = Class.new(StandardError)
|
10
|
+
|
11
|
+
def initialize(stack)
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve(uri)
|
15
|
+
zone_name = uri.host
|
16
|
+
zone_type = uri.path[1..-1]
|
17
|
+
raise InvalidParameterError, "Invalid hostedzone parameter #{uri}" if !zone_name
|
18
|
+
zone_name += "."
|
19
|
+
route53 = Aws::Route53::Client.new
|
20
|
+
res = route53.list_hosted_zones
|
21
|
+
zones = res.hosted_zones.select do |hz|
|
22
|
+
hz.name == zone_name && zone_type_matches(zone_type, hz.config.private_zone)
|
23
|
+
end
|
24
|
+
raise NotFoundError, "Could not find hosted zone #{uri}" if !zones || zones.empty?
|
25
|
+
raise MultipleMatchesError, "Multiple candidates for hosted zone #{uri}. Use public/private discrimiator." if zones.size > 1
|
26
|
+
zones[0].id.split("/")[-1]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def zone_type_matches(required_zone_type, is_private_zone)
|
32
|
+
return true if !required_zone_type || required_zone_type.empty?
|
33
|
+
(required_zone_type == "private" && is_private_zone) || (required_zone_type == "public" && !is_private_zone)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/bora/stack.rb
CHANGED
@@ -2,28 +2,39 @@ require 'tempfile'
|
|
2
2
|
require 'colorize'
|
3
3
|
require 'cfndsl'
|
4
4
|
require 'bora/cfn/stack'
|
5
|
-
require 'bora/cfn_param_resolver'
|
6
5
|
require 'bora/stack_tasks'
|
6
|
+
require 'bora/parameter_resolver'
|
7
7
|
|
8
8
|
class Bora
|
9
9
|
class Stack
|
10
|
+
STACK_ACTION_SUCCESS_MESSAGE = "%s stack '%s' completed successfully"
|
11
|
+
STACK_ACTION_FAILURE_MESSAGE = "%s stack '%s' failed"
|
12
|
+
STACK_ACTION_NOT_CHANGED_MESSAGE = "%s stack '%s' skipped as template has not changed"
|
13
|
+
STACK_DOES_NOT_EXIST_MESSAGE = "Stack '%s' does not exist"
|
14
|
+
STACK_EVENTS_DO_NOT_EXIST_MESSAGE = "Stack '%s' has no events"
|
15
|
+
STACK_EVENTS_MESSAGE = "Events for stack '%s'"
|
16
|
+
STACK_OUTPUTS_DO_NOT_EXIST_MESSAGE = "Stack '%s' has no outputs"
|
17
|
+
STACK_VALIDATE_SUCCESS_MESSAGE = "Template for stack '%s' is valid"
|
18
|
+
|
10
19
|
def initialize(stack_name, template_file, stack_config)
|
11
20
|
@stack_name = stack_name
|
12
21
|
@cfn_stack_name = stack_config['stack_name'] || @stack_name
|
13
22
|
@template_file = template_file
|
14
23
|
@stack_config = stack_config
|
24
|
+
@region = @stack_config['default_region']
|
15
25
|
@cfn_options = extract_cfn_options(stack_config)
|
16
|
-
@cfn_stack = Cfn::Stack.new(@cfn_stack_name)
|
26
|
+
@cfn_stack = Cfn::Stack.new(@cfn_stack_name, @region)
|
27
|
+
@resolver = ParameterResolver.new(self)
|
17
28
|
end
|
18
29
|
|
19
|
-
attr_reader :stack_name
|
30
|
+
attr_reader :stack_name, :stack_config, :region
|
20
31
|
|
21
32
|
def rake_tasks
|
22
33
|
StackTasks.new(self)
|
23
34
|
end
|
24
35
|
|
25
|
-
def apply(override_params = {})
|
26
|
-
generate(override_params)
|
36
|
+
def apply(override_params = {}, pretty_json = false)
|
37
|
+
generate(override_params, pretty_json)
|
27
38
|
success = invoke_action(@cfn_stack.exists? ? "update" : "create", @cfn_options)
|
28
39
|
if success
|
29
40
|
outputs = @cfn_stack.outputs
|
@@ -32,6 +43,7 @@ class Bora
|
|
32
43
|
outputs.each { |output| puts output }
|
33
44
|
end
|
34
45
|
end
|
46
|
+
success
|
35
47
|
end
|
36
48
|
|
37
49
|
def delete
|
@@ -47,37 +59,15 @@ class Bora
|
|
47
59
|
events = @cfn_stack.events
|
48
60
|
if events
|
49
61
|
if events.length > 0
|
50
|
-
puts
|
51
|
-
|
62
|
+
puts STACK_EVENTS_MESSAGE % @cfn_stack_name
|
63
|
+
events.each { |e| puts e }
|
52
64
|
else
|
53
|
-
puts
|
65
|
+
puts STACK_EVENTS_DO_NOT_EXIST_MESSAGE % @cfn_stack_name
|
54
66
|
end
|
55
67
|
else
|
56
|
-
puts
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def generate(override_params = {})
|
61
|
-
params = process_params(override_params)
|
62
|
-
if File.extname(@template_file) == ".rb"
|
63
|
-
template_body = run_cfndsl(@template_file, params)
|
64
|
-
template_json = JSON.parse(template_body)
|
65
|
-
if template_json["Parameters"]
|
66
|
-
cfn_param_keys = template_json["Parameters"].keys
|
67
|
-
cfn_params = params.select { |k, v| cfn_param_keys.include?(k) }.map do |k, v|
|
68
|
-
{ parameter_key: k, parameter_value: v }
|
69
|
-
end
|
70
|
-
@cfn_options[:parameters] = cfn_params if !cfn_params.empty?
|
71
|
-
end
|
72
|
-
@cfn_options[:template_body] = template_body
|
73
|
-
else
|
74
|
-
@cfn_options[:template_url] = @template_file
|
75
|
-
if !params.empty?
|
76
|
-
@cfn_options[:parameters] = params.map do |k, v|
|
77
|
-
{ parameter_key: k, parameter_value: v }
|
78
|
-
end
|
79
|
-
end
|
68
|
+
puts STACK_DOES_NOT_EXIST_MESSAGE % @cfn_stack_name
|
80
69
|
end
|
70
|
+
events
|
81
71
|
end
|
82
72
|
|
83
73
|
def outputs
|
@@ -87,11 +77,12 @@ class Bora
|
|
87
77
|
puts "Outputs for stack '#{@cfn_stack_name}'"
|
88
78
|
outputs.each { |output| puts output }
|
89
79
|
else
|
90
|
-
puts
|
80
|
+
puts STACK_OUTPUTS_DO_NOT_EXIST_MESSAGE % @cfn_stack_name
|
91
81
|
end
|
92
82
|
else
|
93
|
-
puts
|
83
|
+
puts STACK_DOES_NOT_EXIST_MESSAGE % @cfn_stack_name
|
94
84
|
end
|
85
|
+
outputs
|
95
86
|
end
|
96
87
|
|
97
88
|
def recreate(override_params = {})
|
@@ -106,7 +97,7 @@ class Bora
|
|
106
97
|
|
107
98
|
def show_current
|
108
99
|
template = @cfn_stack.template
|
109
|
-
puts template ? template :
|
100
|
+
puts template ? template : (STACK_DOES_NOT_EXIST_MESSAGE % @cfn_stack_name)
|
110
101
|
end
|
111
102
|
|
112
103
|
def status
|
@@ -115,32 +106,59 @@ class Bora
|
|
115
106
|
|
116
107
|
def validate(override_params = {})
|
117
108
|
generate(override_params)
|
118
|
-
|
109
|
+
is_valid = @cfn_stack.validate(@cfn_options)
|
110
|
+
puts STACK_VALIDATE_SUCCESS_MESSAGE % @cfn_stack_name if is_valid
|
111
|
+
is_valid
|
119
112
|
end
|
120
113
|
|
121
114
|
|
122
115
|
protected
|
123
116
|
|
117
|
+
def generate(override_params = {}, pretty_json = false)
|
118
|
+
params = process_params(override_params)
|
119
|
+
if File.extname(@template_file) == ".rb"
|
120
|
+
template_body = run_cfndsl(@template_file, params, pretty_json)
|
121
|
+
template_json = JSON.parse(template_body)
|
122
|
+
if template_json["Parameters"]
|
123
|
+
cfn_param_keys = template_json["Parameters"].keys
|
124
|
+
cfn_params = params.select { |k, v| cfn_param_keys.include?(k) }.map do |k, v|
|
125
|
+
{ parameter_key: k, parameter_value: v }
|
126
|
+
end
|
127
|
+
@cfn_options[:parameters] = cfn_params if !cfn_params.empty?
|
128
|
+
end
|
129
|
+
@cfn_options[:template_body] = template_body
|
130
|
+
else
|
131
|
+
@cfn_options[:template_url] = @template_file
|
132
|
+
if !params.empty?
|
133
|
+
@cfn_options[:parameters] = params.map do |k, v|
|
134
|
+
{ parameter_key: k, parameter_value: v }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
124
140
|
def invoke_action(action, *args)
|
125
|
-
|
141
|
+
region_text = @region ? "in region #{@region}" : "in default region"
|
142
|
+
puts "#{action.capitalize} stack '#{@cfn_stack_name}' #{region_text}"
|
126
143
|
success = @cfn_stack.send(action, *args) { |event| puts event }
|
127
144
|
if success
|
128
|
-
puts
|
145
|
+
puts STACK_ACTION_SUCCESS_MESSAGE % [action.capitalize, @cfn_stack_name]
|
129
146
|
else
|
130
147
|
if success == nil
|
131
|
-
puts
|
148
|
+
puts STACK_ACTION_NOT_CHANGED_MESSAGE % [action.capitalize, @cfn_stack_name]
|
132
149
|
else
|
133
|
-
raise(
|
150
|
+
raise(STACK_ACTION_FAILURE_MESSAGE % [action.capitalize, @cfn_stack_name])
|
134
151
|
end
|
135
152
|
end
|
136
153
|
success
|
137
154
|
end
|
138
155
|
|
139
|
-
def run_cfndsl(template_file, params)
|
156
|
+
def run_cfndsl(template_file, params, pretty_json)
|
140
157
|
temp_extras = Tempfile.new(["bora", ".yaml"])
|
141
158
|
temp_extras.write(params.to_yaml)
|
142
159
|
temp_extras.close
|
143
|
-
|
160
|
+
cfndsl_model = CfnDsl.eval_file_with_extras(template_file, [[:yaml, temp_extras.path]])
|
161
|
+
template_body = pretty_json ? JSON.pretty_generate(cfndsl_model) : cfndsl_model.to_json
|
144
162
|
temp_extras.unlink
|
145
163
|
template_body
|
146
164
|
end
|
@@ -148,20 +166,7 @@ class Bora
|
|
148
166
|
def process_params(override_params)
|
149
167
|
params = @stack_config['params'] || {}
|
150
168
|
params.merge!(override_params) if override_params
|
151
|
-
|
152
|
-
end
|
153
|
-
|
154
|
-
def process_param_substitutions(val)
|
155
|
-
return val unless val.is_a? String
|
156
|
-
old_val = nil
|
157
|
-
while old_val != val
|
158
|
-
old_val = val
|
159
|
-
val = val.sub(/\${[^}]+}/) do |m|
|
160
|
-
token = m[2..-2]
|
161
|
-
CfnParamResolver.new(token).resolve
|
162
|
-
end
|
163
|
-
end
|
164
|
-
val
|
169
|
+
@resolver.resolve(params)
|
165
170
|
end
|
166
171
|
|
167
172
|
def extract_cfn_options(config)
|
data/lib/bora/template.rb
CHANGED
@@ -2,13 +2,15 @@ require 'bora/stack'
|
|
2
2
|
|
3
3
|
class Bora
|
4
4
|
class Template
|
5
|
-
|
5
|
+
INHERITABLE_PROPERTIES = ["capabilities", "default_region"]
|
6
|
+
|
7
|
+
def initialize(template_name, template_config, override_config = {})
|
6
8
|
@template_name = template_name
|
7
9
|
@template_config = template_config
|
8
10
|
@stacks = {}
|
9
11
|
template_config['stacks'].each do |stack_name, stack_config|
|
10
12
|
stack_name = "#{template_name}-#{stack_name}"
|
11
|
-
stack_config = resolve_stack_config(template_config, stack_config)
|
13
|
+
stack_config = resolve_stack_config(template_config, stack_config, override_config)
|
12
14
|
@stacks[stack_name] = Stack.new(stack_name, template_config['template_file'], stack_config)
|
13
15
|
end
|
14
16
|
end
|
@@ -28,13 +30,12 @@ class Bora
|
|
28
30
|
|
29
31
|
private
|
30
32
|
|
31
|
-
def resolve_stack_config(template_config, stack_config)
|
32
|
-
|
33
|
+
def resolve_stack_config(template_config, stack_config, override_config)
|
34
|
+
inheritable_properties(template_config).merge(stack_config).merge(inheritable_properties(override_config))
|
33
35
|
end
|
34
36
|
|
35
|
-
def
|
36
|
-
|
37
|
-
config.select { |k| valid_options.include?(k) }
|
37
|
+
def inheritable_properties(config)
|
38
|
+
config.select { |k| INHERITABLE_PROPERTIES.include?(k) }
|
38
39
|
end
|
39
40
|
|
40
41
|
end
|
data/lib/bora/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bora
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Charles Blaxland
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '3.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.12'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.12'
|
125
139
|
description:
|
126
140
|
email:
|
127
141
|
- charles.blaxland@gmail.com
|
@@ -147,8 +161,12 @@ files:
|
|
147
161
|
- lib/bora/cfn/stack.rb
|
148
162
|
- lib/bora/cfn/stack_status.rb
|
149
163
|
- lib/bora/cfn/status.rb
|
150
|
-
- lib/bora/cfn_param_resolver.rb
|
151
164
|
- lib/bora/cli.rb
|
165
|
+
- lib/bora/parameter_resolver.rb
|
166
|
+
- lib/bora/parameter_resolver_loader.rb
|
167
|
+
- lib/bora/resolver/cfn.rb
|
168
|
+
- lib/bora/resolver/credstash.rb
|
169
|
+
- lib/bora/resolver/hostedzone.rb
|
152
170
|
- lib/bora/stack.rb
|
153
171
|
- lib/bora/stack_tasks.rb
|
154
172
|
- lib/bora/tasks.rb
|
@@ -165,7 +183,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
165
183
|
requirements:
|
166
184
|
- - ">="
|
167
185
|
- !ruby/object:Gem::Version
|
168
|
-
version:
|
186
|
+
version: 2.1.0
|
169
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
188
|
requirements:
|
171
189
|
- - ">="
|
@@ -1,30 +0,0 @@
|
|
1
|
-
require 'bora/cfn/stack'
|
2
|
-
|
3
|
-
class Bora
|
4
|
-
class CfnParamResolver
|
5
|
-
def initialize(param)
|
6
|
-
@param = param
|
7
|
-
end
|
8
|
-
|
9
|
-
def resolve
|
10
|
-
stack_name, section, name = @param.split("/")
|
11
|
-
if !stack_name || !section || !name || section != 'outputs'
|
12
|
-
raise "Invalid parameter substitution: #{@param}"
|
13
|
-
end
|
14
|
-
|
15
|
-
stack = Cfn::Stack.new(stack_name)
|
16
|
-
if !stack.exists?
|
17
|
-
raise "Output #{name} not found in stack #{stack_name} as the stack does not exist"
|
18
|
-
end
|
19
|
-
|
20
|
-
outputs = stack.outputs || []
|
21
|
-
matching_output = outputs.find { |output| output.key == name }
|
22
|
-
if !matching_output
|
23
|
-
raise "Output #{name} not found in stack #{stack_name}"
|
24
|
-
end
|
25
|
-
|
26
|
-
matching_output.value
|
27
|
-
end
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|