mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-14 14:47:12 +00:00
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>
342 lines
11 KiB
Python
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()
|