TazLab K8s: Halloween Detail

Level 3 (Detail) — halloween.tazlab.net, a cooperative FastAPI + WebSocket party game deployed through the teaching/wiki GitOps lane.

Concept

halloween is a one-evening game: players find printed QR codes, solve a riddle per letter of a 15-letter password (three QR codes per letter, 45 total), and the winning password reveals the configured 4-digit code of a physical padlock. The application is stateless on disk by design: game state lives in the process and is snapshotted to an emptyDir; a pod recreation resets the game.

Live: https://halloween.tazlab.net/ — certificate Ready=True, pod 1/1, image tazzo/tazlab-halloween:halloween-9-5577a28….

Source: github.com/tazzo/halloween.tazlab.net (branch main), CI tags halloween-<run>-<sha>.

Manifests (tazlab-k8s)

FilePurpose
infrastructure/operators/halloween/{namespace,kustomization}.yamlNamespace, registered in operators/core
apps/base/halloween/halloween.yamlDeployment + Service + TLS Ingress
apps/base/halloween/middlewares.yamlredirectScheme (permanent)
apps/base/halloween/redirect-ingress.yamlWeb-only redirect, priority 1
apps/base/halloween/certificate.yamlcert-manager Certificate, letsencrypt-issuer
apps/base/halloween/kustomization.yamlBase resources
apps/cluster/halloween/kustomization.yamlPure overlay
clusters/tazlab-k8s/apps-halloween.yamlFlux Kustomization, dependsOn core + configs
infrastructure/automation/halloween/{automation,kustomization}.yamlImageRepository/Policy/UpdateAutomation
infrastructure/instances/homepage/homepage.yamlHomepage tile

No VSO/Vault/Secret; no PVC. Registered in infrastructure/operators/core, infrastructure/cluster-instances and the Flux root.

Workload

FieldValue
Imagetazzo/tazlab-halloween:halloween-<N>-<sha> with Flux setter marker
Port8080 (http), Service 80→http
Replicas1
StrategyRecreate
SecurityUID/GID 10001, no fsGroup, read-only rootfs, drop ALL, seccomp RuntimeDefault
/dataemptyDir 64Mi (game.json/settings.json) — pod-scoped
/tmpemptyDir 64Mi
Startup probesh -c 'touch /data/.write-test && rm -f /data/.write-test', 2s/15
Readiness/liveness/healthz, 5s/2 and 20s/5
Resourcesrequests 100m/128Mi; limits 500m/512Mi
ShutdownpreStop 5s, grace 45s
Uvicornone worker (in-process game state)

emptyDir semantics: a container restart keeps /data; pod recreation, rollout, reschedule or node loss discards it. The master creates a new session; players reload/rescan.

Exposure and TLS

  • Traefik traefik IngressClass, VIP 192.168.1.240.
  • Main Ingress web,websecure + router.tls: "true", host halloween.tazlab.net, TLS secret halloween-tazlab.net-tls.
  • Separate web-only redirect Ingress + redirectScheme, priority 1, ACME-safe.
  • Certificate per host, letsencrypt-issuer / ClusterIssuer, RSA 2048, rotation Always.
  • DNS: halloween.tazlab.net CNAME → tazlab.net → 101.58.6.104.
  • Live redirect: GET → 301, HEAD → 308; HTTPS → 200 valid Let’s Encrypt chain.

Image automation

infrastructure/automation/halloween/automation.yaml, all in flux-system:

  • ImageRepository tazzo/tazlab-halloween, 1m, no secret (public);
  • ImagePolicy ^halloween-(?P<value>[0-9]+)-.*$, extract: '$value', numerical asc;
  • ImageUpdateAutomation to tazlab-k8s/master, path ./apps/base/halloween, Setters.

The first deployment, the scanner/audio fix, the master layout and the password/QR release were delivered automatically: halloween-1 → halloween-2 → halloween-3 → halloween-4 → halloween-5 → halloween-6 → halloween-7 → halloween-8 → halloween-9. No manual tag change.

