128 lines
4.2 KiB
Python
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}"
|