fix: make challenge toggle actually work end-to-end (5 root-cause bugs)
Found by testing a real enable/disable cycle (art, fjb, gift-card): 1. compose_gen always swapped build->image, so a never-built challenge produced 'pull access denied for services-<name>'. Now it only reuses the image when it exists locally, otherwise keeps build: so 'docker compose up --build' builds it. 2. Canonical templates use 'build: context: .' (written for the shared services/ tree). In the per-team compose that resolves to the team dir which has no Dockerfile -> 'failed to read dockerfile'. The renderer now rewrites the main service's context to ./<name>. 3. Teams created before the XVI/XVII import had no xvi/xvii subpackages under their local challenges/ dir, so the regenerated receiver main.py crash-looped on import. gen_receiver_main now mirrors ALL shared checkers (native + xvi + xvii) into every team receiver on each sync. 4. systemd Environment= keys can't contain hyphens, so CHALLENGE_PORT_GIFT-CARD was silently dropped. Keys are now normalized to underscores on both the writer and reader side. 5. Several checkers called 'docker exec' with no timeout; against a container with accumulated chall.py zombies that blocks forever and stalls the whole SLA loop. Added mandatory timeouts (Phew, Sheesh, Carbeat, Poke, Warmup). Also: enabling a challenge now copies its source tree into each team's services/ dir (team dirs only held challenges enabled at create_team time), and the XVII checkers were rewritten to be protocol-aware (gift-card/gift-voucher are socat TCP, not HTTP) with strict timeouts.
This commit is contained in:
1 parent
c6fd9ec268
commit
d4c741926d
10 files changed
+176
-106
No files matched your search
@@ -11,6 +11,7 @@ from three packages:
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from teams import TEAMS_DIR, load_registry
|
||||
@@ -98,26 +99,33 @@ app = FastAPI()
|
||||
security = HTTPBasic()
|
||||
settings = get_settings()
|
||||
|
||||
def _envkey(name: str) -> str:
|
||||
# systemd Environment= keys can't contain hyphens; gen_receiver_services
|
||||
# writes CHALLENGE_PORT_GIFT_CARD for the challenge "gift-card".
|
||||
return name.upper().replace("-", "_")
|
||||
|
||||
def _ch_port(name: str, default: int) -> int:
|
||||
# read from .env manually (pydantic settings has fixed fields)
|
||||
val = os.environ.get(f"CHALLENGE_PORT_{{name.upper()}}")
|
||||
key = _envkey(name)
|
||||
val = os.environ.get(f"CHALLENGE_PORT_{{key}}")
|
||||
if not val:
|
||||
try:
|
||||
with open(os.path.join(os.path.dirname(__file__), ".env")) as f:
|
||||
for line in f:
|
||||
if line.startswith(f"CHALLENGE_PORT_{{name.upper()}}="):
|
||||
if line.startswith(f"CHALLENGE_PORT_{{key}}="):
|
||||
val = line.strip().split("=", 1)[1]
|
||||
except Exception:
|
||||
pass
|
||||
return int(val) if val else default
|
||||
|
||||
def _ch_container(name: str, default: str) -> str:
|
||||
val = os.environ.get(f"CHALLENGE_CONTAINER_{{name.upper()}}")
|
||||
key = _envkey(name)
|
||||
val = os.environ.get(f"CHALLENGE_CONTAINER_{{key}}")
|
||||
if not val:
|
||||
try:
|
||||
with open(os.path.join(os.path.dirname(__file__), ".env")) as f:
|
||||
for line in f:
|
||||
if line.startswith(f"CHALLENGE_CONTAINER_{{name.upper()}}="):
|
||||
if line.startswith(f"CHALLENGE_CONTAINER_{{key}}="):
|
||||
val = line.strip().split("=", 1)[1]
|
||||
except Exception:
|
||||
pass
|
||||
@@ -253,6 +261,32 @@ def validate(credentials, challenge):
|
||||
"""
|
||||
|
||||
|
||||
def _sync_checker_packages(recv_dir) -> None:
|
||||
"""Mirror the shared receiver's challenge checkers into a team receiver.
|
||||
|
||||
Two reasons this must run on every sync, not just at create_team time:
|
||||
1. Teams created before the XVI/XVII import have no xvi/xvii subpackages,
|
||||
so a regenerated main.py that imports them would crash-loop the receiver.
|
||||
2. Native checkers (Blogpost/Phew/...) get bug fixes (e.g. mandatory
|
||||
subprocess timeouts); a team copy made earlier keeps the stale version.
|
||||
"""
|
||||
shared_challenges = Path("/opt/gemastik18-final/receiver/challenges")
|
||||
dst_root = recv_dir / "challenges"
|
||||
dst_root.mkdir(parents=True, exist_ok=True)
|
||||
for src_file in sorted(shared_challenges.glob("*.py")):
|
||||
if src_file.name == "__init__.py":
|
||||
continue
|
||||
shutil.copy2(src_file, dst_root / src_file.name)
|
||||
for pkg in ("xvi", "xvii"):
|
||||
src = shared_challenges / pkg
|
||||
if not src.exists():
|
||||
continue
|
||||
dst = dst_root / pkg
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
||||
|
||||
|
||||
def sync_team_receivers() -> None:
|
||||
"""Regenerate main.py for every existing team from its state + registry."""
|
||||
for d in sorted(TEAMS_DIR.glob("team*")):
|
||||
@@ -262,6 +296,7 @@ def sync_team_receivers() -> None:
|
||||
st = json.loads(sf.read_text())
|
||||
idx = st["index"]
|
||||
enabled = [c for c in load_registry()["challenges"] if c.get("enabled")]
|
||||
_sync_checker_packages(d / "receiver")
|
||||
text = render_receiver_main(enabled, {c["name"]: st["ports"][c["name"]]["chall"] for c in enabled})
|
||||
(d / "receiver" / "main.py").write_text(text)
|
||||
print(f"team{idx}: receiver main.py regenerated ({len(enabled)} challenges)")
|
||||
|
||||
Reference in new issue
Block a user