From d18904c4669c552c030746d3dc679ecc3725e336 Mon Sep 17 00:00:00 2001 From: Austin DeLauney Date: Mon, 18 Aug 2025 21:31:38 -0700 Subject: [PATCH 1/2] Fix improper reading of .testcontainers.properties The environment variables were not overridden from the .testcontainers.properties file for ryuk variables. This causes the properties file to never actually be used. This commit detects the environment variable, and if unspecified falls back to the properties file, and if not specifed, defaults to false --- core/testcontainers/core/config.py | 40 +++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 9eac25e0..016836b1 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -11,6 +11,8 @@ import docker +ENABLE_FLAGS = ("yes", "true", "t", "y", "1") + class ConnectionMode(Enum): bridge_ip = "bridge_ip" @@ -52,7 +54,7 @@ def get_bool_env(name: str) -> bool: Defaults to False. """ value = environ.get(name, "") - return value.lower() in ("yes", "true", "t", "y", "1") + return value.lower() in ENABLE_FLAGS TC_FILE = ".testcontainers.properties" @@ -96,11 +98,20 @@ def read_tc_properties() -> dict[str, str]: @dataclass class TestcontainersConfiguration: + def _render_bool(self, env_name: str, prop_name: str) -> bool: + env_val = environ.get(env_name, None) + if env_val is not None: + return env_val.lower() in ENABLE_FLAGS + prop_val = self.tc_properties.get(prop_name, None) + if prop_val is not None: + return prop_val.lower() in ENABLE_FLAGS + return False + max_tries: int = int(environ.get("TC_MAX_TRIES", "120")) sleep_time: float = float(environ.get("TC_POOLING_INTERVAL", "1")) ryuk_image: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.8.1") - ryuk_privileged: bool = get_bool_env("TESTCONTAINERS_RYUK_PRIVILEGED") - ryuk_disabled: bool = get_bool_env("TESTCONTAINERS_RYUK_DISABLED") + _ryuk_privileged: Optional[bool] = None + _ryuk_disabled: Optional[bool] = None _ryuk_docker_socket: str = "" ryuk_reconnection_timeout: str = environ.get("RYUK_RECONNECTION_TIMEOUT", "10s") tc_properties: dict[str, str] = field(default_factory=read_tc_properties) @@ -129,6 +140,29 @@ def docker_auth_config(self, value: str) -> None: def tc_properties_get_tc_host(self) -> Union[str, None]: return self.tc_properties.get("tc.host") + @property + def ryuk_privileged(self) -> bool: + if self._ryuk_privileged: + return self._ryuk_privileged + self._ryuk_privileged = self._render_bool("TESTCONTAINERS_RYUK_PRIVILEGED", "ryuk.container.privileged") + return self._ryuk_privileged + + @ryuk_privileged.setter + def ryuk_privileged(self, value: bool) -> None: + self._ryuk_privileged = value + + @property + def ryuk_disabled(self) -> bool: + if self._ryuk_disabled: + return self._ryuk_disabled + + self._ryuk_disabled = self._render_bool("TESTCONTAINERS_RYUK_DISABLED", "ryuk.disabled") + return self._ryuk_disabled + + @ryuk_disabled.setter + def ryuk_disabled(self, value: bool) -> None: + self._ryuk_disabled = value + @property def timeout(self) -> float: return self.max_tries * self.sleep_time From e0362a14a86e1384530a1f5e82891175285b1fce Mon Sep 17 00:00:00 2001 From: Austin DeLauney Date: Sat, 23 Aug 2025 12:39:37 -0700 Subject: [PATCH 2/2] Added better tests for TestcontainersConfig class Removed unused method get_bool_env, as it was replaced with _render_bool Added more tests for proper test coverage for loading variables via testcontainers.properties file --- core/testcontainers/core/config.py | 19 ++------- core/tests/test_config.py | 62 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 016836b1..cca5d65a 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -47,16 +47,6 @@ def get_docker_socket() -> str: return "/var/run/docker.sock" -def get_bool_env(name: str) -> bool: - """ - Get environment variable named `name` and convert it to bool. - - Defaults to False. - """ - value = environ.get(name, "") - return value.lower() in ENABLE_FLAGS - - TC_FILE = ".testcontainers.properties" TC_GLOBAL = Path.home() / TC_FILE @@ -142,8 +132,8 @@ def tc_properties_get_tc_host(self) -> Union[str, None]: @property def ryuk_privileged(self) -> bool: - if self._ryuk_privileged: - return self._ryuk_privileged + if self._ryuk_privileged is not None: + return bool(self._ryuk_privileged) self._ryuk_privileged = self._render_bool("TESTCONTAINERS_RYUK_PRIVILEGED", "ryuk.container.privileged") return self._ryuk_privileged @@ -153,9 +143,8 @@ def ryuk_privileged(self, value: bool) -> None: @property def ryuk_disabled(self) -> bool: - if self._ryuk_disabled: - return self._ryuk_disabled - + if self._ryuk_disabled is not None: + return bool(self._ryuk_disabled) self._ryuk_disabled = self._render_bool("TESTCONTAINERS_RYUK_DISABLED", "ryuk.disabled") return self._ryuk_disabled diff --git a/core/tests/test_config.py b/core/tests/test_config.py index 30001d71..43586031 100644 --- a/core/tests/test_config.py +++ b/core/tests/test_config.py @@ -28,6 +28,68 @@ def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None: assert config.tc_properties == {"tc.host": "some_value"} +def test_set_tc_properties(monkeypatch: MonkeyPatch) -> None: + """ + Ensure the configuration file variables can be read if no environment variable is set + """ + with tempfile.TemporaryDirectory() as tmpdirname: + file = f"{tmpdirname}/{TC_FILE}" + with open(file, "w") as f: + f.write("ryuk.disabled=true\n") + f.write("ryuk.container.privileged=false\n") + + monkeypatch.setattr("testcontainers.core.config.TC_GLOBAL", file) + + config = TCC() + + assert config.ryuk_disabled == True + assert config.ryuk_privileged == False + + +def test_override_tc_properties_1(monkeypatch: MonkeyPatch) -> None: + """ + Ensure that we can re-set the configuration variables programattically to override + testcontainers.properties + """ + with tempfile.TemporaryDirectory() as tmpdirname: + file = f"{tmpdirname}/{TC_FILE}" + with open(file, "w") as f: + f.write("ryuk.disabled=true\n") + f.write("ryuk.container.privileged=false\n") + + monkeypatch.setattr("testcontainers.core.config.TC_GLOBAL", file) + + config = TCC() + config.ryuk_disabled = False + config.ryuk_privileged = True + + assert config.ryuk_disabled == False + assert config.ryuk_privileged == True + + +def test_override_tc_properties_2(monkeypatch: MonkeyPatch) -> None: + """ + Ensure that we can override the testcontainers.properties with environment variables + """ + with tempfile.TemporaryDirectory() as tmpdirname: + file = f"{tmpdirname}/{TC_FILE}" + with open(file, "w") as f: + f.write("ryuk.disabled=true\n") + f.write("ryuk.container.privileged=false\n") + + monkeypatch.setattr("testcontainers.core.config.TC_GLOBAL", file) + + import os + + os.environ["TESTCONTAINERS_RYUK_DISABLED"] = "false" + os.environ["TESTCONTAINERS_RYUK_PRIVILEGED"] = "true" + + config = TCC() + + assert config.ryuk_disabled == False + assert config.ryuk_privileged == True + + @mark.parametrize("docker_auth_config_env", ["key=value", ""]) @mark.parametrize("warning_dict", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}]) @mark.parametrize("warning_dict_post", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}])