app/models/app.rb

Summary

Maintainability
A
1 hr
Test Coverage
class App < ActiveRecord::Base
  validates :name, uniqueness: true,
                   presence: true,
                   format: { with: /\A[a-z][a-z\d-]+\z/ }, # a-z + 0-9 + -, must start with a-z
                   length: { minimum: 3, maximum: 16 }

  validates :logplex_id, uniqueness: true,
                         presence: true

  before_validation :ensure_name,            unless: ->(model){ model.persisted? }
  before_validation :create_logplex_channel, unless: ->(model){ model.persisted? }
  before_destroy    :delete_logplex_channel

  def version
    releases.count
  end

  def env
    releases.last.try(:env) || {}
  end

  def release!(penv=env)
    image_name = "#{user.username.downcase}/#{name}"
    releases.create!(image: image_name, env: penv)
  end

  # restarts the application (restart the gears)
  def restart
    gears.each(&:restart)
  end

  def proctypes
    return {}
    # normal operation starts here
    return {} if releases.empty?
    repo_path # TODO: fix this, it no longer exists
    Dir.chdir repo_path do
      default_procfile_name = '/app/tmp/heroku-buildpack-release-step.yml'

      app_container = Docker::Container.create(
        'Image' => releases.last.image,
        'Cmd'   => ['cat', default_procfile_name]
      )
      yml = app_container.tap(&:start).attach[0][0] # attach[0][0] the format is so weird..

      def_proc = YAML.safe_load(yml)['default_process_types']
      app_proc = YAML.safe_load(`git show master:Procfile`)

      begin
        app_container.kill.delete force: true
      rescue Docker::Error::NotFoundError, Excon::Errors::SocketError
      end

      return def_proc.merge(app_proc)
    end
  end

  # scales the application to a particular size (in gears)
  def scale(options)
    # retrieve the App's Procfile data
    allowed_proctypes = proctypes.keys
    old_formation = self.formation

    allowed_proctypes.each do |gear_proctype|
      old_count = old_formation[gear_proctype].to_i
      count = options[gear_proctype].to_i || old_count || 0
      if old_count
        diff = count - old_count
      else
        diff = count
      end
      # determine whether we need to add or remove gears
      if diff > 0
        diff.times { gears.create!(proctype: gear_proctype) }
      elsif diff < 0
        # get rid of diff number of gears, from the highest worker number down
        gears.where(type: gear_proctype)
             .order(number: :desc)
             .limit(diff.abs)
             .destroy
      end
    end

    # we keep missing values and overwrite duplicates with new ones
    # --> Hash#merge
    self.formation = old_formation.merge(options)
    self.save!
  end

  # returns url to a log session
  def logs(num: 100, tail: false)
    body = {channel_id: logplex_id.to_s, num: num.to_s}
    # this is fucked up, but if we add tail key, regardless of it's
    # value, it will tail! (even on {tail: false}/{tail: false.to_s})
    body.merge!(tail: tail) if tail
    JSON.parse(Logplex.post(
      expects: 201,
      path: '/v2/sessions',
      body: body.to_json,
    ).body)['url']
  end

  # runs the command inside a new one-time gear/container
  def run(command)
    # ...
  end

  def url
    "#{name}.#{ENV['DAWN_APP_HOST']}"
  end

  def ensure_name
    if name.blank?
      loop do
        self.name = Forgery(:dawn).app_name
        break if name.size.between?(3, 16) &&
                 name =~ /\A[a-z][a-z\d-]+\z/ &&
                 !App.where(name: name).exists?
      end
    end
  end

  def create_logplex_channel
    # create a new logplex channel
    resp = Logplex.post(
      expects: 201,
      path: '/channels',
      body: {tokens: [:app, :dawn]}.to_json,
      headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
    )
    resp = JSON.parse(resp.body)

    self.logplex_id = resp['channel_id']
    self.logplex_tokens = resp['tokens'].symbolize_keys
  end

  # delete logplex channel
  def delete_logplex_channel
    Logplex.delete(path: "/v2/channels/#{logplex_id}")
  end

  belongs_to :user

  has_many :releases, -> { order(created_at: :desc) }, dependent: :destroy
  has_many :gears,    dependent: :destroy
  # since deleting the chan deletes the drains, don't trigger callback
  has_many :drains,   dependent: :delete_all
  has_many :domains,  dependent: :destroy
end