Public Access
DEVX-155: fix: prefer /run/host-docker.sock over inner dockerd
The start_docker.py script was checking /var/run/docker.sock first, which inside CI containers is an inner dockerd (v29.5.3) with data root on the container's 38G overlay (often 100% full). When _get_docker_free_bytes() returned 0 (data root path not accessible from inside container), the script assumed it was the host Docker with plenty of space and returned True immediately — without trying the host's rootless Docker socket at /run/host-docker.sock. Fix: try /run/host-docker.sock FIRST (before /var/run/docker.sock). The host socket is mounted by the gitea runner config and has access to the host's full filesystem (455G). Only trust free_bytes == 0 (= data root not accessible from container) for /run/host-docker.sock, since the host's root dir is genuinely outside the container. For other sockets (inner dockerd), free_bytes == 0 means the path doesn't exist inside the container — don't trust it. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
1470cdae27
commit
0e25810b84
+161
-54
@@ -7,6 +7,8 @@ from click.testing import CliRunner
|
||||
|
||||
from devx.molecule.start_docker import (
|
||||
DOCKER_SOCK,
|
||||
HOST_DOCKER_SOCK,
|
||||
ROOTLESS_SOCK,
|
||||
_diagnose_socket,
|
||||
_get_docker_free_bytes,
|
||||
_try_socket,
|
||||
@@ -124,91 +126,192 @@ class TestDiagnoseSocket:
|
||||
mock_exists.assert_called_with(DOCKER_SOCK)
|
||||
|
||||
|
||||
def _exists_map(paths: set[str]) -> MagicMock:
|
||||
"""Return a mock os.path.exists that returns True only for *paths*."""
|
||||
return MagicMock(side_effect=lambda p: p in paths)
|
||||
|
||||
|
||||
class TestStartDockerDaemon:
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=100 * 1024**3)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
def test_host_socket_available_with_space(
|
||||
self, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should return immediately if host Docker has enough space."""
|
||||
"""Should return immediately if /var/run/docker.sock has enough space."""
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
mock_ready.assert_called_once()
|
||||
mock_diag.assert_called_once()
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=0)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||
def test_host_socket_inaccessible_root_uses_host(
|
||||
self, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({HOST_DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
def test_host_rootless_inaccessible_root_uses_host(
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should use host Docker when root dir is inaccessible (free=0)."""
|
||||
"""Should use host rootless Docker when root dir is inaccessible (free=0)."""
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
mock_ready.assert_called_once()
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=5 * 1024**3)
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=0)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||
def test_host_socket_low_space_tries_rootless(
|
||||
self, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||
@patch("devx.molecule.start_docker.time.sleep")
|
||||
def test_inner_dockerd_free_zero_starts_local(
|
||||
self,
|
||||
mock_sleep: MagicMock,
|
||||
mock_ntf: MagicMock,
|
||||
mock_popen: MagicMock,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should try rootless sockets if host Docker has low space."""
|
||||
with (
|
||||
patch("devx.molecule.start_docker.glob.glob", return_value=[]),
|
||||
patch("devx.molecule.start_docker.os.path.exists", return_value=False),
|
||||
patch("devx.molecule.start_docker.subprocess.run"),
|
||||
patch("devx.molecule.start_docker.subprocess.Popen"),
|
||||
patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile"),
|
||||
patch("devx.molecule.start_docker.time.sleep"),
|
||||
):
|
||||
# Host ready but low space, no rootless sockets, starts local dockerd
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
"""Inner dockerd with free=0 (not host socket) should NOT be trusted — start local."""
|
||||
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||
# is_docker_ready: first check (inner dockerd) True, then local daemon checks
|
||||
mock_ready.side_effect = [True, False, False, False, False, True]
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
mock_popen.assert_called_once()
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes")
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||
def test_rootless_socket_with_space(
|
||||
self, mock_exists: MagicMock, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||
@patch("devx.molecule.start_docker.time.sleep")
|
||||
def test_host_socket_low_space_starts_local(
|
||||
self,
|
||||
mock_sleep: MagicMock,
|
||||
mock_ntf: MagicMock,
|
||||
mock_popen: MagicMock,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should use rootless socket if host has low space and rootless has enough."""
|
||||
# Host ready but low space, rootless ready with enough space
|
||||
"""Should start local dockerd if host Docker has low space and no rootless sockets."""
|
||||
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||
mock_ready.side_effect = [True, False, False, False, False, True]
|
||||
mock_free.return_value = 5 * 1024**3
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
mock_popen.assert_called_once()
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes")
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK, HOST_DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
def test_host_rootless_preferred_over_inner_dockerd(
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should prefer /run/host-docker.sock over /var/run/docker.sock."""
|
||||
# host rootless ready with space, inner dockerd never tried
|
||||
mock_ready.side_effect = [True]
|
||||
mock_free.side_effect = [200 * 1024**3]
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
# DOCKER_HOST should be set to host socket
|
||||
assert os.environ.get("DOCKER_HOST") == f"unix://{HOST_DOCKER_SOCK}"
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=100 * 1024**3)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK, HOST_DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
def test_host_rootless_not_ready_falls_to_inner(
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should fall through to inner dockerd if host rootless socket is not ready."""
|
||||
mock_ready.side_effect = [False, True]
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
assert os.environ.get("DOCKER_HOST") == f"unix://{DOCKER_SOCK}"
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=100 * 1024**3)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||
@patch(
|
||||
"devx.molecule.start_docker.os.path.exists",
|
||||
side_effect=_exists_map({DOCKER_SOCK, HOST_DOCKER_SOCK, ROOTLESS_SOCK, "/run/user/999/docker.sock"}),
|
||||
)
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=["/run/user/999/docker.sock"])
|
||||
def test_glob_finds_extra_rootless_socket(
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should include rootless sockets found via glob scan."""
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes")
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK, HOST_DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
def test_inner_dockerd_fallback_when_host_rootless_low_space(
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should fall back to inner dockerd if host rootless has low space."""
|
||||
# host rootless ready low space, inner dockerd ready with space
|
||||
mock_ready.side_effect = [True, True]
|
||||
mock_free.side_effect = [5 * 1024**3, 200 * 1024**3]
|
||||
own_sock = f"/run/user/{os.getuid()}/docker.sock"
|
||||
with patch("devx.molecule.start_docker.glob.glob", return_value=[own_sock]):
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
assert os.environ.get("DOCKER_HOST") == f"unix://{DOCKER_SOCK}"
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes")
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||
def test_alt_rootless_socket_found(
|
||||
self, mock_exists: MagicMock, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
) -> None:
|
||||
"""Should find rootless socket at a different UID via glob scan."""
|
||||
# Host not ready, own rootless not ready, alt rootless ready with space
|
||||
mock_ready.side_effect = [False, False, True]
|
||||
mock_free.side_effect = [200 * 1024**3]
|
||||
own_sock = f"/run/user/{os.getuid()}/docker.sock"
|
||||
alt_sock = "/run/user/999/docker.sock"
|
||||
with patch("devx.molecule.start_docker.glob.glob", return_value=[own_sock, alt_sock]):
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes")
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
def test_all_sockets_low_space_falls_back(
|
||||
self, mock_exists: MagicMock, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""Should fall back to a low-space socket if no better option exists."""
|
||||
# Host ready low space, rootless ready low space, falls back to rootless
|
||||
mock_ready.side_effect = [True, True, True, True]
|
||||
mock_free.side_effect = [5 * 1024**3, 5 * 1024**3, 5 * 1024**3, 5 * 1024**3]
|
||||
own_sock = f"/run/user/{os.getuid()}/docker.sock"
|
||||
with patch("devx.molecule.start_docker.glob.glob", return_value=[own_sock]):
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@@ -232,7 +335,7 @@ class TestStartDockerDaemon:
|
||||
mock_glob: MagicMock,
|
||||
) -> None:
|
||||
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||
# Host fails, rootless doesn't exist, local daemon starts
|
||||
# No sockets exist, local daemon starts
|
||||
mock_ready.side_effect = [False, False, False, False, True]
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
mock_popen.assert_called_once()
|
||||
@@ -317,18 +420,22 @@ class TestStartDockerDaemon:
|
||||
mock_glob: MagicMock,
|
||||
) -> None:
|
||||
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||
# Host fails, rootless doesn't exist, local ready on first loop check
|
||||
# No sockets exist, local ready on second loop check
|
||||
mock_ready.side_effect = [False, False, True]
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
assert mock_popen.call_count == 1
|
||||
assert mock_sleep.call_count == 1
|
||||
assert mock_sleep.call_count == 2
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=100 * 1024**3)
|
||||
@patch("devx.molecule.start_docker.os.environ")
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||
@patch("devx.molecule.start_docker.os.path.exists", side_effect=_exists_map({DOCKER_SOCK}))
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
def test_sets_docker_host(
|
||||
self,
|
||||
mock_glob: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_environ: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
|
||||
Reference in New Issue
Block a user