class Humidifier::Stack

Represents a CFN stack

Constants

AWS_REGION

The AWS region, can be set through the environment, defaults to us-east-1

ENUMERABLE_RESOURCES

Lists of objects linked to the stack

MAX_TEMPLATE_BODY_SIZE

The maximum size a template body can be before it has to be put somewhere and referenced through a URL

MAX_TEMPLATE_URL_SIZE

The maximum size a template body can be inside of an S3 bucket

MAX_WAIT

The maximum amount of time that Humidifier should wait for a stack to complete a CRUD operation

STATIC_RESOURCES

Single settings on the stack

Attributes

client[W]
default_identifier[R]
id[RW]
name[R]

Public Class Methods

new(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 68
def initialize(opts = {})
  @name = opts[:name]
  @id = opts[:id]
  @default_identifier = self.class.next_default_identifier

  ENUMERABLE_RESOURCES.each_value do |property|
    instance_variable_set(:"@#{property}", opts.fetch(property, {}))
  end

  STATIC_RESOURCES.each_value do |property|
    instance_variable_set(:"@#{property}", opts[property])
  end
end
next_default_identifier() click to toggle source
# File lib/humidifier/stack.rb, line 212
def self.next_default_identifier
  @count ||= 0
  @count += 1
  "humidifier-stack-template-#{@count}"
end

Public Instance Methods

add(name, resource, attributes = {}) click to toggle source
# File lib/humidifier/stack.rb, line 82
def add(name, resource, attributes = {})
  resources[name] = resource
  resource.update_attributes(attributes) if attributes.any?
  resource
end
add_condition(name, opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 88
def add_condition(name, opts = {})
  conditions[name] = Condition.new(opts)
end
add_mapping(name, opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 92
def add_mapping(name, opts = {})
  mappings[name] = Mapping.new(opts)
end
add_output(name, opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 96
def add_output(name, opts = {})
  outputs[name] = Output.new(opts)
end
add_parameter(name, opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 100
def add_parameter(name, opts = {})
  parameters[name] = Parameter.new(opts)
end
client() click to toggle source
# File lib/humidifier/stack.rb, line 104
def client
  @client ||= Aws::CloudFormation::Client.new(region: AWS_REGION)
end
create(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 121
def create(opts = {})
  params = { stack_name: name }.merge!(template_for(opts)).merge!(opts)

  try_valid do
    client.create_stack(params).tap { |response| @id = response.stack_id }
  end
end
create_and_wait(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 129
def create_and_wait(opts = {})
  perform_and_wait(:create, opts)
end
create_change_set(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 133
def create_change_set(opts = {})
  raise NoResourcesError.new(self, :change) unless resources.any?

  params = {
    stack_name: identifier,
    change_set_name: "changeset-#{Time.now.strftime('%Y-%m-%d-%H-%M-%S')}"
  }
  params.merge!(template_for(opts)).merge!(opts)

  try_valid { client.create_change_set(params) }
end
delete(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 145
def delete(opts = {})
  client.delete_stack({ stack_name: identifier }.merge!(opts))
  true
end
delete_and_wait(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 150
def delete_and_wait(opts = {})
  perform_and_wait(:delete, opts)
end
deploy(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 154
def deploy(opts = {})
  raise NoResourcesError.new(self, :deploy) unless resources.any?

  exists? ? update(opts) : create(opts)
end
deploy_and_wait(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 160
def deploy_and_wait(opts = {})
  perform_and_wait(exists? ? :update : :create, opts)
end
deploy_change_set(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 164
def deploy_change_set(opts = {})
  exists? ? create_change_set(opts) : create(opts)
end
exists?() click to toggle source
# File lib/humidifier/stack.rb, line 168
def exists?
  Aws::CloudFormation::Stack.new(name: identifier).exists?
end
identifier() click to toggle source
# File lib/humidifier/stack.rb, line 108
def identifier
  id || name || default_identifier
end
to_cf(serializer = :json) click to toggle source
# File lib/humidifier/stack.rb, line 112
def to_cf(serializer = :json)
  resources = static_resources.merge!(enumerable_resources)

  case serializer
  when :json then JSON.pretty_generate(resources)
  when :yaml then YAML.dump(resources)
  end
end
update(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 172
def update(opts = {})
  params = {
    capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM],
    stack_name: identifier
  }

  params.merge!(template_for(opts)).merge!(opts)

  try_valid { client.update_stack(params) }
end
update_and_wait(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 183
def update_and_wait(opts = {})
  perform_and_wait(:update, opts)
end
upload() click to toggle source
# File lib/humidifier/stack.rb, line 187
def upload
  raise NoResourcesError.new(self, :upload) unless resources.any?

  bucket = Humidifier.config.s3_bucket
  raise UploadNotConfiguredError, identifier unless bucket

  Aws.config.update(region: AWS_REGION)
  key = "#{Humidifier.config.s3_prefix}#{identifier}.json"

  Aws::S3::Client.new.put_object(body: to_cf, bucket: bucket, key: key)
  Aws::S3::Object.new(bucket, key).presigned_url(:get)
end
valid?(opts = {}) click to toggle source
# File lib/humidifier/stack.rb, line 200
    def valid?(opts = {})
      params = template_for(opts).merge!(opts)

      try_valid { client.validate_template(params) }
    rescue Aws::CloudFormation::Errors::AccessDenied
      raise Error, <<~MSG
        The authenticated AWS profile does not have the requisite permissions
        to run this command. Ensure the profile has the
        "cloudformation:ValidateTemplate" IAM permission.
      MSG
    end

Private Instance Methods

bytesize() click to toggle source
# File lib/humidifier/stack.rb, line 222
def bytesize
  to_cf.bytesize.tap do |size|
    raise TemplateTooLargeError, size if size > MAX_TEMPLATE_URL_SIZE
  end
end
enumerable_resources() click to toggle source
# File lib/humidifier/stack.rb, line 228
def enumerable_resources
  ENUMERABLE_RESOURCES.each_with_object({}) do |(name, prop), list|
    resources = public_send(prop)
    next if resources.empty?

    list[name] =
      resources.to_h do |resource_name, resource|
        [resource_name, resource.to_cf]
      end
  end
end
perform_and_wait(method, opts) click to toggle source
# File lib/humidifier/stack.rb, line 240
def perform_and_wait(method, opts)
  public_send(method, opts).tap do
    signal = :"stack_#{method}_complete"

    client.wait_until(signal, stack_name: identifier) do |waiter|
      waiter.max_attempts = (opts.delete(:max_wait) || MAX_WAIT) / 5
      waiter.delay = 5
    end
  end
end
static_resources() click to toggle source
# File lib/humidifier/stack.rb, line 251
def static_resources
  STATIC_RESOURCES.each_with_object({}) do |(name, prop), list|
    resource = public_send(prop)
    list[name] = resource if resource
  end
end
template_for(opts) click to toggle source
# File lib/humidifier/stack.rb, line 258
def template_for(opts)
  @template ||=
    if opts.delete(:force_upload) ||
       Humidifier.config.force_upload ||
       bytesize > MAX_TEMPLATE_BODY_SIZE

      { template_url: upload }
    else
      { template_body: to_cf }
    end
end
try_valid() { ||| true| ... } click to toggle source
# File lib/humidifier/stack.rb, line 270
def try_valid
  yield || true
rescue Aws::CloudFormation::Errors::ValidationError => error
  warn(error.message)
  warn(error.backtrace)
  false
end