Narven
8/6/2014 - 1:32 PM

deploy.rb

require 'mina/bundler'
require 'mina/rails'
require 'mina/git'
require 'mina/rvm'

# Usually mina focuses on deploying to one host and the deploy options are therefore simple.
# In our case, there is a number of possible servers to deploy to, it is therefore necessary to
# specify the host that we are targeting.
server = ENV['server']
# Since the same host can have multiple applications running in parallel, it is necessary to
# specify further which application we want to deploy
version = ENV['version']

# Set the repository (here on BitBucket)
set :repository, 'git@bitbucket.org:username/project.git'
# setting the term_mode to system disable the "pretty-print" but prevent some other issues
set :term_mode, :system

# Manually create these paths in shared/ (eg: shared/config/database.yml) in your server.
# They will be linked in the 'deploy:link_shared_paths' step.
set :shared_paths, ['config/database.yml', 'log']

# Optional SSH settings:
# SSH forward agent to ensure that credentials are passed through for git operations
set :forward_agent, true

##########################################################################
#
# Setup environment
#
##########################################################################

# This task is the environment that is loaded for most commands, such as
# `mina deploy` or `mina rake`.
task :environment do
  # Ensure that a server has been set
  unless server
    print_error "A server needs to be specified."
    exit
  end

  # Remote application folder
  set :deploy_to, "/home/username/project/#{version}"

  # Set the basic environment variables based on the server and version
  case server
  when 'qa'
    # The hostname to SSH to
    set :domain, 'qa-domain.com'
    # SSH Optional settings
    set :user, 'foobar'    # Username in the server to SSH to.
    # set :port, '30000'     # SSH port number.
    # Rails environment
    set :rails_env, 'qa'
  when 'prod'
    # The hostname to SSH to
    set :domain, 'prod-domain.com'
    # SSH Optional settings
    set :user, 'foobar'    # Username in the server to SSH to.
    # set :port, '30000'     # SSH port number.
    # Rails environment
    set :rails_env, 'production'
  end

  # For those using RVM, use this to load an RVM version@gemset.
  invoke :'rvm:use[ruby-1.9.3-p327@project]'
end

##########################################################################
#
# Create new host tasks
# Tasks below are related to deploying a new version of the application
#
##########################################################################

# Function extracted from http://blog.nicolai86.eu/posts/2013-05-06/syncing-database-content-down-with-mina
# allowing to read the content of the database.yml file
RYAML = <<-BASH
function ryaml {
  ruby -ryaml -e 'puts ARGV[1..-1].inject(YAML.load(File.read(ARGV[0]))) {|acc, key| acc[key] }' "$@"
};
BASH

# Execute all setup tasks defined below
desc "Create new folder structure + database.yml + DB + VirtualHost"
task :'setup:all' => :environment do
  queue! %[echo "-----> Setup folder structure on server"]
  invoke :setup
  queue! %[echo "-----> Setup the DB (create user / DB)"]
  invoke :'setup:db'
  queue! %[echo "-----> Setup Apache VirtualHost Configuration"]
  invoke :'setup:apache'
  queue! %[echo "-----> Deploy Master for this version"]
  invoke :deploy
  queue! %[echo "-----> Enable Apache host and restart Apache"]
  invoke :'apache:enable'
end

