Skip to main content

lib/testcontainer_ex/connection/resolver.ex

defmodule TestcontainerEx.Connection.Resolver do
  @moduledoc """
  Orchestrates host resolution strategies.

  When an explicit engine is requested, only that strategy is tried.
  When `:auto` (default), tries all strategies in priority order until one succeeds.

  Returns `{:ok, url}` or `{:error, reasons}`.
  """

  alias TestcontainerEx.Connection.Strategies

  @type engine ::
          :auto
          | :docker
          | :podman
          | :colima
          | :minikube
          | :apple_container

  # Default auto-detection order.
  # Apple Container is placed late because it requires the `container` binary
  # and socket to both be present; without that check it can claim the engine
  # even when Docker/Colima is the intended runtime.
  @strategies [
    Strategies.Properties,
    Strategies.Env,
    Strategies.Dotenv,
    Strategies.ContainerEnv,
    Strategies.Colima,
    Strategies.Socket,
    Strategies.Minikube,
    Strategies.AppleContainer
  ]

  # Map engine atoms to their strategy modules.
  @engine_strategies %{
    docker: [Strategies.Socket, Strategies.Colima, Strategies.ContainerEnv],
    podman: [Strategies.Socket, Strategies.ContainerEnv],
    colima: [Strategies.Colima],
    minikube: [Strategies.Minikube],
    apple_container: [Strategies.AppleContainer]
  }

  @doc """
  Resolves the container engine host URL.

  ## Options

    * `:engine` — explicitly select an engine:
      * `:auto` (default) — try all strategies in priority order
      * `:docker` — Docker Desktop, Colima, or socket-based Docker
      * `:podman` — Podman socket or container env
      * `:colima` — Colima only
      * `:minikube` — Minikube only
      * `:apple_container` — Apple Container only

  The engine can also be set via the `CONTAINER_ENGINE` environment variable
  (e.g. `CONTAINER_ENGINE=docker`). The explicit option takes precedence.
  """
  @spec resolve(keyword()) :: {:ok, String.t()} | {:error, [term()]}
  def resolve(options \\ []) do
    engine = Keyword.get(options, :engine, engine_from_env())

    strategies =
      case engine do
        :auto -> @strategies
        atom when is_atom(atom) -> Map.get(@engine_strategies, atom, @strategies)
      end

    do_resolve(strategies, [])
  end

  defp engine_from_env do
    case System.get_env("CONTAINER_ENGINE") do
      nil -> :auto
      "" -> :auto
      "auto" -> :auto
      "docker" -> :docker
      "podman" -> :podman
      "colima" -> :colima
      "minikube" -> :minikube
      "apple_container" -> :apple_container
      _ -> :auto
    end
  end

  defp do_resolve([], errors), do: {:error, Enum.reverse(errors)}

  defp do_resolve([strategy | rest], errors) do
    case strategy.resolve() do
      {:ok, url} -> {:ok, url}
      {:error, reason} -> do_resolve(rest, [reason | errors])
    end
  end
end