class KumoKeisei::Stack

Constants

RECOVERABLE_STATUSES
TERMINATED_STATUSES
UNRECOVERABLE_STATUSES
UPDATEABLE_STATUSES

Attributes

env_name[R]
stack_name[R]

Public Class Methods

exists?(app_name, environment_name) click to toggle source
# File lib/kumo_keisei/stack.rb, line 31
def self.exists?(app_name, environment_name)
  self.new(app_name, environment_name).exists?
end
new(app_name, environment_name, options = { confirmation_timeout: 30, waiter_delay: 20, waiter_attempts: 90 }) click to toggle source
# File lib/kumo_keisei/stack.rb, line 35
def initialize(app_name, environment_name, options = { confirmation_timeout: 30, waiter_delay: 20, waiter_attempts: 90 })
  @env_name = environment_name
  @app_name = app_name
  @stack_name = "#{app_name}-#{ environment_name }"
  @confirmation_timeout = options[:confirmation_timeout]
  @waiter_delay = options[:waiter_delay]
  @waiter_attempts = options[:waiter_attempts]
end

Public Instance Methods

apply!(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 44
def apply!(stack_config)
  stack_config.merge!(env_name: @env_name)

  raise UsageError.new('You must provide a :template_path in the stack config hash for an apply! operation') unless stack_config.has_key?(:template_path)

  if updatable?
    update!(stack_config)
  else
    ensure_deleted!
    ConsoleJockey.write_line "Creating your new stack #{@stack_name}"
    create!(stack_config)
  end
end
config(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 81
def config(stack_config)
  raise UsageError.new('You must provide a :config_path in the stack config hash to retrieve the stack\'s config') unless stack_config.has_key?(:config_path)
  environment_config(stack_config).config
end
destroy!() click to toggle source
# File lib/kumo_keisei/stack.rb, line 58
def destroy!
  return if get_stack.nil?

  flash_message "Warning! You are about to delete the CloudFormation Stack #{@stack_name}, enter 'yes' to continue."
  return unless ConsoleJockey.get_confirmation(@confirmation_timeout)
  wait_until_ready(false)
  ensure_deleted!
end
exists?() click to toggle source
# File lib/kumo_keisei/stack.rb, line 77
def exists?
  !get_stack.nil?
end
logical_resource(resource_name) click to toggle source
# File lib/kumo_keisei/stack.rb, line 71
def logical_resource(resource_name)
  response = cloudformation.describe_stack_resource(stack_name: @stack_name, logical_resource_id: resource_name)
  stack_resource = response.stack_resource_detail
  stack_resource.each_pair.reduce({}) {|acc, (k, v)| acc.merge(transform_logical_resource_id(k) => v) }
end
outputs(name) click to toggle source
# File lib/kumo_keisei/stack.rb, line 67
def outputs(name)
  return GetStackOutput.new(get_stack).output(name)
end
params_template_path(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 91
def params_template_path(stack_config)
  stack_config.has_key?(:template_path) ? File.absolute_path(File.join(File.dirname(stack_config[:template_path]), "#{File.basename(stack_config[:template_path], '.*')}.yml.erb")) : nil
end
plain_text_secrets(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 86
def plain_text_secrets(stack_config)
  raise UsageError.new('You must provide a :config_path in the stack config hash to retrieve the stack\'s plain_text_secrets') unless stack_config.has_key?(:config_path)
  environment_config(stack_config).plain_text_secrets
end

Private Instance Methods

cf_params(stack_config, environment_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 97
def cf_params(stack_config, environment_config)
  erb = params_template_erb(stack_config)
  return [] unless erb

  stack_params = YAML.load(erb.result(environment_config.get_binding))
  KumoKeisei::ParameterBuilder.new(stack_params).params
end
cloudformation() click to toggle source
# File lib/kumo_keisei/stack.rb, line 124
def cloudformation
  @cloudformation ||= Aws::CloudFormation::Client.new
end
create!(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 154
def create!(stack_config)

  cloudformation.create_stack(
    stack_name: @stack_name,
    template_body: File.read(stack_config[:template_path]),
    parameters: cf_params(stack_config, environment_config(stack_config)),
    capabilities: ["CAPABILITY_IAM"],
    on_failure: "DELETE"
  )

  begin
    cloudformation.wait_until(:stack_create_complete, stack_name: @stack_name) { |waiter| waiter.delay = @waiter_delay; waiter.max_attempts = @waiter_attempts }
  rescue Aws::Waiters::Errors::UnexpectedError => ex
    handle_unexpected_error(ex)
  end
end
ensure_deleted!() click to toggle source
# File lib/kumo_keisei/stack.rb, line 128
def ensure_deleted!
  stack = get_stack
  return if stack.nil?
  return if TERMINATED_STATUSES.include? stack.stack_status

  cloudformation.delete_stack(stack_name: @stack_name)
  cloudformation.wait_until(:stack_delete_complete, stack_name: @stack_name) { |waiter| waiter.delay = @waiter_delay; waiter.max_attempts = @waiter_attempts }
end
environment_config(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 191
def environment_config(stack_config)
  KumoConfig::EnvironmentConfig.new(stack_config)
end
flash_message(message) click to toggle source
# File lib/kumo_keisei/stack.rb, line 234
def flash_message(message)
  ConsoleJockey.flash_message(message)
end
get_stack(options={}) click to toggle source
# File lib/kumo_keisei/stack.rb, line 116
def get_stack(options={})
  @stack = nil if options[:dump_cache]

  @stack ||= cloudformation.describe_stacks(stack_name: @stack_name).stacks.find { |stack| stack.stack_name == @stack_name }
rescue Aws::CloudFormation::Errors::ValidationError
  nil
end
handle_unexpected_error(error) click to toggle source
# File lib/kumo_keisei/stack.rb, line 225
def handle_unexpected_error(error)
  if error.message =~ /does not exist/
    ConsoleJockey.write_line "There was an error during stack creation for #{@stack_name}, and the stack has been cleaned up."
    raise CreateError.new("There was an error during stack creation. The stack has been deleted.")
  else
    raise error
  end
end
params_template_erb(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 105
def params_template_erb(stack_config)
  template_path = params_template_path(stack_config)

  return nil unless template_path && File.exist?(template_path)
  ERB.new(File.read(template_path))
end
stack_events_url() click to toggle source
# File lib/kumo_keisei/stack.rb, line 195
def stack_events_url
  "https://console.aws.amazon.com/cloudformation/home?region=#{ENV['AWS_DEFAULT_REGION']}#/stacks?filter=active&tab=events&stackId=#{get_stack.stack_id}"
end
stack_operation_failed?(last_event_status) click to toggle source
# File lib/kumo_keisei/stack.rb, line 221
def stack_operation_failed?(last_event_status)
  last_event_status =~ /ROLLBACK/
end
stack_ready?(last_event_status) click to toggle source
# File lib/kumo_keisei/stack.rb, line 217
def stack_ready?(last_event_status)
  last_event_status =~ /COMPLETE/ || last_event_status =~ /ROLLBACK_FAILED/
end
transform_logical_resource_id(id) click to toggle source
# File lib/kumo_keisei/stack.rb, line 112
def transform_logical_resource_id(id)
  id.to_s.split('_').map {|w| w.capitalize }.join
end
updatable?() click to toggle source
# File lib/kumo_keisei/stack.rb, line 137
def updatable?
  stack = get_stack
  return false if stack.nil?

  return true if UPDATEABLE_STATUSES.include? stack.stack_status

  return false if TERMINATED_STATUSES.include? stack.stack_status

  if RECOVERABLE_STATUSES.include? stack.stack_status
    ConsoleJockey.write_line "There's a previous stack called #{@stack_name} that didn't create properly, it will be deleted and rebuilt."
    return false
  end

  raise UpdateError.new("Stack is in an unrecoverable state") if UNRECOVERABLE_STATUSES.include? stack.stack_status
  raise UpdateError.new("Stack is busy, try again soon")
end
update!(stack_config) click to toggle source
# File lib/kumo_keisei/stack.rb, line 171
def update!(stack_config)
  wait_until_ready(false)

  cloudformation.update_stack(
    stack_name: @stack_name,
    template_body: File.read(stack_config[:template_path]),
    parameters: cf_params(stack_config, environment_config(stack_config)),
    capabilities: ["CAPABILITY_IAM"]
  )

  cloudformation.wait_until(:stack_update_complete, stack_name: @stack_name) { |waiter| waiter.delay = @waiter_delay; waiter.max_attempts = @waiter_attempts }
rescue Aws::CloudFormation::Errors::ValidationError => ex
  raise ex unless ex.message == "No updates are to be performed."
  ConsoleJockey.write_line "No changes need to be applied for #{@stack_name}."
rescue Aws::Waiters::Errors::FailureStateError
  ConsoleJockey.write_line "Failed to apply the environment update. The stack has been rolled back. It is still safe to apply updates."
  ConsoleJockey.write_line "Find error details in the AWS CloudFormation console: #{stack_events_url}"
  raise UpdateError.new("Stack update failed for #{@stack_name}.")
end
wait_until_ready(raise_on_error=true) click to toggle source
# File lib/kumo_keisei/stack.rb, line 199
def wait_until_ready(raise_on_error=true)
  loop do
    stack = get_stack(dump_cache: true)

    if stack_ready?(stack.stack_status)
      if raise_on_error && stack_operation_failed?(stack.stack_status)
        raise stack.stack_status
      end

      break
    end
    puts "waiting for #{@stack_name} to be READY, current: #{last_event_status}"
    sleep 10
  end
rescue Aws::CloudFormation::Errors::ValidationError
  nil
end