Files
computer-playbook/tests/unit/python/cli/administration/deploy/development/test_compose.py
T
kevinveenbirkenbachandClaude Opus 5.5 ca6faac318 fix(ci): share the vulnerability databases with act runs and local deploys
Under act the deploy job pinned an engine but skipped the cache restores, so
every local swarm-* run stopped on a missing Grype database. The swarm
matrix also ran inside the act job container, where the shared host
directory did not exist, and the workspace job would have kept a copy of
about 5 GB in every worktree.

- scripts/tests/deploy/act/workflow.sh requires
  INFINITO_VULNERABILITY_DB_HOST_PATH, creates it on the host and binds it
  into the job container at the same path, because that container drives
  the host daemon, which resolves every stack and swarm node bind on the
  host.
- call-test-deploy.yml pins engine and database path in one step that runs
  only outside act; under act the deploy draws an engine and refreshes the
  shared directory. call-test-workspace.yml takes the shared path under act.
- The refresh lock moves into the shared directory, so host deploys and act
  runs serialize on one file, and a root refresh hands its files to the
  directory owner. hand_over skips paths a mocked download never created,
  which failed four TestRefresh cases when the gate ran as root.
- A missing INFINITO_VULNERABILITY_DB_HOST_PATH stops compose up with a
  "run 'make dotenv'" hint instead of a KeyError.
- Rationale paragraphs in the database and compose docstrings are dropped.

Part of #1324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:44:59 +01:00

342 lines
11 KiB
Python

