class Stacco::Stack

Public Class Methods

new(stack_bucket) click to toggle source
# File lib/stacco/stack.rb, line 14
def initialize(stack_bucket)
  @bucket = stack_bucket
  @bucket.cache_dir = Pathname.new(ENV['HOME']) + '.config' + 'stacco' + 'stack' + @bucket.name

  @config_object = @bucket.objects['stack.yml']

  aws_creds = self.aws_credentials
  @services = {
    ec2: AWS::EC2.new(aws_creds),
    s3: AWS::S3.new(aws_creds),
    autoscaling: AWS::AutoScaling.new(aws_creds),
    route53: AWS::Route53.new(aws_creds),
    cloudformation: AWS::CloudFormation.new(aws_creds),
    cloudfront: AWS::CloudFront.new(aws_creds),
    rds: AWS::RDS.new(aws_creds),
    iam: AWS::IAM.new(aws_creds)
  }

  @aws_stack = @services[:cloudformation].stacks[self.name]
  @aws_stack.service_registry = @services
end

Public Instance Methods

available_layer_names() click to toggle source
# File lib/stacco/stack.rb, line 246
def available_layer_names
  Stacco::Resources::LayerTemplates.keys
end
available_layers() click to toggle source
# File lib/stacco/stack.rb, line 250
def available_layers
  self.available_layer_names.map{ |layer_name| Stacco::Layer.load(self, layer_name) }
end
aws_credentials() click to toggle source
# File lib/stacco/stack.rb, line 144
def aws_credentials
  Hash[ *(self.config['aws'].map{ |k, v| [k.intern, v] }.flatten) ]
end
aws_status() click to toggle source
# File lib/stacco/stack.rb, line 75
def aws_status
  @aws_stack.status
end
bake_template(opts = {}) { |baked_template_body, parameters| ... } click to toggle source
# File lib/stacco/stack.rb, line 166
def bake_template(opts = {})
  publish_first = opts.delete(:publish)

  baked_template_body = self.cloudformation_template_body

  env_lns = [
    "cat >/etc/environment.local <<EOF",
    self.config.find_all{ |k, v| v.kind_of?(String) }.map{ |(k, v)| "export #{k.to_s.upcase}=\"#{v}\"" },
    self.secrets.find_all{ |k, v| v.kind_of?(String) }.map{ |k, v| "export #{k.to_s.upcase}=\"#{v}\"" },
    "EOF",
    "source /etc/environment.local"
  ].flatten.map{ |ln| ln + "\n" }

  parameters = {
    'IAMKeypairNameVar' => self.iam_keypair_name,
    'MainDBAdminUsernameVar' => self.secrets['db_admin_username'],
    'MainDBAdminPasswordVar' => self.secrets['db_admin_password'],
    'DBSnapshotVar' => (self.config['db_snapshot'] || ""),
    'EnvironmentTypeVar' => self.config['environment'],
    'UserDataEnvironmentVar' => env_lns.join
  }

  (self.config['permit_backoffice_access'] || []).each do |rule_name, (auth_type, auth_opts)|
    case auth_type
    when :ip_range
      parameters["#{rule_name.capitalize}IPRange"] = auth_opts
    end
  end

  scaling_groups = self.config['scale']
  self.enabled_layer_names.each do |layer_name|
    next unless scaling_groups.has_key?(layer_name)
    camelized_layer_name = layer_name.split('-').map{ |w| w.capitalize.gsub(/api/i, 'API') }.join
    parameters["Min#{camelized_layer_name}Var"] = scaling_groups[layer_name].to_s
    parameters["Max#{camelized_layer_name}Var"] = (scaling_groups[layer_name] + 1).to_s
  end

  if instance_ami = self.config['base_image']
    parameters['InstanceAMIVar'] = instance_ami
  end

  Stacco::Resources::RoleScripts.each do |role_name, role_script|
    parameters["#{role_name}RoleScriptVar"] = role_script
  end


  bake_id = '%d-%04x' % [Time.now.to_i, rand(36 ** 4)]
  template_object = @bucket.objects["template/#{bake_id}"]

  if publish_first
    template_object.write(baked_template_body, acl: :authenticated_read)
  end

  return [template_object, parameters] unless block_given?

  if block_given?
    new_template_body = yield(baked_template_body, parameters)
  else
    new_template_body = baked_template_body
  end

  unless publish_first and new_template_body == baked_template_body
    if new_template_body
      template_object.write(new_template_body, acl: :authenticated_read)
    else
      template_object.delete if template_object.exists?
    end
  end

  [template_object, parameters]
