Skip to content

Commit 6817582

Browse files
authored
fix(core): Determine docker socket for rootless docker (#779)
fixes #537 Use docker_api to determine the `socket_path` (defined in [`UnixHTTPAdapter`](https://github.com/docker/docker-py/blob/db7f8b8bb67e485a7192846906f600a52e0aa623/docker/transport/unixconn.py#L55)). Replaces: #710
1 parent 46feb1e commit 6817582

File tree

2 files changed

+81
-1
lines changed

2 files changed

+81
-1
lines changed

core/testcontainers/core/config.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from pathlib import Path
77
from typing import Optional, Union
88

9+
import docker
10+
911

1012
class ConnectionMode(Enum):
1113
bridge_ip = "bridge_ip"
@@ -24,14 +26,32 @@ def use_mapped_port(self) -> bool:
2426
return True
2527

2628

29+
def get_docker_socket() -> str:
30+
"""
31+
Determine the docker socket, prefer value given by env variable
32+
33+
Using the docker api ensure we handle rootless docker properly
34+
"""
35+
if socket_path := environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"):
36+
return socket_path
37+
38+
client = docker.from_env()
39+
try:
40+
socket_path = client.api.get_adapter(client.api.base_url).socket_path
41+
# return the normalized path as string
42+
return str(Path(socket_path).absolute())
43+
except AttributeError:
44+
return "/var/run/docker.sock"
45+
46+
2747
MAX_TRIES = int(environ.get("TC_MAX_TRIES", 120))
2848
SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1))
2949
TIMEOUT = MAX_TRIES * SLEEP_TIME
3050

3151
RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.8.1")
3252
RYUK_PRIVILEGED: bool = environ.get("TESTCONTAINERS_RYUK_PRIVILEGED", "false") == "true"
3353
RYUK_DISABLED: bool = environ.get("TESTCONTAINERS_RYUK_DISABLED", "false") == "true"
34-
RYUK_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock")
54+
RYUK_DOCKER_SOCKET: str = get_docker_socket()
3555
RYUK_RECONNECTION_TIMEOUT: str = environ.get("RYUK_RECONNECTION_TIMEOUT", "10s")
3656
TC_HOST_OVERRIDE: Optional[str] = environ.get("TC_HOST", environ.get("TESTCONTAINERS_HOST_OVERRIDE"))
3757

core/tests/test_config.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
TC_FILE,
66
get_user_overwritten_connection_mode,
77
ConnectionMode,
8+
get_docker_socket,
89
)
910

1011
from pytest import MonkeyPatch, mark, LogCaptureFixture
1112

1213
import logging
1314
import tempfile
15+
from unittest.mock import Mock
1416

1517

1618
def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None:
@@ -84,3 +86,61 @@ def test_valid_connection_mode(monkeypatch: pytest.MonkeyPatch, mode: str, use_m
8486
def test_no_connection_mode_given(monkeypatch: pytest.MonkeyPatch) -> None:
8587
monkeypatch.delenv("TESTCONTAINERS_CONNECTION_MODE", raising=False)
8688
assert get_user_overwritten_connection_mode() is None
89+
90+
91+
def test_get_docker_socket_uses_env(monkeypatch: pytest.MonkeyPatch) -> None:
92+
"""
93+
If TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE env var is given prefer it
94+
"""
95+
monkeypatch.setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/test.socket")
96+
assert get_docker_socket() == "/var/test.socket"
97+
98+
99+
@pytest.fixture
100+
def mock_docker_client_connections(monkeypatch: pytest.MonkeyPatch) -> None:
101+
"""
102+
Ensure the docker client does not make any actual network calls
103+
"""
104+
from docker.transport.sshconn import SSHHTTPAdapter
105+
from docker.api.client import APIClient
106+
107+
# ensure that no actual connection is tried
108+
monkeypatch.setattr(SSHHTTPAdapter, "_connect", Mock())
109+
monkeypatch.setattr(SSHHTTPAdapter, "_create_paramiko_client", Mock())
110+
monkeypatch.setattr(APIClient, "_retrieve_server_version", Mock(return_value="1.47"))
111+
112+
113+
@pytest.mark.usefixtures("mock_docker_client_connections")
114+
def test_get_docker_host_default(monkeypatch: pytest.MonkeyPatch) -> None:
115+
"""
116+
If non socket docker-host is given return default
117+
118+
Still ryuk will properly still not work but this is the historical default
119+
120+
"""
121+
monkeypatch.delenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", raising=False)
122+
# Define Fake SSH Docker client
123+
monkeypatch.setenv("DOCKER_HOST", "ssh://remote_host")
124+
assert get_docker_socket() == "/var/run/docker.sock"
125+
126+
127+
@pytest.mark.usefixtures("mock_docker_client_connections")
128+
def test_get_docker_host_non_root(monkeypatch: pytest.MonkeyPatch) -> None:
129+
"""
130+
Use the socket determined by the Docker API Adapter
131+
"""
132+
monkeypatch.delenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", raising=False)
133+
# Define a Non-Root like Docker Client
134+
monkeypatch.setenv("DOCKER_HOST", "unix://var/run/user/1000/docker.sock")
135+
assert get_docker_socket() == "/var/run/user/1000/docker.sock"
136+
137+
138+
@pytest.mark.usefixtures("mock_docker_client_connections")
139+
def test_get_docker_host_root(monkeypatch: pytest.MonkeyPatch) -> None:
140+
"""
141+
Use the socket determined by the Docker API Adapter
142+
"""
143+
monkeypatch.delenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", raising=False)
144+
# Define a Root like Docker Client
145+
monkeypatch.setenv("DOCKER_HOST", "unix://")
146+
assert get_docker_socket() == "/var/run/docker.sock"

0 commit comments

Comments
 (0)