Skip to content

Commit 8bd9983

Browse files
authored
Merge branch 'main' into add-toxiproxy-support-to-test-containers
2 parents 9a2f926 + cf83e2d commit 8bd9983

9 files changed

Lines changed: 224 additions & 31 deletions

File tree

.github/workflows/elixir.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
- name: Install dependencies
5252
run: mix deps.get
5353
- name: Run tests
54-
run: MIX_ENV=test mix citest
54+
run: mix citest
5555
- name: Verify version
5656
run: |
5757
mix run -e "if(Testcontainers.Constants.library_version() == Mix.Project.config()[:version], do: IO.puts("OK"), else: System.halt(\"version mismatch\"))"

lib/container.ex

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,30 @@ defmodule Testcontainers.Container do
9292
Adds a _port_ to be exposed on the _container_.
9393
"""
9494
def with_exposed_port(%__MODULE__{} = config, port) when is_integer(port) do
95-
filtered_ports = config.exposed_ports |> Enum.reject(fn p -> p == port end)
95+
filtered_ports =
96+
config.exposed_ports
97+
|> Enum.reject(fn
98+
{p, _} -> p == port
99+
p -> p == port
100+
end)
96101

97-
%__MODULE__{config | exposed_ports: [port | filtered_ports]}
102+
%__MODULE__{config | exposed_ports: [{port, nil} | filtered_ports]}
98103
end
99104

100105
@doc """
101106
Adds multiple _ports_ to be exposed on the _container_.
102107
"""
103108
def with_exposed_ports(%__MODULE__{} = config, ports) when is_list(ports) do
104-
filtered_ports = config.exposed_ports |> Enum.reject(fn port -> port in ports end)
109+
filtered_ports =
110+
config.exposed_ports
111+
|> Enum.reject(fn
112+
{p, _} -> p in ports
113+
p -> p in ports
114+
end)
105115

106-
%__MODULE__{config | exposed_ports: ports ++ filtered_ports}
116+
new_ports = Enum.map(ports, fn port -> {port, nil} end)
117+
118+
%__MODULE__{config | exposed_ports: new_ports ++ filtered_ports}
107119
end
108120

109121
@doc """
@@ -172,6 +184,16 @@ defmodule Testcontainers.Container do
172184
%__MODULE__{config | auto_remove: auto_remove}
173185
end
174186

187+
@doc """
188+
Sets whether the container should run in privileged mode.
189+
190+
Required for containers that need access to the Docker/Podman socket
191+
when running under SELinux (e.g., Ryuk on Podman).
192+
"""
193+
def with_privileged(%__MODULE__{} = config, privileged) when is_boolean(privileged) do
194+
%__MODULE__{config | privileged: privileged}
195+
end
196+
175197
@doc """
176198
Sets whether the container should be reused if it is already running.
177199
"""
@@ -264,13 +286,10 @@ defmodule Testcontainers.Container do
264286
"""
265287
def mapped_port(%__MODULE__{} = container, port) when is_number(port) do
266288
container.exposed_ports
267-
|> Enum.filter(fn
268-
{exposed_port, _} -> exposed_port == port
269-
port -> port == port
289+
|> Enum.find_value(nil, fn
290+
{^port, host_port} -> host_port
291+
_ -> nil
270292
end)
271-
|> List.first({})
272-
|> Tuple.to_list()
273-
|> List.last()
274293
end
275294

276295
@doc """

lib/docker/api.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,6 @@ defmodule Testcontainers.Docker.Api do
301301
container_config.exposed_ports
302302
|> Enum.map(fn
303303
{container_port, _host_port} -> {container_port, %{}}
304-
port -> {port, %{}}
305304
end)
306305
|> Enum.into(%{})
307306
end
@@ -314,11 +313,11 @@ defmodule Testcontainers.Docker.Api do
314313
defp map_port_bindings(%Container{} = container_config) do
315314
container_config.exposed_ports
316315
|> Enum.map(fn
316+
{container_port, host_port} when is_nil(host_port) ->
317+
{container_port, [%{"HostIp" => "0.0.0.0", "HostPort" => ""}]}
318+
317319
{container_port, host_port} ->
318320
{container_port, [%{"HostIp" => "0.0.0.0", "HostPort" => to_string(host_port)}]}
319-
320-
port ->
321-
{port, [%{"HostIp" => "0.0.0.0", "HostPort" => ""}]}
322321
end)
323322
|> Enum.into(%{})
324323
end

lib/testcontainers.ex

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,29 @@ defmodule Testcontainers do
5050
defp setup(options) do
5151
{conn, docker_host_url, docker_host} = Connection.get_connection(options)
5252

