Compare commits

..
9 Commits
Author SHA1 Message Date
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
devx-ci-bot 011cf3e093 release: v0.9.2 [skip ci] 2026-06-24 02:34:47 +02:00
emil daf99c5fed DEVX-22: fix: use vfs storage driver for Docker-in-Docker in CI
Post-merge / detect-type (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 12s
Post-merge / release (push) Successful in 42s
Post-merge / vikunja (push) Successful in 8s
Post-merge / sync-wiki (push) Successful in 38s
Post-merge / badges (push) Successful in 40s
2026-06-24 00:33:48 +00:00
emil 7154e3ad7c DEVX-21: chore: trigger auto-merge after Vikunja title fix
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / release (push) Successful in 30s
Post-merge / vikunja (push) Successful in 14s
Post-merge / sync-wiki (push) Successful in 35s
Post-merge / badges (push) Successful in 42s
2026-06-24 00:28:18 +00:00
devx-ci-bot 6053fb9fba release: v0.9.1 [skip ci] 2026-06-24 01:50:03 +02:00
emil e76741bfad DEVX-21: fix: always start dockerd in CI runner for molecule tests
Post-merge / detect-type (push) Successful in 6s
Post-merge / validate-commit-msg (push) Successful in 14s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / release (push) Successful in 40s
Post-merge / vikunja (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 51s
Post-merge / badges (push) Successful in 59s
2026-06-23 23:49:13 +00:00
6 changed files with 115 additions and 45 deletions
+1 -1
View File
@@ -1 +1 @@
DEVX-20
DEVX-24
+24
View File
@@ -2,6 +2,30 @@
All notable changes to this project will be documented in this file.
## [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
### Bug Fixes
- Use vfs storage driver for Docker-in-Docker in CI
## [0.9.1] - 2026-06-23
### Bug Fixes
- Always start dockerd in CI runner for molecule tests
## [0.9.0] - 2026-06-23
### Features
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.9.0"
__version__ = "0.9.4"
+30 -15
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python3
"""Start a Docker daemon inside a CI runner container (Docker-in-Docker).
CI runners (e.g. ``gitea/runner-images:ubuntu-latest``) may not have a
Docker daemon running. This module starts ``dockerd`` in the background
and waits for it to become ready, or exits immediately if Docker is
already available.
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
nested containers. This module starts ``dockerd`` on a separate socket
and sets ``DOCKER_HOST`` so both the CLI and Python library connect to
the local daemon.
Usage::
@@ -13,8 +14,10 @@ Usage::
from __future__ import annotations
import os
import subprocess # nosec B404
import sys
import tempfile
import time
import click
@@ -22,34 +25,46 @@ import click
from devx.i18n import _
DEFAULT_TIMEOUT = 30
DOCKERD_LOG = "/var/log/dockerd.log"
DOCKER_SOCK = "/tmp/dockerd.sock" # nosec B108
def is_docker_ready() -> bool:
"""Check if the Docker daemon is responding."""
"""Check if the local Docker daemon is responding."""
result = subprocess.run( # nosec B603 B607
["docker", "info"],
capture_output=True,
check=False,
env={**os.environ, "DOCKER_HOST": f"unix://{DOCKER_SOCK}"},
)
return result.returncode == 0
def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
"""Start dockerd in the background and wait for it to be ready.
"""Start dockerd on a separate socket and wait for it to be ready.
Returns ``True`` if Docker is ready (either already running or
successfully started), ``False`` if it failed to start within
the timeout.
Uses ``/tmp/dockerd.sock`` instead of the default ``/var/run/docker.sock``
to avoid conflicts with host-mounted sockets. Sets ``DOCKER_HOST`` in the
current environment so molecule's Python docker library connects to the
local daemon.
Returns ``True`` if Docker is ready, ``False`` if it failed to
start within the timeout.
"""
if is_docker_ready():
click.echo(_("Docker daemon already running"))
return True
# Point Docker CLI and Python library to our local socket
os.environ["DOCKER_HOST"] = f"unix://{DOCKER_SOCK}"
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
["dockerd"],
[
"dockerd",
"--storage-driver",
"vfs",
"-H",
f"unix://{DOCKER_SOCK}",
],
stdout=log_file,
stderr=subprocess.STDOUT,
start_new_session=True,
-7
View File
@@ -419,13 +419,6 @@
"ru": "Created release commit.",
"zh": "Created release commit."
},
"Docker daemon already running": {
"bg": "Docker daemon already running",
"de": "Docker-Daemon läuft bereits",
"en": "Docker daemon already running",
"ru": "Docker-демон уже запущен",
"zh": "Docker 守护进程已在运行"
},
"Docker daemon failed to start": {
"bg": "Docker daemon failed to start",
"de": "Docker-Daemon konnte nicht gestartet werden",
+59 -21
View File
@@ -1,10 +1,10 @@
"""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 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:
@@ -12,7 +12,9 @@ class TestIsDockerReady:
def test_ready(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0)
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")
def test_not_ready(self, mock_run: MagicMock) -> None:
@@ -21,40 +23,41 @@ class TestIsDockerReady:
class TestStartDockerDaemon:
@patch("devx.molecule.start_docker.is_docker_ready", return_value=True)
def test_already_running(self, mock_ready: MagicMock) -> None:
assert start_docker_daemon() is True
mock_ready.assert_called_once()
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready")
@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(
self,
mock_file: MagicMock,
mock_ntf: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
# First call: initial check (not ready). Second: first loop iteration (ready).
mock_ntf.return_value = MagicMock()
mock_ready.side_effect = [False, True]
assert start_docker_daemon(timeout=5) is True
mock_popen.assert_called_once()
mock_sleep.assert_not_called()
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
mock_sleep.assert_called_once_with(1)
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready", return_value=False)
@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(
self,
mock_file: MagicMock,
mock_ntf: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
# is_docker_ready always returns False: 1 initial + 3 loop iterations = 4 calls
mock_ntf.return_value = MagicMock()
assert start_docker_daemon(timeout=3) is False
mock_popen.assert_called_once()
assert mock_sleep.call_count == 3
@@ -62,20 +65,55 @@ class TestStartDockerDaemon:
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready")
@patch("devx.molecule.start_docker.subprocess.Popen")
@patch("builtins.open", new_callable=mock_open)
def test_custom_timeout(
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
def test_ready_on_first_check(
self,
mock_file: MagicMock,
mock_ntf: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
# First call: initial check (not ready). Then 9 loop iterations (not ready),
# 10th iteration (ready).
mock_ready.side_effect = [False] * 10 + [True]
mock_ntf.return_value = MagicMock()
mock_ready.return_value = True
assert start_docker_daemon(timeout=5) is True
mock_popen.assert_called_once()
mock_sleep.assert_not_called()
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready")
@patch("devx.molecule.start_docker.subprocess.Popen")
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
def test_custom_timeout(
self,
mock_ntf: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
) -> None:
mock_ntf.return_value = MagicMock()
mock_ready.side_effect = [False] * 9 + [True]
assert start_docker_daemon(timeout=10) is True
assert mock_sleep.call_count == 9
@patch("devx.molecule.start_docker.os.environ")
@patch("devx.molecule.start_docker.time.sleep")
@patch("devx.molecule.start_docker.is_docker_ready")
@patch("devx.molecule.start_docker.subprocess.Popen")
@patch("devx.molecule.start_docker.tempfile.NamedTemporaryFile")
def test_sets_docker_host(
self,
mock_ntf: MagicMock,
mock_popen: MagicMock,
mock_ready: MagicMock,
mock_sleep: MagicMock,
mock_environ: MagicMock,
) -> None:
"""DOCKER_HOST must be set so molecule connects to local daemon."""
mock_ntf.return_value = MagicMock()
mock_ready.return_value = True
start_docker_daemon(timeout=5)
mock_environ.__setitem__.assert_called_with("DOCKER_HOST", f"unix://{DOCKER_SOCK}")
class TestMain:
@patch("devx.molecule.start_docker.start_docker_daemon", return_value=True)