from __future__ import annotations import os import subprocess from pathlib import Path from typing import TYPE_CHECKING from . import PROJECT_ROOT from .env import resolve_distro if TYPE_CHECKING: import argparse def _resolve_docker_root() -> Path: raw = (os.environ.get("INFINITO_DOCKER_VOLUME") or "").strip().rstrip("/") if not raw: raise RuntimeError("INFINITO_DOCKER_VOLUME must be set") return Path(raw) def _wipe_docker_root(docker_root: Path) -> None: 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}") subprocess.run(["sudo", "rm", "-rf", str(docker_root)], check=True) subprocess.run(["sudo", "mkdir", "-p", str(docker_root)], check=True) subprocess.run( ["sudo", "chown", f"{os.getuid()}:{os.getgid()}", str(docker_root)], check=True, ) leftovers = sorted(entry.name for entry in docker_root.iterdir()) if leftovers: raise RuntimeError( f"Docker root survived the wipe: {docker_root} still holds {leftovers}" ) def _should_wipe_docker_root() -> bool: from .profile import Profile if not Profile().docker_root_ephemeral(): return False return os.environ.get("INFINITO_PRESERVE_DOCKER_CACHE") != "true" def _cleanup_docker_root() -> None: if not _should_wipe_docker_root(): docker_vol = os.environ.get("INFINITO_DOCKER_VOLUME") or "(unset)" print(f">>> Skipping Docker root wipe: {docker_vol}") return docker_root = _resolve_docker_root() _wipe_docker_root(docker_root) def down_stack(*, repo_root: Path, distro: str) -> None: from .compose import Compose print(">>> Stopping compose stack and removing volumes") try: Compose(repo_root=repo_root, distro=distro).run( ["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=PROJECT_ROOT, distro=resolve_distro()) return 0