Packages
testcontainers
2.2.0
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/testcontainers.ex
defmodule Testcontainers do
use GenServer
@moduledoc """
The main entry point into Testcontainers.
This is a GenServer that needs to be started before anything can happen.
"""
require Logger
alias Testcontainers.CopyTo
alias Testcontainers.Constants
alias Testcontainers.WaitStrategy
alias Testcontainers.Docker.Api
alias Testcontainers.Connection
alias Testcontainers.Container
alias Testcontainers.ContainerBuilder
alias Testcontainers.PullPolicy
alias Testcontainers.Util.PropertiesParser
alias Testcontainers.DockerCompose
alias Testcontainers.Compose.Cli, as: ComposeCli
alias Testcontainers.Compose.ComposeService
alias Testcontainers.Compose.ComposeEnvironment
import Testcontainers.Constants
import Testcontainers.Container, only: [os_type: 0]
@timeout 300_000
@doc """
Starts the Testcontainers application.
This will terminate when the calling process exits, for ex a task.
"""
def start_link(options \\ []) do
GenServer.start_link(__MODULE__, options, name: Keyword.get(options, :name, __MODULE__))
end
@doc """
Starts the Testcontainers application.
This will NOT terminate when the calling process exits, for ex a task.
"""
def start(options \\ []) do
GenServer.start(__MODULE__, options, name: Keyword.get(options, :name, __MODULE__))
end
@impl true
def init(options \\ []) do
Process.flag(:trap_exit, true)
setup(options)
end
defp setup(options) do
{conn, docker_host_url, docker_host} = Connection.get_connection(options)
# Read testcontainer properties
{:ok, properties} = PropertiesParser.read_property_sources()
session_id =
:crypto.hash(:sha, "#{inspect(self())}#{DateTime.utc_now() |> DateTime.to_string()}")
|> Base.encode16()
with {:ok, docker_hostname} <- get_docker_hostname(docker_host_url, conn, properties),
use_container_ip <- should_use_container_ip?(docker_hostname),
{:ok} <- start_reaper(conn, session_id, properties, docker_host, docker_hostname) do
if use_container_ip do
Logger.info(
"Testcontainers initialized in container networking mode " <>
"(using container IPs directly)"
)
else
Logger.info("Testcontainers initialized")
end
{:ok,
%{
conn: conn,
docker_hostname: docker_hostname,
use_container_ip: use_container_ip,
session_id: session_id,
properties: properties,
networks: MapSet.new(),
containers: MapSet.new(),
images: MapSet.new(),
compose_envs: []
}}
else
error ->
{:stop, error}
end
end
@doc false
def get_host(), do: wait_for_call(:get_host, __MODULE__)
@doc """
Returns the host to use for connecting to the given container.
In standard mode, returns the same as `get_host/0` (the Docker host).
In container networking mode (DooD), returns the container's internal IP
since mapped ports on the bridge gateway may be unreachable.
"""
def get_host(%Container{} = container), do: get_host(container, __MODULE__)
def get_host(name) when is_atom(name), do: wait_for_call(:get_host, name)
def get_host(%Container{} = container, name) do
if use_container_ip?(container, name) do
container.ip_address
else
wait_for_call(:get_host, name)
end
end
@doc """
Returns the port to use for connecting to the given container on the specified internal port.
In standard mode, returns the host-mapped port (same as `Container.mapped_port/2`).
In container networking mode (DooD), returns the internal port directly
since we connect to the container's IP on the bridge network.
"""
def get_port(%Container{} = container, port), do: get_port(container, port, __MODULE__)
def get_port(%Container{} = container, port, name) do
if use_container_ip?(container, name) do
port
else
Container.mapped_port(container, port)
end
end
# Returns true when we should use the container's internal IP and port.
# Only applies when in container_ip mode AND the container is on the default
# bridge network. Containers on custom networks are not reachable from the
# test container via internal IP.
defp use_container_ip?(%Container{} = container, name) do
case wait_for_call(:get_connection_mode, name) do
:container_ip ->
is_binary(container.ip_address) and container.ip_address != "" and
is_nil(container.network)
_ ->
false
end
end
@doc """
Starts a new container based on the provided configuration, applying any specified wait strategies.
This function performs several steps:
1. Pulls the necessary Docker image.
2. Creates and starts a container with the specified configuration.
3. Registers the container with a reaper process for automatic cleanup, ensuring it is stopped and removed when the current process exits or in case of unforeseen failures.
## Parameters
- `config`: A `%Container{}` struct containing the configuration settings for the container, such as the image to use, environment variables, bound ports, and volume bindings.
## Examples
iex> config = Testcontainers.MySqlContainer.new()
iex> {:ok, container} = Testcontainers.start_container(config)
## Returns
- `{:ok, container}` if the container is successfully created, started, and passes all wait strategies.
- An error tuple, such as `{:error, reason}`, if there is a failure at any step in the process.
## Notes
- The container is automatically registered with a reaper process, ensuring it is stopped and removed when the current process exits, or in the case of unforeseen failures.
- It's important to specify appropriate wait strategies to ensure the container is fully ready for interaction, especially for containers that may take some time to start up services internally.
"""
def start_container(config_builder, name \\ __MODULE__) do
wait_for_call({:start_container, config_builder}, name)
end
@doc """
Stops a running container.
This sends a stop command to the specified container. The Docker daemon terminates the container process gracefully.
## Parameters
- `container_id`: The ID of the container to stop, as a string.
## Returns
- `:ok` if the container stops successfully.
- `{:error, reason}` on failure.
## Examples
:ok = Testcontainers.Connection.stop_container("my_container_id")
"""
def stop_container(container_id, name \\ __MODULE__) when is_binary(container_id) do
wait_for_call({:stop_container, container_id}, name)
end
@doc """
Starts a Docker Compose environment based on the provided configuration.
## Parameters
- `config`: A `%DockerCompose{}` struct containing the configuration.
## Returns
- `{:ok, compose_env}` if the compose environment starts successfully.
- `{:error, reason}` on failure.
"""
def start_compose(config, name \\ __MODULE__) do
wait_for_call({:start_compose, config}, name)
end
@doc """
Stops a running Docker Compose environment.
## Parameters
- `compose_env`: A `%ComposeEnvironment{}` struct representing the running environment.
## Returns
- `:ok` if the compose environment stops successfully.
- `{:error, reason}` on failure.
"""
def stop_compose(compose_env, name \\ __MODULE__) do
wait_for_call({:stop_compose, compose_env}, name)
end
@doc """
Creates a Docker network.
Networks allow containers to communicate with each other using hostnames.
Use `Container.with_network/2` to attach a container to a network.
## Parameters
- `network_name`: The name of the network to create.
- `name`: The name of the Testcontainers GenServer (defaults to `Testcontainers`).
## Returns
- `{:ok, network_id}` if the network is created successfully.
- `{:ok, :already_exists}` if the network already exists.
- `{:error, reason}` on failure.
"""
def create_network(network_name, name \\ __MODULE__) when is_binary(network_name) do
wait_for_call({:create_network, network_name}, name)
end
@doc """
Removes a Docker network.
## Parameters
- `network_name`: The name of the network to remove.
- `name`: The name of the Testcontainers GenServer (defaults to `Testcontainers`).
## Returns
- `:ok` if the network is removed successfully.
- `{:error, reason}` on failure.
"""
def remove_network(network_name, name \\ __MODULE__) when is_binary(network_name) do
wait_for_call({:remove_network, network_name}, name)
end
@impl true
def handle_cast({:track_container, container_id, image}, state) do
{:noreply,
%{
state
| containers: MapSet.put(state.containers, container_id),
images: MapSet.put(state.images, image)
}}
end
def handle_cast({:track_image, image}, state) do
{:noreply, %{state | images: MapSet.put(state.images, image)}}
end
def handle_cast({:track_compose_env, compose_env}, state) do
{:noreply, %{state | compose_envs: [compose_env | state.compose_envs]}}
end
def handle_cast({:untrack_compose_env, compose_env}, state) do
updated =
Enum.reject(state.compose_envs, fn env ->
env.project_name == compose_env.project_name
end)
{:noreply, %{state | compose_envs: updated}}
end
@impl true
def handle_info(_msg, state) do
{:noreply, state}
end
@impl true
def terminate(_reason, state) do
for compose_env <- Map.get(state, :compose_envs, []) do
Logger.info("Stopping compose environment #{compose_env.project_name}")
ComposeCli.down(compose_env.compose)
end
for container_id <- state.containers do
Logger.info("Terminating container #{container_id}")
Api.stop_container(container_id, state.conn)
end
for network <- state.networks do
Logger.info("Removing network #{network}")
Api.remove_network(network, state.conn)
end
if Map.get(state.properties, "cleanup.images", "false") == "true" do
for image <- state.images do
Logger.info("Removing image #{image}")
Api.delete_image(image, state.conn)
end
end
:ok
end
@impl true
def handle_call({:start_container, config_builder}, from, state) do
self_pid = self()
Task.async(fn ->
result = start_and_wait(config_builder, state)
case result do
{:ok, container} ->
GenServer.cast(self_pid, {:track_container, container.container_id, container.image})
_ ->
# Track the image even on failure so it gets cleaned up on terminate
case config_builder do
%Container{image: image} when is_binary(image) ->
GenServer.cast(self_pid, {:track_image, image})
_ ->
:ok
end
end
GenServer.reply(from, result)
end)
{:noreply, state}
end
@impl true
def handle_call({:stop_container, container_id}, from, state) do
Task.async(fn -> GenServer.reply(from, Api.stop_container(container_id, state.conn)) end)
{:noreply, %{state | containers: MapSet.delete(state.containers, container_id)}}
end
@impl true
def handle_call(:get_host, _from, state) do
{:reply, state.docker_hostname, state}
end
@impl true
def handle_call(:get_connection_mode, _from, state) do
mode = if state.use_container_ip, do: :container_ip, else: :mapped_port
{:reply, mode, state}
end
@impl true
def handle_call({:create_network, network_name}, from, state) do
labels = %{
Constants.container_sessionId_label() => state.session_id,
Constants.container_version_label() => Constants.library_version(),
Constants.container_lang_label() => Constants.container_lang_value(),
Constants.container_label() => "true",
Constants.container_reuse() => "false"
}
Task.async(fn ->
result = Api.create_network(network_name, state.conn, labels: labels)
GenServer.reply(from, result)
end)
{:noreply, %{state | networks: MapSet.put(state.networks, network_name)}}
end
@impl true
def handle_call({:remove_network, network_name}, from, state) do
Task.async(fn ->
result = Api.remove_network(network_name, state.conn)
GenServer.reply(from, result)
end)
{:noreply, %{state | networks: MapSet.delete(state.networks, network_name)}}
end
@impl true
def handle_call({:start_compose, %DockerCompose{} = compose}, from, state) do
self_pid = self()
Task.async(fn ->
result = start_compose_env(compose, state)
case result do
{:ok, compose_env} ->
GenServer.cast(self_pid, {:track_compose_env, compose_env})
_ ->
:ok
end
GenServer.reply(from, result)
end)
{:noreply, state}
end
@impl true
def handle_call({:stop_compose, %ComposeEnvironment{} = compose_env}, from, state) do
self_pid = self()
Task.async(fn ->
result = ComposeCli.down(compose_env.compose)
GenServer.cast(self_pid, {:untrack_compose_env, compose_env})
GenServer.reply(from, result)
end)
{:noreply, state}
end
# private functions
defp should_use_container_ip?(docker_hostname) do
if running_in_container?() and docker_hostname != "localhost" do
# Probe the bridge gateway to check if mapped ports are reachable.
# If hairpin NAT is blocked (common in DooD), we fall back to
# connecting to containers via their internal IPs.
case :gen_tcp.connect(~c"#{docker_hostname}", 0, [], 2000) do
{:ok, socket} ->
:gen_tcp.close(socket)
false
{:error, :econnrefused} ->
# Connection refused means the host IS reachable, just no service on port 0
false
{:error, reason} ->
Logger.info(
"Bridge gateway #{docker_hostname} unreachable (#{inspect(reason)}). " <>
"Switching to container networking mode (direct IP access)"
)
true
end
else
false
end
end
@doc false
def running_in_container?(
dockerenv_path \\ "/.dockerenv",
cgroup_path \\ "/proc/1/cgroup"
) do
if File.exists?(dockerenv_path) do
true
else
case File.read(cgroup_path) do
{:ok, content} ->
Regex.match?(~r/(docker|kubepods|lxc|containerd)/, content)
{:error, _} ->
false
end
end
end
defp start_compose_env(%DockerCompose{} = compose, state) do
with :ok <- ComposeCli.up(compose),
{:ok, ps_entries} <- ComposeCli.ps(compose) do
services =
ps_entries
|> Enum.map(fn entry ->
service_name = Map.get(entry, "Service", "")
container_id = Map.get(entry, "ID", "")
service_state = Map.get(entry, "State", "")
publishers = Map.get(entry, "Publishers", [])
ports = ComposeCli.parse_publishers(publishers)
%ComposeService{
service_name: service_name,
container_id: container_id,
state: service_state,
exposed_ports: ports
}
end)
|> Map.new(fn service -> {service.service_name, service} end)
# Run per-service wait strategies if configured
with :ok <- run_compose_wait_strategies(compose, services, state) do
compose_env = %ComposeEnvironment{
compose: compose,
project_name: compose.project_name,
docker_host: state.docker_hostname,
services: services
}
{:ok, compose_env}
end
end
end
@doc false
def parse_gateway_from_proc_route(content) do
content
|> String.split("\n")
|> Enum.drop(1)
|> Enum.map(&String.split(&1, "\t"))
|> Enum.find(fn
[_iface, destination | _rest] -> destination == "00000000"
_ -> false
end)
|> case do
[_iface, _destination, gateway_hex | _rest] ->
decode_hex_gateway(gateway_hex)
_ ->
{:error, :no_default_route}
end
end
defp decode_hex_gateway(hex) when byte_size(hex) == 8 do
{value, ""} = Integer.parse(hex, 16)
a = Bitwise.band(value, 0xFF)
b = Bitwise.band(Bitwise.bsr(value, 8), 0xFF)
c = Bitwise.band(Bitwise.bsr(value, 16), 0xFF)
d = Bitwise.band(Bitwise.bsr(value, 24), 0xFF)
{:ok, "#{a}.#{b}.#{c}.#{d}"}
end
defp decode_hex_gateway(_), do: {:error, :invalid_gateway}
defp run_compose_wait_strategies(%DockerCompose{} = compose, services, state) do
Enum.reduce_while(compose.wait_strategies, :ok, fn {service_name, strategies}, :ok ->
case Map.get(services, service_name) do
nil ->
{:halt, {:error, {:service_not_found, service_name}}}
%ComposeService{container_id: container_id} ->
case Api.get_container(container_id, state.conn) do
{:ok, container} ->
result =
Enum.reduce(strategies, :ok, fn
strategy, :ok ->
WaitStrategy.wait_until_container_is_ready(strategy, container, state.conn)
_, error ->
error
end)
case result do
:ok -> {:cont, :ok}
error -> {:halt, error}
end
{:error, _} = error ->
{:halt, error}
end
end
end)
end
defp get_docker_hostname(docker_host_url, conn, properties) do
# Check for explicit host override first
host_override =
Map.get(properties, "tc.host.override") ||
System.get_env("TESTCONTAINERS_HOST_OVERRIDE")
if host_override do
Logger.debug("Using host override: #{host_override}")
{:ok, host_override}
else
do_get_docker_hostname(docker_host_url, conn)
end
end
defp do_get_docker_hostname(docker_host_url, conn) do
case URI.parse(docker_host_url) do
uri when uri.scheme == "http" or uri.scheme == "https" ->
{:ok, uri.host}
uri when uri.scheme == "http+unix" ->
if running_in_container?() do
Logger.debug("Running in docker environment, trying to get bridge network gateway")
with {:ok, gateway} <- Api.get_bridge_gateway(conn) do
{:ok, gateway}
else
{:error, reason} ->
Logger.debug(
"Failed to get bridge gateway: #{inspect(reason)}. Trying /proc/net/route"
)
case File.read("/proc/net/route") do
{:ok, content} ->
case parse_gateway_from_proc_route(content) do
{:ok, gateway} ->
Logger.debug("Found gateway from /proc/net/route: #{gateway}")
{:ok, gateway}
{:error, _} ->
Logger.debug("Failed to parse /proc/net/route. Using localhost")
{:ok, "localhost"}
end
{:error, _} ->
Logger.debug("Cannot read /proc/net/route. Using localhost")
{:ok, "localhost"}
end
end
else
Logger.debug("Not running in docker environment, using localhost")
{:ok, "localhost"}
end
end
end
defp wait_for_call(call, name) do
GenServer.call(name, call, @timeout)
end
defp start_reaper(conn, session_id, properties, docker_host, docker_hostname) do
ryuk_disabled = Map.get(properties, "ryuk.disabled", "false") == "true"
case ryuk_disabled do
true ->
ryukDisabledMessage =
"""
********************************************************************************
Ryuk has been disabled. This can cause unexpected behavior in your environment.
********************************************************************************
"""
IO.puts(ryukDisabledMessage)
{:ok}
_ ->
start_ryuk(conn, session_id, properties, docker_host, docker_hostname)
end
end
defp start_ryuk(conn, session_id, properties, docker_host, docker_hostname) do
ryuk_privileged = Map.get(properties, "ryuk.container.privileged", "false") == "true"
ryuk_config =
Container.new("testcontainers/ryuk:#{Constants.ryuk_version()}")
|> Container.with_exposed_port(8080)
|> then(&apply_docker_socket_volume_binding(&1, docker_host))
|> Container.with_auto_remove(true)
|> Container.with_privileged(ryuk_privileged)
ryuk_config = resolve_pull_policy(ryuk_config, properties)
with :ok <- maybe_pull_image(ryuk_config, conn),
{:ok, ryuk_container_id} <- Api.create_container(ryuk_config, conn),
:ok <- Api.start_container(ryuk_container_id, conn),
{:ok, container} <- Api.get_container(ryuk_container_id, conn),
:ok <- connect_and_register_ryuk(container, docker_hostname, session_id) do
{:ok}
end
end
defp connect_and_register_ryuk(container, docker_hostname, session_id, attempt \\ 1)
defp connect_and_register_ryuk(container, docker_hostname, session_id, attempt)
when attempt <= 5 do
with {:ok, socket} <- create_ryuk_socket(container, docker_hostname),
:ok <- register_ryuk_filter(session_id, socket) do
:ok
else
error ->
Logger.info(
"Failed to connect and register ryuk filter: #{inspect(error)}. Retrying... Attempt #{attempt}/5"
)
:timer.sleep(1000)
connect_and_register_ryuk(container, docker_hostname, session_id, attempt + 1)
end
end
defp connect_and_register_ryuk(_container, _docker_hostname, _session_id, _attempt) do
{:error, :ryuk_connection_failed}
end
defp create_ryuk_socket(container, docker_hostname, reattempt_count \\ 0)
defp create_ryuk_socket(%Container{} = container, docker_hostname, reattempt_count)
when reattempt_count < 5 do
host_port = Container.mapped_port(container, 8080)
case try_tcp_connect(docker_hostname, host_port) do
{:ok, connected} ->
{:ok, connected}
{:error, reason} ->
# If connecting via docker_hostname:mapped_port fails and we're in a container,
# try the container's internal IP on its internal port. In DooD (Docker-outside-of-Docker)
# both containers are on the same bridge network, so direct IP access works.
case try_container_internal_connect(container, 8080, reason) do
{:ok, connected} ->
{:ok, connected}
{:error, _} ->
Logger.info(
"Connection to Ryuk failed (#{inspect(reason)}). Retrying... Attempt #{reattempt_count + 1}/5"
)
:timer.sleep(1000)
create_ryuk_socket(container, docker_hostname, reattempt_count + 1)
end
end
end
defp create_ryuk_socket(%Container{} = _container, _docker_hostname, _reattempt_count) do
Logger.info("Ryuk host refused to connect")
{:error, :econnrefused}
end
defp try_tcp_connect(host, port) do
:gen_tcp.connect(~c"#{host}", port, [
:binary,
active: false,
packet: :line,
send_timeout: 10000
], 5000)
end
defp try_container_internal_connect(%Container{ip_address: ip}, internal_port, original_reason)
when is_binary(ip) and ip != "" do
if running_in_container?() do
Logger.info(
"Connection via mapped port failed (#{inspect(original_reason)}). " <>
"Trying container internal IP #{ip}:#{internal_port}"
)
try_tcp_connect(ip, internal_port)
else
{:error, original_reason}
end
end
defp try_container_internal_connect(_container, _internal_port, original_reason) do
{:error, original_reason}
end
defp register_ryuk_filter(value, socket) do
:gen_tcp.send(
socket,
"label=#{container_sessionId_label()}=#{value}&" <>
"label=#{container_version_label()}=#{library_version()}&" <>
"label=#{container_lang_label()}=#{container_lang_value()}&" <>
"label=#{container_label()}=#{true}&" <>
"label=#{container_reuse()}=#{false}\n"
)
case :gen_tcp.recv(socket, 0, 2_000) do
{:ok, "ACK\n"} ->
:ok
{:error, reason} ->
{:error, {:failed_to_register_ryuk_filter, reason}}
end
end
defp start_and_wait(config_builder, state) do
case Testcontainers.ContainerBuilderHelper.build(config_builder, state) do
{:reuse, config, hash} ->
case Api.get_container_by_hash(hash, state.conn) do
{:error, :no_container} ->
Logger.debug("Container does not exist with hash: #{hash}")
create_and_start_container(
config,
config_builder,
state
)
{:error, error} ->
Logger.debug("Failed to get container by hash: #{inspect(error)}")
{:error, error}
{:ok, container} ->
Logger.debug("Container already exists with hash: #{hash}")
{:ok, container}
end
{:noreuse, config, _} ->
create_and_start_container(
config,
config_builder,
state
)
end
end
defp create_and_start_container(config, config_builder, state) do
config = resolve_pull_policy(config, state.properties)
with :ok <- maybe_pull_image(config, state.conn),
{:ok, id} <- Api.create_container(config, state.conn),
:ok <- copy_to_container(id, config, state.conn) do
start_and_wait_container(id, config, config_builder, state)
end
end
defp resolve_pull_policy(%{pull_policy: nil} = config, properties) do
pull_policy =
case Map.get(properties, "pull.policy", "always") do
"missing" -> PullPolicy.never_pull()
"never" -> PullPolicy.never_pull()
_ -> PullPolicy.always_pull()
end
%{config | pull_policy: pull_policy}
end
defp resolve_pull_policy(config, _properties), do: config
defp start_and_wait_container(id, config, config_builder, state) do
with :ok <- Api.start_container(id, state.conn),
{:ok, container} <- Api.get_container(id, state.conn),
:ok <- ContainerBuilder.after_start(config_builder, container, state.conn),
:ok <- wait_for_container(container, config.wait_strategies || [], state.conn) do
{:ok, container}
else
error ->
Logger.info("Cleaning up container #{id} after failed start")
Api.stop_container(id, state.conn)
error
end
end
defp maybe_pull_image(config = %{pull_policy: %{always_pull: true}}, conn) do
case Api.pull_image(config.image, conn, auth: config.auth) do
{:ok, _nil} -> :ok
error -> error
end
end
defp maybe_pull_image(config = %{pull_policy: %{pull_condition: expr}}, conn)
when is_function(expr) do
with {:eval, true} <- {:eval, expr.(config, conn)},
{:ok, _nil} <- Api.pull_image(config.image, conn, auth: config.auth) do
:ok
else
{:eval, reason} ->
Logger.debug(
"Pull policy expression evaluated to: #{inspect(reason)}, image will not be fetched"
)
:ok
error ->
error
end
end
defp maybe_pull_image(_config, _conn) do
:ok
end
defp copy_to_container(id, config, conn) do
Enum.reduce(config.copy_to, :ok, fn
copy_to, :ok ->
CopyTo.copy_to(conn, id, copy_to)
_, error ->
error
end)
end
defp wait_for_container(container, wait_strategies, conn) do
Enum.reduce(wait_strategies, :ok, fn
wait_strategy, :ok ->
WaitStrategy.wait_until_container_is_ready(wait_strategy, container, conn)
_, error ->
error
end)
end
defp apply_docker_socket_volume_binding(config, docker_host) do
socket_override = System.get_env("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE")
if socket_override do
Logger.debug("Using docker socket override: #{socket_override}")
Container.with_bind_mount(
config,
socket_override,
"/var/run/docker.sock",
"rw"
)
else
case {os_type(), URI.parse(docker_host)} do
{os, uri} -> handle_docker_socket_binding(config, os, uri)
end
end
end
@dialyzer {:nowarn_function, handle_docker_socket_binding: 3}
defp handle_docker_socket_binding(config, :linux, %URI{scheme: "unix", path: docker_socket_path}) do
Container.with_bind_mount(
config,
docker_socket_path,
"/var/run/docker.sock",
"rw"
)
end
defp handle_docker_socket_binding(config, :macos, %URI{scheme: "unix", path: docker_socket_path}) do
Container.with_bind_mount(
config,
docker_socket_path,
"/var/run/docker.sock",
"rw"
)
end
defp handle_docker_socket_binding(config, :windows, _) do
Container.with_bind_mount(
config,
"//var/run/docker.sock",
"/var/run/docker.sock",
"rw"
)
end
defp handle_docker_socket_binding(config, _, _), do: config
end