bora 1.0.1 → 1.1.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 +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
|