Runtime behaviour and first-playtest fixes

  • Master-only story: intro, victory, game over, retry and wrong-password narration play on the master. Player welcome is visual only.
  • Master dashboard: viewport-fitted topbar + content + fixed action bar; the player QR is capped at 36vh and only inner lists scroll, so controls are always visible.
  • Player cues: the current player’s phone speaks location hint, riddle and wrong/decoy answers.
  • One-shot scanner: the first decode stops the camera; another scan is blocked until the wrong/decoy cue ends. The server also ignores a duplicate scan of the same code within 3 seconds. This fixes the scanner → repeated wrong voice loop.
  • Password model: full word 15 letters; each position owns 3 codes (main + 2 recovery), 45 total, no decoys. Settings can select a 4–15 length for tests; a 4-letter test uses the first 12 codes. The master never sees the word.
  • Wrong password: the green/red grid persists on the phone with position numbers, and a recovery riddle for the first wrong position starts automatically. The master can send more for the other red positions.
  • Give up: if a code cannot be found, the player (or master) can ask for the next code for the same letter; the turn does not advance. After the second recovery, the master can skip the turn.
  • Padlock code: configured in Settings, hidden behind an eye button; players receive it only on victory.
  • Player password sheet: /download/password-sheet.pdf, a crest sheet with monsters and one empty numbered box per letter (15 or 5). No solution on it.
  • Joining: a confirmation automatically exposes the next QR; there is no add-player button, the player list rows have a stable height, and the trailing unconfirmed QR is discarded at briefing.
  • Password gate: the phone hides the password button and the server rejects attempts until every position is found. The master sees the same green/red feedback as the phone; recovery searches show the password position and highlight that cell. If a position is still wrong after one recovery, the second recovery code is offered; the final attempt cannot be skipped just because a position was recovered once.
  • Recovery feedback: after a wrong word every phone shows a persistent “Parola sbagliata” banner with the wrong positions and the active recovery target; the feedback modal and master panel repeat it. A solved recovery automatically chains the next wrong position to the new current player. When all positions are found the turn shows “Provate la parola” instead of a dead “Preparati”. Placement sheets show the position for recovery codes too.
  • Audio: spoken-cue history is cleared per session/retry, and each hint/riddle has a Ripeti la voce button. Scan beeps are player-targeted; a phone that leaves turn cancels its queue. On a wrong word all phones are muted (silent=true recovery clue), so only the master speaks; the player can tap Ripeti la voce. Chrome/Chromium tests are closed after use; no local browser processes are left running.
  • Victory: the master plays the win narration then an original synthesized victory.mp3 theme. A licensed track can replace it locally; copyrighted audio is not committed.
  • Client logs: /api/client-log keeps a bounded 80-event ring of connection/state revision/audio/scan/replay/error events; the master has a Log telefoni panel. No passwords or lock codes are logged.
  • Settings: compact two-column modal that fits 1024×768 (and 800×600) with no internal scroll; all fields and three printable PDFs visible.
  • The public master and /download/* endpoints are unauthenticated; the host is on every join QR. Accepted for a one-off game.

Verification (2026-10-21)

curl -I http://halloween.tazlab.net/           # 301 GET / 308 HEAD
curl -I https://halloween.tazlab.net/          # 200, valid certificate
curl -s https://halloween.tazlab.net/healthz   # {"ok":true,"phase":"lobby"}
kubectl -n halloween get deploy,pod,ingress,certificate
kubectl -n halloween get pvc                   # empty
kubectl -n flux-system get imagepolicy halloween

Full simulator against production: two players, 5-letter test (15 QR), wrong password, four auto-chained recovery positions, victory; then reset to lobby and restored settings 20/15/0427. The production QR sheet (45 labels, 2 pages), placement sheet and 15-box password sheet all return 200. Production Playwright check: Settings modal 364/364 px (no scroll), 4 fields, 3 PDFs; player 1 confirmation exposed player 2 automatically; cache version 20261022c. Victory MP3 serves audio/mpeg; client-log POST is visible in the master state. Image halloween-9 is live.

Event runbook

  • Freeze the app repo main; set ImageUpdateAutomation.spec.suspend: true in Git; keep tazlab-k8s on master.
  • Verify certificate, health, 45-min master WebSocket, phone camera, image digest.
  • After a pod loss, the master creates a new session; players reload/rescan.
  • After the event, unfreeze and resume the automation.

Rollback

  • After the first deploy, the previously deployed tag is the known-good fallback (halloween-1-eec65ff…, halloween-2-c91765e…, halloween-3-f3bc566…, halloween-4-66bdef6…, halloween-5-7cea6f7…, halloween-6-0d7fa5e…, halloween-7-65c6218…, halloween-8-0af63b0…).
  • Suspend the automation, pin the previous tag in apps/base/halloween/halloween.yaml, reconcile. There is no PVC/PV/Longhorn cleanup; emptyDir disappears with the pod.
  • If the first image is bad: revert app code, rebuild, retest, publish a new tag.

See Also