Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d398c8e971 | ||
|
|
39526d8e6a | ||
|
|
37730e2187 | ||
|
|
e2f66ca70a | ||
|
|
0b88c211f1 | ||
|
|
ea4ee0d303 | ||
|
|
16fba17b03 |
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.9.5] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use host Docker socket with DOCKER_HOST fallback to local dockerd
|
||||||
|
- Use host Docker socket with DOCKER_HOST fallback to local dockerd
|
||||||
|
|
||||||
|
## [0.9.4] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use separate Docker socket for DinD in CI
|
||||||
|
|
||||||
|
## [0.9.3] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use tempfile for dockerd log to fix CI permission error
|
||||||
|
|
||||||
## [0.9.2] - 2026-06-24
|
## [0.9.2] - 2026-06-24
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||||
|
|
||||||
__version__ = "0.9.2"
|
__version__ = "0.9.5"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Start a Docker daemon inside a CI runner container (Docker-in-Docker).
|
"""Ensure Docker is available for molecule tests in CI.
|
||||||
|
|
||||||
CI runners (e.g. ``gitea/runner-images:ubuntu-latest``) may have the host's
|
CI runners (e.g. ``gitea/runner-images:ubuntu-latest``) may have the host's
|
||||||
Docker socket mounted, but molecule needs a local Docker daemon to create
|
Docker socket mounted. This module verifies Docker is accessible and
|
||||||
nested containers. This module always starts ``dockerd`` in the background
|
sets ``DOCKER_HOST`` explicitly so molecule's Python docker library
|
||||||
and waits for it to become ready.
|
connects to the same socket as the Docker CLI.
|
||||||
|
|
||||||
|
If the host socket is not available, it starts a local ``dockerd``
|
||||||
|
with the vfs storage driver (requires privileged container).
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
@@ -13,8 +16,10 @@ Usage::
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import subprocess # nosec B404
|
import subprocess # nosec B404
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -22,33 +27,51 @@ import click
|
|||||||
from devx.i18n import _
|
from devx.i18n import _
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 30
|
DEFAULT_TIMEOUT = 30
|
||||||
DOCKERD_LOG = "/var/log/dockerd.log"
|
DOCKER_SOCK = "/var/run/docker.sock"
|
||||||
|
|
||||||
|
|
||||||
def is_docker_ready() -> bool:
|
def is_docker_ready() -> bool:
|
||||||
"""Check if the Docker daemon is responding."""
|
"""Check if Docker daemon is responding on the configured socket."""
|
||||||
result = subprocess.run( # nosec B603 B607
|
result = subprocess.run( # nosec B603 B607
|
||||||
["docker", "info"],
|
["docker", "info"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=False,
|
check=False,
|
||||||
|
env={**os.environ, "DOCKER_HOST": f"unix://{DOCKER_SOCK}"},
|
||||||
)
|
)
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
|
def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
|
||||||
"""Start dockerd in the background and wait for it to be ready.
|
"""Ensure Docker is ready for molecule tests.
|
||||||
|
|
||||||
Always starts a local dockerd even if ``docker info`` succeeds,
|
First tries the host socket. If that works, sets ``DOCKER_HOST`` and
|
||||||
because the host socket may be mounted but not suitable for
|
returns immediately. If not, starts a local ``dockerd`` with vfs
|
||||||
molecule's nested container creation.
|
storage driver (requires privileged container).
|
||||||
|
|
||||||
Returns ``True`` if Docker is ready, ``False`` if it failed to
|
Returns ``True`` if Docker is ready, ``False`` if it failed to
|
||||||
start within the timeout.
|
start within the timeout.
|
||||||
"""
|
"""
|
||||||
|
# Point Docker CLI and Python library to the socket explicitly
|
||||||
|
os.environ["DOCKER_HOST"] = f"unix://{DOCKER_SOCK}"
|
||||||
|
|
||||||
|
# Check if host Docker is already available
|
||||||
|
if is_docker_ready():
|
||||||
|
click.echo(_("Docker daemon already running"))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Start local dockerd (requires privileged container)
|
||||||
click.echo(_("Starting Docker daemon..."))
|
click.echo(_("Starting Docker daemon..."))
|
||||||
log_file = open(DOCKERD_LOG, "w") # noqa: SIM115
|
log_file = tempfile.NamedTemporaryFile( # noqa: SIM115
|
||||||
|
mode="w", suffix="dockerd.log", delete=False
|
||||||
|
)
|
||||||
subprocess.Popen( # nosec B603 B607
|
subprocess.Popen( # nosec B603 B607
|
||||||
["dockerd", "--storage-driver", "vfs"],
|
[
|
||||||
|
"dockerd",
|
||||||
|
"--storage-driver",
|
||||||
|
"vfs",
|
||||||
|
"-H",
|
||||||
|
f"unix://{DOCKER_SOCK}",
|
||||||
|
],
|
||||||
stdout=log_file,
|
stdout=log_file,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
start_new_session=True,
|
start_new_session=True,
|
||||||
|
|||||||
@@ -419,6 +419,13 @@
|
|||||||
"ru": "Created release commit.",
|
"ru": "Created release commit.",
|
||||||
"zh": "Created release commit."
|
"zh": "Created release commit."
|
||||||
},
|
},
|
||||||
|
"Docker daemon already running": {
|
||||||
|
"bg": "Докер демонът вече работи",
|
||||||
|
"de": "Docker-Daemon läuft bereits",
|
||||||
|
"en": "Docker daemon already running",
|
||||||
|
"ru": "Демон Docker уже работает",
|
||||||
|
"zh": "Docker 守护进程已在运行"
|
||||||
|
},
|
||||||
"Docker daemon failed to start": {
|
"Docker daemon failed to start": {
|
||||||
"bg": "Docker daemon failed to start",
|
"bg": "Docker daemon failed to start",
|
||||||
"de": "Docker-Daemon konnte nicht gestartet werden",
|
"de": "Docker-Daemon konnte nicht gestartet werden",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Unit tests for devx.molecule.start_docker."""
|
"""Unit tests for devx.molecule.start_docker."""
|
||||||
|
|
||||||
from unittest.mock import MagicMock, mock_open, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from devx.molecule.start_docker import is_docker_ready, main, start_docker_daemon
|
from devx.molecule.start_docker import DOCKER_SOCK, is_docker_ready, main, start_docker_daemon
|
||||||
|
|
||||||
|
|
||||||
class TestIsDockerReady:
|
class TestIsDockerReady:
|
||||||
@@ -12,7 +12,9 @@ class TestIsDockerReady:
|
|||||||
def test_ready(self, mock_run: MagicMock) -> None:
|
def test_ready(self, mock_run: MagicMock) -> None:
|
||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
assert is_docker_ready() is True
|
assert is_docker_ready() is True
|
||||||
mock_run.assert_called_once_with(["docker", "info"], capture_output=True, check=False)
|
call_kwargs = mock_run.call_args
|
||||||
|
assert call_kwargs.args[0] == ["docker", "info"]
|
||||||
|
assert call_kwargs.kwargs["env"]["DOCKER_HOST"] == f"unix://{DOCKER_SOCK}"
|
||||||
|
|
||||||
@patch("devx.molecule.start_docker.subprocess.run")
|
@patch("devx.molecule.start_docker.subprocess.run")
|
||||||
def test_not_ready(self, mock_run: MagicMock) -> None:
|
def test_not_ready(self, mock_run: MagicMock) -> None:
|
||||||
@@ -21,34 +23,48 @@ class TestIsDockerReady:
|
|||||||
|
|
||||||
|
|
||||||
class TestStartDockerDaemon:
|
class TestStartDockerDaemon:
|
||||||
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||||
|
def test_host_socket_available(self, mock_ready: MagicMock) -> None:
|
||||||
|
"""Should return immediately if host Docker is available."""
|
||||||
|
assert start_docker_daemon(timeout=5) is True
|
||||||
|
mock_ready.assert_called_once()
|
||||||
|
|
||||||
@patch("devx.molecule.start_docker.time.sleep")
|
@patch("devx.molecule.start_docker.time.sleep")
|
||||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||||
@patch("devx.molecule.start_docker.subprocess.Popen")
|
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||||
@patch("builtins.open", new_callable=mock_open)
|
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||||
def test_starts_successfully(
|
def test_starts_local_daemon(
|
||||||
self,
|
self,
|
||||||
mock_file: MagicMock,
|
mock_ntf: MagicMock,
|
||||||
mock_popen: MagicMock,
|
mock_popen: MagicMock,
|
||||||
mock_ready: MagicMock,
|
mock_ready: MagicMock,
|
||||||
mock_sleep: MagicMock,
|
mock_sleep: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
# First loop iteration: dockerd not ready yet. Second: ready.
|
mock_ntf.return_value = MagicMock()
|
||||||
mock_ready.side_effect = [False, True]
|
# Host socket not available, then local daemon starts on third check
|
||||||
|
mock_ready.side_effect = [False, False, False, True]
|
||||||
assert start_docker_daemon(timeout=5) is True
|
assert start_docker_daemon(timeout=5) is True
|
||||||
mock_popen.assert_called_once()
|
mock_popen.assert_called_once()
|
||||||
mock_sleep.assert_called_once_with(1)
|
popen_args = mock_popen.call_args.args[0]
|
||||||
|
assert "dockerd" in popen_args
|
||||||
|
assert "--storage-driver" in popen_args
|
||||||
|
assert "vfs" in popen_args
|
||||||
|
assert "-H" in popen_args
|
||||||
|
assert f"unix://{DOCKER_SOCK}" in popen_args
|
||||||
|
assert mock_sleep.call_count == 2
|
||||||
|
|
||||||
@patch("devx.molecule.start_docker.time.sleep")
|
@patch("devx.molecule.start_docker.time.sleep")
|
||||||
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
|
||||||
@patch("devx.molecule.start_docker.subprocess.Popen")
|
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||||
@patch("builtins.open", new_callable=mock_open)
|
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||||
def test_fails_after_timeout(
|
def test_fails_after_timeout(
|
||||||
self,
|
self,
|
||||||
mock_file: MagicMock,
|
mock_ntf: MagicMock,
|
||||||
mock_popen: MagicMock,
|
mock_popen: MagicMock,
|
||||||
mock_ready: MagicMock,
|
mock_ready: MagicMock,
|
||||||
mock_sleep: MagicMock,
|
mock_sleep: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
mock_ntf.return_value = MagicMock()
|
||||||
assert start_docker_daemon(timeout=3) is False
|
assert start_docker_daemon(timeout=3) is False
|
||||||
mock_popen.assert_called_once()
|
mock_popen.assert_called_once()
|
||||||
assert mock_sleep.call_count == 3
|
assert mock_sleep.call_count == 3
|
||||||
@@ -56,34 +72,31 @@ class TestStartDockerDaemon:
|
|||||||
@patch("devx.molecule.start_docker.time.sleep")
|
@patch("devx.molecule.start_docker.time.sleep")
|
||||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
@patch("devx.molecule.start_docker.is_docker_ready")
|
||||||
@patch("devx.molecule.start_docker.subprocess.Popen")
|
@patch("devx.molecule.start_docker.subprocess.Popen")
|
||||||
@patch("builtins.open", new_callable=mock_open)
|
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
|
||||||
def test_ready_on_first_check(
|
def test_local_daemon_ready_on_first_check(
|
||||||
self,
|
self,
|
||||||
mock_file: MagicMock,
|
mock_ntf: MagicMock,
|
||||||
mock_popen: MagicMock,
|
mock_popen: MagicMock,
|
||||||
mock_ready: MagicMock,
|
mock_ready: MagicMock,
|
||||||
mock_sleep: MagicMock,
|
mock_sleep: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_ready.return_value = True
|
mock_ntf.return_value = MagicMock()
|
||||||
|
# Host not available, local daemon ready on first loop check
|
||||||
|
mock_ready.side_effect = [False, False, True]
|
||||||
assert start_docker_daemon(timeout=5) is True
|
assert start_docker_daemon(timeout=5) is True
|
||||||
mock_popen.assert_called_once()
|
mock_popen.assert_called_once()
|
||||||
mock_sleep.assert_not_called()
|
mock_sleep.assert_called_once_with(1)
|
||||||
|
|
||||||
@patch("devx.molecule.start_docker.time.sleep")
|
@patch("devx.molecule.start_docker.os.environ")
|
||||||
@patch("devx.molecule.start_docker.is_docker_ready")
|
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
|
||||||
@patch("devx.molecule.start_docker.subprocess.Popen")
|
def test_sets_docker_host(
|
||||||
@patch("builtins.open", new_callable=mock_open)
|
|
||||||
def test_custom_timeout(
|
|
||||||
self,
|
self,
|
||||||
mock_file: MagicMock,
|
|
||||||
mock_popen: MagicMock,
|
|
||||||
mock_ready: MagicMock,
|
mock_ready: MagicMock,
|
||||||
mock_sleep: MagicMock,
|
mock_environ: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
# 9 iterations not ready, 10th ready.
|
"""DOCKER_HOST must be set so molecule connects to correct socket."""
|
||||||
mock_ready.side_effect = [False] * 9 + [True]
|
start_docker_daemon(timeout=5)
|
||||||
assert start_docker_daemon(timeout=10) is True
|
mock_environ.__setitem__.assert_called_with("DOCKER_HOST", f"unix://{DOCKER_SOCK}")
|
||||||
assert mock_sleep.call_count == 9
|
|
||||||
|
|
||||||
|
|
||||||
class TestMain:
|
class TestMain:
|
||||||
|
|||||||
Reference in New Issue
Block a user