class Harrison::Deploy

Attributes

artifact[RW]
host[RW]
phases[RW]
release_dir[RW]
rollback[RW]

Public Class Methods

new(opts={}) click to toggle source
Calls superclass method Harrison::Base::new
# File lib/harrison/deploy.rb, line 15
def initialize(opts={})
  # Config helpers for Harrisonfile.
  self.class.option_helper(:hosts)
  self.class.option_helper(:base_dir)
  self.class.option_helper(:deploy_via)
  self.class.option_helper(:keep)
  self.class.option_helper(:confirm)

  # Command line opts for this action. Will be merged with common opts.
  arg_opts = [
    [ :hosts, "List of remote hosts to deploy to. Can also be specified in Harrisonfile.", :type => :strings ],
    [ :keep, "Number of recent deploys to keep after a successful deploy. (Including the most recent deploy.) Defaults to keeping all deploys forever.", :type => :integer ],
    [ :confirm, "Whether to interactively confirm the list of target hosts for deployment.", :type => :flag, :default => true ],
  ]

  super(arg_opts, opts)

  self.add_default_phases
end

Public Instance Methods

add_phase(name, &block) click to toggle source
# File lib/harrison/deploy.rb, line 49
def add_phase(name, &block)
  @_phases ||= Hash.new

  @_phases[name] = Harrison::Deploy::Phase.new(name, &block)
end
cleanup_deploys(limit) click to toggle source
# File lib/harrison/deploy.rb, line 171
def cleanup_deploys(limit)
  # Grab a list of deploys to be removed.
  purge_deploys = self.deploys.sort.reverse.slice(limit..-1) || []

  if purge_deploys.size > 0
    puts "[#{self.host}]   Purging #{purge_deploys.size} old deploys. (Keeping #{limit}...)"

    purge_deploys.each do |stale_deploy|
      remote_exec("cd deploys && rm -f #{stale_deploy}")
    end
  end
end
cleanup_releases() click to toggle source
# File lib/harrison/deploy.rb, line 184
def cleanup_releases
  # Figure out which releases need to be kept.
  keep_releases = self.active_releases

  self.releases.each do |release|
    unless keep_releases.include?(release)
      remote_exec("cd releases && rm -rf #{release}")
    end
  end
end
close(host=nil) click to toggle source
# File lib/harrison/deploy.rb, line 195
def close(host=nil)
  if host
    @_conns[host].close if @_conns && @_conns[host]
  elsif @_conns
    @_conns.keys.each do |host|
      @_conns[host].close unless @_conns[host].closed?
    end
  end
end
invoke_user_block()
Alias for: run
parse(args) click to toggle source
Calls superclass method Harrison::Base#parse
# File lib/harrison/deploy.rb, line 35
def parse(args)
  super

  # Preserve argv hosts if it's been passed.
  @_argv_hosts = self.hosts.dup if self.hosts

  self.rollback = args[0] == 'rollback'

  unless self.rollback
    # Make sure they passed an artifact.
    self.artifact = args[1] || abort("ERROR: You must specify the artifact to be deployed as an argument to this command.")
  end
end
remote_exec(cmd) click to toggle source
Calls superclass method Harrison::Base#remote_exec
# File lib/harrison/deploy.rb, line 55
def remote_exec(cmd)
  super("cd #{remote_project_dir} && #{cmd}")
end
run() click to toggle source
# File lib/harrison/deploy.rb, line 79
def run
  # Override Harrisonfile hosts if it was passed on argv.
  self.hosts = @_argv_hosts if @_argv_hosts

  if self.hosts.respond_to?(:call)
    resolved_hosts = self.hosts.call(self)
    self.hosts = resolved_hosts
  end

  # Require at least one host.
  if !self.hosts || self.hosts.empty?
    abort("ERROR: You must specify one or more hosts to deploy/rollback on, either in your Harrisonfile or via --hosts.")
  end

  if self.confirm
    self.hosts.each { |h| puts " - #{h}" }

    exit unless HighLine.new.agree("\nProceed with above-listed hosts?")

    puts ""
  end

  # Default to just built in deployment phases.
  self.phases ||= [ :upload, :extract, :link, :cleanup ]

  # Default base_dir.
  self.base_dir ||= '/opt'

  if self.rollback
    puts "Rolling back \"#{project}\" to previously deployed release on #{hosts.size} hosts...\n\n"

    # Find the prior deploy on the first host.
    self.host = hosts[0]
    last_deploy = self.deploys.sort.reverse[1] || abort("ERROR: No previous deploy to rollback to.")
    self.release_dir = remote_exec("cd deploys && readlink -vn #{last_deploy}")

    # No need to upload or extract for rollback.
    self.phases.delete(:upload)
    self.phases.delete(:extract)

    # Don't cleanup old deploys either.
    self.phases.delete(:cleanup)
  else
    puts "Deploying #{artifact} for \"#{project}\" onto #{hosts.size} hosts...\n\n"
    self.release_dir = "#{remote_project_dir}/releases/" + File.basename(artifact, '.tar.gz')
  end

  self.deploy_link = "#{remote_project_dir}/deploys/" + Time.new.utc.strftime('%Y-%m-%d_%H%M%S')

  progress_stack = []

  failed = catch(:failure) do
    self.phases.each do |phase_name|
      phase = @_phases[phase_name] || abort("ERROR: Could not resolve \"#{phase_name}\" as a deployment phase.")

      self.hosts.each do |host|
        self.host = host

        phase._run(self)

        # Track what phases we have completed on which hosts, in a stack.
        progress_stack << { host: host, phase: phase_name }
      end
    end

    # We want "failed" to be false if nothing was caught.
    false
  end

  if failed
    print "\n"

    progress_stack.reverse.each do |progress|
      self.host = progress[:host]
      phase = @_phases[progress[:phase]]

      # Don't let failures interrupt the rest of the process.
      catch(:failure) do
        phase._fail(self)
      end
    end

    abort "\nDeployment failed, previously completed deployment actions have been reverted."
  else
    if self.rollback
      puts "\nSucessfully rolled back #{project} on #{hosts.join(', ')}."
    else
      puts "\nSucessfully deployed #{artifact} to #{hosts.join(', ')}."
    end
  end