# tests/cli/administration/deploy/development/test_compose.py
from __future__ import annotations
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from cli.administration.deploy.development.compose import (
Compose,
prepare_vulnerability_db,
)
class TestComposeUpRetries(unittest.TestCase):
def _compose(self) -> Compose:
return Compose(repo_root=Path("/tmp/infinito-nexus"), distro="arch")
@patch("time.sleep", autospec=True)
def test_compose_up_with_retries_succeeds_after_transient_failures(
self, sleep_mock: MagicMock
) -> None:
compose = self._compose()
compose.run = MagicMock(
side_effect=[
subprocess.CalledProcessError(1, ["docker", "compose"], "out1", "err1"),
subprocess.CalledProcessError(1, ["docker", "compose"], "out2", "err2"),
subprocess.CompletedProcess(
["docker", "compose"], 0, stdout="", stderr=""
),
]
)
args = [
"up",
"-d",
"--no-build",
"coredns",
"infinito",
]
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
self.assertEqual(compose.run.call_count, 3)
self.assertEqual(sleep_mock.call_count, 2)
sleep_mock.assert_any_call(30)
self.assertTrue(all(call.args == (30,) for call in sleep_mock.call_args_list))
@patch("time.sleep", autospec=True)
def test_compose_up_with_retries_raises_after_exhausting_attempts(
self, sleep_mock: MagicMock
) -> None:
compose = self._compose()
compose.run = MagicMock(
side_effect=subprocess.CalledProcessError(
1, ["docker", "compose"], "out", "err"
)
)
args = ["up", "-d", "coredns", "infinito"]
with self.assertRaises(subprocess.CalledProcessError):
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
self.assertEqual(compose.run.call_count, 6)
self.assertEqual(sleep_mock.call_count, 5)
self.assertTrue(all(call.args == (30,) for call in sleep_mock.call_args_list))
@patch("time.sleep", autospec=True)
def test_compose_up_with_retries_no_sleep_if_first_try_succeeds(
self, sleep_mock: MagicMock
) -> None:
compose = self._compose()
compose.run = MagicMock(
return_value=subprocess.CompletedProcess(
["docker", "compose"], 0, stdout="", stderr=""
)
)
args = ["up", "-d", "coredns", "infinito"]
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
self.assertEqual(compose.run.call_count, 1)
sleep_mock.assert_not_called()
@patch.dict(
os.environ,
{
"INFINITO_IMAGE": "test-image/arch",
"GITHUB_ACTIONS": "true",
"INFINITO_RUNNING_ON_GITHUB": "true",
"CI": "true",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
"INFINITO_CACHE_STACK": "",
"INFINITO_GHCR_MIRROR": "",
},
clear=False,
)
@patch(
"cli.administration.deploy.development.runner_mirror.write_override",
return_value="build/compose/runner-mirror.override.yml",
)
@patch("subprocess.run", autospec=True)
def test_run_skips_cache_override_on_github_runner(
self, run_mock: MagicMock, _write_override: MagicMock
) -> None:
compose = self._compose()
run_mock.return_value = subprocess.CompletedProcess(
["docker", "compose", "ps", "-q", "infinito"],
0,
stdout="cid\n",
stderr="",
)
r = compose.run(["ps", "-q", "infinito"], check=False, capture=True)
self.assertEqual(r.returncode, 0)
self.assertEqual(run_mock.call_count, 1)
cmd = run_mock.call_args.args[0]
env = run_mock.call_args.kwargs["env"]
self.assertEqual(
cmd,
[
"docker",
"compose",
"-f",
"compose.yml",
"-f",
"build/compose/runner-mirror.override.yml",
"ps",
"-q",
"infinito",
],
)
self.assertEqual(env["INFINITO_DISTRO"], "arch")
self.assertNotIn("COMPOSE_PROFILES", env)
@patch.dict(
os.environ,
{
"INFINITO_IMAGE": "test-image/arch",
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"CI": "",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
"INFINITO_CACHE_STACK": "",
"INFINITO_GHCR_MIRROR": "",
},
clear=False,
)
@patch("subprocess.run", autospec=True)
def test_run_layers_cache_override_locally(self, run_mock: MagicMock) -> None:
compose = self._compose()
run_mock.return_value = subprocess.CompletedProcess(
["docker", "compose"], 0, stdout="", stderr=""
)
compose.run(["ps", "-q", "infinito"], check=False, capture=True)
cmd = run_mock.call_args.args[0]
env = run_mock.call_args.kwargs["env"]
self.assertEqual(
cmd,
[
"docker",
"compose",
"-f",
"compose.yml",
"-f",
"compose/cache.override.yml",
"ps",
"-q",
"infinito",
],
)
self.assertEqual(
env["INFINITO_CACHE_REGISTRY_PROXY_CONF"], # nocheck: test-fixture
"./compose/registry-cache/proxy.conf",
)
self.assertTrue(
env["INFINITO_CACHE_PACKAGE_FRONTEND_CA_FILE"].endswith(
"/ca.crt"
) # nocheck: test-fixture
)
@patch.dict(
os.environ,
{
"INFINITO_BUILD": "1",
"INFINITO_IMAGE": "infinito-debian",
"INFINITO_PULL_POLICY": "never",
"CI": "true",
"GITHUB_ACTIONS": "true",
"INFINITO_RUNNING_ON_GITHUB": "true",
"INFINITO_CACHE_STACK": "",
"INFINITO_GHCR_MIRROR": "",
},
clear=False,
)
def test_up_builds_when_build_flag_is_enabled(self) -> None:
compose = self._compose()
compose._render_coredns_corefile = MagicMock()
compose._prepare_vulnerability_db = MagicMock()
compose._compose_up_with_retries = MagicMock()
compose.wait_for_healthy = MagicMock()
compose.up(run_entry_init=False)
compose._compose_up_with_retries.assert_called_once_with(
["up", "-d", "coredns", "infinito"],
attempts=6,
delay_s=30,
)
compose.wait_for_healthy.assert_called_once_with()
@patch.dict(
os.environ,
{
"INFINITO_BUILD": "0",
"INFINITO_IMAGE": "test-image/arch",
"CI": "true",
"GITHUB_ACTIONS": "true",
"INFINITO_RUNNING_ON_GITHUB": "true",
"INFINITO_CACHE_STACK": "",
"INFINITO_GHCR_MIRROR": "",
},
clear=False,
)
def test_up_skips_build_when_build_flag_is_disabled(self) -> None:
compose = self._compose()
compose._render_coredns_corefile = MagicMock()
compose._prepare_vulnerability_db = MagicMock()
compose._compose_up_with_retries = MagicMock()
compose.wait_for_healthy = MagicMock()
compose.up(run_entry_init=False)
compose._compose_up_with_retries.assert_called_once_with(
[
"up",
"-d",
"--no-build",
"coredns",
"infinito",
],
attempts=6,
delay_s=30,
)
compose.wait_for_healthy.assert_called_once_with()
@patch.dict(
os.environ,
{
"INFINITO_BUILD": "1",
"INFINITO_IMAGE": "infinito-debian",
"INFINITO_PULL_POLICY": "never",
"CI": "",
"GITHUB_ACTIONS": "",
"INFINITO_RUNNING_ON_GITHUB": "",
"INFINITO_GIT_COMMON_DIR": "",
"INFINITO_CACHE_NETWORK": "",
"INFINITO_CACHE_STACK": "",
"INFINITO_GHCR_MIRROR": "",
},
clear=False,
)
def test_up_includes_cache_services_when_local(self) -> None:
compose = self._compose()
compose._render_coredns_corefile = MagicMock()
compose._prepare_vulnerability_db = MagicMock()
compose._compose_up_with_retries = MagicMock()
compose.wait_for_healthy = MagicMock()
compose._bootstrap_package_cache = MagicMock()
compose._generate_package_frontend_certs = MagicMock()
compose._install_package_frontend_ca_in_runner = MagicMock()
compose.up(run_entry_init=False)
compose._compose_up_with_retries.assert_called_once_with(
[
"up",
"-d",
"registry-cache",
"package-cache",
"package-cache-frontend",
"coredns",
"infinito",
],
attempts=6,
delay_s=30,
)
compose.wait_for_healthy.assert_called_once_with()
compose._bootstrap_package_cache.assert_called_once()
compose._generate_package_frontend_certs.assert_called_once()
compose._install_package_frontend_ca_in_runner.assert_called_once()
class TestPrepareVulnerabilityDb(unittest.TestCase):
def test_the_shared_directory_is_created_as_the_calling_user(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "cache" / "vulnerability-db"
prepare_vulnerability_db(path)
self.assertTrue(path.is_dir())
def test_a_directory_the_user_may_not_create_is_left_to_docker(self) -> None:
path = MagicMock()
path.mkdir.side_effect = PermissionError("read-only")
prepare_vulnerability_db(path)
path.mkdir.assert_called_once_with(parents=True, exist_ok=True)
def test_up_prepares_the_configured_directory(self) -> None:
compose = Compose(repo_root=Path("/tmp/infinito-nexus"), distro="arch")
with patch(
"cli.administration.deploy.development.compose.prepare_vulnerability_db"
) as prepare:
compose._prepare_vulnerability_db(
{"INFINITO_VULNERABILITY_DB_HOST_PATH": "/srv/db"}
)
prepare.assert_called_once_with(Path("/srv/db"))
def test_a_stale_dotenv_fails_with_the_regeneration_hint(self) -> None:
compose = Compose(repo_root=Path("/tmp/infinito-nexus"), distro="arch")
with self.assertRaisesRegex(SystemExit, "make dotenv"):
compose._prepare_vulnerability_db({})
if __name__ == "__main__": # pragma: no cover
unittest.main()