Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ If this is of interest to you, please have a look at the [GitHub issue](https://
## Related Projects
The following projects provided inspiration for or are similar to Bora.
If Bora doesn't meet your needs, one of these might.
* [CfnDsl](https://github.com/stevenjack/cfndsl) - A Ruby DSL for CloudFormation templates
* [CfnDsl](https://github.com/cfndsl/cfndsl) - A Ruby DSL for CloudFormation templates
* [StackMaster](https://github.com/envato/stack_master) - Very similar in goals to Bora
* [CloudFormer](https://github.com/kunday/cloudformer) - Rake tasks for CloudFormation
* [Cumulus](https://github.com/cotdsa/cumulus) - A Python YAML based tool for working with CloudFormation
Expand Down
10 changes: 4 additions & 6 deletions bora.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# coding: utf-8

lib = File.expand_path('../lib', __FILE__)
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'bora/version'

Expand All @@ -17,7 +15,7 @@ Gem::Specification.new do |spec|
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']
spec.required_ruby_version = '>= 2.2.0'
spec.required_ruby_version = '>= 2.3.0'

spec.add_dependency 'aws-sdk', '~> 2.0'
spec.add_dependency 'cfndsl', '~> 0.4'
Expand All @@ -29,8 +27,8 @@ Gem::Specification.new do |spec|

spec.add_development_dependency 'bundler', '~> 1.7'
spec.add_development_dependency 'hashie', '~> 3.4.6'
spec.add_development_dependency 'pry'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'simplecov', '~> 0.12'
spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'pry'
spec.add_development_dependency 'simplecov', '~> 0.12'
end
1 change: 1 addition & 0 deletions lib/bora.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize(config_file_or_hash: DEFAULT_CONFIG_FILE, override_config: {}, co
config = load_config(config_file_or_hash)
String.disable_colorization = !colorize
raise 'No templates defined' unless config['templates']

config['templates'].each do |template_name, template_config|
resolved_config = resolve_template_config(config, template_config, override_config)
@templates[template_name] = Template.new(template_name, resolved_config, override_config)
Expand Down
1 change: 1 addition & 0 deletions lib/bora/cfn/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def initialize(event)

def respond_to_missing?(method_name, include_private = false)
return false if method_name == :to_ary

super
end

Expand Down
12 changes: 11 additions & 1 deletion lib/bora/cfn/stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,26 @@ def delete(&block)

def events
return unless exists?

events = cloudformation.describe_stack_events(stack_name: underlying_stack.stack_id).stack_events
events.reverse.map { |e| Event.new(e) }
end

def outputs
return unless exists?

underlying_stack.outputs.map { |output| Output.new(output) }
end

def parameters
return unless exists?

underlying_stack.parameters.map { |parameter| Parameter.new(parameter) }
end

def template
return unless exists?

cloudformation.get_template(stack_name: @stack_name).template_body
end

Expand All @@ -86,6 +90,7 @@ def create_change_set(change_set_name, options)
loop do
change_set = ChangeSet.new(cloudformation.describe_change_set(change_set_options))
return change_set if change_set.status_complete?

sleep 5
end
end
Expand All @@ -111,7 +116,7 @@ def execute_change_set(change_set_name, &block)
private

def cloudformation
@cfn ||= begin
@cloudformation ||= begin
options = { retry_limit: 10 }
options[:region] = @region if @region
Aws::CloudFormation::Client.new(options)
Expand All @@ -121,13 +126,15 @@ def cloudformation
def call_cfn_action(action, options = {}, &block)
underlying_stack(refresh: true)
return true if action == :delete_stack && !exists?

@previous_event_time = last_event_time
begin
action_options = { stack_name: @stack_name }.merge(options)
cloudformation.method(action.to_s.downcase).call(action_options)
wait_for_completion(&block)
rescue Aws::CloudFormation::Errors::ValidationError => e
raise e unless e.message.include?(NO_UPDATE_MESSAGE)

return nil
end
(action == :delete_stack && !underlying_stack) || status.success?
Expand All @@ -141,6 +148,7 @@ def wait_for_completion
e.resource_type == 'AWS::CloudFormation::Stack' && e.logical_resource_id == @stack_name && e.status_complete?
end
break if finished

sleep 10
end
underlying_stack(refresh: true)
Expand All @@ -160,6 +168,7 @@ def underlying_stack(refresh: false)

def unprocessed_events
return [] unless underlying_stack

events = cloudformation.describe_stack_events(stack_name: underlying_stack.stack_id).stack_events
unprocessed_events = events.select do |event|
!@processed_events.include?(event.event_id) && @previous_event_time < event.timestamp
Expand All @@ -170,6 +179,7 @@ def unprocessed_events

def last_event_time
return Time.at(0) unless underlying_stack

events = cloudformation.describe_stack_events(stack_name: @stack_name).stack_events
!events.empty? ? events[0].timestamp : Time.at(0)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/bora/cfn/stack_status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def exists?
end

def success?
@status && @status.success?
@status&.success?
end

def to_s
Expand Down
4 changes: 3 additions & 1 deletion lib/bora/cli_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ class Bora
class CliBase < Thor
# Fix for incorrect subcommand help. See https://github.com/erikhuda/thor/issues/261
def self.banner(command, _namespace = nil, subcommand = false)
# rubocop:disable Lint/ShadowedArgument
subcommand = subcommand_prefix
# rubocop:enable Lint/ShadowedArgument
subcommand_str = subcommand ? " #{subcommand}" : ''
"#{basename}#{subcommand_str} #{command.usage}"
end
Expand All @@ -25,7 +27,7 @@ def stack(config_file, stack_name)
bora = bora(config_file, override_config)
stack = bora.stack(stack_name)
unless stack
STDERR.puts "Could not find stack #{stack_name}"
warn "Could not find stack #{stack_name}"
exit(1)
end
stack
Expand Down
14 changes: 5 additions & 9 deletions lib/bora/parameter_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ParameterResolver
# Regular expression that can match placeholders nested to two levels.
# For example it will match: "${foo-${bar}}".
# See https://stackoverflow.com/questions/17759004/how-to-match-string-within-parentheses-nested-in-java
PLACEHOLDER_REGEX = /\${([^{}]*|{[^{}]*})*}/
PLACEHOLDER_REGEX = /\${([^{}]*|{[^{}]*})*}/.freeze

def initialize(stack)
@stack = stack
Expand All @@ -27,9 +27,7 @@ def resolve(params)
placeholders_were_substituted ||= resolved_value != v
params[k] = resolved_value
end
if unresolved_placeholders_still_remain && !placeholders_were_substituted
raise UnresolvedSubstitutionError, "Parameter substitutions could not be resolved:\n#{unresolved_placeholders_as_string(params)}"
end
raise UnresolvedSubstitutionError, "Parameter substitutions could not be resolved:\n#{unresolved_placeholders_as_string(params)}" if unresolved_placeholders_still_remain && !placeholders_were_substituted
end
params
end
Expand Down Expand Up @@ -82,14 +80,12 @@ def unresolved_placeholder?(val)
result
end

def parse_uri(s)
uri = URI(s)
def parse_uri(uri_to_parse)
uri = URI(uri_to_parse)

# Support for legacy CFN substitutions without a scheme, eg: ${stack/outputs/foo}.
# Will be removed in next breaking version.
if !uri.scheme && uri.path && uri.path.count('/') == 2
uri = URI("cfn://#{s}")
end
uri = URI("cfn://#{uri_to_parse}") if !uri.scheme && uri.path && uri.path.count('/') == 2
uri
end

Expand Down
6 changes: 4 additions & 2 deletions lib/bora/resolver/ami.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def resolve(uri)
owners = []
ami_prefix = uri.host
raise InvalidParameter, "Invalid ami parameter #{uri}" unless ami_prefix

if !uri.query.nil? && uri.query.include?('owner')
query = URI.decode_www_form(uri.query).to_h
owners = query['owner'].split(',')
Expand All @@ -27,11 +28,11 @@ def resolve(uri)
owners: owners,
filters: [
{
name: 'name',
name: 'name',
values: [ami_prefix]
},
{
name: 'state',
name: 'state',
values: ['available']
}
]
Expand All @@ -41,6 +42,7 @@ def resolve(uri)
end

raise NoAMI, "No Matching AMI's for prefix #{ami_prefix}" if images.empty?

images.sort! { |a, b| DateTime.parse(a.creation_date) <=> DateTime.parse(b.creation_date) }.last.image_id
end
end
Expand Down
12 changes: 3 additions & 9 deletions lib/bora/resolver/cfn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,17 @@ def initialize(stack)
def resolve(uri)
stack_name = uri.host
section, name = uri.path.split('/').reject(&:empty?)
if !stack_name || !section || !name || section != 'outputs'
raise InvalidParameter, "Invalid parameter substitution: #{uri}"
end
raise InvalidParameter, "Invalid parameter substitution: #{uri}" if !stack_name || !section || !name || section != 'outputs'

stack_name, uri_region = stack_name.split('.')
region = uri_region || @stack.region

param_stack = @stack_cache[stack_name] || Bora::Cfn::Stack.new(stack_name, region)
unless param_stack.exists?
raise StackDoesNotExist, "Output #{name} not found in stack #{stack_name} as the stack does not exist"
end
raise StackDoesNotExist, "Output #{name} not found in stack #{stack_name} as the stack does not exist" unless param_stack.exists?

outputs = param_stack.outputs || []
matching_output = outputs.find { |output| output.key == name }
unless matching_output
raise ValueNotFound, "Output #{name} not found in stack #{stack_name}"
end
raise ValueNotFound, "Output #{name} not found in stack #{stack_name}" unless matching_output

matching_output.value
end
Expand Down
4 changes: 4 additions & 0 deletions lib/bora/resolver/credstash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ def initialize(stack)

def resolve(uri)
raise InvalidParameter, "Invalid credstash parameter #{uri}: no credstash key" unless uri.path

key = uri.path[1..-1]
raise InvalidParameter, "Invalid credstash parameter #{uri}: no credstash key" if !key || key.empty?

region = resolve_region(uri, @stack)
context = parse_key_context(uri)
output = `credstash --region #{region} get #{key}#{context}`
# exit_code = $?
raise NotFound, output unless $CHILD_STATUS.success?

output.rstrip
end

Expand All @@ -32,6 +35,7 @@ def resolve_region(uri, stack)

def parse_key_context(uri)
return '' unless uri.query

query = URI.decode_www_form(uri.query).to_h
context_params = query.map { |k, v| "#{k}=#{v}" }.join(' ')
" #{context_params}"
Expand Down
3 changes: 3 additions & 0 deletions lib/bora/resolver/hostedzone.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def resolve(uri)
zone_name = uri.host
zone_type = uri.path[1..-1]
raise InvalidParameterError, "Invalid hostedzone parameter #{uri}" unless zone_name

zone_name += '.'
route53 = Aws::Route53::Client.new
res = route53.list_hosted_zones
Expand All @@ -22,13 +23,15 @@ def resolve(uri)
end
raise NotFoundError, "Could not find hosted zone #{uri}" if !zones || zones.empty?
raise MultipleMatchesError, "Multiple candidates for hosted zone #{uri}. Use public/private discrimiator." if zones.size > 1

zones[0].id.split('/')[-1]
end

private

def zone_type_matches(required_zone_type, is_private_zone)
return true if !required_zone_type || required_zone_type.empty?

(required_zone_type == 'private' && is_private_zone) || (required_zone_type == 'public' && !is_private_zone)
end
end
Expand Down
20 changes: 7 additions & 13 deletions lib/bora/stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ class Stack
def initialize(stack_name, template_file, stack_config)
@stack_name = stack_name
@cfn_stack_name = stack_config['cfn_stack_name'] || stack_config['stack_name'] || @stack_name
if stack_config['stack_name']
puts "DEPRECATED: The 'stack_name' setting is deprecated. Please use 'cfn_stack_name' instead."
end
puts "DEPRECATED: The 'stack_name' setting is deprecated. Please use 'cfn_stack_name' instead." if stack_config['stack_name']
@template_file = template_file
@stack_config = stack_config
@region = @stack_config['default_region'] || Aws::CloudFormation::Client.new.config[:region]
Expand Down Expand Up @@ -123,7 +121,7 @@ def show(override_params = {})

def show_current
template = current_template
puts template ? template : (STACK_DOES_NOT_EXIST_MESSAGE % @cfn_stack_name)
puts template || (STACK_DOES_NOT_EXIST_MESSAGE % @cfn_stack_name)
end

def status
Expand Down Expand Up @@ -180,6 +178,7 @@ def diff_parameters(cfn_options)
current_params_str = params_as_string(current_params)
new_params_str = params_as_string(new_params)
return unless current_params_str || new_params_str

puts 'Parameters'.colorize(mode: :bold)
puts '----------'
diff = Diffy::Diff.new(current_params_str, new_params_str).to_s(String.disable_colorization ? :text : :color).chomp
Expand All @@ -197,27 +196,21 @@ def template_default_parameters(cfn_options)
template = JSON.parse(cfn_options[:template_body])
if template['Parameters']
params_with_defaults = template['Parameters'].select { |_, v| v['Default'] }
unless params_with_defaults.empty?
params = params_with_defaults.map { |k, v| [k, v['Default']] }.to_h
end
params = params_with_defaults.map { |k, v| [k, v['Default']] }.to_h unless params_with_defaults.empty?
end
params
end

def current_cfn_parameters
params = nil
if @cfn_stack.parameters && !@cfn_stack.parameters.empty?
params = @cfn_stack.parameters.map { |p| [p.key, p.value] }.to_h
end
params = @cfn_stack.parameters.map { |p| [p.key, p.value] }.to_h if @cfn_stack.parameters && !@cfn_stack.parameters.empty?
params
end

def new_bora_parameters(cfn_options)
params = nil
cfn_parameters = cfn_options[:parameters]
if cfn_parameters && !cfn_parameters.empty?
params = cfn_parameters.map { |p| [p[:parameter_key], p[:parameter_value]] }.to_h
end
params = cfn_parameters.map { |p| [p[:parameter_key], p[:parameter_value]] }.to_h if cfn_parameters && !cfn_parameters.empty?
params
end

Expand Down Expand Up @@ -250,6 +243,7 @@ def diff_template(context_lines, cfn_options)
def diff_change_set(cfn_options)
change_set_name = "cs-#{SecureRandom.uuid}"
return unless @cfn_stack.exists?

change_set = @cfn_stack.create_change_set(change_set_name, cfn_options)
@cfn_stack.delete_change_set(change_set_name)
puts 'Changes'.colorize(mode: :bold)
Expand Down
Loading