from __future__ import annotations import fcntl import json import subprocess import tempfile import threading from dataclasses import dataclass from pathlib import Path from typing import TextIO POSTGRES_DATA_PATH = "/var/lib/postgresql/data" POSTGRES_TMPFS_SPEC = f"{POSTGRES_DATA_PATH}:rw,nosuid,nodev,noexec,size=384m" VOLUME_GUARD_LOCK = Path(tempfile.gettempdir()) / "livingip-teleo-docker-volume-lifecycle.lock" @dataclass(frozen=True) class DockerVolumeSetGuard: before: frozenset[str] context: str owner_thread_id: int _PROCESS_GUARD = threading.RLock() _LOCK_FILE: TextIO | None = None _GUARD_STACK: list[DockerVolumeSetGuard] = [] def _docker(*arguments: str, check: bool = True) -> subprocess.CompletedProcess[str]: completed = subprocess.run( ["docker", *arguments], text=True, capture_output=True, check=False, timeout=120, ) if check and completed.returncode != 0: raise AssertionError(completed.stderr or completed.stdout) return completed def docker_volume_names() -> frozenset[str]: completed = _docker("volume", "ls", "--quiet") return frozenset(name for name in completed.stdout.splitlines() if name) def assert_docker_volume_set_unchanged(before: frozenset[str], *, context: str) -> None: after = docker_volume_names() added = sorted(after - before) removed = sorted(before - after) assert after == before, ( f"daemon-wide Docker volume drift while running {context}; another Docker job may be responsible; " f"added={added!r}; removed={removed!r}; before_count={len(before)}; after_count={len(after)}" ) def acquire_docker_volume_set_guard(*, context: str) -> DockerVolumeSetGuard: global _LOCK_FILE _PROCESS_GUARD.acquire() opened_here = False try: if _LOCK_FILE is None: lock_file = VOLUME_GUARD_LOCK.open("a+", encoding="utf-8") try: fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) except BaseException: lock_file.close() raise _LOCK_FILE = lock_file opened_here = True guard = DockerVolumeSetGuard( before=docker_volume_names(), context=context, owner_thread_id=threading.get_ident(), ) _GUARD_STACK.append(guard) return guard except BaseException: if opened_here and _LOCK_FILE is not None: fcntl.flock(_LOCK_FILE.fileno(), fcntl.LOCK_UN) _LOCK_FILE.close() _LOCK_FILE = None _PROCESS_GUARD.release() raise def release_docker_volume_set_guard(guard: DockerVolumeSetGuard) -> None: global _LOCK_FILE assert guard.owner_thread_id == threading.get_ident(), "Docker volume guard released from a different thread" assert _GUARD_STACK and _GUARD_STACK[-1] is guard, "Docker volume guards must be released in LIFO order" try: assert_docker_volume_set_unchanged(guard.before, context=guard.context) finally: _GUARD_STACK.pop() if not _GUARD_STACK: assert _LOCK_FILE is not None fcntl.flock(_LOCK_FILE.fileno(), fcntl.LOCK_UN) _LOCK_FILE.close() _LOCK_FILE = None _PROCESS_GUARD.release() def inspect_postgres_tmpfs_container(name: str) -> dict: inspected = json.loads(_docker("inspect", name).stdout)[0] assert inspected["Mounts"] == [], f"{name} unexpectedly uses Docker volumes or bind mounts" tmpfs = inspected["HostConfig"]["Tmpfs"] assert POSTGRES_DATA_PATH in tmpfs, f"{name} does not mount PostgreSQL data on tmpfs" return inspected def remove_container_and_volumes(name: str) -> None: removed = _docker("rm", "--force", "--volumes", name, check=False) if removed.returncode != 0 and "no such container" not in removed.stderr.lower(): raise AssertionError(removed.stderr or removed.stdout) assert _docker("container", "inspect", name, check=False).returncode != 0 names = _docker( "container", "ls", "--all", "--filter", f"name=^/{name}$", "--format", "{{.Names}}", ) assert names.stdout.strip() == "", f"container still exists after cleanup: {name}"