class Moonshot::Stack
The Stack
wraps all CloudFormation actions performed by Moonshot
. It stores the state of the active stack running on AWS, but contains a reference to the StackTemplate
that would be applied with an update action.
Attributes
app_name[R]
name[R]
Public Class Methods
new(config) { |config| ... }
click to toggle source
# File lib/moonshot/stack.rb, line 24 def initialize(config) @config = config @ilog = config.interactive_logger @name = [@config.app_name, @config.environment_name].join('-') yield @config if block_given? end
Public Instance Methods
create()
click to toggle source
# File lib/moonshot/stack.rb, line 32 def create should_wait = true @ilog.start "Creating #{stack_name}." do |s| if stack_exists? s.success "#{stack_name} already exists." should_wait = false else create_stack s.success "Created #{stack_name}." end end should_wait ? wait_for_stack_state(:stack_create_complete, 'created') : true end
default_values()
click to toggle source
Return a Hash of the default values defined in the stack template.
# File lib/moonshot/stack.rb, line 145 def default_values h = {} template.parameters.each do |p| h[p.name] = h.default end h end
delete()
click to toggle source
# File lib/moonshot/stack.rb, line 65 def delete should_wait = true @ilog.start "Deleting #{stack_name}." do |s| if stack_exists? cf_client.delete_stack(stack_name: @name) s.success "Initiated deletion of #{stack_name}." else s.success "#{stack_name} does not exist." should_wait = false end end should_wait ? wait_for_stack_state(:stack_delete_complete, 'deleted') : true end
exists?()
click to toggle source
# File lib/moonshot/stack.rb, line 107 def exists? cf_client.describe_stacks(stack_name: @name) true rescue Aws::CloudFormation::Errors::ValidationError false end
Also aliased as: stack_exists?
outputs()
click to toggle source
# File lib/moonshot/stack.rb, line 100 def outputs get_stack(@name) .outputs .map { |o| [o.output_key, o.output_value] } .to_h end
overrides()
click to toggle source
Build a hash of overrides that would be applied to this stack by an update.
# File lib/moonshot/stack.rb, line 136 def overrides if File.exist?(parameters_file) YAML.load_file(parameters_file) || {} else {} end end
parameters()
click to toggle source
# File lib/moonshot/stack.rb, line 93 def parameters get_stack(@name) .parameters .map { |p| [p.parameter_key, p.parameter_value] } .to_h end
parameters_file()
click to toggle source
@return [String] the path to the parameters file.
# File lib/moonshot/stack.rb, line 171 def parameters_file File.join(@config.project_root, 'cloud_formation', 'parameters', "#{@name}.yml") end
physical_id_for(logical_id)
click to toggle source
@return [String, nil]
# File lib/moonshot/stack.rb, line 120 def physical_id_for(logical_id) resource_summary = resource_summaries.find do |r| r.logical_resource_id == logical_id end resource_summary.physical_resource_id if resource_summary end
resource_summaries()
click to toggle source
# File lib/moonshot/stack.rb, line 115 def resource_summaries cf_client.list_stack_resources(stack_name: @name).stack_resource_summaries end
resources_of_type(type)
click to toggle source
@return [Array<Aws::CloudFormation::Types::StackResourceSummary>]
# File lib/moonshot/stack.rb, line 128 def resources_of_type(type) resource_summaries.select do |r| r.resource_type == type end end
status()
click to toggle source
# File lib/moonshot/stack.rb, line 80 def status if exists? puts "#{stack_name} exists." t = UnicodeTable.new('') StackParameterPrinter.new(self, t).print StackOutputPrinter.new(self, t).print StackASGPrinter.new(self, t).print t.draw_children else puts "#{stack_name} does NOT exist." end end
template()
click to toggle source
# File lib/moonshot/stack.rb, line 153 def template @template ||= load_template_file end
template_file()
click to toggle source
@return [String] the path to the template file.
# File lib/moonshot/stack.rb, line 158 def template_file json = json_template_path yaml = yaml_template_path @template_file ||= Dir[json].first || Dir[yaml].first raise 'CloudFormation template not found at'\ "#{json} or #{yaml}!" unless @template_file @template_file end
update()
click to toggle source
# File lib/moonshot/stack.rb, line 47 def update raise "No stack found #{@name.blue}!" unless stack_exists? should_wait = true @ilog.start "Updating #{stack_name}." do |s| if update_stack s.success "Initiated update for #{stack_name}." else s.success 'No Stack update required.' should_wait = false end end success = should_wait ? wait_for_stack_state(:stack_update_complete, 'updated') : true raise 'Failed to update the CloudFormation Stack.' unless success success end
Private Instance Methods
create_stack()
click to toggle source
# File lib/moonshot/stack.rb, line 223 def create_stack cf_client.create_stack( stack_name: @name, template_body: template.body, capabilities: ['CAPABILITY_IAM'], parameters: @config.parameters.values.map(&:to_cf), tags: make_tags ) rescue Aws::CloudFormation::Errors::AccessDenied raise 'You are not authorized to perform create_stack calls.' end
doctor_check_template_against_aws()
click to toggle source
# File lib/moonshot/stack.rb, line 332 def doctor_check_template_against_aws cf_client.validate_template(template_body: template.body) success('CloudFormation template is valid.') rescue => e critical('Invalid CloudFormation template!', e.message) end
doctor_check_template_exists()
click to toggle source
# File lib/moonshot/stack.rb, line 324 def doctor_check_template_exists if File.exist?(template_file) success "CloudFormation template found at '#{template_file}'." else critical "CloudFormation template not found at '#{template_file}'!" end end
format_event(event)
click to toggle source
# File lib/moonshot/stack.rb, line 309 def format_event(event) str = case event.resource_status when /FAILED/ event.resource_status.red when /IN_PROGRESS/ event.resource_status.yellow else event.resource_status.green end str << " #{event.logical_resource_id}" str << " #{event.resource_status_reason.light_black}" if event.resource_status_reason str end
get_stack(name)
click to toggle source
@return [Aws::CloudFormation::Types::Stack]
# File lib/moonshot/stack.rb, line 214 def get_stack(name) stacks = cf_client.describe_stacks(stack_name: name).stacks raise "Could not describe stack: #{name}" if stacks.empty? stacks.first rescue Aws::CloudFormation::Errors::ValidationError raise "Could not describe stack: #{name}" end
json_template_path()
click to toggle source
# File lib/moonshot/stack.rb, line 181 def json_template_path "#{raw_template_file_name}.json" end
load_template_file()
click to toggle source
# File lib/moonshot/stack.rb, line 195 def load_template_file json_template = JsonStackTemplate.new(json_template_path) yaml_template = YamlStackTemplate.new(yaml_template_path) case when json_template.exist? json_template when yaml_template.exist? yaml_template else raise "CloudFormation template not found at #{json_template_path} "\ "or #{yaml_template_path}!" unless @template_file end end
raw_template_file_name()
click to toggle source
@return [String] the path to the template file without extension.
# File lib/moonshot/stack.rb, line 190 def raw_template_file_name @raw_template_file_name ||= File.join(@config.project_root, 'cloud_formation', @config.app_name) end
stack_name()
click to toggle source
# File lib/moonshot/stack.rb, line 177 def stack_name "CloudFormation Stack #{@name.blue}" end
stack_parameters()
click to toggle source
# File lib/moonshot/stack.rb, line 209 def stack_parameters template.parameters.map(&:name) end
update_stack()
click to toggle source
@return [Boolean]
true if a stack update was required and initiated, false otherwise.
# File lib/moonshot/stack.rb, line 237 def update_stack cf_client.update_stack( stack_name: @name, template_body: template.body, capabilities: ['CAPABILITY_IAM'], parameters: @config.parameters.values.map(&:to_cf) ) true rescue Aws::CloudFormation::Errors::ValidationError => e raise e.message unless e.message == 'No updates are to be performed.' false end
wait_for_stack_state(wait_target, past_tense_verb)
click to toggle source
TODO: Refactor this into it's own class.
# File lib/moonshot/stack.rb, line 252 def wait_for_stack_state(wait_target, past_tense_verb) result = true stack_id = get_stack(@name).stack_id events = StackEventsPoller.new(cf_client, stack_id) events.show_only_errors unless @config.show_all_stack_events @ilog.start_threaded "Waiting for #{stack_name} to be successfully #{past_tense_verb}." do |s| begin cf_client.wait_until(wait_target, stack_name: stack_id) do |w| w.delay = 10 w.max_attempts = 180 # 30 minutes. w.before_wait do |attempt, resp| begin events.latest_events.each { |e| @ilog.error(format_event(e)) } # rubocop:disable Lint/HandleExceptions rescue Aws::CloudFormation::Errors::ValidationError # Do nothing. The above event logging block may result in # a ValidationError while waiting for a stack to delete. end # rubocop:enable Lint/HandleExceptions if attempt == w.max_attempts - 1 s.failure "#{stack_name} was not #{past_tense_verb} after 30 minutes." result = false # We don't want the interactive logger to catch an exception. throw :success end s.continue "Waiting for CloudFormation Stack to be successfully #{past_tense_verb}, current status '#{resp.stacks.first.stack_status}'." # rubocop:disable LineLength end end s.success "#{stack_name} successfully #{past_tense_verb}." if result rescue Aws::Waiters::Errors::FailureStateError result = false s.failure "#{stack_name} failed to update." end end result end
yaml_template_path()
click to toggle source
# File lib/moonshot/stack.rb, line 185 def yaml_template_path "#{raw_template_file_name}.yml" end