Skip to main content

lib/mix/tasks/host_kit.instance.ex

defmodule Mix.Tasks.HostKit.Instance do
  @moduledoc """
  Manages lifecycle for a declared HostKit instance.

  This task is backend-neutral at the CLI boundary: it loads an `instance`
  declaration, then delegates to the instance's declared backend.
  Backend-specific operational knobs stay in backend configuration or environment.
  """

  use Mix.Task

  @shortdoc "Manage a declared HostKit instance"

  @impl true
  def run(args) do
    Mix.Task.run("app.start")

    {opts, positional} =
      OptionParser.parse!(args,
        strict: [
          require: :keep
        ]
      )

    case positional do
      [command, name | rest] when command in ["status", "ensure", "destroy"] ->
        path = List.first(rest) || "infra/config.exs"
        project = HostKit.load!(path, require: Keyword.get_values(opts, :require))
        instance = fetch_instance!(project, name)
        run_command(command, instance)

      _other ->
        Mix.raise("expected: mix host_kit.instance status|ensure|destroy INSTANCE [config.exs]")
    end
  end

  defp fetch_instance!(project, name) do
    case HostKit.Project.fetch_instance(project, name) do
      {:ok, instance} -> instance
      :error -> Mix.raise("instance #{inspect(name)} is not declared in #{inspect(project.name)}")
    end
  end

  defp run_command("status", instance) do
    case HostKit.Instance.Backend.read(instance, []) do
      {:ok, %HostKit.Instance{} = actual} ->
        IO.puts("present #{format_instance(actual)}")

      {:ok, nil} ->
        IO.puts("absent #{format_instance(instance)}")

      {:error, reason} ->
        Mix.raise("could not read instance #{instance.name}: #{inspect(reason)}")
    end
  end

  defp run_command("ensure", instance) do
    case HostKit.Instance.Backend.apply(instance, []) do
      :ok ->
        IO.puts("ensured #{format_instance(instance)}")

      {:error, reason} ->
        Mix.raise("could not ensure instance #{instance.name}: #{inspect(reason)}")
    end
  end

  defp run_command("destroy", instance) do
    case HostKit.Instance.Backend.delete(instance, []) do
      :ok ->
        IO.puts("destroyed #{format_instance(instance)}")

      {:error, reason} ->
        Mix.raise("could not destroy instance #{instance.name}: #{inspect(reason)}")
    end
  end

  defp format_instance(%HostKit.Instance{} = instance) do
    "#{instance.name} backend=#{instance.backend || "none"} lifecycle=#{instance.lifecycle}"
  end
end