Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to override image validation #108

Merged
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ config = Testcontainers.RedisContainer.new()
{:ok, container} = Testcontainers.start_container(config)
```

If you want to use a predefined container, such as `RedisContainer`, with an alternative image, for example, `valkey/valkey`, it's possible:

```elixir
{:ok, _} = Testcontainers.start_link()
config =
Testcontainers.RedisContainer.new()
|> Testcontainers.RedisContainer.with_image("valkey/valkey:latest")
|> Testcontainers.RedisContainer.with_check_image("valkey/valkey")
{:ok, container} = Testcontainers.start_container(config)
```

### ExUnit tests

Given you have added Testcontainers.start_link() to test_helper.exs:
Expand Down
53 changes: 52 additions & 1 deletion lib/container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,17 @@ defmodule Testcontainers.Container do
labels: %{},
auto_remove: false,
container_id: nil,
check_image: ~r/.*/,
network_mode: nil
]

@doc """
Returns `true` if `term` is a valid `check_image`, otherwise returns `false`.
"""
@doc guard: true
defguard is_valid_image(check_image)
when is_binary(check_image) or is_struct(check_image, Regex)

@doc """
A constructor function to make it easier to construct a container
"""
Expand Down Expand Up @@ -152,6 +160,20 @@ defmodule Testcontainers.Container do
%__MODULE__{config | auth: registry_auth_token}
end

@doc """
Set the regular expression to check the image validity.

When using a string, it will compile it to a regular expression. If the compilation fails, it will raise a `Regex.CompileError`.
"""
def with_check_image(%__MODULE__{} = config, check_image) when is_binary(check_image) do
regex = Regex.compile!(check_image)
with_check_image(config, regex)
end

def with_check_image(%__MODULE__{} = config, %Regex{} = check_image) do
%__MODULE__{config | check_image: check_image}
end

@doc """
Sets a network mode to apply to the container object in docker.
"""
Expand All @@ -173,10 +195,39 @@ defmodule Testcontainers.Container do
|> List.last()
end

@doc """
Check if the provided image is compatible with the expected default image.

Raises:

ArgumentError when image isn't compatible.
"""
def valid_image!(%__MODULE__{} = config) do
case valid_image(config) do
{:ok, config} ->
config

{:error, message} ->
raise ArgumentError, message: message
end
end

@doc """
Check if the provided image is compatible with the expected default image.
"""
def valid_image(%__MODULE__{image: image, check_image: check_image} = config) do
if Regex.match?(check_image, image) do
{:ok, config}
else
{:error,
"Unexpected image #{image}. If this is a valid image, provide a broader `check_image` regex to the container configuration."}
end
end

defimpl Testcontainers.ContainerBuilder do
@impl true
def build(%Testcontainers.Container{} = config) do
config
Testcontainers.Container.valid_image!(config)
end

@doc """
Expand Down
19 changes: 12 additions & 7 deletions lib/container/cassandra_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defmodule Testcontainers.CassandraContainer do
alias Testcontainers.ContainerBuilder
alias Testcontainers.Container

import Testcontainers.Container, only: [is_valid_image: 1]

@default_image "cassandra"
@default_tag "3.11.2"
@default_image_with_tag "#{@default_image}:#{@default_tag}"
Expand All @@ -18,7 +20,7 @@ defmodule Testcontainers.CassandraContainer do
@default_wait_timeout 60_000

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

def new,
do: %__MODULE__{
Expand All @@ -30,6 +32,13 @@ defmodule Testcontainers.CassandraContainer do
%{config | image: image}
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

def default_image, do: @default_image

def default_port, do: @default_port
Expand All @@ -56,12 +65,6 @@ defmodule Testcontainers.CassandraContainer do
@impl true
@spec build(%CassandraContainer{}) :: %Container{}
def build(%CassandraContainer{} = config) do
if not String.starts_with?(config.image, CassandraContainer.default_image()) do
raise ArgumentError,
message:
"Image #{config.image} is not compatible with #{CassandraContainer.default_image()}"
end

new(config.image)
|> with_exposed_port(CassandraContainer.default_port())
|> with_environment(:CASSANDRA_SNITCH, "GossipingPropertyFileSnitch")
Expand All @@ -79,6 +82,8 @@ defmodule Testcontainers.CassandraContainer do
config.wait_timeout
)
)
|> with_check_image(config.check_image)
|> valid_image!()
end

@impl true
Expand Down
26 changes: 20 additions & 6 deletions lib/container/ceph_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defmodule Testcontainers.CephContainer do
alias Testcontainers.ContainerBuilder
alias Testcontainers.Container

import Testcontainers.Container, only: [is_valid_image: 1]

@default_image "quay.io/ceph/demo"
@default_tag "latest-quincy"
@default_image_with_tag "#{@default_image}:#{@default_tag}"
Expand All @@ -19,7 +21,15 @@ defmodule Testcontainers.CephContainer do
@default_wait_timeout 300_000

@enforce_keys [:image, :access_key, :secret_key, :bucket, :port, :wait_timeout]
defstruct [:image, :access_key, :secret_key, :bucket, :port, :wait_timeout]
defstruct [
:image,
:access_key,
:secret_key,
:bucket,
:port,
:wait_timeout,
check_image: @default_image
]

@doc """
Creates a new `CephContainer` struct with default attributes.
Expand Down Expand Up @@ -128,6 +138,13 @@ defmodule Testcontainers.CephContainer do
%{config | wait_timeout: wait_timeout}
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 """
Retrieves the default Docker image used for the Ceph container.

Expand Down Expand Up @@ -208,11 +225,6 @@ defmodule Testcontainers.CephContainer do
@spec build(%CephContainer{}) :: %Container{}
@impl true
def build(%CephContainer{} = config) 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")
Expand All @@ -229,6 +241,8 @@ defmodule Testcontainers.CephContainer do
5000
)
)
|> with_check_image(config.check_image)
|> valid_image!()
end

@impl true
Expand Down
19 changes: 13 additions & 6 deletions lib/container/emqx_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ defmodule Testcontainers.EmqxContainer do
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}"
Expand All @@ -26,7 +28,8 @@ defmodule Testcontainers.EmqxContainer do
:mqtt_over_ws_port,
:mqtt_over_wss_port,
:dashboard_port,
:wait_timeout
:wait_timeout,
check_image: @default_image
]

@doc """
Expand Down Expand Up @@ -76,6 +79,13 @@ defmodule Testcontainers.EmqxContainer do
}
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 """
Retrieves the default Docker image for the Emqx container.
"""
Expand All @@ -100,14 +110,11 @@ defmodule Testcontainers.EmqxContainer do
"""
@impl true
def build(%EmqxContainer{} = config) do
if not String.starts_with?(config.image, EmqxContainer.default_image()) do
raise ArgumentError,
message: "Image #{config.image} is not compatible with #{EmqxContainer.default_image()}"
end

new(config.image)
|> with_exposed_ports(exposed_ports(config))
|> with_waiting_strategies(waiting_strategies(config))
|> with_check_image(config.check_image)
|> valid_image!()
end

defp exposed_ports(config),
Expand Down
28 changes: 21 additions & 7 deletions lib/container/mysql_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule Testcontainers.MySqlContainer do
alias Testcontainers.MySqlContainer
alias Testcontainers.LogWaitStrategy

import Testcontainers.Container, only: [is_valid_image: 1]

@default_image "mysql"
@default_tag "8"
@default_image_with_tag "#{@default_image}:#{@default_tag}"
Expand All @@ -22,7 +24,16 @@ defmodule Testcontainers.MySqlContainer do
@default_wait_timeout 180_000

@enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume]
defstruct [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume]
defstruct [
:image,
:user,
:password,
:database,
:port,
:wait_timeout,
:persistent_volume,
check_image: @default_image
]

@doc """
Creates a new `MySqlContainer` struct with default configurations.
Expand Down Expand Up @@ -131,6 +142,13 @@ defmodule Testcontainers.MySqlContainer do
%{config | wait_timeout: wait_timeout}
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 """
Retrieves the default exposed port for the MySQL container.
"""
Expand Down Expand Up @@ -188,12 +206,6 @@ defmodule Testcontainers.MySqlContainer do
@spec build(%MySqlContainer{}) :: %Container{}
@impl true
def build(%MySqlContainer{} = config) do
if not String.starts_with?(config.image, MySqlContainer.default_image()) do
raise ArgumentError,
message:
"Image #{config.image} is not compatible with #{MySqlContainer.default_image()}"
end

new(config.image)
|> then(MySqlContainer.container_port_fun(config.port))
|> with_environment(:MYSQL_USER, config.user)
Expand All @@ -204,6 +216,8 @@ defmodule Testcontainers.MySqlContainer do
|> with_waiting_strategy(
LogWaitStrategy.new(~r/.*port: 3306 MySQL Community Server.*/, config.wait_timeout)
)
|> with_check_image(config.check_image)
|> valid_image!()
end

@impl true
Expand Down
28 changes: 21 additions & 7 deletions lib/container/postgres_container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule Testcontainers.PostgresContainer do
alias Testcontainers.Container
alias Testcontainers.ContainerBuilder

import Testcontainers.Container, only: [is_valid_image: 1]

@default_image "postgres"
@default_tag "15-alpine"
@default_image_with_tag "#{@default_image}:#{@default_tag}"
Expand All @@ -22,7 +24,16 @@ defmodule Testcontainers.PostgresContainer do
@default_wait_timeout 60_000

@enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume]
defstruct [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume]
defstruct [
:image,
:user,
:password,
:database,
:port,
:wait_timeout,
:persistent_volume,
check_image: @default_image
]

@doc """
Creates a new `PostgresContainer` struct with default configurations.
Expand Down Expand Up @@ -131,6 +142,13 @@ defmodule Testcontainers.PostgresContainer do
%{config | wait_timeout: wait_timeout}
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 """
Retrieves the default exposed port for the Postgres container.
"""
Expand Down Expand Up @@ -188,12 +206,6 @@ defmodule Testcontainers.PostgresContainer do
@spec build(%PostgresContainer{}) :: %Container{}
@impl true
def build(%PostgresContainer{} = config) do
if not String.starts_with?(config.image, PostgresContainer.default_image()) do
raise ArgumentError,
message:
"Image #{config.image} is not compatible with #{PostgresContainer.default_image()}"
end

new(config.image)
|> then(PostgresContainer.container_port_fun(config.port))
|> with_environment(:POSTGRES_USER, config.user)
Expand All @@ -210,6 +222,8 @@ defmodule Testcontainers.PostgresContainer do
config.wait_timeout
)
)
|> with_check_image(config.check_image)
|> valid_image!()
end

@impl true
Expand Down
Loading
Loading