end
Also aliased as: invoke_user_block

Protected Instance Methods

active_releases() click to toggle source

Return a list of releases with at least 1 deploy pointing to them, unsorted.

# File lib/harrison/deploy.rb, line 295
def active_releases
  self.deploys.collect { |deploy| remote_exec("cd deploys && basename `readlink #{deploy}`") }.uniq
end
add_default_phases() click to toggle source
# File lib/harrison/deploy.rb, line 207
def add_default_phases
  self.add_phase :upload do |phase|
    phase.on_run do |h|
      h.ensure_remote_dir("#{h.remote_project_dir}/deploys")
      h.ensure_remote_dir("#{h.remote_project_dir}/releases")

      # Remove if it already exists.
      # TODO: if --force only?
      h.remote_exec("rm -f #{h.remote_project_dir}/releases/#{File.basename(h.artifact)}")

      if match = h.remote_regex.match(h.artifact)
        # Copy artifact to host from remote source.
        src_user, src_host, src_path = match.captures
        src_user ||= h.user

        h.remote_exec("scp #{src_user}@#{src_host}:#{src_path} #{h.remote_project_dir}/releases/")
      else
        # Upload artifact to host.
        h.upload(h.artifact, "#{h.remote_project_dir}/releases/")
      end
    end

    phase.on_fail do |h|
      # Remove staged artifact.
      h.remote_exec("rm -f #{h.remote_project_dir}/releases/#{File.basename(h.artifact)}")
    end
  end

  self.add_phase :extract do |phase|
    phase.on_run do |h|
      # Make folder for release or bail if it already exists.
      h.remote_exec("mkdir #{h.release_dir}")

      # Unpack.
      h.remote_exec("cd #{h.release_dir} && tar -xzf ../#{File.basename(h.artifact)}")

      # Clean up artifact.
      h.remote_exec("rm -f #{h.remote_project_dir}/releases/#{File.basename(h.artifact)}")
    end

    phase.on_fail do |h|
      # Remove release.
      h.remote_exec("rm -rf #{h.release_dir}")
    end
  end

  self.add_phase :link do |phase|
    phase.on_run do |h|
      # Symlink new deploy to this release.
      h.remote_exec("ln -s #{h.release_dir} #{h.deploy_link}")
    end

    phase.on_fail do |h|
      # Remove broken deploy.
      h.remote_exec("rm -f #{h.deploy_link}")
    end
  end

  self.add_phase :cleanup do |phase|
    phase.on_run do |h|
      if (h.keep)
        h.cleanup_deploys(h.keep)
        h.cleanup_releases
      end
    end
  end
end
deploys() click to toggle source

Return a list of deploys, unsorted.

# File lib/harrison/deploy.rb, line 285
def deploys
  remote_exec("cd deploys && ls -1").split("\n")
end
releases() click to toggle source

Return a list of all releases, unsorted.

# File lib/harrison/deploy.rb, line 290
def releases
  remote_exec("cd releases && ls -1").split("\n")
end
remote_project_dir() click to toggle source
# File lib/harrison/deploy.rb, line 280
def remote_project_dir
  "#{base_dir}/#{project}"
end
ssh() click to toggle source
# File lib/harrison/deploy.rb, line 275
def ssh
  @_conns ||= {}
  @_conns[self.host] ||= Harrison::SSH.new(host: self.host, user: @options[:user], proxy: self.deploy_via)
end