fix: get all imported challenges building and running

Root causes found by prebuilding every challenge image in parallel:
- fjb: ghcr.io base is not anonymously pullable here -> official httpd:2.4.
  pnpm 12 (via corepack on node:20) fails the install with
  ERR_PNPM_IGNORED_BUILDS unless build scripts are approved; neither
  onlyBuiltDependencies in pnpm-workspace.yaml nor --no-ignore-scripts
  suppresses it. The working sequence is:
    pnpm install --ignore-scripts && pnpm approve-builds --all && pnpm rebuild
- xl + kode-viewer: node:20-slim-bookworm is not a real tag -> node:20-bookworm-slim.
- burvesigner: python-dev no longer exists in bookworm -> dropped (python3-dev
  was already there and the source has no py2 syntax).
- burvesigner/hirnfick/s3: apt update and install were separate RUN layers;
  with the bundled apt-insecure.conf the second invocation re-resolved against
  the EOL bullseye-security mirror and 404'd every package. Merged into one
  'update && install' layer (fix_apt_layers.py, idempotent).
- consolidate_images.sh: teams used to build a private image per team
  (team1-x ... team4-x) because no shared image existed. Since the password is
  applied at runtime via chpasswd, one shared services-<name> build is enough;
  this reclaims ~1.5 GB, which matters on a 79 GB disk.
- reconcile_team_state(): a challenge enabled while a team was down left
  state.json without ports/flag/password, so the next compose render died with
  KeyError. Now both the API and the CLI tools reconcile first.
This commit is contained in:
MythEclipse
2026-09-25 21:03:29 +08:00
parent 6d1ede8c2b
commit ef385c3397
10 changed files with 224 additions and 17 deletions
+57
View File
@@ -84,6 +84,63 @@ def set_challenge_enabled(name: str, enabled: bool) -> dict:
_SYNC_LOCK = threading.Lock()
def reconcile_team_state() -> list[str]:
"""Make every team's state.json agree with the registry's enabled set.
Needed whenever the registry is edited directly (panel/set_enabled.py) or
a challenge is enabled but a team was offline/stopped while it happened:
state.json must carry ports + a chall password + a flag for every enabled
challenge, otherwise render_team_compose() raises KeyError on ports[name].
Returns a list of human-readable actions taken.
"""
reg = load_registry()
enabled = enabled_challenges()
# rebuild the derived CHALLENGES for fresh creates
global CHALLENGES
CHALLENGES = [(c["name"], c["chall_offset"], c["ssh_offset"]) for c in enabled]
notes: list[str] = []
for d in sorted(TEAMS_DIR.glob("team*")):
sf = d / "state.json"
if not sf.exists():
continue
st = json.loads(sf.read_text())
idx = st["index"]
dirty = False
st.setdefault("ports", {})
st.setdefault("chall_passwords", {})
st.setdefault("flags", {})
for ch in enabled:
name = ch["name"]
if name not in st["ports"]:
st["ports"][name] = {"chall": 30000 + idx * 1000 + ch["chall_offset"],
"ssh": 30000 + idx * 1000 + ch["ssh_offset"]}
dirty = True
notes.append(f"team{idx}: allocated ports for {name}")
if not st["chall_passwords"].get(name):
st["chall_passwords"][name] = f"chall{idx}_{name}_{secrets.token_hex(4)}"
dirty = True
if not st["flags"].get(name):
flag = f"GEMASTIK18{{TEAM{idx}_{name.upper()}_{secrets.token_hex(6)}}}"
st["flags"][name] = flag
fd = d / "receiver" / "flags"
fd.mkdir(parents=True, exist_ok=True)
(fd / f"{name}.txt").write_text(flag)
dirty = True
notes.append(f"team{idx}: minted flag for {name}")
# make sure the source tree is present for a later `build:`
ts = d / "services" / name
if not ts.exists():
src = SERVICES_SRC / name
if src.exists():
shutil.copytree(src, ts,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git"))
notes.append(f"team{idx}: copied source for {name}")
if dirty:
sf.write_text(json.dumps(st, indent=2))
return notes
def sync_challenge_runtime(name: str, enabled: bool) -> dict:
"""Apply one challenge's enabled flag to every live team.