defmodule Mix.Tasks.HostKit.Read do
@moduledoc """
Reads current target state for the resources declared by a HostKit config.
mix host_kit.read [options] [config.exs]
Use `--local` for local reads or `--host NAME config.exs` for a declared remote host.
"""
use Mix.Task
alias Mix.Tasks.HostKit.Options
@shortdoc "Read current state for declared HostKit resources"
@impl true
def run(args) do
Mix.Task.run("app.start")
{opts, positional} = parse!(args)
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.Project.audit(project, target_opts) do
{:ok, plan} -> IO.puts(format_read(plan, opts))
{:error, reason} -> Mix.raise("HostKit read failed: #{inspect(reason)}")
end
end)
end
defp parse!(args) do
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
]
)
end
defp format_read(plan, opts) do
case Keyword.get(opts, :format, "text") do
"json" ->
%{
report: read_report(plan),
resources:
Enum.map(plan.changes, fn change ->
%{
resource_id: HostKit.Resource.dump(change.resource_id),
status: read_status(change),
action: change.action,
before: HostKit.Resource.dump(change.before)
}
end)
}
|> Jason.encode_to_iodata!(pretty: true)
"inspect" ->
%{report: read_report(plan), resources: Enum.map(plan.changes, & &1.before)}
|> inspect(pretty: true, limit: :infinity, structs: true)
"text" ->
[format_read_report(plan), "\n", Enum.map_join(plan.changes, "\n", &format_read_change/1)]
format ->
Mix.raise("unknown --format #{inspect(format)}, expected text, inspect, or json")
end
end
defp read_report(plan) do
actions = HostKit.Plan.Summary.action_counts(plan)
%{
managed_resources: length(plan.resources),
resources_by_type: HostKit.Plan.Summary.resource_counts(plan),
present:
Map.fetch!(actions, "no_op") + Map.fetch!(actions, "update") +
Map.fetch!(actions, "delete"),
missing: Map.fetch!(actions, "create"),
read_errors: Map.fetch!(actions, "read")
}
end
defp format_read_report(plan) do
report = read_report(plan)
[
"Read: ",
to_string(report.present),
" present, ",
to_string(report.missing),
" missing, ",
to_string(report.read_errors),
" read errors across ",
to_string(report.managed_resources),
" managed resources",
"\nResources: ",
Mix.Tasks.HostKit.Output.format_counts(report.resources_by_type)
]
end
defp format_read_change(change) do
"#{format_resource_id(change.resource_id)} #{read_status(change)}"
end
defp read_status(%{action: :read}), do: "read_error"
defp read_status(%{action: :create}), do: "missing"
defp read_status(%{before: nil}), do: "missing"
defp read_status(_change), do: "present"
defp format_resource_id({type, name}), do: "#{type}.#{name}"
defp format_resource_id(resource_id), do: inspect(resource_id)
end