Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tag: v1.4.16
Fetching contributors…

Cannot retrieve contributors at this time

394 lines (339 sloc) 14.373 kb
# stolen wholesale from capistrano, thanks Jamis!
require 'base64'
require 'fileutils'
require 'json'
require 'engineyard-serverside/rails_asset_support'
module EY
module Serverside
class DeployBase < Task
include LoggedOutput
include ::EY::Serverside::RailsAssetSupport
# default task
def deploy
debug "Starting deploy at #{Time.now.asctime}"
update_repository_cache
cached_deploy
end
def cached_deploy
debug "Deploying app from cached copy at #{Time.now.asctime}"
require_custom_tasks
push_code
info "~> Starting full deploy"
copy_repository_cache
with_failed_release_cleanup do
create_revision_file
run_with_callbacks(:bundle)
symlink_configs
conditionally_enable_maintenance_page
run_with_callbacks(:migrate)
run_with_callbacks(:compile_assets) # defined in RailsAssetSupport
callback(:before_symlink)
# We don't use run_with_callbacks for symlink because we need
# to clean up manually if it fails.
symlink
end
callback(:after_symlink)
run_with_callbacks(:restart)
disable_maintenance_page
cleanup_old_releases
debug "Finished deploy at #{Time.now.asctime}"
rescue Exception
debug "Finished failing to deploy at #{Time.now.asctime}"
puts_deploy_failure
raise
end
def restart_with_maintenance_page
require_custom_tasks
conditionally_enable_maintenance_page
restart
disable_maintenance_page
end
def enable_maintenance_page
maintenance_page_candidates = [
"public/maintenance.html.custom",
"public/maintenance.html.tmp",
"public/maintenance.html",
"public/system/maintenance.html.default",
].map do |file|
File.join(c.latest_release, file)
end
# this one is guaranteed to exist
maintenance_page_candidates << File.expand_path(
"default_maintenance_page.html",
File.dirname(__FILE__)
)
# put in the maintenance page
maintenance_file = maintenance_page_candidates.detect do |file|
File.exists?(file)
end
@maintenance_up = true
roles :app_master, :app, :solo do
maint_page_dir = File.join(c.shared_path, "system")
visible_maint_page = File.join(maint_page_dir, "maintenance.html")
run Escape.shell_command(['mkdir', '-p', maint_page_dir])
run Escape.shell_command(['cp', maintenance_file, visible_maint_page])
end
end
def conditionally_enable_maintenance_page
if c.migrate? || required_downtime_stack?
enable_maintenance_page
end
end
def required_downtime_stack?
%w[ nginx_mongrel glassfish ].include? c.stack
end
def disable_maintenance_page
@maintenance_up = false
roles :app_master, :app, :solo do
run "rm -f #{File.join(c.shared_path, "system", "maintenance.html")}"
end
end
def run_with_callbacks(task)
callback("before_#{task}")
send(task)
callback("after_#{task}")
end
# task
def push_code
info "~> Pushing code to all servers"
barrier *(EY::Serverside::Server.all.map do |server|
need_later { server.sync_directory(config.repository_cache) }
end)
end
# task
def restart
@restart_failed = true
info "~> Restarting app servers"
roles :app_master, :app, :solo do
run(restart_command)
end
@restart_failed = false
end
def restart_command
"/engineyard/bin/app_#{c.app} deploy"
end
def clean_environment
"env -i PATH=$PATH HOME=$HOME"
end
# task
def bundle
if File.exist?("#{c.release_path}/Gemfile")
info "~> Gemfile detected, bundling gems"
lockfile = File.join(c.release_path, "Gemfile.lock")
bundler_installer = if File.exist?(lockfile)
get_bundler_installer(lockfile)
else
warn_about_missing_lockfile
get_default_bundler_installer
end
sudo "#{clean_environment} #{serverside_bin} install_bundler #{bundler_installer.version}"
bundled_gems_path = File.join(c.shared_path, "bundled_gems")
ruby_version_file = File.join(bundled_gems_path, "RUBY_VERSION")
system_version_file = File.join(bundled_gems_path, "SYSTEM_VERSION")
ruby_version = `ruby -v`
system_version = `uname -m`
if File.directory?(bundled_gems_path)
rebundle = false
rebundle = true if File.exist?(ruby_version_file) && File.read(ruby_version_file) != ruby_version
rebundle = true if File.exist?(system_version_file) && File.read(system_version_file) != system_version
if rebundle
info "~> Ruby version change detected, cleaning bundled gems"
run "rm -Rf #{bundled_gems_path}"
end
end
run "cd #{c.release_path} && #{clean_environment} ruby -S bundle _#{bundler_installer.version}_ install #{bundler_installer.options}"
run "mkdir -p #{bundled_gems_path} && ruby -v > #{ruby_version_file} && uname -m > #{system_version_file}"
end
if File.exist?("#{c.release_path}/package.json")
unless run("which npm")
abort "*** [Error] package.json detected, but npm was not installed"
else
info "~> package.json detected, installing npm packages"
run "cd #{c.release_path} && npm install"
end
end
end
# task
def cleanup_old_releases
@cleanup_failed = true
info "~> Cleaning up old releases"
sudo "ls #{c.release_dir} | head -n -3 | xargs -I{} rm -rf #{c.release_dir}/{}"
@cleanup_failed = false
end
# task
def rollback
if c.all_releases.size > 1
rolled_back_release = c.latest_release
c.release_path = c.previous_release(rolled_back_release)
revision = File.read(File.join(c.release_path, 'REVISION')).strip
info "~> Rolling back to previous release: #{short_log_message(revision)}"
run_with_callbacks(:symlink)
sudo "rm -rf #{rolled_back_release}"
bundle
info "~> Restarting with previous release"
with_maintenance_page { run_with_callbacks(:restart) }
else
info "~> Already at oldest release, nothing to roll back to"
exit(1)
end
end
# task
def migrate
return unless c.migrate?
@migrations_reached = true
roles :app_master, :solo do
cmd = "cd #{c.release_path} && PATH=#{c.binstubs_path}:$PATH #{c.framework_envs} #{c.migration_command}"
info "~> Migrating: #{cmd}"
run(cmd)
end
end
# task
def copy_repository_cache
info "~> Copying to #{c.release_path}"
run("mkdir -p #{c.release_path} && rsync -aq #{c.exclusions} #{c.repository_cache}/ #{c.release_path}")
info "~> Ensuring proper ownership"
sudo("chown -R #{c.user}:#{c.group} #{c.deploy_to}")
end
def create_revision_file
run create_revision_file_command
end
def symlink_configs(release_to_link=c.release_path)
info "~> Preparing shared resources for release"
symlink_tasks(release_to_link).each do |what, cmd|
info "~> #{what}"
run(cmd)
end
owner = [c.user, c.group].join(':')
info "~> Setting ownership to #{owner}"
sudo "chown -R #{owner} #{release_to_link}"
end
def symlink_tasks(release_to_link)
[
["Set group write permissions", "chmod -R g+w #{release_to_link}"],
["Remove revision-tracked shared directories from deployment", "rm -rf #{release_to_link}/log #{release_to_link}/public/system #{release_to_link}/tmp/pids"],
["Create tmp directory", "mkdir -p #{release_to_link}/tmp"],
["Symlink shared log directory", "ln -nfs #{c.shared_path}/log #{release_to_link}/log"],
["Create public directory if needed", "mkdir -p #{release_to_link}/public"],
["Create config directory if needed", "mkdir -p #{release_to_link}/config"],
["Create system directory if needed", "ln -nfs #{c.shared_path}/system #{release_to_link}/public/system"],
["Symlink shared pids directory", "ln -nfs #{c.shared_path}/pids #{release_to_link}/tmp/pids"],
["Symlink other shared config files", "find #{c.shared_path}/config -type f -not -name 'database.yml' -exec ln -s {} #{release_to_link}/config \\;"],
["Symlink mongrel_cluster.yml", "ln -nfs #{c.shared_path}/config/mongrel_cluster.yml #{release_to_link}/config/mongrel_cluster.yml"],
["Symlink database.yml", "ln -nfs #{c.shared_path}/config/database.yml #{release_to_link}/config/database.yml"],
["Symlink newrelic.yml if needed", "if [ -f \"#{c.shared_path}/config/newrelic.yml\" ]; then ln -nfs #{c.shared_path}/config/newrelic.yml #{release_to_link}/config/newrelic.yml; fi"],
]
end
# task
def symlink(release_to_link=c.release_path)
info "~> Symlinking code"
run "rm -f #{c.current_path} && ln -nfs #{release_to_link} #{c.current_path} && chown -R #{c.user}:#{c.group} #{c.current_path}"
@symlink_changed = true
rescue Exception
sudo "rm -f #{c.current_path} && ln -nfs #{c.previous_release(release_to_link)} #{c.current_path} && chown -R #{c.user}:#{c.group} #{c.current_path}"
@symlink_changed = false
raise
end
def callback(what)
@callbacks_reached ||= true
if File.exist?("#{c.release_path}/deploy/#{what}.rb")
run Escape.shell_command(base_callback_command_for(what)) do |server, cmd|
per_instance_args = [
'--current-roles', server.roles.join(' '),
'--config', c.to_json,
]
per_instance_args << '--current-name' << server.name.to_s if server.name
cmd << " " << Escape.shell_command(per_instance_args)
end
end
end
protected
def base_callback_command_for(what)
[serverside_bin, 'hook', what.to_s,
'--app', config.app.to_s,
'--release-path', config.release_path.to_s,
'--framework-env', c.environment.to_s,
].compact
end
def serverside_bin
basedir = File.expand_path('../../..', __FILE__)
File.join(basedir, 'bin', 'engineyard-serverside')
end
def puts_deploy_failure
if @cleanup_failed
info "~> [Relax] Your site is running new code, but cleaning up old deploys failed"
elsif @maintenance_up
info "~> [Attention] Maintenance page still up, consider the following before removing:"
info " * any deploy hooks ran, be careful if they were destructive" if @callbacks_reached
info " * any migrations ran, be careful if they were destructive" if @migrations_reached
if @symlink_changed
info " * your new code is symlinked as current"
else
info " * your old code is still symlinked as current"
end
info " * application servers failed to restart" if @restart_failed
else
info "~> [Relax] Your site is still running old code and nothing destructive could have occurred"
end
end
def with_maintenance_page
conditionally_enable_maintenance_page
yield if block_given?
disable_maintenance_page
end
def with_failed_release_cleanup
yield
rescue Exception
sudo "rm -rf #{c.release_path}"
raise
end
def warn_about_missing_lockfile
info "!>"
info "!> WARNING: Gemfile.lock is missing!"
info "!> You can get different gems in production than what you tested with."
info "!> You can get different gems on every deployment even if your Gemfile hasn't changed."
info "!> Deploying may take a long time."
info "!>"
info "!> Fix this by running \"git add Gemfile.lock; git commit\" and deploying again."
info "!> If you don't have a Gemfile.lock, run \"bundle lock\" to create one."
info "!>"
info "!> This deployment will use bundler #{LockfileParser.default_version} to run 'bundle install'."
info "!>"
end
def get_bundler_installer(lockfile)
parser = LockfileParser.new(File.read(lockfile))
case parser.lockfile_version
when :bundler09
bundler_09_installer(parser.bundler_version)
when :bundler10
bundler_10_installer(parser.bundler_version)
else
raise "Unknown lockfile version #{parser.lockfile_version}"
end
end
public :get_bundler_installer
def get_default_bundler_installer
# deployment mode is not supported without a Gemfile.lock, so we turn that off.
bundler_10_installer(LockfileParser.default_version, deployment_mode = false)
end
public :get_default_bundler_installer
def bundler_09_installer(version)
BundleInstaller.new(version, '--without=development --without=test')
end
# Set +deploymemt_mode+ to false to avoid passing the --deployment flag.
def bundler_10_installer(version, deployment_mode = true)
options = ["--path #{c.shared_path}/bundled_gems",
"--binstubs #{c.binstubs_path}",
"--without development test"]
options.unshift('--deployment') if deployment_mode
BundleInstaller.new(version, options.join(' '))
end
end # DeployBase
class Deploy < DeployBase
def self.new(config)
# include the correct fetch strategy
include EY::Serverside::Strategies.const_get(config.strategy)::Helpers
super
end
end
end
end
Jump to Line
Something went wrong with that request. Please try again.