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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 89c886f84ef125c1287e7da22178e0e05add41f1
4
- data.tar.gz: 95c81333c0ce5d1889917cf93e16edf4e821e337
3
+ metadata.gz: 97bb5e174fb739e5d972f9e5de97fb3cace2719c
4
+ data.tar.gz: 680167b8916dc55560d6ad7558529bf036dd4d06
5
5
  SHA512:
6
- metadata.gz: b6141f199f67513cdfd3e6b6d4758670d9d922f2b7d54c8fc38fc0aa2706688cb4b3eddd4976eca0e81c8fc62388a23c59d4a8352ceef41810523ea231de1fe5
7
- data.tar.gz: dd4a98f29d7d3b62167c8d5f4f0d79de642fd37574b53c725a58e1930f264cece311b4ce9fecb96327edfc2a02456169d6a6d0b3f933315b0f615f8ec32127dd
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
- ## Command Line
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
- ## Rake Tasks
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
- @templates[template_name] = Template.new(template_name, template_config)
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
@@ -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 ||= Aws::CloudFormation::Client.new
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
- "Stack does not exist"
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, type: :string, aliases: :f, default: Bora::DEFAULT_CONFIG_FILE, desc: "The Bora config file to use"
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
- bora = bora(config_file)
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 "Events for stack '#{@cfn_stack_name}'"
51
- @cfn_stack.events.each { |e| puts e }
62
+ puts STACK_EVENTS_MESSAGE % @cfn_stack_name
63
+ events.each { |e| puts e }
52
64
  else
53
- puts "Stack '#{@cfn_stack_name}' has no events"
65
+ puts STACK_EVENTS_DO_NOT_EXIST_MESSAGE % @cfn_stack_name
54
66
  end
55
67
  else
56
- puts "Stack '#{@cfn_stack_name}' does not exist"
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 "Stack '#{@cfn_stack_name}' has no outputs"
80
+ puts STACK_OUTPUTS_DO_NOT_EXIST_MESSAGE % @cfn_stack_name
91
81
  end
92
82
  else
93
- puts "Stack '#{@cfn_stack_name}' does not exist"
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 : "Stack '#{@cfn_stack_name}' does not exist"
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
- puts "Template for stack '#{@cfn_stack_name}' is valid" if @cfn_stack.validate(@cfn_options)
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
- puts "#{action.capitalize} stack '#{@cfn_stack_name}'"
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 "#{action.capitalize} stack '#{@cfn_stack_name}' completed successfully"
145
+ puts STACK_ACTION_SUCCESS_MESSAGE % [action.capitalize, @cfn_stack_name]
129
146
  else
130
147
  if success == nil
131
- puts "#{action.capitalize} stack '#{@cfn_stack_name}' skipped as template has not changed"
148
+ puts STACK_ACTION_NOT_CHANGED_MESSAGE % [action.capitalize, @cfn_stack_name]
132
149
  else
133
- raise("#{action.capitalize} stack '#{@cfn_stack_name}' failed")
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
- template_body = CfnDsl.eval_file_with_extras(template_file, [[:yaml, temp_extras.path]]).to_json
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
- params.map { |k, v| [k, process_param_substitutions(v)] }.to_h
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
- def initialize(template_name, template_config)
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
- extract_cfn_options(template_config).merge(stack_config)
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 extract_cfn_options(config)
36
- valid_options = ["capabilities"]
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
@@ -1,3 +1,3 @@
1
1
  class Bora
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
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.1
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-06-03 00:00:00.000000000 Z
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: '0'
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