53+
# Read properties first so we can configure Ryuk appropriately
54+
{:ok, properties} = PropertiesParser.read_property_sources()
55+
5356
session_id =
5457
:crypto.hash(:sha, "#{inspect(self())}#{DateTime.utc_now() |> DateTime.to_string()}")
5558
|> Base.encode16()
5659

60+
ryuk_privileged = Map.get(properties, "ryuk.container.privileged", "false") == "true"
61+
5762
ryuk_config =
5863
Container.new("testcontainers/ryuk:#{Constants.ryuk_version()}")
5964
|> Container.with_exposed_port(8080)
6065
|> then(&apply_docker_socket_volume_binding(&1, docker_host))
6166
|> Container.with_auto_remove(true)
67+
|> Container.with_privileged(ryuk_privileged)
6268

6369
with {:ok, _} <- Api.pull_image(ryuk_config.image, conn),
6470
{:ok, docker_hostname} <- get_docker_hostname(docker_host_url, conn),
6571
{:ok, ryuk_container_id} <- Api.create_container(ryuk_config, conn),
6672
:ok <- Api.start_container(ryuk_container_id, conn),
6773
{:ok, container} <- Api.get_container(ryuk_container_id, conn),
6874
{:ok, socket} <- create_ryuk_socket(container, docker_hostname),
69-
:ok <- register_ryuk_filter(session_id, socket),
70-
{:ok, properties} <- PropertiesParser.read_property_file() do
75+
:ok <- register_ryuk_filter(session_id, socket) do
7176
Logger.info("Testcontainers initialized")
7277

