Public Access
DEVX-155: fix: prefer rootless Docker socket over low-space inner DinD daemon
When a CI container has an inner dockerd (DinD) writing to the container's overlay (e.g. 38 GB), image pulls fail with ENOSPC. start_docker.py now checks the daemon's free disk space and tries rootless sockets (which have access to the host's full filesystem) when the default socket has less than 20 GB free. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
committed by
emil
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
0341ee74c9
commit
23bd480e80
@@ -8,6 +8,8 @@ from click.testing import CliRunner
|
||||
from devx.molecule.start_docker import (
|
||||
DOCKER_SOCK,
|
||||
_diagnose_socket,
|
||||
_get_docker_free_bytes,
|
||||
_try_socket,
|
||||
is_docker_ready,
|
||||
main,
|
||||
start_docker_daemon,
|
||||
@@ -40,6 +42,42 @@ class TestIsDockerReady:
|
||||
assert call_kwargs.kwargs["env"]["DOCKER_HOST"] == rootless
|
||||
|
||||
|
||||
class TestGetDockerFreeBytes:
|
||||
@patch("devx.molecule.start_docker.shutil.disk_usage")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||
@patch("devx.molecule.start_docker.subprocess.run")
|
||||
def test_returns_free_bytes(self, mock_run: MagicMock, mock_exists: MagicMock, mock_du: MagicMock) -> None:
|
||||
mock_run.return_value = MagicMock(stdout="/var/lib/docker\n", returncode=0, text="")
|
||||
mock_du.return_value = MagicMock(free=50 * 1024**3)
|
||||
with patch.dict("os.environ", {"DOCKER_HOST": f"unix://{DOCKER_SOCK}"}, clear=False):
|
||||
assert _get_docker_free_bytes() == 50 * 1024**3
|
||||
|
||||
@patch("devx.molecule.start_docker.subprocess.run")
|
||||
def test_daemon_not_reachable(self, mock_run: MagicMock) -> None:
|
||||
mock_run.return_value = MagicMock(returncode=1, stdout="", text="")
|
||||
assert _get_docker_free_bytes() == 0
|
||||
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||
@patch("devx.molecule.start_docker.subprocess.run")
|
||||
def test_data_root_not_accessible(self, mock_run: MagicMock, mock_exists: MagicMock) -> None:
|
||||
mock_run.return_value = MagicMock(stdout="/some/path\n", returncode=0, text="")
|
||||
assert _get_docker_free_bytes() == 0
|
||||
|
||||
@patch("devx.molecule.start_docker.subprocess.run", side_effect=FileNotFoundError)
|
||||
def test_subprocess_not_found(self, mock_run: MagicMock) -> None:
|
||||
assert _get_docker_free_bytes() == 0
|
||||
|
||||
|
||||
class TestTrySocket:
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||
def test_ready(self, mock_ready: MagicMock) -> None:
|
||||
assert _try_socket("/run/user/999/docker.sock") is True
|
||||
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||
def test_not_ready(self, mock_ready: MagicMock) -> None:
|
||||
assert _try_socket("/run/user/999/docker.sock") is False
|
||||
|
||||
|
||||
class TestDiagnoseSocket:
|
||||
@patch("devx.molecule.start_docker.os.stat")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||
@@ -88,61 +126,79 @@ class TestDiagnoseSocket:
|
||||
|
||||
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)
|
||||
def test_host_socket_available(self, mock_ready: MagicMock, mock_diag: MagicMock) -> None:
|
||||
"""Should return immediately if host Docker is available."""
|
||||
def test_host_socket_available_with_space(
|
||||
self, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
) -> None:
|
||||
"""Should return immediately if host Docker 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.os.path.exists", return_value=True)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
def test_rootless_socket_available(
|
||||
self, mock_ready: MagicMock, mock_exists: MagicMock, mock_diag: MagicMock
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=5 * 1024**3)
|
||||
@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
|
||||
) -> None:
|
||||
"""Should use rootless socket if host socket fails."""
|
||||
# First check (host) fails, second check (rootless) succeeds
|
||||
mock_ready.side_effect = [False, True]
|
||||
with patch("devx.molecule.start_docker.glob.glob", return_value=[]):
|
||||
"""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),
|
||||
):
|
||||
# Host ready but low space, no rootless sockets, falls back to host
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||
@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
|
||||
) -> None:
|
||||
"""Should use rootless socket if host has low space and rootless has enough."""
|
||||
# Host ready but low space, rootless ready with enough 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
|
||||
|
||||
@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_ready: MagicMock, mock_exists: MagicMock, mock_diag: MagicMock
|
||||
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 fails, own rootless fails, alt rootless succeeds
|
||||
# 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=[alt_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.os.path.exists", return_value=True)
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes")
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
def test_alt_rootless_socket_skips_own(
|
||||
self, mock_ready: MagicMock, mock_exists: MagicMock, mock_diag: MagicMock
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=True)
|
||||
def test_all_sockets_low_space_falls_back(
|
||||
self, mock_exists: MagicMock, mock_ready: MagicMock, mock_free: MagicMock, mock_diag: MagicMock
|
||||
) -> None:
|
||||
"""Should skip the own rootless socket in glob scan (already tried)."""
|
||||
# Host fails, own rootless fails, alt rootless also fails, dockerd fails
|
||||
mock_ready.side_effect = [False, False, False, False, False, False]
|
||||
"""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"
|
||||
alt_sock = "/run/user/999/docker.sock"
|
||||
with (
|
||||
patch("devx.molecule.start_docker.glob.glob", return_value=[own_sock, alt_sock]),
|
||||
patch("devx.molecule.start_docker.time.sleep"),
|
||||
patch("devx.molecule.start_docker.subprocess.Popen"),
|
||||
patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile") as mock_ntf,
|
||||
patch("builtins.open", mock_open(read_data="err")),
|
||||
):
|
||||
mock_ntf.return_value = MagicMock(name="/tmp/dockerd.log")
|
||||
assert start_docker_daemon(timeout=2) is False
|
||||
with patch("devx.molecule.start_docker.glob.glob", return_value=[own_sock]):
|
||||
assert start_docker_daemon(timeout=5) is True
|
||||
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=0)
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||
@patch("devx.molecule.start_docker.time.sleep")
|
||||
@@ -155,6 +211,7 @@ class TestStartDockerDaemon:
|
||||
mock_sleep: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
mock_glob: MagicMock,
|
||||
) -> None:
|
||||
@@ -171,6 +228,7 @@ class TestStartDockerDaemon:
|
||||
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=0)
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||
@patch("devx.molecule.start_docker.time.sleep")
|
||||
@@ -183,6 +241,7 @@ class TestStartDockerDaemon:
|
||||
mock_sleep: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
mock_glob: MagicMock,
|
||||
) -> None:
|
||||
@@ -194,6 +253,7 @@ class TestStartDockerDaemon:
|
||||
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=0)
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||
@patch("devx.molecule.start_docker.time.sleep")
|
||||
@@ -206,6 +266,7 @@ class TestStartDockerDaemon:
|
||||
mock_sleep: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
mock_glob: MagicMock,
|
||||
) -> None:
|
||||
@@ -216,6 +277,7 @@ class TestStartDockerDaemon:
|
||||
|
||||
@patch("devx.molecule.start_docker.glob.glob", return_value=[])
|
||||
@patch("devx.molecule.start_docker._diagnose_socket")
|
||||
@patch("devx.molecule.start_docker._get_docker_free_bytes", return_value=0)
|
||||
@patch("devx.molecule.start_docker.os.path.exists", return_value=False)
|
||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||
@patch("devx.molecule.start_docker.time.sleep")
|
||||
@@ -228,6 +290,7 @@ class TestStartDockerDaemon:
|
||||
mock_sleep: MagicMock,
|
||||
mock_ready: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
mock_glob: MagicMock,
|
||||
) -> None:
|
||||
@@ -239,12 +302,14 @@ class TestStartDockerDaemon:
|
||||
assert mock_sleep.call_count == 1
|
||||
|
||||
@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)
|
||||
def test_sets_docker_host(
|
||||
self,
|
||||
mock_ready: MagicMock,
|
||||
mock_environ: MagicMock,
|
||||
mock_free: MagicMock,
|
||||
mock_diag: MagicMock,
|
||||
) -> None:
|
||||
"""DOCKER_HOST must be set so molecule connects to correct socket."""
|
||||
|
||||
Reference in New Issue
Block a user