# Put any custom mkdir's in here for when `mina setup` is ran.
# For Rails apps, we'll make some of the shared paths that are shared between
# all releases.
task :setup => :environment do
  queue! %[mkdir -p "#{deploy_to}/shared/log"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/log"]

  queue! %[mkdir -p "#{deploy_to}/shared/config"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/config"]

  queue! %[touch "#{deploy_to}/shared/config/database.yml"]
  queue  %[echo "-----> Fill in information below to populate 'shared/config/database.yml'."]
  invoke :'setup:db:database_yml'
end

# Populate file database.yml with the appropriate rails_env
# Database name and user name are based on convention
# Password is defined by the user during setup
desc "Populate database.yml"
task :'setup:db:database_yml' => :environment do
  puts "Enter a name for the new database"
  db_name = STDIN.gets.chomp
  puts "Enter a user for the new database"
  db_username = STDIN.gets.chomp
  puts "Enter a password for the new database"
  db_pass = STDIN.gets.chomp
  # Virtual Host configuration file
  database_yml = <<-DATABASE.dedent
    #{rails_env}:
      adapter: mysql2
      encoding: utf8
      database: #{db_name}
      username: #{db_username}
      password: #{db_pass}
      host: localhost
      timeout: 5000
  DATABASE
  queue! %{
    echo "-----> Populating database.yml"
    echo "#{database_yml}" > #{deploy_to!}/shared/config/database.yml
    echo "-----> Done"
  }
end

# Create the new database based on information from database.yml
# In this application DB, user is given full access to the new DB
desc "Create new database"
task :'setup:db' => :environment do
  queue! %{
    echo "-----> Import RYAML function"
    #{RYAML}
    echo "-----> Read database.yml"
    USERNAME=$(ryaml #{deploy_to!}/#{shared_path!}/config/database.yml #{rails_env} username)
    PASSWORD=$(ryaml #{deploy_to!}/#{shared_path!}/config/database.yml #{rails_env} password)
    DATABASE=$(ryaml #{deploy_to!}/#{shared_path!}/config/database.yml #{rails_env} database)
    echo "-----> Create SQL query"
    Q1="CREATE DATABASE IF NOT EXISTS $DATABASE;"
    Q2="GRANT USAGE ON *.* TO $USERNAME@localhost IDENTIFIED BY '$PASSWORD';"
    Q3="GRANT ALL PRIVILEGES ON $DATABASE.* TO $USERNAME@localhost;"
    Q4="FLUSH PRIVILEGES;"
    SQL="${Q1}${Q2}${Q3}${Q4}"
    echo "-----> Execute SQL query to create DB and user"
    echo "-----> Enter MySQL root password on prompt below"
    #{echo_cmd %[mysql -uroot -p -e "$SQL"]}
    echo "-----> Done"
  }
end

# Create a new VirtualHost file
# Server name is defined by convention
# Script executes some sudo operations
desc "Create Apache site file"
task :'setup:apache' => :environment do
  # Get variable for virtual host configuration file
  fqdn = get_fqdn(server, version)
  fqdn_ext = external_fqdn(server, version)
  # Virtual Host configuration file
  vhost = <<-HOSTFILE.dedent
    <VirtualHost *:80>
      ServerAdmin user@your-website.com
      ServerName #{get_fqdn(server, version)}
      DocumentRoot #{deploy_to!}/#{current_path!}/public
      RailsEnv #{rails_env}
      <Directory #{deploy_to!}/#{current_path!}/public>
        Options -MultiViews
        AllowOverride all
      </Directory>
      PassengerMinInstances 5
      # Maintenance page
      ErrorDocument 503 /503.html
      RewriteEngine On
      RewriteCond %{REQUEST_URI} !.(css|gif|jpg|png)$
      RewriteCond %{DOCUMENT_ROOT}/503.html -f
      RewriteCond %{SCRIPT_FILENAME} !503.html
      RewriteRule ^.*$ - [redirect=503,last]
    </VirtualHost>
  HOSTFILE
  queue! %{
    echo "-----> Create Temporary Apache Virtual Host"
    echo "#{vhost}" > #{fqdn}.tmp
    echo "-----> Copy Virtual Host file to /etc/apache2/sites-available/ (requires sudo)"
    #{echo_cmd %[sudo cp #{fqdn}.tmp /etc/apache2/sites-available/#{fqdn}]}
    echo "-----> Remove Temporary Apache Virtual Host"
    rm #{fqdn}.tmp
    echo "-----> Done"
  }
end

# Enable the new Virtual Host and restart Apache
desc "Enable new Apache host file"
task :'apache:enable' => :environment do
  fqdn = get_fqdn(server, version)
  queue! %{
    echo "-----> Enable Apache Virtual Host"
    #{echo_cmd %[sudo a2ensite #{fqdn}]}
    echo "-----> Remove Temporary Apache Virtual Host"
    #{echo_cmd %[sudo service apache2 reload]}
  }
end

##########################################################################
#
# Deployment related task
#
##########################################################################

desc "Deploys the current version to the server."
task :deploy => :environment do
  deploy do
    # Put things that will set up an empty directory into a fully set-up
    # instance of your project.
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile:force'

    to :launch do
      queue "touch #{deploy_to}/#{current_path}/tmp/restart.txt"
    end
  end
end

#########################################################################
#
# Helper functions
#
##########################################################################

#
# Get the main domain based on the server
#
# @return [String] the main domain
def main_domain(server)
  case server
  when 'qa'
    "qa-domain.com"
  when 'prod'
    "prod-domain.com"
  end
end

#
# Fully Qualified Domain Name of the host
# Concatenation of the version and the domain name
#
# @return [String] the FQDN
def get_fqdn(server, version)
  fqdn = "#{version}.#{main_domain(server)}"
  return fqdn
end

#########################################################################
#
# Libraries
#
##########################################################################

#
# See https://github.com/cespare/ruby-dedent/blob/master/lib/dedent.rb
#
class String
  def dedent
    lines = split "\n"
    return self if lines.empty?
    indents = lines.map do |line|
      line =~ /\S/ ? (line.start_with?(" ") ? line.match(/^ +/).offset(0)[1] : 0) : nil
    end
    min_indent = indents.compact.min
    return self if min_indent.zero?
    lines.map { |line| line =~ /\S/ ? line.gsub(/^ {#{min_indent}}/, "") : line }.join "\n"
  end
end