- fix_dup_volumes.py: 4 canonical templates had TWO volumes: keys inside one service (invalid YAML -> 'mapping key volumes already defined'), which broke every enable for anti-alchemy/burvesigner/gemas-notes/kode-viewer. - fjb: ghcr.io base is not anonymously pullable on this host; swapped to the official httpd:2.4 (its httpd.conf only uses stock modules). Added onlyBuiltDependencies to package.json (pnpm >=10 blocks esbuild's postinstall). - xl + kode-viewer: node:20-slim-bookworm is not a real tag; use node:20-bookworm-slim. gift-voucher: buster -> bookworm. - prebuild_images.py: build each challenge's shared services-<name> image once in parallel (passes a placeholder PASSWORD build-arg, since several Dockerfiles run chpasswd and fail on an empty arg). - set_enabled.py / sync_all_challenges.py: batch registry flip + runtime apply that survives panel restarts and reports per-team results. - Challenge toggle is now async: PATCH returns a job id, the client polls /api/challenges/jobs/<id> so a multi-minute build no longer blocks the panel. Added _SYNC_LOCK to serialize concurrent compose rewrites.
89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Pre-build every challenge's shared image (`services-<name>`) in parallel.
|
|
|
|
The admin toggle only reuses `image: services-<name>` when that image already
|
|
exists locally; otherwise it keeps `build:` and each of the 4 teams rebuilds it
|
|
separately (team1-x, team2-x, ...), which wastes minutes and gigabytes. Since a
|
|
challenge's image content is identical for every team (the SSH password is
|
|
applied at runtime via chpasswd, not baked into the image), one shared build
|
|
suffices.
|
|
|
|
Runs up to --jobs builds at a time and prints a per-challenge PASS/FAIL summary.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
SERVICES = Path("/opt/gemastik18-final/services")
|
|
REGISTRY = Path("/opt/gemastik18-final/teams/challenge_registry.json")
|
|
|
|
|
|
def image_exists(name: str) -> bool:
|
|
r = subprocess.run(["docker", "image", "inspect", f"services-{name}"],
|
|
capture_output=True)
|
|
return r.returncode == 0
|
|
|
|
|
|
def build(name: str, force: bool) -> tuple[str, bool, str]:
|
|
if not force and image_exists(name):
|
|
return name, True, "already built"
|
|
d = SERVICES / name
|
|
if not (d / "Dockerfile").exists():
|
|
return name, False, "no Dockerfile (multi-service challenge)"
|
|
t0 = time.time()
|
|
# Several challenge Dockerfiles do `echo root:${PASSWORD} | chpasswd`, so an
|
|
# empty build-arg makes the RUN step fail. Pass a throwaway value: the real
|
|
# per-team password is applied at container start via chpasswd, never baked
|
|
# into the image, so this value never matters.
|
|
r = subprocess.run(
|
|
["docker", "build", "-t", f"services-{name}",
|
|
"--build-arg", "PASSWORD=prebuild-placeholder", "."],
|
|
cwd=d, capture_output=True, text=True,
|
|
)
|
|
if r.returncode == 0:
|
|
return name, True, f"built in {time.time()-t0:.0f}s"
|
|
tail = [l for l in r.stderr.splitlines() if l.strip()][-1:] or ["unknown error"]
|
|
return name, False, tail[0][:160]
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("names", nargs="*", help="challenge names (default: all in registry)")
|
|
ap.add_argument("--jobs", type=int, default=4)
|
|
ap.add_argument("--force", action="store_true", help="rebuild even if present")
|
|
args = ap.parse_args()
|
|
|
|
if args.names:
|
|
names = args.names
|
|
else:
|
|
reg = json.loads(REGISTRY.read_text())
|
|
names = [c["name"] for c in reg["challenges"]]
|
|
|
|
print(f"building {len(names)} challenge images with {args.jobs} parallel jobs\n", flush=True)
|
|
t0 = time.time()
|
|
results: list[tuple[str, bool, str]] = []
|
|
with ThreadPoolExecutor(max_workers=args.jobs) as ex:
|
|
for res in ex.map(lambda n: build(n, args.force), names):
|
|
results.append(res)
|
|
mark = "PASS" if res[1] else "FAIL"
|
|
print(f"[{mark}] {res[0]:16s} {res[2]}", flush=True)
|
|
|
|
ok = [r for r in results if r[1]]
|
|
bad = [r for r in results if not r[1]]
|
|
print(f"\n{len(ok)}/{len(results)} images ready in {(time.time()-t0)/60:.1f} min")
|
|
if bad:
|
|
print("\nfailures:")
|
|
for n, _, why in bad:
|
|
print(f" {n}: {why}")
|
|
return 1 if bad else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|