From 28b103748235602ebea8408d295950b939120332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarl=20Andr=C3=A9=20H=C3=BCbenthal?= Date: Wed, 22 Apr 2026 08:27:57 +0200 Subject: [PATCH] feat(ryuk): honor ryuk.container.privileged property and env var Mirror testcontainers-dotnet behavior and resolve SELinux denials on Fedora (issue #183) by letting users run Ryuk in privileged mode via either the `ryuk.container.privileged` property in `~/.testcontainers.properties` or the `TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED` environment variable. Both `"true"` and `"1"` are treated as truthy. The environment variable takes precedence over the property when both are set, matching the dotnet implementation. The resolver is extracted as `Testcontainers.ryuk_privileged?/1` (@doc false) so it can be unit-tested without launching Ryuk. Fixes #183 --- lib/testcontainers.ex | 28 +++++++++++- test/testcontainers_test.exs | 83 +++++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/lib/testcontainers.ex b/lib/testcontainers.ex index 31c2d57..6d982f0 100644 --- a/lib/testcontainers.ex +++ b/lib/testcontainers.ex @@ -449,6 +449,32 @@ defmodule Testcontainers do end end + @doc false + # Resolves whether Ryuk should run in privileged mode. + # + # Mirrors testcontainers-dotnet: honors the `ryuk.container.privileged` + # property and the `TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED` environment + # variable. The environment variable takes precedence when both are set. + # Values `"true"` and `"1"` are treated as truthy. + def ryuk_privileged?(properties) when is_map(properties) do + env_value = System.get_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") + prop_value = Map.get(properties, "ryuk.container.privileged") + + value = env_value || prop_value + + truthy?(value) + end + + defp truthy?(value) when is_binary(value) do + case String.downcase(String.trim(value)) do + "true" -> true + "1" -> true + _ -> false + end + end + + defp truthy?(_), do: false + @doc false def running_in_container?( dockerenv_path \\ "/.dockerenv", @@ -667,7 +693,7 @@ defmodule Testcontainers do end defp start_ryuk(conn, session_id, properties, docker_host, docker_hostname) do - ryuk_privileged = Map.get(properties, "ryuk.container.privileged", "false") == "true" + ryuk_privileged = ryuk_privileged?(properties) ryuk_config = Container.new("testcontainers/ryuk:#{Constants.ryuk_version()}") diff --git a/test/testcontainers_test.exs b/test/testcontainers_test.exs index c140c99..f1f7c00 100644 --- a/test/testcontainers_test.exs +++ b/test/testcontainers_test.exs @@ -2,7 +2,88 @@ defmodule TestcontainersTest do alias Testcontainers.Connection alias Testcontainers.Container alias Testcontainers.Docker - use ExUnit.Case, async: true + # async: false because ryuk_privileged? tests mutate process environment + use ExUnit.Case, async: false + + @ryuk_privileged_env "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED" + @ryuk_privileged_prop "ryuk.container.privileged" + + describe "ryuk_privileged?/1" do + setup do + original = System.get_env(@ryuk_privileged_env) + + on_exit(fn -> + case original do + nil -> System.delete_env(@ryuk_privileged_env) + value -> System.put_env(@ryuk_privileged_env, value) + end + end) + + :ok + end + + test "returns false when neither property nor env var is set" do + System.delete_env(@ryuk_privileged_env) + refute Testcontainers.ryuk_privileged?(%{}) + end + + test "returns true when property is 'true'" do + System.delete_env(@ryuk_privileged_env) + assert Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "true"}) + end + + test "returns true when property is '1'" do + System.delete_env(@ryuk_privileged_env) + assert Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "1"}) + end + + test "returns false when property is 'false'" do + System.delete_env(@ryuk_privileged_env) + refute Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "false"}) + end + + test "returns false when property is '0'" do + System.delete_env(@ryuk_privileged_env) + refute Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "0"}) + end + + test "returns true when env var is 'true'" do + System.put_env(@ryuk_privileged_env, "true") + assert Testcontainers.ryuk_privileged?(%{}) + end + + test "returns true when env var is '1'" do + System.put_env(@ryuk_privileged_env, "1") + assert Testcontainers.ryuk_privileged?(%{}) + end + + test "returns false when env var is 'false'" do + System.put_env(@ryuk_privileged_env, "false") + refute Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "true"}) + end + + test "env var takes precedence over property (env false, prop true)" do + System.put_env(@ryuk_privileged_env, "false") + refute Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "true"}) + end + + test "env var takes precedence over property (env true, prop false)" do + System.put_env(@ryuk_privileged_env, "true") + assert Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "false"}) + end + + test "treats arbitrary strings as falsy" do + System.delete_env(@ryuk_privileged_env) + refute Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "yes"}) + refute Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => ""}) + end + + test "is case-insensitive and trims whitespace" do + System.delete_env(@ryuk_privileged_env) + assert Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => "TRUE"}) + assert Testcontainers.ryuk_privileged?(%{@ryuk_privileged_prop => " true "}) + end + end test "cleans up containers on terminate" do {:ok, pid} = Testcontainers.start_link(name: :cleanup_test1)