Compare commits

...
7 Commits
Author SHA1 Message Date
devx-ci-bot d398c8e971 release: v0.9.5 [skip ci] 2026-06-24 01:37:15 +00:00
emil 39526d8e6a DEVX-26: fix: use host Docker socket with DOCKER_HOST fallback to local dockerd
Post-merge / detect-type (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Successful in 46s
Post-merge / vikunja (push) Successful in 10s
Post-merge / badges (push) Successful in 41s
Post-merge / sync-wiki (push) Successful in 43s
2026-06-24 01:36:21 +00:00
emil 37730e2187 DEVX-25: fix: use host Docker socket with DOCKER_HOST fallback to local dockerd
Post-merge / detect-type (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 41s
Post-merge / vikunja (push) Successful in 12s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / badges (push) Successful in 52s
2026-06-24 01:24:00 +00:00
devx-ci-bot e2f66ca70a release: v0.9.4 [skip ci] 2026-06-24 01:11:08 +00:00
emil 0b88c211f1 DEVX-24: fix: use separate Docker socket for DinD in CI
Post-merge / detect-type (push) Successful in 14s
Post-merge / validate-commit-msg (push) Successful in 15s
Post-merge / configure-repo (push) Successful in 43s
Post-merge / release (push) Successful in 51s
Post-merge / vikunja (push) Successful in 18s
Post-merge / sync-wiki (push) Successful in 45s
Post-merge / badges (push) Successful in 44s
2026-06-24 01:08:10 +00:00
devx-ci-bot ea4ee0d303 release: v0.9.3 [skip ci] 2026-06-24 02:46:26 +02:00
emil 16fba17b03 DEVX-23: fix: use tempfile for dockerd log to fix CI permission error
Post-merge / detect-type (push) Successful in 15s
Post-merge / validate-commit-msg (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / release (push) Successful in 40s
Post-merge / vikunja (push) Successful in 22s
Post-merge / sync-wiki (push) Successful in 42s
Post-merge / badges (push) Successful in 54s
2026-06-24 00:45:28 +00:00
6 changed files with 104 additions and 42 deletions
+1 -1
View File
@@ -1 +1 @@
DEVX-22 DEVX-26
+19
View File
@@ -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 -1
View File
@@ -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"
+35 -12
View File
@@ -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,
+7
View File
@@ -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",
+41 -28
View File
@@ -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: