-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add RabbitMQ container module * Add note to module doc about starting command. * Update docs for amqp URI specification regarding scheme and virtual host * Allow overriding of default port in RabbitMQ node in container
- Loading branch information
Showing
4 changed files
with
343 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
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 | ||
|
||
@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] | ||
|
||
@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 """ | ||
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 | ||
|
||
@doc """ | ||
Check warning on line 211 in lib/container/rabbitmq_container.ex GitHub Actions / Test example projects
Check warning on line 211 in lib/container/rabbitmq_container.ex GitHub Actions / Test example projects
|
||
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 | ||
if not String.starts_with?(config.image, RabbitMQContainer.default_image()) do | ||
raise ArgumentError, | ||
message: | ||
"Image #{config.image} is not compatible with #{RabbitMQContainer.default_image()}" | ||
end | ||
|
||
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 | ||
) | ||
) | ||
end | ||
|
||
@impl true | ||
@spec is_starting(%RabbitMQContainer{}, %Container{}, %Tesla.Env{}) :: :ok | ||
def is_starting(_config, _container, _conn), do: :ok | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
defmodule Testcontainers.Container.RabbitMQContainerTest do | ||
use ExUnit.Case, async: true | ||
import Testcontainers.ExUnit | ||
|
||
alias Testcontainers.RabbitMQContainer | ||
|
||
@moduletag timeout: 300_000 | ||
|
||
describe "with default configuration" do | ||
container(:rabbitmq, RabbitMQContainer.new()) | ||
|
||
test "provides a ready-to-use rabbitmq container by using connection parameters", %{ | ||
rabbitmq: rabbitmq | ||
} do | ||
{:ok, connection} = | ||
RabbitMQContainer.connection_parameters(rabbitmq) | ||
|> AMQP.Connection.open() | ||
|
||
do_assertion(connection) | ||
end | ||
|
||
test "provides a ready-to-use rabbitmq container by using connection URL", %{ | ||
rabbitmq: rabbitmq | ||
} do | ||
{:ok, connection} = | ||
RabbitMQContainer.connection_url(rabbitmq) | ||
|> AMQP.Connection.open() | ||
|
||
do_assertion(connection) | ||
end | ||
end | ||
|
||
describe "with custom configuration" do | ||
@custom_rabbitmq RabbitMQContainer.new() | ||
|> RabbitMQContainer.with_image("rabbitmq:3-management-alpine") | ||
|> RabbitMQContainer.with_port(5671) | ||
|> RabbitMQContainer.with_username("custom-user") | ||
|> RabbitMQContainer.with_password("custom_password") | ||
|> RabbitMQContainer.with_virtual_host("custom-virtual-host") | ||
|
||
container(:rabbitmq, @custom_rabbitmq) | ||
|
||
test "provides a rabbitmq container compliant with specified configuration", %{ | ||
rabbitmq: rabbitmq | ||
} do | ||
{:ok, connection} = | ||
RabbitMQContainer.connection_parameters(rabbitmq) | ||
|> AMQP.Connection.open() | ||
|
||
do_assertion(connection) | ||
end | ||
end | ||
|
||
defp do_assertion(connection) do | ||
{:ok, channel} = AMQP.Channel.open(connection) | ||
AMQP.Queue.declare(channel, "channel") | ||
AMQP.Basic.publish(channel, "", "channel", "Hello") | ||
AMQP.Basic.consume(channel, "channel", nil, no_ack: true) | ||
|
||
assert_receive {:basic_consume_ok, %{consumer_tag: _consumer_tag}} | ||
assert_receive {:basic_deliver, "Hello", _meta} | ||
AMQP.Connection.close(connection) | ||
end | ||
end |