Skip to main content

lib/compose/docker_compose.ex

defmodule TestcontainerEx.DockerCompose do
  @moduledoc """
  A struct with builder functions for creating a Docker Compose configuration.
  """

  defstruct [
    :filepath,
    compose_files: [],
    project_name: nil,
    env: %{},
    wait_strategies: %{},
    wait_timeout: 120_000,
    pull: :missing,
    services: [],
    build: false,
    profiles: [],
    remove_volumes: true
  ]

  @doc """
  Creates a new DockerCompose configuration.

  The `filepath` can be a path to a directory containing a docker-compose.yml file,
  or a path to a specific compose file.
  """
  def new(filepath) when is_binary(filepath) do
    %__MODULE__{
      filepath: filepath,
      project_name: generate_project_name()
    }
  end

  @doc """
  Sets an environment variable for the compose environment.
  """
  def with_env(%__MODULE__{} = config, key, value)
      when (is_binary(key) or is_atom(key)) and is_binary(value) do
    %__MODULE__{config | env: Map.put(config.env, to_string(key), value)}
  end

  @doc """
  Sets a wait strategy for a specific service.
  """
  def with_wait_strategy(%__MODULE__{} = config, service_name, wait_strategy)
      when is_binary(service_name) and is_struct(wait_strategy) do
    strategies = Map.get(config.wait_strategies, service_name, [])

    %__MODULE__{
      config
      | wait_strategies:
          Map.put(config.wait_strategies, service_name, [wait_strategy | strategies])
    }
  end

  @doc """
  Sets the specific services to start. If empty, all services are started.
  """
  def with_services(%__MODULE__{} = config, services) when is_list(services) do
    %__MODULE__{config | services: services}
  end

  @doc """
  Sets whether to build images before starting containers.
  """
  def with_build(%__MODULE__{} = config, build) when is_boolean(build) do
    %__MODULE__{config | build: build}
  end

  @doc """
  Adds a profile to enable when starting compose.
  """
  def with_profile(%__MODULE__{} = config, profile) when is_binary(profile) do
    %__MODULE__{config | profiles: [profile | config.profiles]}
  end

  @doc """
  Sets the pull policy for compose services.
  """
  def with_pull(%__MODULE__{} = config, pull) when pull in [:always, :missing, :never] do
    %__MODULE__{config | pull: pull}
  end

  @doc """
  Sets whether to remove volumes when stopping compose.
  """
  def with_remove_volumes(%__MODULE__{} = config, remove_volumes)
      when is_boolean(remove_volumes) do
    %__MODULE__{config | remove_volumes: remove_volumes}
  end

  @doc """
  Sets the wait timeout in milliseconds.
  """
  def with_wait_timeout(%__MODULE__{} = config, timeout)
      when is_integer(timeout) and timeout > 0 do
    %__MODULE__{config | wait_timeout: timeout}
  end

  @doc """
  Sets the project name for the compose environment.
  """
  def with_project_name(%__MODULE__{} = config, project_name) when is_binary(project_name) do
    %__MODULE__{config | project_name: project_name}
  end

  @doc """
  Adds additional compose files to use with the -f flag.
  """
  def with_compose_file(%__MODULE__{} = config, file) when is_binary(file) do
    %__MODULE__{config | compose_files: config.compose_files ++ [file]}
  end

  defp generate_project_name do
    hex =
      :crypto.strong_rand_bytes(8)
      |> Base.encode16(case: :lower)
      |> binary_part(0, 12)

    "tc-#{hex}"
  end
end