*: Switch to python
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import atexit
|
||||
import hashlib
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from src import log
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mount:
|
||||
src: Path
|
||||
dst: str
|
||||
readonly: bool = False
|
||||
|
||||
def as_arg(self) -> str:
|
||||
flag = ":ro" if self.readonly else ""
|
||||
return f"{self.src}:{self.dst}{flag}"
|
||||
|
||||
|
||||
_active: set["Container"] = set()
|
||||
_lock = threading.Lock()
|
||||
_handlers_installed = False
|
||||
|
||||
|
||||
def _install_handlers() -> None:
|
||||
global _handlers_installed
|
||||
if _handlers_installed:
|
||||
return
|
||||
_handlers_installed = True
|
||||
|
||||
def cleanup(*_):
|
||||
_shutdown_all()
|
||||
|
||||
atexit.register(_shutdown_all)
|
||||
for s in (signal.SIGINT, signal.SIGTERM):
|
||||
try:
|
||||
signal.signal(s, lambda *_: (_shutdown_all(), os._exit(130)))
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
def _shutdown_all() -> None:
|
||||
with _lock:
|
||||
cs = list(_active)
|
||||
_active.clear()
|
||||
for c in cs:
|
||||
try:
|
||||
subprocess.run(
|
||||
["podman", "rm", "-f", c.name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Container:
|
||||
name: str
|
||||
image: str
|
||||
mounts: list[Mount] = field(default_factory=list)
|
||||
tmpfs: list[str] = field(default_factory=list)
|
||||
network: bool = False
|
||||
extra_env: dict[str, str] = field(default_factory=dict)
|
||||
started: bool = False
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self is other
|
||||
|
||||
def start(self) -> None:
|
||||
if self.started:
|
||||
return
|
||||
_install_handlers()
|
||||
# Ensure no stale container.
|
||||
subprocess.run(
|
||||
["podman", "rm", "-f", self.name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
argv = [
|
||||
"podman",
|
||||
"run",
|
||||
"-d",
|
||||
"--rm",
|
||||
"--name",
|
||||
self.name,
|
||||
]
|
||||
for t in ("/tmp", "/dest", "/var/cache", *self.tmpfs):
|
||||
argv += ["--tmpfs", t]
|
||||
argv += ["--network=none"] if not self.network else []
|
||||
for m in self.mounts:
|
||||
m.src.mkdir(parents=True, exist_ok=True) if not m.src.exists() else None
|
||||
argv += ["-v", m.as_arg()]
|
||||
argv += [self.image, "sleep", "infinity"]
|
||||
log.debug(f"container start: {' '.join(argv)}")
|
||||
r = subprocess.run(argv, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"podman run failed: {r.stderr.strip()}")
|
||||
with _lock:
|
||||
_active.add(self)
|
||||
self.started = True
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self.started:
|
||||
return
|
||||
subprocess.run(
|
||||
["podman", "rm", "-f", self.name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
with _lock:
|
||||
_active.discard(self)
|
||||
self.started = False
|
||||
|
||||
def exec(
|
||||
self,
|
||||
argv: list[str],
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
check: bool = True,
|
||||
user: str | None = None,
|
||||
) -> int:
|
||||
if not self.started:
|
||||
raise RuntimeError("container not started")
|
||||
cmd = ["podman", "exec"]
|
||||
if cwd:
|
||||
cmd += ["-w", cwd]
|
||||
if user:
|
||||
cmd += ["-u", user]
|
||||
merged: dict[str, str] = {}
|
||||
merged.update(self.extra_env)
|
||||
if env:
|
||||
merged.update(env)
|
||||
for k, v in merged.items():
|
||||
cmd += ["-e", f"{k}={v}"]
|
||||
cmd += [self.name, *argv]
|
||||
log.debug(f"exec: {' '.join(argv)}")
|
||||
rc = subprocess.call(cmd)
|
||||
if check and rc != 0:
|
||||
raise RuntimeError(f"command failed (exit {rc}): {' '.join(argv)}")
|
||||
return rc
|
||||
|
||||
def exec_shell(
|
||||
self,
|
||||
script: str,
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
check: bool = True,
|
||||
) -> int:
|
||||
return self.exec(["sh", "-ec", script], env=env, cwd=cwd, check=check)
|
||||
|
||||
def cp_out(self, src_in: str, dst_host: Path) -> None:
|
||||
dst_host.parent.mkdir(parents=True, exist_ok=True)
|
||||
r = subprocess.run(
|
||||
["podman", "cp", f"{self.name}:{src_in}", str(dst_host)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"podman cp failed: {r.stderr.strip()}")
|
||||
|
||||
|
||||
def hash_dockerfile(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def image_exists(tag: str) -> bool:
|
||||
r = subprocess.run(["podman", "image", "exists", tag])
|
||||
return r.returncode == 0
|
||||
|
||||
|
||||
def build_image(dockerfile: Path, tag: str) -> None:
|
||||
log.info(f"building container image {tag}")
|
||||
r = subprocess.run(
|
||||
["podman", "build", "-t", tag, "-f", str(dockerfile), str(dockerfile.parent)]
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError("podman build failed")
|
||||
|
||||
|
||||
def ensure_image(dockerfile: Path, tag: str, hash_file: Path) -> None:
|
||||
cur = hash_dockerfile(dockerfile) + "\n" + tag
|
||||
if hash_file.exists() and hash_file.read_text() == cur and image_exists(tag):
|
||||
log.debug(f"image {tag} up-to-date")
|
||||
return
|
||||
build_image(dockerfile, tag)
|
||||
hash_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
hash_file.write_text(cur)
|
||||
Reference in New Issue
Block a user