Current section
Files
Jump to
Current section
Files
lib/mix/tasks/deploy.ex
defmodule Mix.Tasks.Deploy do
# Name of app, used to get config from app environment
@app :mix_deploy
# Directory under build directory where module stores generated files
@output_dir "deploy"
# Name of directory where user can override templates
@template_dir "rel/templates/deploy"
alias MixDeploy.User
@spec parse_args(OptionParser.argv()) :: Keyword.t
def parse_args(argv) do
opts = [
strict: [
version: :string,
]
]
{overrides, _} = OptionParser.parse!(argv, opts)
mix_config = Mix.Project.config()
user_config = Application.get_all_env(@app)
app_name = mix_config[:app]
ext_name = app_name
|> to_string
|> String.replace("_", "-")
service_name = ext_name
base_dir = user_config[:base_dir] || "/srv"
build_path = Mix.Project.build_path()
{{cur_user, _cur_uid}, {cur_group, _cur_gid}, _} = User.get_id()
defaults = [
mix_env: Mix.env(),
# LANG environment var for running scripts
env_lang: "en_US.UTF-8",
# Elixir application name
app_name: app_name,
# Name of directories
ext_name: ext_name,
# Name of service
service_name: ext_name,
# App version
version: mix_config[:version],
# Base directory on target system
base_dir: base_dir,
# Directory for release files on target
deploy_dir: "#{base_dir}/#{ext_name}",
# Mix build_path
build_path: build_path,
# Staging output directory for generated files
output_dir: Path.join(build_path, @output_dir),
# Directory with templates which override defaults
template_dir: @template_dir,
# OS user to own files and run app
deploy_user: cur_user,
deploy_group: cur_group,
# Whether app uses conform
conform: false,
conform_conf_path: "/etc/#{ext_name}/#{app_name}.conf",
# Target systemd version
systemd_version: 219, # CentOS 7
# systemd_version: 229, # Ubuntu 16.04
# Whether to create /etc/suders.d file allowing deploy or app user to restart app
sudo_deploy: false,
sudo_app: false,
restart_method: :systemctl, # :systemd_flag | :systemctl | :touch
runtime_environment_service: false, # enable and start app runtime-environment.service
dirs: [
:runtime, # needed for runtime-environment or conform
:configuration, # needed for conform or other external app config file
# :logs, # needed for external log file, not journald
# :cache, # app cache files which can be deleted
# :state, # app state persisted between runs
# :tmp, # app temp files
],
# These directories may be automatically created by newer versions of
# systemd, otherwise we need to create them the app uses them
cache_directory: service_name,
cache_directory_base: "/var/cache",
cache_directory_mode: "750",
configuration_directory: service_name,
configuration_directory_base: "/etc",
configuration_directory_mode: "750",
logs_directory: service_name,
logs_directory_base: "/var/log",
logs_directory_mode: "750",
runtime_directory: service_name,
runtime_directory_base: "/run",
runtime_directory_mode: "750",
runtime_directory_preserve: "no",
state_directory: service_name,
state_directory_base: "/var/lib",
state_directory_mode: "750",
tmp_directory: service_name,
tmp_directory_base: "/var/tmp",
tmp_directory_mode: "750",
templates: [
"deploy-clean-target",
"deploy-copy-files",
"deploy-create-dirs",
"deploy-create-users",
"deploy-enable",
"deploy-extract-release",
"deploy-migrate",
"deploy-runtime-environment-file",
"deploy-runtime-environment-wrap",
"deploy-release",
"deploy-remote-console",
"deploy-restart",
"deploy-rollback",
"deploy-set-cookie-ssm",
"deploy-set-perms",
"deploy-start",
"deploy-stop",
"deploy-sync-config-s3",
# "deploy",
]
]
cfg = defaults
|> Keyword.merge(user_config)
|> Keyword.merge(overrides)
# Default OS user and group names
cfg = Keyword.merge([
app_user: cfg[:deploy_user],
app_group: cfg[:deploy_group],
], cfg)
# Mix.shell.info "cfg: #{inspect cfg}"
# Data calculated from other things
Keyword.merge([
releases_dir: Path.join(cfg[:deploy_dir], "releases"),
scripts_dir: Path.join(cfg[:deploy_dir], "bin"),
flags_dir: Path.join(cfg[:deploy_dir], "flags"),
current_dir: Path.join(cfg[:deploy_dir], "current"),
runtime_dir: Path.join(cfg[:runtime_directory_base], cfg[:runtime_directory]),
configuration_dir: Path.join(cfg[:configuration_directory_base], cfg[:configuration_directory]),
logs_dir: Path.join(cfg[:logs_directory_base], cfg[:logs_directory]),
tmp_dir: Path.join(cfg[:tmp_directory_base], cfg[:tmp_directory]),
state_dir: Path.join(cfg[:state_directory_base], cfg[:state_directory]),
cache_dir: Path.join(cfg[:cache_directory_base], cfg[:cache_directory]),
], cfg)
end
end
defmodule Mix.Tasks.Deploy.Init do
@moduledoc """
Initialize template files.
## Command line options
* `--template_dir` - target directory
## Usage
# Copy default templates into your project
mix systemd.init
"""
@shortdoc "Initialize template files"
use Mix.Task
@app :mix_deploy
@spec run(OptionParser.argv()) :: no_return
def run(args) do
cfg = Mix.Tasks.Deploy.parse_args(args)
template_dir = cfg[:template_dir]
app_dir = Application.app_dir(@app, ["priv", "templates"])
:ok = File.mkdir_p(template_dir)
{:ok, _files} = File.cp_r(app_dir, template_dir)
end
end
defmodule Mix.Tasks.Deploy.Generate do
@moduledoc """
Create deploy scripts and files for project.
## Usage
# Create scripts and files for MIX_ENV=dev (the default)
mix deploy.generate
# Create unit files for prod
MIX_ENV=prod mix deploy.generate
"""
@shortdoc "Create deploy scripts and files"
use Mix.Task
alias MixDeploy.Templates
def flags_dir(cfg) do
if cfg[:restart_method] in [:systemd_flag, :touch] do
perms = if cfg[:restart_method] == :touch do
0o770 # app needs to be able to delete file at runtime
else
0o750
end
[{cfg[:flags_dir], cfg[:deploy_user], cfg[:app_group], perms, "Flag files"}]
else
[]
end
end
@spec run(OptionParser.argv()) :: no_return
def run(args) do
cfg = Mix.Tasks.Deploy.parse_args(args)
ext_name = cfg[:ext_name]
output_dir = cfg[:output_dir]
# deploy_user = cfg[:deploy_user]
# # deploy_group = cfg[:deploy_group]
# app_user = cfg[:app_user]
# app_group = cfg[:app_group]
create_flags_dir = cfg[:restart_method] in [:systemd_flag, :touch]
flags_dir_perms = if cfg[:restart_method] == :touch do
0o770 # app needs to be able to delete file at runtime
else
0o750
end
dirs = [
{true, cfg[:deploy_dir], "$DEPLOY_USER", "$APP_GROUP", 0o750, "Base dir"},
{true, cfg[:releases_dir], "$DEPLOY_USER", "$APP_GROUP", 0o750, "Releases"},
{true, cfg[:scripts_dir], "$DEPLOY_USER", "$APP_GROUP", 0o750, "Target scripts"},
{create_flags_dir, cfg[:flags_dir], "$DEPLOY_USER", "$APP_GROUP", flags_dir_perms, "Flag files"}
]
if cfg[:systemd_version] < 235 do
# systemd will automatically create directories in newer versions
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RuntimeDirectory=
[
# We always need runtime dir, as we use it for RELEASE_MUTABLE_DIR
{true, cfg[:runtime_dir], "$APP_USER", "$APP_GROUP", 0o750, "systemd RuntimeDirectory"},
# Needed for conform or other exernal config file
{:configuration in cfg[:dirs], cfg[:configuration_dir], "$DEPLOY_USER", "$APP_GROUP", 0o750, "systemd ConfigurationDirectory"},
{:logs in cfg[:dirs], cfg[:logs_dir], "$APP_USER", "$APP_GROUP", 0o700, "systemd LogsDirectory"},
{:state in cfg[:dirs], cfg[:state_dir], "$APP_USER", "$APP_GROUP", 0o700, "systemd StateDirectory"},
{:cache in cfg[:dirs], cfg[:cache_dir], "$APP_USER", "$APP_GROUP", 0o700, "systemd CacheDirectory"},
# Better handled by PrivateTmp in newer systemd
{:tmp in cfg[:dirs], cfg[:tmp_dir], "$APP_USER", "$APP_GROUP", 0o700, "Temp directory"},
]
else
[]
end
files = [
# {"bin/deploy", Path.join(cfg[:scripts_dir], "deploy"), "$DEPLOY_USER", "$APP_GROUP", 0o750},
# {"bin/remote_console", Path.join(cfg[:scripts_dir], "remote_console"), "$DEPLOY_USER", "$APP_GROUP", 0o750},
]
vars = cfg ++ [create_dirs: dirs, copy_files: files]
for template <- cfg[:templates], do: write_template(vars, "bin", template)
if cfg[:sudo_deploy] or cfg[:sudo_app] do
# Give deploy and/or app user ability to run start/stop commands via sudo
# root:root 600
write_template(cfg, Path.join(output_dir, "/etc/sudoers.d"), "sudoers", ext_name)
end
end
defp write_template(cfg, dest_dir, template), do: write_template(cfg, dest_dir, template, template)
defp write_template(cfg, dest_dir, template, file) do
target_file = Path.join(dest_dir, file)
Mix.shell.info "Generating #{target_file} from template #{template}"
Templates.write_template(cfg, dest_dir, template, file)
end
end
defmodule Mix.Tasks.Deploy.Local do
@shortdoc "Deploy release to local machine"
@moduledoc """
This task deploys a Distillery release to the local machine.
It extracts the release tar to a timestamped directory like
`/srv/:app/releases/20170619175601`, then makes a symlink
from `/srv/:app/current` to it.
This module looks for configuration in the mix project, to get the app and version,
and under the application environment under `mix_deploy`.
* `base_dir` sets the base directory, default `/srv`.
* `deploy_dir` sets the target directory completely manually, ignoring `base_dir` and `app`.
```
"""
use Mix.Task
@spec run(OptionParser.argv()) :: no_return
def run(args) do
# IO.puts (inspect args)
config = Mix.Tasks.Deploy.parse_args(args)
deploy_release(config)
end
@spec deploy_release(Keyword.t) :: no_return
def deploy_release(cfg) do
release_dir = Path.join(cfg[:releases_dir], create_timestamp())
Mix.shell.info "Deploying release to #{release_dir}"
:ok = File.mkdir_p(release_dir)
app = to_string(cfg[:app_name])
tar_file = Path.join([cfg[:build_path], "rel", app, "releases", cfg[:version], "#{app}.tar.gz"])
Mix.shell.info "Extracting tar #{tar_file}"
:ok = :erl_tar.extract(to_charlist(tar_file), [{:cwd, release_dir}, :compressed])
current_link = cfg[:current_path]
if File.exists?(current_link) do
:ok = File.rm(current_link)
end
:ok = File.ln_s(release_dir, current_link)
end
def create_timestamp do
{{year, month, day}, {hour, minute, second}} = :calendar.now_to_universal_time(:os.timestamp())
timestamp = :io_lib.format("~4..0B~2..0B~2..0B~2..0B~2..0B~2..0B", [year, month, day, hour, minute, second])
timestamp |> List.flatten |> to_string
end
end
defmodule Mix.Tasks.Deploy.Local.Rollback do
@moduledoc """
Update current symlink to point to the previous release directory.
"""
use Mix.Task
def run(args) do
cfg = Mix.Tasks.Deploy.parse_args(args)
dirs = cfg[:releases_path] |> File.ls! |> Enum.sort |> Enum.reverse
rollback(dirs, cfg)
end
@spec rollback([Path.t], Keyword.t) :: :ok
defp rollback([_current, prev | _rest], cfg) do
release_path = Path.join(cfg[:releases_path], prev)
current_dir = cfg[:current_dir]
Mix.shell.info "Making link from #{release_path} to #{current_dir}"
:ok = remove_link(current_dir)
:ok = File.ln_s(release_path, current_dir)
end
defp rollback(dirs, _cfg) do
Mix.shell.info "Nothing to roll back to: releases = #{inspect dirs}"
:ok
end
@spec remove_link(Path.t) :: :ok | {:error, :file.posix()}
defp remove_link(current_path) do
case File.read_link(current_path) do
{:ok, target} ->
Mix.shell.info "Removing link from #{target} to #{current_path}"
File.rm(current_path)
{:error, _reason} ->
Mix.shell.info "No current link #{current_path}"
:ok
end
end
end