Current section
Files
Jump to
Current section
Files
lib/mix/tasks/dockerate.ex
defmodule Mix.Tasks.Dockerate do
use Mix.Task
@default_base_image "elixir:latest"
@ssh_agent_image "nardeas/ssh-agent:latest"
@shortdoc "Assemble a Docker image"
def run(args) do
# Determine app name
app = Mix.Project.config |> Keyword.get(:app)
version = Mix.Project.config |> Keyword.get(:version)
info "Assembling a Docker image for app #{app} #{version}, env = #{Mix.env}..."
# Determine release name
# FIXME read this from rel/config.exs
rel_name = app
# Determine base image
base_image =
case Mix.Project.config |> Keyword.get(:dockerator_base_image) do
nil ->
@default_base_image
image when is_binary(image) ->
image
other ->
error "Invalid base image #{inspect(other)}"
Kernel.exit(:invalid_base_image)
end
info "Using #{base_image} as a base Docker image"
# Determine target tag
target_tag = case args do
["release"] ->
version
[] ->
"latest"
other ->
error "Invalid argument #{inspect(other)}, please pass \"release\" or leave it blank"
Kernel.exit(:invalid_argument)
end
# Determine target image
target_image =
case Mix.Project.config |> Keyword.get(:dockerator_target_image) do
nil ->
error "Target image is unset, please set :dockerator_target_image in the app's config in mix.exs"
Kernel.exit(:unset_target_image)
image when is_binary(image) ->
image
other ->
error "Invalid target image #{inspect(other)}"
Kernel.exit(:invalid_target_image)
end
target_image_build = "#{target_image}/build:#{target_tag}"
target_image_release = "#{target_image}:#{target_tag}"
info "Using #{target_image_release} as a target Docker image"
# Determine extra release commands
release_extra_docker_commands =
case Mix.Project.config |> Keyword.get(:dockerator_release_extra_docker_commands) do
nil ->
[]
other when is_list(other) ->
info "Using #{inspect(other)} as a list of extra commands for the release Docker image"
other
other ->
error "Invalid extra release commands #{inspect(other)}"
Kernel.exit(:invalid_release_extra_docker_commands)
end
# Determine templates' path
templates_path =
Mix.Project.deps_paths[:dockerator]
|> Path.join("priv")
|> Path.join("templates")
# Determine build path
build_path =
Mix.Project.build_path
|> Path.join("dockerator")
build_output_path =
build_path
|> Path.join("output")
build_output_path_relative =
build_output_path
|> Path.relative_to_cwd
build_scripts_path =
build_path
|> Path.join("scripts")
# Create directory for build files
with \
:ok <- File.mkdir_p(build_path),
{:ok, _} <- File.rm_rf(build_output_path),
:ok <- File.mkdir_p(build_output_path),
:ok <- File.mkdir_p(build_scripts_path)
do
info "Using #{build_path} as a temporary build path"
else
e ->
error "Failed to create build directory #{build_path}: #{inspect(e)}"
Kernel.exit(:failed_build_directory)
end
# Check if we're using any git dependencies
git_deps_urls =
Mix.Project.config
|> Keyword.get(:deps)
|> Enum.filter(fn({_dep_name, dep_config}) ->
Keyword.has_key?(dep_config, :git)
end)
|> Enum.map(fn({_dep_name, dep_config}) ->
uri = Keyword.get(dep_config, :git)
case URI.parse(uri) do
%URI{scheme: nil} ->
"ssh://#{uri}"
other ->
other
end
end)
case git_deps_urls do
[] ->
info "Found no git dependencies"
other ->
info "Found the following git dependencies:"
Enum.each(other, fn(git_dep_url) ->
info " #{git_dep_url}"
end)
end
# Generate scripts from templates
dockerfile_build =
Path.join(templates_path, "build.Dockerfile.eex")
|> EEx.eval_file([base_image: base_image, mix_env: Mix.env, git_deps_urls: git_deps_urls])
dockerfile_release =
Path.join(templates_path, "release.Dockerfile.eex")
|> EEx.eval_file([base_image: base_image, mix_env: Mix.env, build_output_path_relative: build_output_path_relative, release_extra_docker_commands: release_extra_docker_commands, rel_name: rel_name])
# Remove empty lines in Dockerfile as they're deprecated
dockerfile_build = Regex.replace(~r/\n+/, dockerfile_build, "\n")
dockerfile_release = Regex.replace(~r/\n+/, dockerfile_release, "\n")
dockerfile_build_path =
build_scripts_path
|> Path.join("build.Dockerfile")
dockerfile_release_path =
build_scripts_path
|> Path.join("release.Dockerfile")
with \
:ok <- File.write(dockerfile_build_path, dockerfile_build),
:ok <- File.write(dockerfile_release_path, dockerfile_release)
do
info "Succesfully generated build scripts"
else
e ->
error "Failed to generate build scripts: #{inspect(e)}"
Kernel.exit(:failed_build_scripts)
end
# Check if we need a SSH agent
ssh_agent =
case Mix.Project.config |> Keyword.get(:dockerator_ssh_agent) do
nil ->
false
other when is_boolean(other) ->
other
other ->
error "Invalid SSH agent setting #{inspect(other)}"
Kernel.exit(:invalid_ssh_agent)
end
ssh_agent_docker_name =
String.replace(target_image, "/", "_") <> "-sshagent"
if ssh_agent do
case System.cmd "docker", ["inspect", "-f", "'{{.State.Running}}'", ssh_agent_docker_name] do
{"'true'\n", 0} ->
info "SSH agent seems to be already running"
info " If you want to clean it, kill it by invoking the following command:"
info " docker rm #{ssh_agent_docker_name} -f"
{"'false'\n", 0} ->
info "SSH agent seems to be present but not"
error "TODO: This is not supported yet"
Kernel.exit(:todo)
{"\n", 1} ->
info "Starting SSH agent (#{ssh_agent_docker_name})"
case docker_cmd_passthrough ["run", "-d", "--name=#{ssh_agent_docker_name}", @ssh_agent_image] do
:ok ->
case :os.type do
{:unix, :darwin} ->
info "Adding your SSH keys to the SSH agent."
info " Please type password for your SSH keys in the new Terminal window if they're password-protected."
# This hack is necessary because it is very hard to call
# PTY-enabled command from Elixir/Erlang, so we just spawn new
# Terminal window in case of users' SSH keys are
# password-protected.
#
# The last line kills the terminal window so we don't wait for
# Command+Q.
tmp_script_path = "/tmp/#{ssh_agent_docker_name}.sh"
tmp_script_body = """
#!/bin/sh
docker run --rm --volumes-from=#{ssh_agent_docker_name} -v ~/.ssh:/.ssh -it #{@ssh_agent_image} ssh-add /root/.ssh/id_rsa
kill -9 $(ps -p $(ps -p $PPID -o ppid=) -o ppid=)
"""
File.write!(tmp_script_path, tmp_script_body)
File.chmod!(tmp_script_path, 0o700)
System.cmd "open", ["-W", "-a", "Terminal.app", tmp_script_path]
File.rm!(tmp_script_path)
_ ->
error "TODO: This operating system is not supported yet"
Kernel.exit(:todo)
end
{:error, code} ->
error "Docker run returned error code #{code}"
Kernel.exit(:failed_docker_run_ssh_agent)
end
end
end
# Phase 1: Prepare build image
info "Building image for the build phase (#{target_image_build})"
case docker_cmd_passthrough ["build", "-t", target_image_build, "-f", dockerfile_build_path, "."] do
:ok ->
info "Built image for the build phase"
{:error, code} ->
error "Docker build returned error code #{code}"
Kernel.exit(:failed_docker_build)
end
# Phase 2: Prepare release
info "Building release"
release_docker_extra_args = if ssh_agent do
["--volumes-from=#{ssh_agent_docker_name}", "-e", "SSH_AUTH_SOCK=/.ssh-agent/socket"]
else
[]
end
release_docker_args = ["run"] ++
release_docker_extra_args ++
[
"--mount", "type=bind,source=#{build_output_path},target=/root/output",
"--rm",
"-t", target_image_build,
"sh", "-c", "(mix deps.get && mix compile && mix release && mv -v _build/#{Mix.env}/rel/#{rel_name} /root/output/app/)"
]
case docker_cmd_passthrough release_docker_args do
:ok ->
info "Built release"
{:error, code} ->
error "Release returned error code #{code}"
Kernel.exit(:failed_release)
end
# Phase 3: Prepare release image
info "Building image for the release phase (#{target_image_release})"
case docker_cmd_passthrough ["build", "-t", target_image_release, "-f", dockerfile_release_path, "."] do
:ok ->
info "Built image for the release phase"
{:error, code} ->
error "Docker build returned error code #{code}"
Kernel.exit(:failed_docker_build)
end
info "Done. Your app is bundled in the image #{target_image_release}."
end
defp docker_cmd_passthrough(args) do
case System.cmd("docker", args, into: IO.binstream(:stdio, :line)) do
{_, 0} ->
:ok
{_, code} ->
{:error, code}
end
end
defp info(message) do
Mix.Shell.IO.info "[Dockerator INFO] #{message}"
end
defp error(message) do
Mix.Shell.IO.error "[Dockerator ERROR] #{message}"
end
end