Skip to content

fix(core): Determine docker socket for rootless docker #779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion core/testcontainers/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from pathlib import Path
from typing import Optional, Union

import docker


class ConnectionMode(Enum):
bridge_ip = "bridge_ip"
Expand All @@ -24,14 +26,32 @@ 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

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"))

Expand Down
60 changes: 60 additions & 0 deletions core/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"