defmodule Mix.Tasks.HostKit.Plan do
@moduledoc """
Builds and prints a HostKit plan.
Prefer `--host NAME config.exs` for remote targets declared with HostKit's
`host` DSL. Raw SSH flags (`--remote`, `--user`, `--port`, `--identity-file`,
`--password-env`) are available as an escape hatch.
Pass `--local` for read-only local inspection.
"""
use Mix.Task
alias Mix.Tasks.HostKit.Options
@shortdoc "Plan HostKit resources"
@impl true
def run(args) do
Mix.Task.run("app.start")
{opts, positional} =
OptionParser.parse!(args,
strict: [
local: :boolean,
host: :string,
remote: :string,
user: :string,
port: :integer,
identity_file: :string,
password: :string,
password_env: :string,
silently_accept_hosts: :boolean,
sudo: :boolean,
require: :keep,
format: :string,
out: :string,
service: :keep,
ignore: :keep,
package_lock: :string,
write_package_lock: :string,
repology_cache: :string,
repology_cache_ttl: :integer,
repology_no_cache: :boolean,
show_graph: :boolean,
graph_format: :string
]
)
path = List.first(positional) || "infra/config.exs"
project = HostKit.load!(path, require: Keyword.get_values(opts, :require))
Options.with_target_opts(opts, project, fn target_opts ->
case HostKit.plan(project, plan_opts(opts, target_opts)) do
{:ok, plan} ->
maybe_write_artifact(plan, opts, target_opts)
IO.puts(Mix.Tasks.HostKit.Output.format_plan(plan, opts))
{:error, %HostKit.Diagnostics{} = diagnostics} ->
Mix.raise(HostKit.Diagnostics.Format.format(diagnostics))
{:error, reason} ->
Mix.raise("HostKit plan failed: #{inspect(reason)}")
end
end)
end
defp maybe_write_artifact(plan, opts, target_opts) do
case Keyword.get(opts, :out) do
nil ->
:ok
path ->
case HostKit.Plan.Artifact.save(path, plan,
target_metadata: target_metadata(plan, opts, target_opts)
) do
:ok ->
:ok
{:error, reason} ->
Mix.raise("could not write HostKit plan artifact: #{inspect(reason)}")
end
end
end
defp target_metadata(plan, _opts, target_opts) do
%{}
|> put_metadata("kind", target_kind(target_opts))
|> put_metadata("package_manager", package_manager(plan, target_opts))
|> put_metadata("package_repo", package_repo(plan, target_opts))
end
defp target_kind(target_opts) do
cond do
Keyword.has_key?(target_opts, :target) -> "remote"
Keyword.get(target_opts, :reader) == HostKit.Local -> "local"
true -> nil
end
end
defp package_manager(plan, target_opts) do
plan.opts
|> Keyword.get(:package_manager, Keyword.get(target_opts, :package_manager))
|> case do
nil -> nil
manager -> to_string(manager)
end
end
defp package_repo(plan, target_opts) do
case Keyword.get(target_opts, :package_repo) do
repo when is_binary(repo) -> resolved_package_repo(repo)
_other -> resolved_package_repo(plan)
end
end
defp resolved_package_repo(repo) when is_binary(repo), do: repo
defp resolved_package_repo(plan) do
plan.resources
|> Enum.flat_map(fn
%HostKit.Resources.Package{meta: %{resolution: %{repo: repo}}} when is_binary(repo) ->
[repo]
_resource ->
[]
end)
|> Enum.uniq()
|> case do
[repo] -> repo
_other -> nil
end
end
defp put_metadata(metadata, _key, nil), do: metadata
defp put_metadata(metadata, key, value), do: Map.put(metadata, key, value)
defp plan_opts(opts, target_opts) do
target_opts
|> Options.expand_target_opts()
|> Keyword.put(:ignore, Options.ignored_resources(opts))
|> put_present(:services, Options.selected_services(opts))
|> put_package_lock(opts)
end
defp put_package_lock(plan_opts, opts) do
plan_opts
|> put_present(:package_lock, Keyword.get(opts, :package_lock))
|> put_present(:package_lock_write, Keyword.get(opts, :write_package_lock))
|> Options.put_repology_cache(opts)
end
defp put_present(opts, _key, nil), do: opts
defp put_present(opts, key, value), do: Keyword.put(opts, key, value)
end