defmodule Testcontainers.EmqxContainer do @moduledoc """ Provides functionality for creating and managing EMQX container configurations. """ alias Testcontainers.ContainerBuilder alias Testcontainers.Container alias Testcontainers.PortWaitStrategy alias Testcontainers.EmqxContainer import Testcontainers.Container, only: [is_valid_image: 1] @default_image "emqx" @default_tag "5.6.0" @default_image_with_tag "#{@default_image}:#{@default_tag}" @default_mqtt_port 1883 @default_mqtts_port 8883 @default_mqtt_over_ws_port 8083 @default_mqtt_over_wss_port 8084 @default_dashboard_port 18083 @default_wait_timeout 60_000 @enforce_keys [:image, :mqtt_port, :wait_timeout] defstruct [ :image, :mqtt_port, :mqtts_port, :mqtt_over_ws_port, :mqtt_over_wss_port, :dashboard_port, :wait_timeout, check_image: @default_image, reuse: false ] @doc """ Creates a new `EmqxContainer` struct with default configurations. """ def new do %__MODULE__{ image: @default_image_with_tag, wait_timeout: @default_wait_timeout, mqtt_port: @default_mqtt_port, mqtts_port: @default_mqtts_port, mqtt_over_ws_port: @default_mqtt_over_ws_port, mqtt_over_wss_port: @default_mqtt_over_wss_port, dashboard_port: @default_dashboard_port } end @doc """ Overrides the default image used for the Emqx container. ## Examples iex> config = EmqxContainer.new() iex> new_config = EmqxContainer.with_image(config, "emqx:xyz") iex> new_config.image "emqx:xyz" """ def with_image(%__MODULE__{} = config, image) when is_binary(image) do %{config | image: image} end def with_ports( %__MODULE__{} = config, mqtt_port \\ @default_mqtt_port, mqtts_port \\ @default_mqtts_port, mqtt_over_ws_port \\ @default_mqtt_over_ws_port, mqtt_over_wss_port \\ @default_mqtt_over_wss_port, dashboard_port \\ @default_dashboard_port ) do %{ config | mqtt_port: mqtt_port, mqtts_port: mqtts_port, mqtt_over_ws_port: mqtt_over_ws_port, mqtt_over_wss_port: mqtt_over_wss_port, dashboard_port: dashboard_port } 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 Emqx container. """ def default_image, do: @default_image @doc """ Returns the address on the _host machine_ where the Emqx container is listening. """ def host, do: Testcontainers.get_host() @doc """ Returns the port on the _host machine_ where the Emqx container is listening. """ def mqtt_port(%Container{} = container), do: Container.mapped_port(container, @default_mqtt_port) defimpl ContainerBuilder do import Container @doc """ Builds a new container instance based on the provided configuration. """ @impl true def build(%EmqxContainer{} = config) do new(config.image) |> with_exposed_ports(exposed_ports(config)) |> with_waiting_strategies(waiting_strategies(config)) |> with_check_image(config.check_image) |> with_reuse(config.reuse) |> valid_image!() end defp exposed_ports(config), do: [ config.mqtt_port, config.mqtts_port, config.mqtt_over_ws_port, config.mqtt_over_wss_port, config.dashboard_port ] defp waiting_strategies(config), do: [ PortWaitStrategy.new(EmqxContainer.host(), config.mqtt_port, config.wait_timeout, 1000), PortWaitStrategy.new(EmqxContainer.host(), config.mqtts_port, config.wait_timeout, 1000), PortWaitStrategy.new( EmqxContainer.host(), config.mqtt_over_ws_port, config.wait_timeout, 1000 ), PortWaitStrategy.new( EmqxContainer.host(), config.mqtt_over_wss_port, config.wait_timeout, 1000 ), PortWaitStrategy.new( EmqxContainer.host(), config.dashboard_port, config.wait_timeout, 1000 ) ] @impl true # TODO Implement the `after_start/3` function for the `ContainerBuilder` protocol. def after_start(_config, _container, _conn), do: :ok end end