lib/container/ceph_container.ex

# SPDX-License-Identifier: MIT
defmodule Testcontainers.Container.CephContainer do
  @moduledoc """
  Module for building Ceph container configurations.

  This module provides functions for creating and manipulating configurations for Ceph containers.
  It allows the setting of specific parameters like image, access keys, secret keys,
  and other parameters related to the Ceph container.
  """

  alias Testcontainers.WaitStrategy.LogWaitStrategy
  alias Testcontainers.Container.CephContainer
  alias Testcontainers.ContainerBuilder
  alias Testcontainers.Container

  @default_image "quay.io/ceph/demo"
  @default_tag "latest-quincy"
  @default_port 8080
  @wait_timeout 300_000

  defstruct image: "#{@default_image}:#{@default_tag}",
            wait_timeout: @wait_timeout,
            port: @default_port,
            access_key: "test",
            secret_key: "test",
            bucket: "test"

  @doc """
  Creates a new `CephContainer` struct with default attributes.
  """
  def new, do: %__MODULE__{}

  @doc """
  Sets the `image` of the Ceph container configuration.

  ## Examples

      iex> config = CephContainer.new()
      iex> new_config = CephContainer.with_image(config, "quay.io/ceph/alternative")
      iex> new_config.image
      "quay.io/ceph/alternative"
  """
  def with_image(%__MODULE__{} = config, image) when is_binary(image) do
    %{config | image: image}
  end

  @doc """
  Sets the `access_key` used for authentication with the Ceph container.

  ## Examples

      iex> config = CephContainer.new()
      iex> new_config = CephContainer.with_access_key(config, "new_access_key")
      iex> new_config.access_key
      "new_access_key"
  """
  def with_access_key(%__MODULE__{} = config, access_key) when is_binary(access_key) do
    %{config | access_key: access_key}
  end

  @doc """
  Sets the `secret_key` used for authentication with the Ceph container.

  ## Examples

      iex> config = CephContainer.new()
      iex> new_config = CephContainer.with_secret_key(config, "new_secret_key")
      iex> new_config.secret_key
      "new_secret_key"
  """
  def with_secret_key(%__MODULE__{} = config, secret_key) when is_binary(secret_key) do
    %{config | secret_key: secret_key}
  end

  @doc """
  Sets the `bucket` that is automatically in the Ceph container.

  ## Examples

      iex> config = CephContainer.new()
      iex> new_config = CephContainer.with_bucket(config, "test_bucket")
      iex> new_config.bucket
      "test_bucket"
  """
  def with_bucket(%__MODULE__{} = config, bucket) when is_binary(bucket) do
    %{config | bucket: bucket}
  end

  @doc """
  Sets the port on which the Ceph container will be exposed.

  ## Parameters

  - `config`: The current Ceph container configuration.
  - `port`: The target port number.

  ## Examples

      iex> config = CephContainer.new()
      iex> new_config = CephContainer.with_port(config, 8081)
      iex> new_config.port
      8081
  """
  def with_port(%__MODULE__{} = config, port) when is_integer(port) do
    %{config | port: port}
  end

  @doc """
  Sets the maximum time (in milliseconds) the system will wait for the Ceph container to be ready before timing out.

  ## Parameters

  - `config`: The current Ceph container configuration.
  - `wait_timeout`: The time to wait in milliseconds.

  ## Examples

      iex> config = CephContainer.new()
      iex> new_config = CephContainer.with_wait_timeout(config, 400_000)
      iex> new_config.wait_timeout
      400_000
  """
  def with_wait_timeout(%__MODULE__{} = config, wait_timeout) when is_integer(wait_timeout) do
    %{config | wait_timeout: wait_timeout}
  end

  @doc """
  Retrieves the default Docker image used for the Ceph container.

  ## Examples

      iex> CephContainer.default_image()
      "quay.io/ceph/demo"
  """
  def default_image, do: @default_image

  @doc """
  Retrieves the port mapped by the Docker host for the Ceph container.

  ## Parameters

  - `container`: The active Ceph container instance.

  ## Examples

      iex> CephContainer.port(container)
      32768 # This value will be different depending on the mapped port.
  """
  def port(%Container{} = container), do: Container.mapped_port(container, @default_port)

  @doc """
  Generates the connection URL for accessing the Ceph service running within the container.

  This URL is based on the standard localhost IP and the mapped port for the container.

  ## Parameters

  - `container`: The active Ceph container instance.

  ## Examples

      iex> CephContainer.connection_url(container)
      "http://localhost:32768" # This value will be different depending on the mapped port.
  """
  def connection_url(%Container{} = container) do
    "http://localhost:#{port(container)}"
  end

  defimpl ContainerBuilder do
    import Container

    @doc """
    Implementation of the `ContainerBuilder` protocol for `CephContainer`.

    This implementation provides the logic for building a container configuration specific to Ceph. It ensures the provided image is compatible, sets up necessary environment variables, configures network settings, and applies a waiting strategy to ensure the container is fully operational before it's used.

    The build process raises an `ArgumentError` if the specified container image is not compatible with the expected Ceph image.

    ## Examples

        # Assuming `ContainerBuilder.build/2` is called from somewhere in the application with a `CephContainer` configuration:
        iex> config = CephContainer.new()
        iex> built_container = ContainerBuilder.build(config, [])
        # `built_container` is now a ready-to-use `%Container{}` configured specifically for Ceph.

    ## Errors

    - Raises `ArgumentError` if the provided image is not compatible with the default Ceph image.
    """
    @spec build(%CephContainer{}, keyword()) :: %Container{}
    @impl true
    def build(%CephContainer{} = config, _options) do
      if not String.starts_with?(config.image, CephContainer.default_image()) do
        raise ArgumentError,
          message: "Image #{config.image} is not compatible with #{CephContainer.default_image()}"
      end

      new(config.image)
      |> with_exposed_port(config.port)
      |> with_environment(:CEPH_DEMO_UID, "demo")
      |> with_environment(:CEPH_DEMO_BUCKET, config.bucket)
      |> with_environment(:CEPH_DEMO_ACCESS_KEY, config.access_key)
      |> with_environment(:CEPH_DEMO_SECRET_KEY, config.secret_key)
      |> with_environment(:CEPH_PUBLIC_NETWORK, "0.0.0.0/0")
      |> with_environment(:MON_IP, "127.0.0.1")
      |> with_environment(:RGW_NAME, "localhost")
      |> with_waiting_strategy(
        LogWaitStrategy.new(
          ~r/.*Bucket 's3:\/\/#{config.bucket}\/' created.*/,
          config.wait_timeout,
          5000
        )
      )
    end
  end
end