class Stacker::Stack

Constants

CLIENT_METHODS
SAFE_UPDATE_POLICY
STATUS_COMPLETE_REGEX

Attributes

name[R]
options[R]
region[R]

Public Class Methods

new(region, name, options = {}) click to toggle source
# File lib/stacker/stack.rb, line 48
def initialize region, name, options = {}
  @region, @name, @options = region, name, options
end

Public Instance Methods

capabilities() click to toggle source
# File lib/stacker/stack.rb, line 86
def capabilities
  @capabilities ||= Capabilities.new self
end
client() click to toggle source
# File lib/stacker/stack.rb, line 52
def client
  res = region.client.describe_stacks(stack_name: name)
  res.stacks.first
rescue Aws::CloudFormation::Errors::ValidationError
  nil
end
create(blocking = true) click to toggle source
# File lib/stacker/stack.rb, line 99
def create blocking = true
  if exists?
    Stacker.logger.warn 'Stack already exists'
    return
  end

  if parameters.missing.any?
    raise MissingParameters.new(
      "Required parameters missing: #{parameters.missing.join ', '}"
    )
  end

  Stacker.logger.info 'Creating stack'

  params = parameters.resolved.map do |k, v|
    {
      parameter_key: k,
      parameter_value: v
    }
  end

  region.client.create_stack(
    stack_name: name,
    template_body: template.local_raw,
    parameters: params,
    capabilities: capabilities.local
  )

  wait_until_complete if blocking
rescue Aws::CloudFormation::Errors::ValidationError, ArgumentError => err
  raise Error.new err.message
end
describe_change_set() click to toggle source
# File lib/stacker/stack.rb, line 173
def describe_change_set
  retries = 6
  changes = []
  while changes.empty?
    resp = region.client.describe_change_set(
      change_set_name: change_set,
      stack_name: name
    )
    changes = resp.changes
    if changes.empty?
      raise CannotDescribeChangeSet.new 'Empty change set' if retries == 0
      retries -= 1
      sleep 1
    end
  end
  changes.map do |c|
    rc = c.resource_change
    {
      type: c.type,
      change: {
        logical_resource_id: rc.logical_resource_id,
        action: rc.action,
        replacement: rc.replacement,
      }
    }
  end
end
exists?() click to toggle source
# File lib/stacker/stack.rb, line 59
def exists?
  !!client
end
outputs() click to toggle source
# File lib/stacker/stack.rb, line 90
def outputs
  @outputs ||= begin
    return {} unless complete?
    Hash[client.outputs.map do |output|
           [ output.output_key, output.output_value ]
         end]
  end
end
parameters() click to toggle source
# File lib/stacker/stack.rb, line 82
def parameters
  @parameters ||= Parameters.new self
end
pretty_change_set() click to toggle source
# File lib/stacker/stack.rb, line 202
def pretty_change_set
  riw = describe_change_set.map do |c|
    c[:change][:logical_resource_id].length
  end.max
  fmt = "%-6s %-#{riw}s %-5s"

  ([fmt % ['Action', 'Resource', 'Replacement?'], '='*(riw+20)] +
    describe_change_set.map do |c|
    change = c[:change]
    fmt % [
      change[:action],
      change[:logical_resource_id],
      change[:replacement]
    ]
  end).join("\n")
end
status() click to toggle source
# File lib/stacker/stack.rb, line 63
def status
  if client
    client.stack_status
  else
    "#{name}:\nStack with id #{name} does not exist"
  end
end
template() click to toggle source
# File lib/stacker/stack.rb, line 78
def template
  @template ||= Template.new self
end
update(options = {}) click to toggle source
# File lib/stacker/stack.rb, line 132
def update options = {}
  options.assert_valid_keys(:blocking, :allow_destructive)

  blocking = options.fetch(:blocking, true)
  allow_destructive = options.fetch(:allow_destructive, false)

  if parameters.missing.any?
    raise MissingParameters.new(
      "Required parameters missing: #{parameters.missing.join ', '}"
    )
  end

  Stacker.logger.info 'Updating stack'

  unless allow_destructive
    raise StackPolicyError if describe_change_set.any? do |c|
      c[:change][:replacement] == 'True' ||
        c[:change][:action] =~ /remove/i
    end
  end

  region.client.execute_change_set(
    change_set_name: change_set,
    stack_name: name
  )

  if blocking
    sleep 4 # Wait a bit for the stack to begin updating
    wait_until_complete
  end
rescue Aws::CloudFormation::Errors::ValidationError => err
  case err.message
  when /does not exist/
    raise DoesNotExistError.new err.message
  when /No updates/
    raise UpToDateError.new err.message
  else
    raise Error.new err.message
  end
end

Private Instance Methods

change_set() click to toggle source
# File lib/stacker/stack.rb, line 226
def change_set
  change_set_name.tap do |csname|
    region.client.create_change_set(
      stack_name: name,
      template_body: template.local_raw,
      parameters: parameters.resolved.map do |k, v|
        {
          parameter_key: k,
          parameter_value: v
        }
      end,
      capabilities: capabilities.local,
      change_set_name: csname
    )
  end
rescue Aws::CloudFormation::Errors::ValidationError => err
  raise Error.new err.message
end
change_set_name() click to toggle source
# File lib/stacker/stack.rb, line 221
def change_set_name
  "stacker-#{SecureRandom.hex}"
end
report_status() click to toggle source
# File lib/stacker/stack.rb, line 246
def report_status
  case status
  when /_COMPLETE$/
    Stacker.logger.info "#{name} Status => #{status}"
  when /_ROLLBACK_IN_PROGRESS$/
    failure_event = client.events.enum(limit: 30).find do |event|
      event.resource_status =~ /_FAILED$/
    end
    failure_reason = failure_event.resource_status_reason
    if failure_reason =~ /stack policy/
      raise StackPolicyError.new failure_reason
    else
      Stacker.logger.fatal "#{name} Status => #{status}"
      raise Error.new "Failure Reason: #{failure_reason}"
    end
  else
    Stacker.logger.debug "#{name} Status => #{status}"
  end
end
wait_until_complete() click to toggle source
# File lib/stacker/stack.rb, line 266
def wait_until_complete
  while !(status =~ STATUS_COMPLETE_REGEX)
    report_status
    sleep 5
  end
  report_status
end