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 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