diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 110a441e..cee09aa7 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import Optional, Union +import docker + class ConnectionMode(Enum): bridge_ip = "bridge_ip" @@ -24,6 +26,24 @@ def use_mapped_port(self) -> bool: return True +def get_docker_socket() -> str: + """ + Determine the docker socket, prefer value given by env variable + + Using the docker api ensure we handle rootless docker properly + """ + if socket_path := environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"): + return socket_path + + client = docker.from_env() + try: + socket_path = client.api.get_adapter(client.api.base_url).socket_path + # return the normalized path as string + return str(Path(socket_path).absolute()) + except AttributeError: + return "/var/run/docker.sock" + + MAX_TRIES = int(environ.get("TC_MAX_TRIES", 120)) SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1)) TIMEOUT = MAX_TRIES * SLEEP_TIME @@ -31,7 +51,7 @@ def use_mapped_port(self) -> bool: RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.8.1") RYUK_PRIVILEGED: bool = environ.get("TESTCONTAINERS_RYUK_PRIVILEGED", "false") == "true" RYUK_DISABLED: bool = environ.get("TESTCONTAINERS_RYUK_DISABLED", "false") == "true" -RYUK_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock") +RYUK_DOCKER_SOCKET: str = get_docker_socket() RYUK_RECONNECTION_TIMEOUT: str = environ.get("RYUK_RECONNECTION_TIMEOUT", "10s") TC_HOST_OVERRIDE: Optional[str] = environ.get("TC_HOST", environ.get("TESTCONTAINERS_HOST_OVERRIDE")) diff --git a/core/tests/test_config.py b/core/tests/test_config.py index 8be68cc7..0ddd8333 100644 --- a/core/tests/test_config.py +++ b/core/tests/test_config.py @@ -5,12 +5,14 @@ TC_FILE, get_user_overwritten_connection_mode, ConnectionMode, + get_docker_socket, ) from pytest import MonkeyPatch, mark, LogCaptureFixture import logging import tempfile +from unittest.mock import Mock def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None: @@ -84,3 +86,61 @@ def test_valid_connection_mode(monkeypatch: pytest.MonkeyPatch, mode: str, use_m def test_no_connection_mode_given(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("TESTCONTAINERS_CONNECTION_MODE", raising=False) assert get_user_overwritten_connection_mode() is None + + +def test_get_docker_socket_uses_env(monkeypatch: pytest.MonkeyPatch) -> None: + """ + If TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE env var is given prefer it + """ + monkeypatch.setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/test.socket") + assert get_docker_socket() == "/var/test.socket" + + +@pytest.fixture +def mock_docker_client_connections(monkeypatch: pytest.MonkeyPatch) -> None: + """ + Ensure the docker client does not make any actual network calls + """ + from docker.transport.sshconn import SSHHTTPAdapter + from docker.api.client import APIClient + + # ensure that no actual connection is tried + monkeypatch.setattr(SSHHTTPAdapter, "_connect", Mock()) + monkeypatch.setattr(SSHHTTPAdapter, "_create_paramiko_client", Mock()) + monkeypatch.setattr(APIClient, "_retrieve_server_version", Mock(return_value="1.47")) + + +@pytest.mark.usefixtures("mock_docker_client_connections") +def test_get_docker_host_default(monkeypatch: pytest.MonkeyPatch) -> None: + """ + If non socket docker-host is given return default + + Still ryuk will properly still not work but this is the historical default + + """ + monkeypatch.delenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", raising=False) + # Define Fake SSH Docker client + monkeypatch.setenv("DOCKER_HOST", "ssh://remote_host") + assert get_docker_socket() == "/var/run/docker.sock" + + +@pytest.mark.usefixtures("mock_docker_client_connections") +def test_get_docker_host_non_root(monkeypatch: pytest.MonkeyPatch) -> None: + """ + Use the socket determined by the Docker API Adapter + """ + monkeypatch.delenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", raising=False) + # Define a Non-Root like Docker Client + monkeypatch.setenv("DOCKER_HOST", "unix://var/run/user/1000/docker.sock") + assert get_docker_socket() == "/var/run/user/1000/docker.sock" + + +@pytest.mark.usefixtures("mock_docker_client_connections") +def test_get_docker_host_root(monkeypatch: pytest.MonkeyPatch) -> None: + """ + Use the socket determined by the Docker API Adapter + """ + monkeypatch.delenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", raising=False) + # Define a Root like Docker Client + monkeypatch.setenv("DOCKER_HOST", "unix://") + assert get_docker_socket() == "/var/run/docker.sock"