end
cancel_operation() click to toggle source
# File lib/stacco/stack.rb, line 65
def cancel_operation
  return unless self.operation_in_progress?
  @aws_stack.cancel_update
end
cancel_operation!() click to toggle source
# File lib/stacco/stack.rb, line 70
def cancel_operation!
  self.cancel_operation
  Kernel.sleep(2) while self.operation_in_progress?
end
cloudformation_template() click to toggle source
# File lib/stacco/stack.rb, line 303
def cloudformation_template
  Stacco::Template.const_get(self.config['template']).new
end
cloudformation_template_body() click to toggle source
# File lib/stacco/stack.rb, line 307
def cloudformation_template_body
  self.cloudformation_template.to_json(stack: self)
end
config() click to toggle source
# File lib/stacco/stack.rb, line 99
def config
  YAML.load(@config_object.read)
end
config=(new_config) click to toggle source
# File lib/stacco/stack.rb, line 103
def config=(new_config)
  @config_object.write(new_config.to_yaml)
end
connections() click to toggle source
# File lib/stacco/stack.rb, line 36
def connections
  connections = {}
  running_instances = @aws_stack.instances.find_all{ |i| i.status == :running }
  running_instances.each{ |i| connections[i.tags["aws:cloudformation:logical-id"]] = i }
  connections
end
databases() click to toggle source
# File lib/stacco/stack.rb, line 43
def databases
  @aws_stack.rds_instances.inject({}) do |dbs, (k, v)|
    (dbs[k] = v) if v.status == "available"
    dbs
  end
end
description() click to toggle source
# File lib/stacco/stack.rb, line 148
def description
  self.config['description']
end
disable_layers(layer_names) click to toggle source
# File lib/stacco/stack.rb, line 124
def disable_layers(layer_names)
  layer_names = layer_names.map(&:to_s)

  self.update_config do |c|
    c['layers'] = self.enabled_layer_names - layer_names
  end
end
domain() click to toggle source
# File lib/stacco/stack.rb, line 83
def domain
  domain = Stacco::Domain.new(self, self.config['domain'].gsub(/\.$/, '').split('.').reverse)
  domain.service_registry = @services
  domain
end
down!() click to toggle source
# File lib/stacco/stack.rb, line 294
def down!
  return false unless self.up?

  @aws_stack.buckets.each{ |bucket| bucket.delete! }
  @aws_stack.delete

  true
end
enable_layers(layer_names) click to toggle source
# File lib/stacco/stack.rb, line 113
def enable_layers(layer_names)
  layer_names = layer_names.map(&:to_s)
  layer_names.each do |layer_name|
    raise ArgumentError, "Layer '#{layer_name}' is not provided by the template definition" unless self.available_layer_names.include? layer_name
  end

  self.update_config do |c|
    c['layers'] = self.enabled_layer_names | layer_names
  end
end
enabled_layer_names() click to toggle source
# File lib/stacco/stack.rb, line 132
def enabled_layer_names
  (self.available_layer_names & (self.config['layers'] || []))
end
enabled_layers() click to toggle source
# File lib/stacco/stack.rb, line 136
def enabled_layers
  self.enabled_layer_names.map{ |layer_name| Stacco::Layer.load(self, layer_name) }
