Skip to content

Commit

Permalink
feat: add support for reusable containers (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
jarlah authored Sep 24, 2024
1 parent d1452ad commit 9e65ccc
Show file tree
Hide file tree
Showing 25 changed files with 314 additions and 63 deletions.
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import Config

config :logger, level: :warning

config :testcontainers, log_level: :warning
config :testcontainers, log_level: :info
2 changes: 1 addition & 1 deletion examples/phoenix_project/mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
%{
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
"castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
Expand Down
39 changes: 2 additions & 37 deletions lib/connection/docker_host_strategy/docker_host_from_properties.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# SPDX-License-Identifier: MIT
defmodule Testcontainers.DockerHostFromPropertiesStrategy do
@moduledoc false

Expand All @@ -7,46 +6,14 @@ defmodule Testcontainers.DockerHostFromPropertiesStrategy do

defimpl Testcontainers.DockerHostStrategy do
alias Testcontainers.DockerUrl
alias Testcontainers.Util.PropertiesParser

def execute(strategy, _input) do
with {:ok, properties} <- read_property_file(expand_path(strategy.filename)),
with {:ok, properties} <- PropertiesParser.read_property_file(strategy.filename),
docker_host <- Map.fetch(properties, strategy.key),
do: handle_docker_host(docker_host)
end

defp read_property_file(filepath) do
if File.exists?(filepath) do
with {:ok, content} <- File.read(filepath),
properties <- parse_properties(content) do
{:ok, properties}
else
error ->
{:error, testcontainer_host_from_properties: error}
end
else
{:error, testcontainer_host_from_properties: :file_does_not_exist}
end
end

defp parse_properties(content) do
content
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == "" or String.starts_with?(&1, "#")))
|> Enum.flat_map(&extract_key_value_pair/1)
|> Enum.into(%{})
end

defp extract_key_value_pair(line) do
case String.split(line, "=", parts: 2) do
[key, value] when is_binary(value) ->
[{String.trim(key), String.trim(value)}]

_other ->
[]
end
end

defp handle_docker_host({:ok, docker_host}) when is_binary(docker_host) do
case DockerUrl.test_docker_host(docker_host) do
:ok ->
Expand All @@ -59,7 +26,5 @@ defmodule Testcontainers.DockerHostFromPropertiesStrategy do

defp handle_docker_host(:error),
do: {:error, testcontainer_host_from_properties: :property_not_found}

defp expand_path(path), do: Path.expand(path)
end
end
13 changes: 12 additions & 1 deletion lib/container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ defmodule Testcontainers.Container do
auto_remove: false,
container_id: nil,
check_image: ~r/.*/,
network_mode: nil
network_mode: nil,
reuse: false
]

@doc """
Expand Down Expand Up @@ -155,6 +156,16 @@ defmodule Testcontainers.Container do
%__MODULE__{config | auto_remove: auto_remove}
end

@doc """
Sets whether the container should be reused if it is already running.
"""
def with_reuse(%__MODULE__{} = config, reuse) when is_boolean(reuse) do
if config.auto_remove do
raise ArgumentError, "Cannot reuse a container that is set to auto-remove"
end
%__MODULE__{config | reuse: reuse}
end

@doc """
Adds authentication token for registries that require a login.
"""
Expand Down
15 changes: 14 additions & 1 deletion lib/container/cassandra_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ defmodule Testcontainers.CassandraContainer do
@default_wait_timeout 60_000

@enforce_keys [:image, :wait_timeout]
defstruct [:image, :wait_timeout, check_image: @default_image]
defstruct [
:image,
:wait_timeout,
check_image: @default_image,
reuse: false
]

def new,
do: %__MODULE__{
Expand All @@ -39,6 +44,13 @@ defmodule Testcontainers.CassandraContainer 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

def default_image, do: @default_image

def default_port, do: @default_port
Expand Down Expand Up @@ -83,6 +95,7 @@ defmodule Testcontainers.CassandraContainer do
)
)
|> with_check_image(config.check_image)
|> with_reuse(config.reuse)
|> valid_image!()
end

Expand Down
11 changes: 10 additions & 1 deletion lib/container/ceph_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ defmodule Testcontainers.CephContainer do
:bucket,
:port,
:wait_timeout,
check_image: @default_image
check_image: @default_image,
reuse: false
]

@doc """
Expand Down Expand Up @@ -145,6 +146,13 @@ defmodule Testcontainers.CephContainer 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 used for the Ceph container.
Expand Down Expand Up @@ -242,6 +250,7 @@ defmodule Testcontainers.CephContainer do
)
)
|> with_check_image(config.check_image)
|> with_reuse(config.reuse)
|> valid_image!()
end

Expand Down
11 changes: 10 additions & 1 deletion lib/container/emqx_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ defmodule Testcontainers.EmqxContainer do
:mqtt_over_wss_port,
:dashboard_port,
:wait_timeout,
check_image: @default_image
check_image: @default_image,
reuse: false
]

