Files
computer-playbook/cli/deploy/development/down.py
T
kevinveenbirkenbachandClaude Opus 4.7 94936f198b feat(compose): gate registry-cache to local dev only via Profile class
Move the registry-cache pull-through proxy out of the always-on `ci`
profile into its own `cache` profile, and select it based on the runtime
environment instead of statically.

Why
   The proxy only earns its keep when image pulls amortize across many
   runs on the same host. That holds on developer machines (cross-run
   image dedup, persistent host disk) but not on GitHub-hosted runners
   (fresh disk per job), where the proxy startup overhead is net loss
   without the corresponding payoff.

How
* Compose: split `registry-cache` into its own `cache` profile;
  `infinito.depends_on.registry-cache` gets `required: false` so Compose
  does not auto-start the proxy via the dependency when the profile is
  inactive; the systemd drop-in bind-mount source is gated by the new
  `INFINITO_REGISTRY_CACHE_PROXY_CONF` env var with `/dev/null` default,
  so dockerd loads an empty drop-in (no `HTTP_PROXY`, no `ExecStartPre`)
  when the proxy is not running and pulls images directly.
* New `cli/deploy/development/profile.py` `Profile` class is the SPOT
  for which compose profiles activate. KISS + SRP: `is_ci()` reads the
  three CI signals (`GITHUB_ACTIONS`, `RUNNING_ON_GITHUB`, `CI`),
  `registry_cache_active()` is its strict inverse, and `args()` returns
  the matching `--profile ...` flag list.
* `Compose` and `down.py` consume the same `Profile` so up/down cannot
  drift on which services are part of the stack.
* Documentation under `compose/registry-cache/README.md`,
  `docs/contributing/artefact/files/compose.yml.md` and
  `docs/contributing/environment/variables.md` describes the new toggle
  and the `INFINITO_REGISTRY_CACHE_PROXY_CONF` env var. Pre-existing
  documentation.md violations in those files (one SPOT self-label, one
  em dash, two path-as-link-text instances) corrected in the same pass
  per the fix-nearby rule.
* Tests: 14 new `Profile` unit tests in `test_profile.py` plus two
  scenario tests in `test_compose.py` that lock down the
  `--profile ci` (CI runner) vs `--profile ci --profile cache` (local)
  argument shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:25:44 +02:00

90 lines
2.7 KiB
Python

from __future__ import annotations
import argparse
import os
import shutil
import subprocess
from pathlib import Path
from .common import resolve_distro
from .profile import Profile
def _repo_root_from_here() -> Path:
# Derive the repository root relative to this file.
# Adjust the number of parents if the project layout changes.
return Path(__file__).resolve().parents[3]
CI_DOCKER_ROOT = Path("/mnt/docker")
CI_DOCKER_ROOT_STR = str(CI_DOCKER_ROOT)
def _base_env(*, distro: str) -> dict[str, str]:
env = dict(os.environ)
env["INFINITO_DISTRO"] = distro
return env
def _compose_run(*, repo_root: Path, distro: str, args: list[str]) -> None:
cmd = ["docker", "compose", "--env-file", "env.ci"]
env_development = repo_root / "env.development"
if env_development.exists():
cmd += ["--env-file", "env.development"]
# Tear the registry-cache down too if it was activated for the
# corresponding `up`. Reuse the Profile decision so up/down cannot
# disagree about which services are part of the stack.
cmd += Profile().args()
cmd += list(args)
env = _base_env(distro=distro)
env.setdefault("NIX_CONFIG", "")
subprocess.run(cmd, cwd=repo_root, env=env, check=True, text=True)
def _cleanup_docker_root() -> None:
docker_root = CI_DOCKER_ROOT
if os.environ.get("RUNNING_ON_GITHUB") != "true":
print(f">>> Not on GitHub - No bind volumes will be deleted: {docker_root}")
return
docker_root_env = os.environ.get("INFINITO_DOCKER_VOLUME", "").strip().rstrip("/")
if docker_root_env and docker_root_env != CI_DOCKER_ROOT_STR:
raise RuntimeError(
"SECURITY VIOLATION: "
f"INFINITO_DOCKER_VOLUME={docker_root_env} is not allowed on GitHub runner. "
f"Only {CI_DOCKER_ROOT_STR} is permitted."
)
if not docker_root.exists():
print(f">>> Docker root does not exist, nothing to clean: {docker_root}")
return
print(f">>> CI cleanup: wiping Docker root: {docker_root}")
shutil.rmtree(docker_root, ignore_errors=True)
docker_root.mkdir(parents=True, exist_ok=True)
def down_stack(*, repo_root: Path, distro: str) -> None:
print(">>> Stopping compose stack and removing volumes")
try:
_compose_run(
repo_root=repo_root,
distro=distro,
args=["down", "--remove-orphans", "-v"],
)
finally:
_cleanup_docker_root()
def add_parser(sub: argparse._SubParsersAction) -> None:
p = sub.add_parser("down", help="Stop compose stack and remove volumes.")
p.set_defaults(_handler=handler)
def handler(args: argparse.Namespace) -> int:
down_stack(repo_root=_repo_root_from_here(), distro=resolve_distro())
return 0