defmodule Edeliver do
@moduledoc """
Execute edeliver tasks on the production / staging nodes.
This internal module provides functions on the nodes which are
used by some edeliver tasks e.g. to get the running release version
(`edeliver version`), show the pending migrations
(`edeliver show migrations`) or install pending migrations
(`edeliver migrate`).
In addition it represents the edeliver application callback module
and starts a process registered locally as `Edeliver` which's onliest
purpose is to be able to detect whether the release was successfully
started. This requires to start edeliver as last application in the
release.
"""
use Application
use GenServer
@doc """
Starts the edeliver application
including the `Edeliver.Supervisor` process supervising the
`Edeliver` generic server.
"""
@spec start(term, term) :: {:ok, pid}
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [worker(__MODULE__, [], [name: __MODULE__])]
options = [strategy: :one_for_one, name: Edeliver.Supervisor]
Supervisor.start_link(children, options)
end
@doc "Starts this gen-server registered locally as `Edeliver`"
@spec start_link() :: {:ok, pid}
def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
@doc """
Runs the edeliver command on the erlang node
started as:
```
bin/$APP rpc 'Elixir.Edeliver.run_command('[command_name, \"$APP\", arguments...])'
```
The first argument must be the name of the command, the second argument the
name of the main application and all further arguments are passed to the
function that's name is equal to the command name.
"""
@spec run_command(args::[term]) :: no_return
def run_command([:monitor_startup_progress, application_name, :verbose]) do
:error_logger.add_report_handler Edeliver.StartupProgress
monitor_startup_progress(application_name)
:error_logger.delete_report_handler Edeliver.StartupProgress
end
def run_command([:monitor_startup_progress, application_name | _]) do
monitor_startup_progress(application_name)
end
def run_command([command_name, application_name | arguments]) when is_atom(command_name) do
application_name = String.to_atom(application_name)
{^application_name, _description, application_version} = :application.which_applications |> List.keyfind(application_name, 0)
application_version = to_string application_version
apply __MODULE__, command_name, [application_name, application_version | arguments]
end
@doc """
Waits until the edeliver application is started.
If the edeliver application is added as last application in the `:applications` section of
the `application/0` fun in the `mix.exs` this waits until all applications are started.
This can be used as rpc call after running the asynchronous `bin/$APP start` command to
wait until all applications started and then return `ok`.
"""
@spec monitor_startup_progress(application_name::atom) :: :ok
def monitor_startup_progress(application_name) do
edeliver_pid = Process.whereis __MODULE__
if is_pid(edeliver_pid) do
:ok
else
receive do after 500 -> :ok end
monitor_startup_progress(application_name)
end
end
@doc """
Returns the running release version
which is either the `:current` version or the `:permanent` version.
"""
@spec release_version(application_name::atom, application_version::String.t) :: String.t
def release_version(application_name, _application_version \\ nil) do
releases = :release_handler.which_releases
application_name = Atom.to_charlist application_name
case (for {name, version, _apps, status} <- releases, status == :current and name == application_name, do: to_string(version)) do
[current_version] -> current_version
_ ->
case (for {name, version, _apps, status} <- releases, status == :permanent and name == application_name, do: to_string(version)) do
[permanent_version] -> String.to_charlist(permanent_version)
end
end
end
@doc """
Prints the pending ecto migrations
"""
def list_pending_migrations(application_name, application_version, ecto_repository \\ '') do
repository = ecto_repository!(application_name, ecto_repository)
migrator = Ecto.Migrator
versions = migrator.migrated_versions(repository)
pending_migrations = migrations_for(migrations_dir(application_name, application_version))
|> Enum.filter(fn {version, _name, _file} -> not (version in versions) end)
|> Enum.reverse
|> Enum.map(fn {version, name, _file} -> {version, name} end)
pending_migrations |> Enum.each(fn {version, name} ->
warning "pending: #{name} (#{version})"
end)
end
@doc """
Runs the pending ecto migrations
"""
def migrate(application_name, application_version, ecto_repository, direction, migration_version \\ :all) when is_atom(direction) do
options = if migration_version == :all, do: [all: true], else: [to: to_string(migration_version)]
migrator = Ecto.Migrator
migrator.run(ecto_repository!(application_name, ecto_repository), migrations_dir(application_name, application_version), direction, options)
end
@doc """
Returns the current directory containing the ecto migrations.
"""
def migrations_dir(application_name, application_version) do
# use priv dir from installed version
lib_dir = :code.priv_dir(application_name) |> to_string |> Path.dirname |> Path.dirname
application_with_version = "#{Atom.to_string(application_name)}-#{application_version}"
Path.join([lib_dir, application_with_version, "priv", "repo", "migrations"])
end
def init(args) do
{:ok, args}
end
defp ecto_repository!(_application_name, ecto_repository = [_|_] ) do
# repository name was passed as ECTO_REPOSITORY env by the erlang-node-execute rpc call
List.to_atom ecto_repository
end
defp ecto_repository!(application_name, _ecto_repository) do
case System.get_env "ECTO_REPOSITORY" do # ECTO_REPOSITORY env was set when the node was started
ecto_repository = <<_,_::binary>> ->
ecto_repository_module = ecto_repository |> to_charlist |> List.to_atom
if maybe_ecto_repo?(ecto_repository_module) do
ecto_repository_module
else
error! "Module '#{ecto_repository_module}' is not an ecto repository.\n Please set the correct repository module in the edeliver config as ECTO_REPOSITORY env\n or remove that value to use autodetection of that module."
end
_ ->
case ecto_repos_from_config(application_name) do
{:ok, [ecto_repository_module]} -> ecto_repository_module
{:ok, modules =[_|_]} -> error! "Found several ecto repository modules (#{inspect modules}).\n Please specify the repository to use in the edeliver config as ECTO_REPOSITORY env."
:error ->
case Enum.filter(:erlang.loaded |> Enum.reverse, &ecto_1_0_repo?/1) do
[ecto_repository_module] -> ecto_repository_module
[] -> error! "No ecto repository module found.\n Please specify the repository in the edeliver config as ECTO_REPOSITORY env."
modules =[_|_] -> error! "Found several ecto repository modules (#{inspect modules}).\n Please specify the repository to use in the edeliver config as ECTO_REPOSITORY env."
end
end
end
end
defp ecto_repos_from_config(application_name) do
Application.fetch_env(application_name, :ecto_repos)
end
defp maybe_ecto_repo?(module) do
if :erlang.module_loaded(module) do
exports = module.module_info(:exports)
# :__adapter__ for ecto versions >= 2.0, :__repo__ for ecto versions < 2.0
Keyword.get(exports, :__adapter__, nil) || Keyword.get(exports, :__repo__, false)
else
false
end
end
defp ecto_1_0_repo?(module) do
if :erlang.module_loaded(module) do
module.module_info(:exports)
|> Keyword.get(:__repo__, false)
else
false
end
end
# taken from https://github.com/elixir-lang/ecto/blob/master/lib/ecto/migrator.ex#L183
defp migrations_for(directory) do
query = Path.join(directory, "*")
for entry <- Path.wildcard(query),
info = extract_migration_info(entry),
do: info
end
defp extract_migration_info(file) do
base = Path.basename(file)
ext = Path.extname(base)
case Integer.parse(Path.rootname(base)) do
{integer, "_" <> name} when ext == ".exs" -> {integer, name, file}
_ -> nil
end
end
# defp info(message), do: IO.puts "==> #{IO.ANSI.green}#{message}#{IO.ANSI.reset}"
defp warning(message), do: IO.puts "==> #{IO.ANSI.yellow}#{message}#{IO.ANSI.reset}"
defp error!(message) do
IO.puts "==> #{IO.ANSI.red}#{message}#{IO.ANSI.reset}"
throw "error"
end
end