@doc """
Expand Down Expand Up @@ -86,6 +87,13 @@ defmodule Testcontainers.EmqxContainer 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.
"""
Expand Down Expand Up @@ -114,6 +122,7 @@ defmodule Testcontainers.EmqxContainer do
|> 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

Expand Down
11 changes: 10 additions & 1 deletion lib/container/kafka_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ defmodule Testcontainers.KafkaContainer do
:wait_timeout,
:consensus_strategy,
:default_topic_partitions,
:start_file_path
:start_file_path,
reuse: false
]

@doc """
Expand Down Expand Up @@ -147,6 +148,13 @@ defmodule Testcontainers.KafkaContainer do
%{config | default_topic_partitions: topic_partitions}
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

defimpl Testcontainers.ContainerBuilder do
import Container

Expand All @@ -158,6 +166,7 @@ defmodule Testcontainers.KafkaContainer do
|> with_listener_config(config)
|> with_topic_config(config)
|> with_startup_script(config)
|> with_reuse(config.reuse)
|> with_waiting_strategy(
CommandWaitStrategy.new(
["kafka-broker-api-versions", "--bootstrap-server", "localhost:#{config.kafka_port}"],
Expand Down
16 changes: 15 additions & 1 deletion lib/container/minio_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ defmodule Testcontainers.MinioContainer do
@default_wait_timeout 60_000

@enforce_keys [:image, :username, :password, :wait_timeout]
defstruct [:image, :username, :password, :wait_timeout]
defstruct [
:image,
:username,
:password,
:wait_timeout,
reuse: false
]

def new,
do: %__MODULE__{
Expand All @@ -28,6 +34,13 @@ defmodule Testcontainers.MinioContainer do
wait_timeout: @default_wait_timeout
}

@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

def get_username, do: @default_username
def get_password, do: @default_password
def default_ui_port, do: @default_ui_port
Expand Down Expand Up @@ -69,6 +82,7 @@ defmodule Testcontainers.MinioContainer do
|> with_exposed_ports([MinioContainer.default_s3_port(), MinioContainer.default_ui_port()])
|> with_environment(:MINIO_ROOT_USER, config.username)
|> with_environment(:MINIO_ROOT_PASSWORD, config.password)
|> with_reuse(config.reuse)
|> with_cmd(["server", "--console-address", ":#{MinioContainer.default_ui_port()}", "/data"])
|> with_waiting_strategy(
LogWaitStrategy.new(~r/.*Status: 1 Online, 0 Offline..*/, config.wait_timeout)
Expand Down
11 changes: 10 additions & 1 deletion lib/container/mysql_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ defmodule Testcontainers.MySqlContainer do
:port,
:wait_timeout,
:persistent_volume,
check_image: @default_image
check_image: @default_image,
reuse: false
]

@doc """
Expand Down Expand Up @@ -149,6 +150,13 @@ defmodule Testcontainers.MySqlContainer 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 exposed port for the MySQL container.
"""
Expand Down Expand Up @@ -217,6 +225,7 @@ defmodule Testcontainers.MySqlContainer do
LogWaitStrategy.new(~r/.*port: 3306 MySQL Community Server.*/, config.wait_timeout)
)
|> with_check_image(config.check_image)
|> with_reuse(config.reuse)
|> valid_image!()
end

Expand Down
11 changes: 10 additions & 1 deletion lib/container/postgres_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ defmodule Testcontainers.PostgresContainer do
:port,
:wait_timeout,
:persistent_volume,
check_image: @default_image
check_image: @default_image,
reuse: false
]

@doc """
Expand Down Expand Up @@ -149,6 +150,13 @@ defmodule Testcontainers.PostgresContainer 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 exposed port for the Postgres container.
"""
Expand Down Expand Up @@ -223,6 +231,7 @@ defmodule Testcontainers.PostgresContainer do
)
)
|> with_check_image(config.check_image)
|> with_reuse(config.reuse)
|> valid_image!()
end

Expand Down
15 changes: 11 additions & 4 deletions lib/container/rabbitmq_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ defmodule Testcontainers.RabbitMQContainer do
:virtual_host,
:cmd,
:wait_timeout,
check_image: @default_image
check_image: @default_image,
reuse: false
]

@doc """
Expand Down Expand Up @@ -152,6 +153,13 @@ defmodule Testcontainers.RabbitMQContainer 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
"""
Expand Down Expand Up @@ -226,9 +234,7 @@ defmodule Testcontainers.RabbitMQContainer do
]
end

@doc """
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.
"""
# 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
"/" -> ""
Expand Down Expand Up @@ -274,6 +280,7 @@ defmodule Testcontainers.RabbitMQContainer do
)
)
|> with_check_image(config.check_image)
|> with_reuse(config.reuse)
|> valid_image!()
end

Expand Down
Loading

0 comments on commit 9e65ccc

Please sign in to comment.