defmodule Testcontainers.RabbitMQContainer do @moduledoc """ Provides functionality for creating and managing RabbitMQ container configurations. NOTE: The default starting command is `chmod 400 /var/lib/rabbitmq/.erlang.cookie; rabbitmq-server`. `chmod 400 /var/lib/rabbitmq/.erlang.cookie` is necessary for the waiting strategy, which calls the command `rabbitmq-diagnostics check_running`; otherwise CLI tools cannot communicate with the RabbitMQ node. """ alias Testcontainers.ContainerBuilder alias Testcontainers.Container alias Testcontainers.CommandWaitStrategy alias Testcontainers.RabbitMQContainer import Testcontainers.Container, only: [is_valid_image: 1] @default_image "rabbitmq" @default_tag "3-alpine" @default_image_with_tag "#{@default_image}:#{@default_tag}" @default_port 5672 @default_username "guest" @default_password "guest" @default_virtual_host "/" @default_command [ "sh", "-c", "chmod 400 /var/lib/rabbitmq/.erlang.cookie; rabbitmq-server" ] @default_wait_timeout 60_000 @enforce_keys [:image, :port, :wait_timeout] defstruct [ :image, :port, :username, :password, :virtual_host, :cmd, :wait_timeout, check_image: @default_image, reuse: false ] @doc """ Creates a new `RabbitMQContainer` struct with default configurations. """ def new, do: %__MODULE__{ image: @default_image_with_tag, port: @default_port, username: @default_username, password: @default_password, virtual_host: @default_virtual_host, cmd: @default_command, wait_timeout: @default_wait_timeout } @doc """ Overrides the default image use for the RabbitMQ container. ## Examples iex> config = RabbitMQContainer.new() |> RabbitMQContainer.with_image("rabbitmq:xyz") iex> config.image "rabbitmq:xyz" """ def with_image(%__MODULE__{} = config, image) do %{config | image: image} end @doc """ Overrides the default port used for the RabbitMQ container. ## Examples iex> config = RabbitMQContainer.new() |> RabbitMQContainer.with_port(1111) iex> config.port 1111 """ def with_port(%__MODULE__{} = config, port) when is_integer(port) do %{config | port: port} end @doc """ Overrides the default wait timeout used for the RabbitMQ container. Note: this timeout will be used for each individual wait strategy. ## Examples iex> config = RabbitMQContainer.new() |> RabbitMQContainer.with_wait_timeout(60000) iex> config.wait_timeout 60000 """ def with_wait_timeout(%__MODULE__{} = config, wait_timeout) when is_integer(wait_timeout) do %{config | wait_timeout: wait_timeout} end @doc """ Overrides the default user used for the RabbitMQ container. ## Examples iex> config = RabbitMQContainer.new() |> RabbitMQContainer.with_username("rabbitmq") iex> config.username "rabbitmq" """ def with_username(%__MODULE__{} = config, username) when is_binary(username) do %{config | username: username} end @doc """ Overrides the default password used for the RabbitMQ container. ## Examples iex> config = RabbitMQContainer.new() |> RabbitMQContainer.with_password("rabbitmq") iex> config.password "rabbitmq" """ def with_password(%__MODULE__{} = config, password) when is_binary(password) do %{config | password: password} end @doc """ Overrides the default virtual host used for the RabbitMQ container. ## Examples iex> config = RabbitMQContainer.new() |> RabbitMQContainer.with_virtual_host("/") iex> config.password "/" """ def with_virtual_host(%__MODULE__{} = config, virtual_host) when is_binary(virtual_host) do %{config | virtual_host: virtual_host} end @doc """ Overrides the default command used for the RabbitMQ container. ## Examples iex> config = RabbitMQContainer.new() |> RabbitMQContainer.with_cmd(["sh", "-c", "rabbitmq-server"]) iex> config.cmd ["sh", "-c", "rabbitmq-server"] """ def with_cmd(%__MODULE__{} = config, cmd) when is_list(cmd) do %{config | cmd: cmd} end @doc """ Set the regular expression to check the image validity. """ def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do %__MODULE__{config | check_image: check_image} end @doc """ Set the reuse flag to reuse the container if it is already running. """ def with_reuse(%__MODULE__{} = config, reuse) when is_boolean(reuse) do %__MODULE__{config | reuse: reuse} end @doc """ Retrieves the default Docker image for the RabbitMQ container """ def default_image, do: @default_image @doc """ Retrieves the default exposed port for the RabbitMQ container """ def default_port, do: @default_port @doc """ Retrieves the default Docker image including tag for the RabbitMQ container """ def default_image_with_tag, do: @default_image <> ":" <> @default_tag @doc """ Returns the port on the _host machine_ where the RabbitMQ container is listening. """ def port(%Container{} = container), do: Container.mapped_port( container, String.to_integer(container.environment[:RABBITMQ_NODE_PORT]) ) @doc """ Generates the connection URL for accessing the RabbitMQ service running within the container. This URI is based on the AMQP 0-9-1, and has the following scheme: amqp://username:password@host:port/vhost ## Parameters - `container`: The active RabbitMQ container instance in the form of a %Container{} struct. ## Examples iex> RabbitMQContainer.connection_url(container) "amqp://guest:guest@localhost:32768" iex> RabbitMQContainer.connection_url(container_with_vhost) "amqp://guest:guest@localhost:32768/vhost" """ def connection_url(%Container{} = container) do "amqp://#{container.environment[:RABBITMQ_DEFAULT_USER]}:#{container.environment[:RABBITMQ_DEFAULT_PASS]}@#{Testcontainers.get_host()}:#{port(container)}#{virtual_host_segment(container)}" end @doc """ Returns the connection parameters to connect to RabbitMQ from the _host machine_. ## Parameters - `container`: The active RabbitMQ container instance in the form of a %Container{} struct. ## Examples iex> RabbitMQContainer.connection_parameters(container) [ host: "localhost", port: 32768, username: "guest", password: "guest", vhost: "/" ] """ def connection_parameters(%Container{} = container) do [ host: Testcontainers.get_host(), port: port(container), username: container.environment[:RABBITMQ_DEFAULT_USER], password: container.environment[:RABBITMQ_DEFAULT_PASS], virtual_host: container.environment[:RABBITMQ_DEFAULT_VHOST] ] end # Provides the virtual host segment used in the AMQP URI specification defined in the AMQP 0-9-1, and interprets the virtual host for the connection URL based on the default value. defp virtual_host_segment(container) do case container.environment[:RABBITMQ_DEFAULT_VHOST] do "/" -> "" vhost -> "/" <> vhost end end defimpl ContainerBuilder do import Container @doc """ Implementation of the `ContainerBuilder` protocol specific to `RabbitMQContainer`. This function builds a new container configuration, ensuring the RabbitMQ image is compatible, setting environment variables, and applying a waiting strategy for the container to be ready. The build process raises an `ArgumentError` if the specified container image is not compatible with the expected RabbitMQ image. ## Examples # Assuming `ContainerBuilder.build/2` is called from somewhere in the application with a `RabbitMQContainer` configuration: iex> config = RabbitMQContainer.new() iex> built_container = ContainerBuilder.build(config, []) # `built_container` is now a ready-to-use `%Container{}` configured specifically for RabbitMQ. ## Errors - Raises `ArgumentError` if the provided image is not compatible with the default RabbitMQ image. """ @impl true @spec build(%RabbitMQContainer{}) :: %Container{} def build(%RabbitMQContainer{} = config) do new(config.image) |> with_exposed_port(config.port) |> with_environment(:RABBITMQ_DEFAULT_USER, config.username) |> with_environment(:RABBITMQ_DEFAULT_PASS, config.password) |> with_environment(:RABBITMQ_DEFAULT_VHOST, config.virtual_host) |> with_environment(:RABBITMQ_NODE_PORT, to_string(config.port)) |> with_cmd(config.cmd) |> with_waiting_strategy( CommandWaitStrategy.new( ["rabbitmq-diagnostics", "check_running"], config.wait_timeout ) ) |> with_check_image(config.check_image) |> with_reuse(config.reuse) |> valid_image!() end @impl true @spec after_start(%RabbitMQContainer{}, %Container{}, %Tesla.Env{}) :: :ok def after_start(_config, _container, _conn), do: :ok end end