fix: bulk challenge enable path (compose YAML, base images, async toggle, tooling)
- 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.
This commit is contained in:
+38
-5
@@ -8,6 +8,7 @@ import os
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import threading
|
||||
import httpx
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Request, HTTPException, WebSocket, WebSocketDisconnect
|
||||
@@ -34,6 +35,10 @@ async def _start_background():
|
||||
name="sla-warmup").start()
|
||||
orch.start_sla_refresher()
|
||||
|
||||
# Toggle jobs: Docker builds take minutes, so PATCH /api/challenges runs the
|
||||
# work on a background thread and the client polls /api/challenges/jobs/<id>.
|
||||
_TOGGLE_JOBS: dict[str, dict] = {}
|
||||
|
||||
CHALLENGES = [
|
||||
{"name": "blogpost", "port": 10000, "ssh": 10022, "category": "web", "desc": "Flask blog with exiftool + SSTI"},
|
||||
{"name": "carbeat", "port": 11000, "ssh": 11022, "category": "pwn", "desc": "Binary exploitation menu"},
|
||||
@@ -389,11 +394,39 @@ async def api_challenge_toggle(challenge: str, req: Request):
|
||||
# skip rebuild when the flag didn't actually change
|
||||
if "unchanged" in changed:
|
||||
return {"ok": True, "name": challenge, "enabled": enabled, "applied": [], "unchanged": True}
|
||||
try:
|
||||
report = await asyncio.to_thread(orch.sync_challenge_runtime, challenge, enabled)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Registry updated tapi runtime gagal: {e}")
|
||||
return {"ok": True, "name": challenge, "enabled": enabled, "applied": report.get("teams", [])}
|
||||
# Applying a challenge means a Docker build per team, which takes minutes.
|
||||
# Run it in the background so the admin UI stays responsive, and let the
|
||||
# client poll /api/challenges/jobs/<job_id> for the per-team report.
|
||||
job_id = f"{challenge}-{'on' if enabled else 'off'}-{int(time.time())}"
|
||||
_TOGGLE_JOBS[job_id] = {
|
||||
"job": job_id, "name": challenge, "enabled": enabled,
|
||||
"state": "running", "applied": [], "detail": "queued",
|
||||
"started": int(time.time()),
|
||||
}
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
report = orch.sync_challenge_runtime(challenge, enabled)
|
||||
_TOGGLE_JOBS[job_id].update(
|
||||
state="done", applied=report.get("teams", []), detail="complete")
|
||||
except Exception as e: # surfaced to the client via the job record
|
||||
_TOGGLE_JOBS[job_id].update(state="error", detail=str(e))
|
||||
finally:
|
||||
_TOGGLE_JOBS[job_id]["finished"] = int(time.time())
|
||||
|
||||
threading.Thread(target=_worker, name=f"toggle-{job_id}", daemon=True).start()
|
||||
return {"ok": True, "name": challenge, "enabled": enabled,
|
||||
"job": job_id, "state": "running"}
|
||||
|
||||
|
||||
@app.get("/api/challenges/jobs/{job_id}")
|
||||
async def api_challenge_job(job_id: str, req: Request):
|
||||
"""Poll the result of an async challenge toggle started by PATCH above."""
|
||||
require_login(req)
|
||||
job = _TOGGLE_JOBS.get(job_id)
|
||||
if not job:
|
||||
raise HTTPException(404, "Unknown job")
|
||||
return job
|
||||
|
||||
@app.get("/api/status")
|
||||
async def api_status(req: Request):
|
||||
|
||||
Reference in New Issue
Block a user