7378
{:ok,

lib/util/properties.ex

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
defmodule Testcontainers.Util.PropertiesParser do
22
@moduledoc false
33

4-
@file_path "~/.testcontainers.properties"
4+
@user_file "~/.testcontainers.properties"
5+
@project_file ".testcontainers.properties"
6+
@env_prefix "TESTCONTAINERS_"
57

6-
def read_property_file(file_path \\ @file_path) do
8+
def read_property_file(file_path \\ @user_file) do
79
if File.exists?(Path.expand(file_path)) do
810
with {:ok, content} <- File.read(Path.expand(file_path)),
911
properties <- parse_properties(content) do
@@ -18,6 +20,78 @@ defmodule Testcontainers.Util.PropertiesParser do
1820
end
1921
end
2022

23+
@doc """
24+
Reads properties from all sources with proper precedence.
25+
26+
Configuration is read from three sources with the following precedence
27+
(highest to lowest):
28+
29+
1. Environment variables (TESTCONTAINERS_* prefix)
30+
2. User file (~/.testcontainers.properties)
31+
3. Project file (.testcontainers.properties)
32+
33+
Environment variables are converted from TESTCONTAINERS_PROPERTY_NAME format
34+
to property.name format (uppercase to lowercase, underscores to dots, prefix removed).
35+
36+
## Options
37+
38+
- `:user_file` - path to user properties file (default: ~/.testcontainers.properties)
39+
- `:project_file` - path to project properties file (default: .testcontainers.properties)
40+
- `:env_prefix` - environment variable prefix (default: TESTCONTAINERS_)
41+
42+
## Returns
43+
44+
- `{:ok, map}` with merged properties.
45+
"""
46+
def read_property_sources(opts \\ []) do
47+
user_file = Keyword.get(opts, :user_file, @user_file)
48+
project_file = Keyword.get(opts, :project_file, @project_file)
49+
env_prefix = Keyword.get(opts, :env_prefix, @env_prefix)
50+
51+
project_props = read_file_silent(project_file)
52+
user_props = read_file_silent(user_file)
53+
env_props = read_env_vars(env_prefix)
54+
55+
# Merge in order of lowest to highest precedence
56+
merged =
57+
project_props
58+
|> Map.merge(user_props)
59+
|> Map.merge(env_props)
60+
61+
{:ok, merged}
62+
end
63+
64+
defp read_file_silent(file_path) do
65+
expanded = Path.expand(file_path)
66+
67+
if File.exists?(expanded) do
68+
case File.read(expanded) do
69+
{:ok, content} -> parse_properties(content)
70+
{:error, _} -> %{}
71+
end
72+
else
73+
%{}
74+
end
75+
end
76+
77+
defp read_env_vars(prefix) do
78+
System.get_env()
79+
|> Enum.filter(fn {key, _value} -> String.starts_with?(key, prefix) end)
80+
|> Enum.map(&env_to_property(&1, prefix))
81+
|> Map.new()
82+
end
83+
84+
# Converts TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED to ryuk.container.privileged
85+
defp env_to_property({key, value}, prefix) do
86+
property_key =
87+
key
88+
|> String.replace_prefix(prefix, "")
89+
|> String.downcase()
90+
|> String.replace("_", ".")
91+
92+
{property_key, value}
93+
end
94+
2195
defp parse_properties(content) do
2296
content
2397
|> String.split("\n")

mix.exs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ defmodule TestcontainersElixir.MixProject do
2323
links: %{"GitHub" => @source_url},
2424
licenses: ["MIT"]
2525
],
26+
preferred_cli_env: [
27+
test: :test,
28+
citest: :test,
29+
"testcontainers.test": :test
30+
],
2631
test_coverage: [
2732
summary: [threshold: 50],
2833
ignore_modules: [
@@ -34,8 +39,8 @@ defmodule TestcontainersElixir.MixProject do
3439
]
3540
end
3641

37-
defp elixirc_paths(:test), do: ["lib", "docker_engine_api", "test/support"]
38-
defp elixirc_paths(_), do: ["lib", "docker_engine_api"]
42+
defp elixirc_paths(:prod), do: ["lib", "docker_engine_api"]
43+
defp elixirc_paths(_), do: ["lib", "test/support", "docker_engine_api"]
3944

4045
def application do
4146
[

test/container_test.exs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ defmodule Testcontainers.ContainerTest do
2121
container2 = ContainerBuilder.build(Testcontainers.PostgresContainer.new())
2222

2323
assert Testcontainers.Util.Hash.struct_to_hash(container1) ==
24-
"2ac8f31c61b760b17176a9b9e51bd9d969eb26adadc9bf3db4cdcac69cfb065f"
24+
"082851e217cae88e1684169a9759a6d702d012016054c14b23d192f7e2559c63"
2525

2626
assert Testcontainers.Util.Hash.struct_to_hash(container2) ==
27-
"2ac8f31c61b760b17176a9b9e51bd9d969eb26adadc9bf3db4cdcac69cfb065f"
27+
"082851e217cae88e1684169a9759a6d702d012016054c14b23d192f7e2559c63"
2828
end
2929
end
3030

@@ -34,7 +34,7 @@ defmodule Testcontainers.ContainerTest do
3434
Container.new("my-image")
3535
|> Container.with_exposed_port(80)
3636

37-
assert container.exposed_ports == [80]
37+
assert container.exposed_ports == [{80, nil}]
3838
end
3939

4040
test "removes duplicate exposed ports" do
@@ -43,7 +43,7 @@ defmodule Testcontainers.ContainerTest do
4343
|> Container.with_exposed_port(80)
4444
|> Container.with_exposed_port(80)
4545

46-
assert container.exposed_ports == [80]
46+
assert container.exposed_ports == [{80, nil}]
4747
end
4848
end
4949

@@ -53,7 +53,7 @@ defmodule Testcontainers.ContainerTest do
5353
Container.new("my-image")
5454
|> Container.with_exposed_ports([80, 443])
5555

56-
assert container.exposed_ports == [80, 443]
56+
assert container.exposed_ports == [{80, nil}, {443, nil}]
5757
end
5858

5959
test "removes duplicate exposed ports" do
@@ -62,7 +62,7 @@ defmodule Testcontainers.ContainerTest do
6262
|> Container.with_exposed_ports([80, 443])
6363
|> Container.with_exposed_ports([80])
6464

65-
assert container.exposed_ports == [80, 443]
65+
assert container.exposed_ports == [{80, nil}, {443, nil}]
6666
end
6767
end
6868

@@ -96,6 +96,11 @@ defmodule Testcontainers.ContainerTest do
9696
container = Container.new("my-image")
9797
assert Container.mapped_port(container, 80) == nil
9898
end
99+
100+
test "returns nil (and does not crash) if the exposed port is present but not mapped (integer)" do
101+
container = Container.new("my-image") |> Container.with_exposed_port(80)
102+
assert Container.mapped_port(container, 80) == nil
103+
end
99104
end
100105

101106
describe "with_network_mode/2" do
@@ -205,4 +210,24 @@ defmodule Testcontainers.ContainerTest do
205210
end
206211
end
207212
end
213+
214+
describe "with_privileged/2" do
215+
test "sets privileged to true" do
216+
container = Container.new("my-image")
217+
assert container.privileged == false
218+
219+
updated_container = Container.with_privileged(container, true)
220+
221+
assert updated_container.privileged == true
222+
end
223+
224+
test "sets privileged to false" do
225+
container =
226+
Container.new("my-image")
227+
|> Container.with_privileged(true)
228+
|> Container.with_privileged(false)
229+
230+
assert container.privileged == false
231+
end
232+
end
208233
end

test/test_helper.exs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
Testcontainers.start_link()
22

33
ExUnit.start()
4-
5-
# Application.put_env(:tesla, DockerEngineAPI.Connection,
6-
# middleware: [{Tesla.Middleware.Logger, log_level: :info}]
7-
# )

0 commit comments

Comments
 (0)