end
iam_keypair_name() click to toggle source
# File lib/stacco/stack.rb, line 337
def iam_keypair_name
  "stacco-%s-%s" % [self.name, self.iam_private_key.key.split('/').last]
end
iam_private_key() click to toggle source
# File lib/stacco/stack.rb, line 333
def iam_private_key
  @bucket.objects.with_prefix("ssh-key/").to_a.sort_by{ |obj| obj.key.split('/').last.to_i }.last
end
initialize_distributions!() click to toggle source
# File lib/stacco/stack.rb, line 279
def initialize_distributions!
  @services[:cloudfront].distributions.each do |dist|
    dist.update do
      next unless stack_dist_cert = @aws_stack.server_certificates(domain: dist.aliases).first

      dist.price_class = :"100"
      dist.certificate = stack_dist_cert.id
    end
  end
end
invalidate_distributed_objects!(dist_cname, obj_keys) click to toggle source
# File lib/stacco/stack.rb, line 290
def invalidate_distributed_objects!(dist_cname, obj_keys)
  @aws_stack.distribution(dist_cname).invalidate(obj_keys)
end
layer_enabled?(layer_name) click to toggle source
# File lib/stacco/stack.rb, line 140
def layer_enabled?(layer_name)
  self.enabled_layer_names.inlude?(layer_name.to_s)
end
must_be_up!() click to toggle source
# File lib/stacco/stack.rb, line 54
def must_be_up!
  unless self.up?
    $stderr.puts "stack #{self.name} is down"
    Kernel.exit 1
  end
end
name() click to toggle source
# File lib/stacco/stack.rb, line 152
def name
  self.config['name']
end
name=(new_name) click to toggle source
# File lib/stacco/stack.rb, line 156
def name=(new_name)
  update_config do |c|
    c['name'] = new_name
  end
end
operation_in_progress?() click to toggle source
# File lib/stacco/stack.rb, line 61
def operation_in_progress?
  @aws_stack.exists? and @aws_stack.status =~ /_IN_PROGRESS$/
end
resource_summaries() click to toggle source
# File lib/stacco/stack.rb, line 50
def resource_summaries
  @aws_stack.resource_summaries
end
roles() click to toggle source
# File lib/stacco/stack.rb, line 242
def roles
  Stacco::Resources::RoleScripts.keys
end
secrets() click to toggle source
# File lib/stacco/stack.rb, line 238
def secrets
  self.config['secrets']
end
status() click to toggle source
# File lib/stacco/stack.rb, line 79
def status
  self.up? ? self.aws_status : "DOWN"
