- 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.
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Batch-apply the enabled challenge set to every live team, one challenge at a
|
|
time, then report per-challenge results.
|
|
|
|
This is the CLI equivalent of hitting PATCH /api/challenges/<name> for each
|
|
registry entry, but it survives panel restarts and reports progress, which
|
|
matters because a full sync is N challenges x 4 teams x (docker build ~1-3 min).
|
|
|
|
Usage:
|
|
python3 panel/sync_all_challenges.py # sync registry -> runtime
|
|
python3 panel/sync_all_challenges.py --status # just show what is up
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import teams as orch # noqa: E402
|
|
|
|
|
|
def container_count(name: str) -> int:
|
|
out = subprocess.run(
|
|
["docker", "ps", "--format", "{{.Names}}"],
|
|
capture_output=True, text=True,
|
|
).stdout.splitlines()
|
|
return sum(1 for n in out if n.startswith(f"{name}_container_team"))
|
|
|
|
|
|
def main() -> int:
|
|
if "--status" in sys.argv:
|
|
reg = orch.load_registry()
|
|
enabled = [c["name"] for c in reg["challenges"] if c.get("enabled")]
|
|
print(f"registry enabled ({len(enabled)}): {', '.join(enabled)}\n")
|
|
running = 0
|
|
for name in enabled:
|
|
n = container_count(name)
|
|
running += 1 if n else 0
|
|
print(f" {name:16s} {n}/4 teams {'OK' if n == 4 else ('PARTIAL' if n else 'MISSING')}")
|
|
print(f"\n{running}/{len(enabled)} challenges have at least one container")
|
|
return 0
|
|
|
|
reg = orch.load_registry()
|
|
enabled = [c for c in reg["challenges"] if c.get("enabled")]
|
|
print(f"syncing {len(enabled)} enabled challenges across live teams\n")
|
|
t_all = time.time()
|
|
for c in enabled:
|
|
name = c["name"]
|
|
if container_count(name) == 4:
|
|
print(f"== {name}: already up on 4 teams, skipping")
|
|
continue
|
|
t0 = time.time()
|
|
print(f"== {name}: applying (this builds one image, then reuses it per team)", flush=True)
|
|
try:
|
|
report = orch.sync_challenge_runtime(name, True)
|
|
except Exception as e: # a single bad challenge must not abort the batch
|
|
print(f" FAILED after {time.time()-t0:.0f}s: {e}", flush=True)
|
|
continue
|
|
for t in report.get("teams", []):
|
|
last = (t.get("detail") or "").strip().splitlines()[-1:] or [""]
|
|
mark = "ok" if t.get("ok") else "FAIL"
|
|
print(f" team{t.get('team')}: {mark} {last[0][:110]}", flush=True)
|
|
print(f" done in {time.time()-t0:.0f}s", flush=True)
|
|
print(f"\nall done in {(time.time()-t_all)/60:.1f} min")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|