Packages
testcontainers
2.0.0-rc2
2.3.1
2.3.0
2.2.0
2.1.0
2.1.0-rc1
2.0.0
2.0.0-rc3
2.0.0-rc2
2.0.0-rc
1.14.1
1.14.0
1.13.5
1.13.4
1.13.3
1.13.2
1.13.1
1.13.0
1.12.0
1.11.8
1.11.7
1.11.6
1.11.5
1.11.4
1.11.3
1.11.2
1.11.1
1.11.0
1.10.5
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
1.9.0
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.7.0
1.6.0
1.5.1
1.5.0
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.11
1.2.10
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-beta.1
1.0.0-beta
0.9.3
0.9.2
0.9.1
0.9.0
Testcontainers is an Elixir library that supports ExUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.
Current section
Files
Jump to
Current section
Files
lib/container.ex
# SPDX-License-Identifier: MIT
# Original by: Marco Dallagiacoma @ 2023 in https://github.com/dallagi/excontainers
# Modified by: Jarl André Hübenthal @ 2023
defmodule Testcontainers.Container do
@moduledoc """
A struct with builder functions for creating a definition of a container.
"""
require Logger
@enforce_keys [:image]
defstruct [
:image,
cmd: nil,
environment: %{},
auth: nil,
exposed_ports: [],
ip_address: nil,
wait_strategies: [],
privileged: false,
bind_mounts: [],
bind_volumes: [],
labels: %{},
auto_remove: false,
container_id: nil,
check_image: nil,
network_mode: nil,
network: nil,
hostname: nil,
reuse: false,
force_reuse: false,
pull_policy: 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)
@os_type (case :os.type() do
{:win32, _} -> :windows
{:unix, :darwin} -> :macos
{:unix, _} -> :linux
end)
@doc guard: true
defguard is_os(name)
when is_atom(name) and name == @os_type
@dialyzer {:nowarn_function, os_type: 0}
def os_type() do
cond do
is_os(:linux) -> :linux
is_os(:macos) -> :macos
is_os(:windows) -> :windows
true -> :unknown
end
end
@doc """
A constructor function to make it easier to construct a container
"""
def new(image) when is_binary(image) do
%__MODULE__{image: image}
end
@doc """
Sets a _waiting strategy_ for the _container_.
"""
def with_waiting_strategy(%__MODULE__{} = config, wait_fn) when is_struct(wait_fn) do
%__MODULE__{config | wait_strategies: [wait_fn | config.wait_strategies]}
end
@doc """
Sets multiple _waiting strategies_ for the _container_.
"""
def with_waiting_strategies(%__MODULE__{} = config, wait_fns) when is_list(wait_fns) do
Enum.reduce(wait_fns, config, fn fun, cfg -> with_waiting_strategy(cfg, fun) end)
end
@doc """
Sets an _environment variable_ to the _container_.
"""
def with_environment(%__MODULE__{} = config, key, value)
when (is_binary(key) or is_atom(key)) and is_binary(value) do
%__MODULE__{config | environment: Map.put(config.environment, key, value)}
end
@doc """
Adds a _port_ to be exposed on the _container_.
"""
def with_exposed_port(%__MODULE__{} = config, port) when is_integer(port) do
filtered_ports =
config.exposed_ports
|> Enum.reject(fn
{p, _} -> p == port
p -> p == port
end)
%__MODULE__{config | exposed_ports: [{port, nil} | filtered_ports]}
end
@doc """
Adds multiple _ports_ to be exposed on the _container_.
"""
def with_exposed_ports(%__MODULE__{} = config, ports) when is_list(ports) do
filtered_ports =
config.exposed_ports
|> Enum.reject(fn
{p, _} -> p in ports
p -> p in ports
end)
new_ports = Enum.map(ports, fn port -> {port, nil} end)
%__MODULE__{config | exposed_ports: new_ports ++ filtered_ports}
end
@doc """
Adds a fixed _port_ to be exposed on the _container_.
This approach to managing ports is not recommended by Testcontainers.
Use at your own risk.
"""
def with_fixed_port(%__MODULE__{} = config, port, host_port \\ nil)
when is_integer(port) and (is_nil(host_port) or is_integer(host_port)) do
filtered_ports =
config.exposed_ports
|> Enum.reject(fn
{p, _} -> p == port
p -> p == port
end)
%__MODULE__{
config
| exposed_ports: [
{port, host_port || port} | filtered_ports
]
}
end
@doc """
Sets a file or the directory on the _host machine_ to be mounted into a _container_.
"""
def with_bind_mount(%__MODULE__{} = config, host_src, container_dest, options \\ "ro")
when is_binary(host_src) and is_binary(container_dest) and is_binary(options) do
new_bind_mount = %{host_src: host_src, container_dest: container_dest, options: options}
%__MODULE__{config | bind_mounts: [new_bind_mount | config.bind_mounts]}
end
@doc """
Sets a volume to be mounted into a container on target path
"""
def with_bind_volume(%__MODULE__{} = config, volume, container_dest, read_only \\ false)
when is_binary(volume) and is_binary(container_dest) and is_boolean(read_only) do
new_bind_volume = %{
volume: volume,
container_dest: container_dest,
read_only: read_only
}
%__MODULE__{config | bind_volumes: [new_bind_volume | config.bind_volumes]}
end
@doc """
Sets a label to apply to the container object in docker.
"""
def with_label(%__MODULE__{} = config, key, value) when is_binary(key) and is_binary(value) do
%__MODULE__{config | labels: Map.put(config.labels, key, value)}
end
@doc """
Sets a cmd to run when the container starts.
"""
def with_cmd(%__MODULE__{} = config, cmd) when is_list(cmd) do
%__MODULE__{config | cmd: cmd}
end
@doc """
Sets whether the container should be automatically removed on exit.
"""
def with_auto_remove(%__MODULE__{} = config, auto_remove) when is_boolean(auto_remove) do
%__MODULE__{config | auto_remove: auto_remove}
end
@doc """
Sets whether the container should run in privileged mode.
Required for containers that need access to the Docker/Podman socket
when running under SELinux (e.g., Ryuk on Podman).
"""
def with_privileged(%__MODULE__{} = config, privileged) when is_boolean(privileged) do
%__MODULE__{config | privileged: privileged}
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
def with_force_reuse(%__MODULE__{} = config, force_reuse) when is_boolean(force_reuse) do
if config.auto_remove do
raise ArgumentError, "Cannot reuse a container that is set to auto-remove"
end
%__MODULE__{config | reuse: true, force_reuse: force_reuse}
end
@doc """
Adds authentication token for registries that require a login.
"""
def with_auth(%__MODULE__{} = config, username, password)
when is_binary(username) and is_binary(password) do
registry_auth_token =
Jason.encode!(%{
username: username,
password: password
})
|> Base.encode64()
%__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.
"""
def with_network_mode(%__MODULE__{} = config, mode) when is_binary(mode) do
mode = String.downcase(mode)
if mode == "host" and not is_os(:linux) do
Logger.warning(
"To use host network mode on non-linux hosts, please see https://docs.docker.com/network/drivers/host"
)
end
%__MODULE__{config | network_mode: mode}
end
def with_network_mode(%__MODULE__{} = config, mode) when is_binary(mode) do
%__MODULE__{config | network_mode: mode}
end
@doc """
Sets the Docker network for the container to join.
Containers on the same network can communicate with each other using their
hostnames. Use `with_hostname/2` to set a custom hostname for the container.
"""
def with_network(%__MODULE__{} = config, network_name) when is_binary(network_name) do
%__MODULE__{config | network: network_name}
end
@doc """
Sets the hostname for the container.
This is useful when containers need to communicate with each other by hostname
on a shared Docker network.
"""
def with_hostname(%__MODULE__{} = config, hostname) when is_binary(hostname) do
%__MODULE__{config | hostname: hostname}
end
@doc """
Gets the host port on the container for the given exposed port.
"""
def mapped_port(%__MODULE__{} = container, port) when is_number(port) do
container.exposed_ports
|> Enum.find_value(nil, fn
{^port, host_port} -> host_port
_ -> nil
end)
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 || ~r/.*/, 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
@doc """
Controls if image is pulled from a remote repository
Available options:
* `TestContainers.PullPolicy.always_pull()` - default, always pulls image from the remote repository
* `TestContainers.PullPolicy.never_pull()` - does not pull images, use when working with local images
* `TestContainers.PullPolicy.pull_condition(expr)` - pulls image if expression returns `true`
"""
def with_pull_policy(%__MODULE__{} = container, %Testcontainers.PullPolicy{} = pull_policy) do
%{container | pull_policy: pull_policy}
end
defimpl Testcontainers.ContainerBuilder do
@impl true
def build(%Testcontainers.Container{} = config) do
Testcontainers.Container.valid_image!(config)
end
@doc """
Do stuff after container has started.
"""
@impl true
@spec after_start(%Testcontainers.Container{}, %Testcontainers.Container{}, %Tesla.Env{}) ::
:ok
def after_start(_config, _container, _conn), do: :ok
end
end