teleo-infrastructure/tests/docker_volume_lifecycle.py
2026-07-22 03:02:50 +02:00

128 lines
4.2 KiB
Python

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}"