end
stream_events() click to toggle source
# File lib/stacco/stack.rb, line 341
def stream_events
  Enumerator.new do |out|
    known_events = Set.new
    ticks_without_add = 0
    current_tick = 0
    current_op = nil
    tracked_resources = Set.new

    while true
      added = 0

      stack_events = @aws_stack.events.to_a rescue []

      current_resources = [@aws_stack.instances, @aws_stack.distributions].flatten
      current_resources.each do |new_rs|
        next if tracked_resources.member? new_rs
        tracked_resources.add new_rs
        new_rs.instance_variable_set('@prev_status', :nonexistent)
      end

      tracked_resources.each do |rs|
        resource_name = [rs.tags['aws:cloudformation:logical-id']]
        if rs.tags['aws:autoscaling:groupName']
          resource_name.push(rs.id.split('-')[1])
        end
        resource_name = resource_name.compact.join('.')

        resource_is_live = (current_tick > 0)
        resource_status_delta = rs.change_in_status

        if resource_is_live and resource_status_delta
          now = Time.now
          evt = OpenStruct.new(
            event_id: "#{rs.id}#{now.to_i}#{resource_status_delta.inspect}",
            live: true,
            logical_resource_id: resource_name,
            status: "CHANGED",
            operation: "UPDATE",
            timestamp: now,
            error: "#{resource_status_delta[:from]} -> #{resource_status_delta[:to]}",
            detail: nil
          )

          if resource_status_delta[:to] == :terminated and rs.respond_to?(:console_output) and logs = rs.console_output
            logs = logs.split("\r\n")
            if cfn_signal_ln = logs.grep("CloudFormation signaled successfully with FAILURE.").last
              logs = logs[0 ... logs.index(cfn_signal_ln)]
            end
            logs = logs[-30 .. -1]
            evt.detail = logs.map{ |ln| ln }
          end

          stack_events.push evt
        end
      end

      stack_events = stack_events.sort_by{ |ev| ev.timestamp }

      stack_events.each do |event|
        next if known_events.include? event.event_id
        known_events.add event.event_id

        if event.resource_type == "AWS::CloudFormation::Stack"
          current_op = event
        end

        event.live = (current_tick > 0)
        event.op = current_op

        out.yield event

        added += 1
        ticks_without_add = 0
      end

      if current_tick == 0 and stack_events.last.op
        stack_events.last.op.live = true
        stack_events.each{ |ev| out.yield(ev) if (ev.op and ev.op.live) }
      end

      current_tick += 1
      ticks_without_add += 1 if added == 0

      if ticks_without_add >= 8 and (Math.log2(ticks_without_add) % 1) == 0.0
        jobs = @aws_stack.resource_summaries
        active_jobs = jobs.find_all{ |job| job[:resource_status] =~ /IN_PROGRESS$/ }.map{ |job| job[:logical_resource_id] }.sort
        unless active_jobs.empty?
          out.yield OpenStruct.new(
            live: true,
            logical_resource_id: "Scheduler",
            status: "WAIT",
            operation: "WAIT",
            timestamp: Time.now,
            error: "waiting on #{active_jobs.join(', ')}",
            detail: nil
          )
        end
      end

      Kernel.sleep 2
    end
  end
end
subdomains() click to toggle source
# File lib/stacco/stack.rb, line 89
def subdomains
  d = self.domain

  self.config['subdomains'].map do |logical_name, prefix_parts|
    sd = prefix_parts.inject(d, &:+)
    sd.logical_name = logical_name
    sd
  end
end
up!() click to toggle source
# File lib/stacco/stack.rb, line 254
def up!
  body_object, params = self.bake_template(publish: true)

  unless @aws_stack.exists?
    return @services[:cloudformation].stacks.create(
      self.name,
      body_object.public_url,
      parameters: params
    )
    #disable_rollback: true
  end

  begin
    @aws_stack.update(template: body_object.public_url, parameters: params)
    true
  rescue AWS::CloudFormation::Errors::ValidationError => e
    raise unless e.message =~ /no updates/i
    false
  end
end
up?() click to toggle source
# File lib/stacco/stack.rb, line 162
def up?
  @aws_stack.exists?
end
up_since() click to toggle source
# File lib/stacco/stack.rb, line 275
def up_since
  @aws_stack.creation_time if @aws_stack.exists?
end
update_config() { |new_config| ... } click to toggle source
# File lib/stacco/stack.rb, line 107
def update_config
  new_config = self.config
  yield(new_config)
  self.config = new_config
end
validate() click to toggle source
# File lib/stacco/stack.rb, line 311
def validate
  body_object, _ = self.bake_template do |body, params|
    tpl.gsub(/"[cm][123]\.(\dx)?(small|medium|large)"/, '"m1.small"')
  end

  Kernel.sleep 1

  begin
    @services[:cloudformation].estimate_template_cost(body_object)
    [true]
  rescue AWS::CloudFormation::Errors::ValidationError => e
    msg = e.message
    match = msg.scan(/^Template format error: JSON not well-formed. \(line (\d+), column (\d+)\)$/)
    if match.length.nonzero?
      line, column = match.to_a.flatten.map{ |el| el.to_i }
      [false, msg, [baked_template_object.read.split("\n")[line.to_i], column]]
    else
      [false, msg]
    end
  end
end