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/docker/api.ex
# SPDX-License-Identifier: MIT
defmodule Testcontainers.Docker.Api do
@moduledoc """
Internal docker api. Only for direct use by `Testcontainers`
"""
alias DockerEngineAPI.Model.ExecConfig
alias DockerEngineAPI.Model.HostConfig
alias DockerEngineAPI.Api
alias Testcontainers.Container
def get_container(container_id, conn)
when is_binary(container_id) do
case Api.Container.container_inspect(conn, container_id) do
{:error, %Tesla.Env{status: other}} ->
{:error, {:http_error, other}}
{:ok, %DockerEngineAPI.Model.ErrorResponse{} = error} ->
{:error, {:failed_to_get_container, error}}
{:ok, response} ->
{:ok, from(response)}
end
end
def get_container_by_hash(hash, conn) do
filters_json =
%{
"label" => ["#{Testcontainers.Constants.container_reuse_hash_label()}=#{hash}"]
}
|> Jason.encode!()
case Api.Container.container_list(conn, filters: filters_json) do
{:ok, %DockerEngineAPI.Model.ErrorResponse{} = error} ->
{:error, {:failed_to_get_container, error}}
{:error, error} ->
{:error, error}
{:ok, []} ->
{:error, :no_container}
{:ok, [container | _]} ->
get_container(container."Id", conn)
end
end
def pull_image(image, conn, opts \\ []) when is_binary(image) do
auth = Keyword.get(opts, :auth, nil)
headers = if auth, do: ["X-Registry-Auth": auth], else: []
case Api.Image.image_create(
conn,
Keyword.merge([fromImage: image], headers)
) do
{:ok, %Tesla.Env{status: 200}} ->
{:ok, nil}
{:error, %Tesla.Env{status: other}} ->
{:error, {:http_error, other}}
{:ok, %DockerEngineAPI.Model.ErrorResponse{} = error} ->
{:error, {:failed_to_pull_image, error}}
end
end
def delete_image(image, conn) when is_binary(image) do
case Api.Image.image_delete(conn, image, force: true) do
{:ok, _} -> :ok
{:error, _} = error -> error
end
end
def create_container(%Container{} = container, conn) do
case Api.Container.container_create(conn, container_create_request(container)) do
{:error, %Tesla.Env{status: other}} ->
{:error, {:http_error, other}}
{:ok, %{Id: id}} ->
{:ok, id}
{:ok, %DockerEngineAPI.Model.ErrorResponse{} = error} ->
{:error, {:failed_to_create_container, error}}
end
end
def start_container(id, conn) when is_binary(id) do
case Api.Container.container_start(conn, id) do
{:ok, %Tesla.Env{status: 204}} ->
:ok
{:error, %Tesla.Env{status: other}} ->
{:error, {:http_error, other}}
{:ok, %DockerEngineAPI.Model.ErrorResponse{} = error} ->
{:error, {:failed_to_start_container, error}}
end
end
def stop_container(container_id, conn) when is_binary(container_id) do
with {:ok, _} <-
Api.Container.container_kill(conn, container_id),
{:ok, _} <-
Api.Container.container_delete(conn, container_id) do
:ok
end
end
def put_file(container_id, connection, path, file_name, file_contents) do
with {:ok, tar_file_contents} <- create_tar_stream(file_name, file_contents),
{:ok, %Tesla.Env{}} <-
Api.Container.put_container_archive(connection, container_id, path, tar_file_contents) do
:ok
end
end
# Helper function to create a tar stream from a file
defp create_tar_stream(file_name, file_contents) do
tar_file = System.tmp_dir!() |> Path.join("#{Uniq.UUID.uuid4()}-#{file_name}.tar")
:ok =
:erl_tar.create(
tar_file,
# file_name must be charlist ref https://til.kaiwern.com/tags/88
[{file_name |> String.to_charlist(), file_contents}],
[:compressed]
)
with {:ok, tar_file_contents} <- File.read(tar_file),
:ok <- File.rm(tar_file) do
{:ok, tar_file_contents}
end
end
def inspect_exec(exec_id, conn) do
case Api.Exec.exec_inspect(conn, exec_id) do
{:ok, %DockerEngineAPI.Model.ExecInspectResponse{} = body} ->
{:ok, parse_inspect_result(body)}
{:ok, %DockerEngineAPI.Model.ErrorResponse{message: message}} ->
{:error, message}
{:error, message} ->
{:error, message}
end
end
def start_exec(container_id, command, conn) do
with {:ok, exec_id} <- create_exec(container_id, command, conn),
:ok <- start_exec(exec_id, conn) do
{:ok, exec_id}
end
end
def stdout_logs(container_id, conn) do
case Api.Container.container_logs(
conn,
container_id,
stdout: true,
stderr: true
) do
{:ok, %Tesla.Env{body: body}} ->
{:ok, body}
{:ok, %DockerEngineAPI.Model.ErrorResponse{message: message}} ->
{:error, message}
{:error, error} ->
{:error, :unknown, error}
end
end
def get_bridge_gateway(conn) do
case Api.Network.network_inspect(conn, "bridge") do
{:ok, %DockerEngineAPI.Model.Network{IPAM: %DockerEngineAPI.Model.Ipam{Config: config}}} ->
with_gateway =
config
|> Enum.filter(fn cfg -> Map.get(cfg, :Gateway, nil) != nil end)
if length(with_gateway) > 0 do
gateway = with_gateway |> Kernel.hd() |> Map.get(:Gateway)
{:ok, gateway}
else
{:error, :no_gateway}
end
{:error, reason} ->
{:error, reason}
end
end
@doc """
Creates a Docker network.
"""
# Suppress Dialyzer warnings - runtime behavior may differ from generated specs
@dialyzer {:nowarn_function, create_network: 3}
def create_network(name, conn, opts \\ []) when is_binary(name) do
driver = Keyword.get(opts, :driver, "bridge")
labels = Keyword.get(opts, :labels, %{})
body = %DockerEngineAPI.Model.NetworkCreateRequest{
Name: name,
Driver: driver,
CheckDuplicate: true,
Labels: labels
}
case Api.Network.network_create(conn, body) do
{:ok, %DockerEngineAPI.Model.NetworkCreateResponse{Id: id}} ->
{:ok, id}
{:ok, %DockerEngineAPI.Model.ErrorResponse{message: message}} ->
{:error, {:failed_to_create_network, message}}
{_, %Tesla.Env{status: 409}} ->
{:ok, :already_exists}
{_, %Tesla.Env{status: status}} ->
{:error, {:http_error, status}}
end
end
@doc """
Removes a Docker network.
"""
# Suppress Dialyzer warnings - runtime behavior may differ from generated specs
@dialyzer {:nowarn_function, remove_network: 2}
def remove_network(name, conn) when is_binary(name) do
case Api.Network.network_delete(conn, name) do
{:ok, nil} ->
:ok
{_, %Tesla.Env{status: 204}} ->
:ok
{_, %Tesla.Env{status: 404}} ->
{:error, :network_not_found}
{:ok, %DockerEngineAPI.Model.ErrorResponse{message: message}} ->
{:error, {:failed_to_remove_network, message}}
{_, %Tesla.Env{status: status}} ->
{:error, {:http_error, status}}
end
end
@doc """
Checks if a network exists.
"""
def network_exists?(name, conn) when is_binary(name) do
case Api.Network.network_inspect(conn, name) do
{:ok, %DockerEngineAPI.Model.Network{}} -> true
_ -> false
end
end
def tag_image(image, repo, tag, conn) do
case Api.Image.image_tag(conn, image, repo: repo, tag: tag) do
{:ok, %Tesla.Env{status: 201}} ->
{:ok, "#{repo}:#{tag}"}
{:ok, %Tesla.Env{status: status}} ->
{:error, {:http_error, status}}
{:ok, %DockerEngineAPI.Model.ErrorResponse{message: message}} ->
{:error, message}
{:error, reason} ->
{:error, reason}
end
end
defp parse_inspect_result(%DockerEngineAPI.Model.ExecInspectResponse{} = json) do
%{running: json."Running", exit_code: json."ExitCode"}
end
defp container_create_request(%Container{} = container_config) do
base_request = %DockerEngineAPI.Model.ContainerCreateRequest{
Image: container_config.image,
Cmd: container_config.cmd,
ExposedPorts: map_exposed_ports(container_config),
Env: map_env(container_config),
Labels: container_config.labels,
Hostname: container_config.hostname,
HostConfig: %HostConfig{
AutoRemove: container_config.auto_remove,
PortBindings: map_port_bindings(container_config),
Privileged: container_config.privileged,
Binds: map_binds(container_config),
Mounts: map_volumes(container_config),
NetworkMode: container_config.network_mode || container_config.network
}
}
# Add NetworkingConfig if a network is specified
if container_config.network do
endpoint_config = %{
container_config.network => %DockerEngineAPI.Model.EndpointSettings{}
}
Map.put(base_request, :NetworkingConfig, %DockerEngineAPI.Model.NetworkingConfig{
EndpointsConfig: endpoint_config
})
else
base_request
end
end
defp map_exposed_ports(%Container{} = container_config) do
container_config.exposed_ports
|> Enum.map(fn
{container_port, _host_port} -> {container_port, %{}}
end)
|> Enum.into(%{})
end
defp map_env(%Container{} = container_config) do
container_config.environment
|> Enum.map(fn {key, value} -> "#{key}=#{value}" end)
end
defp map_port_bindings(%Container{} = container_config) do
container_config.exposed_ports
|> Enum.map(fn
{container_port, host_port} when is_nil(host_port) ->
{container_port, [%{"HostIp" => "0.0.0.0", "HostPort" => ""}]}
{container_port, host_port} ->
{container_port, [%{"HostIp" => "0.0.0.0", "HostPort" => to_string(host_port)}]}
end)
|> Enum.into(%{})
end
defp map_binds(%Container{} = container_config) do
container_config.bind_mounts
|> Enum.map(fn volume_binding ->
"#{volume_binding.host_src}:#{volume_binding.container_dest}:#{volume_binding.options}"
end)
end
defp map_volumes(%Container{} = container_config) do
container_config.bind_volumes
|> Enum.map(fn volume_to_dest ->
%{
Target: volume_to_dest.container_dest,
Source: volume_to_dest.volume,
Type: "volume",
ReadOnly: volume_to_dest.read_only
}
end)
end
defp from(%DockerEngineAPI.Model.ContainerInspectResponse{
Id: container_id,
Image: image,
NetworkSettings: %{IPAddress: ip_address, Ports: ports, Networks: networks},
Config: %{Env: env, Labels: labels}
}) do
# For custom networks, the IP address is in Networks.<network_name>.IPAddress
# The default bridge IPAddress will be empty for custom networks
resolved_ip = resolve_ip_address(ip_address, networks)
make_container(container_id, image, labels, resolved_ip, ports, env)
end
# Also handle when Networks key is missing
defp from(%DockerEngineAPI.Model.ContainerInspectResponse{
Id: container_id,
Image: image,
NetworkSettings: %{IPAddress: ip_address, Ports: ports},
Config: %{Env: env, Labels: labels}
}) do
make_container(container_id, image, labels, ip_address, ports, env)
end
defp make_container(container_id, image, labels, ip_address, ports, env) do
%Container{
container_id: container_id,
image: image,
labels: labels,
ip_address: ip_address,
exposed_ports:
Enum.reduce(ports || [], [], fn {key, ports}, acc ->
acc ++
Enum.map(ports || [], fn %{"HostPort" => host_port} ->
{key |> String.replace("/tcp", "") |> String.to_integer(),
host_port |> String.to_integer()}
end)
end),
environment:
Enum.reduce(env || [], %{}, fn env, acc ->
tokens = String.split(env, "=")
Map.merge(acc, %{"#{List.first(tokens)}": List.last(tokens)})
end)
}
end
# Resolve IP address, preferring custom network IPs if default is empty
defp resolve_ip_address(nil, networks), do: get_ip_from_networks(networks)
defp resolve_ip_address("", networks), do: get_ip_from_networks(networks)
defp resolve_ip_address(ip, _networks) when is_binary(ip) and ip != "", do: ip
defp get_ip_from_networks(nil), do: nil
defp get_ip_from_networks(networks) when is_map(networks) do
# Get the first non-empty IP from any network
networks
|> Enum.find_value(fn
{_name, %{IPAddress: ip}} when is_binary(ip) and ip != "" -> ip
_ -> nil
end)
end
defp create_exec(container_id, command, conn) do
data = %ExecConfig{Cmd: command}
case Api.Exec.container_exec(conn, container_id, data) do
{:ok, %DockerEngineAPI.Model.IdResponse{Id: id}} ->
{:ok, id}
{:ok, %DockerEngineAPI.Model.ErrorResponse{message: message}} ->
{:error, message}
{:error, message} ->
{:error, message}
end
end
defp start_exec(exec_id, conn) do
case Api.Exec.exec_start(conn, exec_id, body: %{:Detach => true}) do
{:ok, %Tesla.Env{status: 200}} ->
:ok
{:ok, %Tesla.Env{status: status}} ->
{:error, {:http_error, status}}
{:ok, %DockerEngineAPI.Model.ErrorResponse{message: message}} ->
{:error, message}
{:error, message} ->
{:error, message}
end
end
end