38 Commits

Author SHA1 Message Date
Josh Creek ca70568fad fix(agones): make the kind gate's Agones lifecycle actually work
Several independent causes, all of which had to be right before the
Fleet could reach Ready.

The supervisor pointed --sdk-base-url at 127.0.0.1:9357, which is the
Agones sidecar's gRPC port; its HTTP surface is 9358, and that is what
AGONES_SDK_HTTP_PORT carries and what agones_sdk.gd reads. An HTTP
client against the gRPC port could never have worked, in kind or in
production.

The supervisor also treated the sidecar's first incomplete /gameserver
response as fatal. The sidecar accepts requests before the controller
populates status.address and status.ports, so this produced a restart
loop precisely during normal Agones startup. It now polls until the
endpoint is assigned or ReadyTimeout elapses.

server_boot.gd started ServerControl and the Agones SDK only under
--allocated-mode, but the kind smoke deliberately strips that flag, so
nothing served the readiness probe and the GameServer could never become
Ready. Lifecycle now keys on AGONES_SDK_HTTP_PORT, which Agones injects
into every managed container, while allocation and roster semantics stay
tied to --allocated-mode. The SDK node is added to the tree
non-deferred, since start_health() creates a Timer immediately.

Fleet: Agones assigns its own SDK service account and masks that token
from the game container while keeping it for the injected sidecar, so
the manifest must not pin serviceAccountName or
automountServiceAccountToken. Godot stores user:// under HOME, so HOME
points at the writable runtime volume to keep the root filesystem
read-only, and fsGroup makes that volume writable for the non-root user.

Namespace: Agones' Dynamic port policy injects a hostPort, which both
the baseline and restricted Pod Security Standards forbid, so the
workload namespace enforces privileged while continuing to audit and
warn against restricted.

NetworkPolicy: the injected sidecar reaches the Kubernetes API over
HTTPS, and NetworkPolicy applies to the whole Pod rather than to the
container whose token was masked.

The kind runner creates the namespace before Helm so Agones can install
its per-namespace SDK RBAC, scopes gameservers.namespaces to it, forces
the allocator and ping Services to ClusterIP because LoadBalancer
ingress never becomes ready in plain kind, and labels the node so the
production Fleet's on-demand/zone constraints are exercised rather than
edited out of the rendered manifest.
2026-09-05 20:50:01 +01:00
Josh Creek a4b362cb01 fix(deploy): supply Steam credentials to the control plane, refresh stale status
The Steam adapter took --steam-publisher-key/--steam-app-id and the
matching env vars, but no manifest supplied them, so a deployed control
plane would have kept sign-in returning 503 even once the App ID from
#15 arrived -- that issue would have unblocked nothing on landing.

Mount them from a new cosmic-clash-steam Secret, into the control-plane
Deployment alone: the publisher key is issued to us, never to a client,
and no other workload (least of all a game server) has any use for it. A
manifest test asserts both the wiring and that the Secret appears in no
other manifest; verified it fails in both directions.

Both keys are optional, so the Deployment still rolls out before the App
ID exists and sign-in simply stays 503.

Also correct task rows this branch made stale: 7.4 (durable ban storage
landed), 8.7 (adapter, bans and secret store landed), 8.39 (cross-replica
fan-out landed), and 8.5's migration range, which stopped at 0013.

Move the branch review into docs/ with a header marking it a point-in-time
artefact -- all thirteen findings are addressed, and its present tense
would otherwise read as current behaviour.

Record gotcha 52: the integration scripts use `docker run --rm`, which
reclaims the container but not its anonymous volume. Sixty-four of them,
~4 GB, accumulated during this session until PostgreSQL stopped starting
-- surfacing only as the script's own readiness timeout, not as a disk
error. That is the real cause behind the "Docker storage exhausted
locally" notes those rows carried.
2026-09-05 12:38:15 +01:00
Josh Creek d40344a2c0 fix(deploy): ship runnable control-plane and matcher workloads
The Kubernetes base deployed a control-plane image the Dockerfile never
built -- cmd/control-plane was absent from the Go build stage and no
target existed -- while the Dockerfile built a matcher image no manifest
ever deployed. Applying the checked-in base therefore could not produce
the advertised topology: one required workload had no repository-defined
artifact, and nothing consumed queued tickets. Tickets could be created
but never became proposals.

Add the production control-plane build and image target, explicitly not
the testkit-api target, which injects a fake login accepting any ticket.
Add casual and ranked matcher Deployments as separate workloads: they
have different match sizes, and separating them keeps a ranked backlog
from delaying casual formation. One replica each -- CreateProposal's
SKIP LOCKED fences make more replicas safe, but they would halve the
candidate pool each worker sees per poll and worsen formation for no
throughput gain at this scale. Their PDB uses maxUnavailable, since
minAvailable against a single replica blocks node drains outright.

Also fix both blocked traffic directions. No ingress policy admitted
UDP/7777 to game-server pods, so an allocated server was unreachable
from the internet under the namespace-wide default deny. And
control-plane ingress admitted only edge-gateway pods, so roster fetch,
registration, connection receipts, shutdown and result submission from
game servers were dropped even inside the cluster, despite their egress
being permitted. The default deny stays.

Manifest tests now assert every required role is deployed, both
playlists are scheduled, every referenced image maps to a real
Dockerfile target, and both traffic directions are permitted. Each was
verified to fail against the defect it covers. The control-plane image
was built and run to confirm the target works.
2026-09-05 10:52:52 +01:00
Josh Creek 5765532409 fix(allocator): publish signed assignment rosters before servers start
The root blocker (issue #14). The worker bound the provider allocation
and stopped. Service.PublishRoster and store.SaveVerifiedAssignmentRoster
both existed, fully tested, with zero non-test callers, and the
production allocator configured neither a roster store nor a signing
key. Nothing ever wrote the assignments table.

The allocated supervisor fetches a non-empty roster before it launches
the game child, so every real allocation failed at that fetch: no match
could reach ASSIGNMENT_READY or accept a player. Existing tests seeded
assignments directly, which is exactly why the missing hand-off went
unnoticed.

The worker now builds one join authorisation per durable participant,
signs each with the active key, and publishes them. Participants are
read through the same query SaveVerifiedAssignmentRoster re-validates
against, so the allocator cannot construct a roster the persistence
boundary would reject. The manifest commits to a digest over the whole
roster, so a server cannot be handed a truncated roster whose surviving
entries are each individually valid.

Persist the provider endpoint on the allocation: it arrived on the
provider response and was never stored, so a worker crashing between
allocating and publishing had no endpoint to recover and would have
stranded the match permanently. Republishing is idempotent, so that
crash now simply retries.

cmd/allocator refuses to start without key material rather than running
an allocator that binds allocations and silently strands every match.
The k8s allocator Deployment mounts the same key set the Fleet does, and
both now take the JSON key map so a rotation can publish several.

New integration test drives the real worker through to the supervisor's
own roster read path without seeding the assignments table. Verified it
fails with "assignments = 0, want 2" when the publish step is removed.
2026-09-05 10:42:31 +01:00
Josh Creek ce17a45afb feat(multiplayer): alert on workload server-mutation conflicts
Closes the 'live duplicate/conflict alerting also remains' gap noted
in §8.10: a durable domain.ErrConflict/ErrResultConflict rejection on
/v1/servers/{id}/{register,connect,disconnect,shutdown,result} was
already logged as a structured 'conflict' stage event, but had no
Prometheus signal distinct from the generic 4xx-class counter, which
also catches ordinary client noise (malformed bodies, expired
tokens). A real duplicate registration, raced reconnect, or replayed
result would have been invisible to alerting until someone went
looking through logs.

observability.Metrics gains ObserveServerConflict(kind), a bounded
counter keyed to serverMutation's own five routes (an unrecognized
kind folds into "other", so a caller mistake can't grow the label
set), exported as cosmic_clash_api_server_conflicts_total. Wired at
each of serverMutation's four conflict branches in server/api/service.go.
deploy/observability/prometheus-rules.yaml adds
CosmicClashControlPlaneServerConflicts, mirroring the existing
allocator quota-denial alert shape, firing on >3 conflicts of one
kind in 15 minutes.

Verified: go build/vet/test -race clean across every server package;
new unit tests cover per-kind counting, the bounded 'other' fallback,
the counter's absence until first observed, and a nil-receiver no-op;
a service-level test proves a real register conflict is exported
through the live /metrics endpoint. scripts/verify_observability_manifests.py
passes against the edited rules file.

Remaining, and explicitly out of scope here: this alert has only been
validated statically, never against a live Prometheus/Alertmanager
firing on real traffic — that requires the same live cluster this
sandbox has never had.
2026-09-04 17:24:09 +01:00
Josh Creek ea1c65acfb fix(multiplayer): bound workload credential lifetime 2026-09-03 21:27:26 +01:00
Josh Creek 25cf1e0cfa fix(multiplayer): keep abandonment maintenance available 2026-09-03 21:07:50 +01:00
Josh Creek bcc2639a33 fix(multiplayer): deploy reconnect abandonment maintenance 2026-09-03 21:05:58 +01:00
Josh Creek aa446cfbfe fix(multiplayer): reconcile authoritative initial connections 2026-09-03 00:02:04 +01:00
Josh Creek 51f8008a38 fix(multiplayer): gate API readiness on database 2026-09-02 19:14:21 +01:00
Josh Creek cbefa86c5c fix(multiplayer): report allocator readiness 2026-09-02 19:11:01 +01:00
Josh Creek 6ebd6e59c1 fix(multiplayer): resolve client IP behind proxies 2026-09-02 19:08:34 +01:00
Josh Creek 670466dbd7 fix(multiplayer): authenticate Agones Kubernetes API 2026-09-02 18:58:57 +01:00
Josh Creek 84372204fd feat(multiplayer): persist ranked arena allocations 2026-09-01 21:04:55 +01:00
Josh Creek b4d2dd8a9f feat(multiplayer): protect control-plane rollouts 2026-09-01 19:23:22 +01:00
Josh Creek bb9f25ee8d fix(multiplayer): wire control-plane runtime secrets 2026-09-01 19:22:06 +01:00
Josh Creek 81346ac149 feat(multiplayer): harden control-plane availability 2026-09-01 19:20:53 +01:00
Josh Creek aa97259165 feat(multiplayer): declare control-plane rate limits 2026-09-01 19:17:09 +01:00
Josh Creek d04523accd feat(multiplayer): package observability resources 2026-09-01 19:10:10 +01:00
Josh Creek e78f805c92 feat(multiplayer): spread allocator replicas 2026-09-01 18:52:15 +01:00
Josh Creek 34cc994598 feat(multiplayer): protect allocator availability 2026-09-01 18:51:18 +01:00
Josh Creek 398d6ade61 feat(multiplayer): harden allocator rollout 2026-09-01 18:50:09 +01:00
Josh Creek 2e1c010fa2 feat(multiplayer): deploy allocator role 2026-09-01 18:48:51 +01:00
Josh Creek 95e82cc719 feat(multiplayer): wire allocator metrics discovery 2026-09-01 18:46:31 +01:00
Josh Creek 9a72dd5eab feat(multiplayer): expose allocator quota metrics 2026-09-01 18:43:55 +01:00
Josh Creek 8129bb2571 fix(multiplayer): correct allocated fleet entrypoint 2026-09-01 17:47:15 +01:00
Josh Creek 533ac1afab ops(multiplayer): wire Prometheus service discovery 2026-09-01 17:24:21 +01:00
Josh Creek 67da422ea6 ops(multiplayer): add control-plane alert rules 2026-09-01 17:22:09 +01:00
Josh Creek 334d8a26ac feat(multiplayer): enforce allocated initial connect policy 2026-09-01 16:36:26 +01:00
Josh Creek 498f219ab9 fix(multiplayer): align NA fleet region runtime 2026-09-01 16:01:38 +01:00
Josh Creek c0861bfcad feat(multiplayer): wire allocated fleet runtime 2026-09-01 16:00:03 +01:00
Josh Creek c0a1ef94f0 fix(server): force line-buffered stdout so a detached server actually logs
Godot's stdout is fully (block) buffered whenever it isn't attached to
a TTY -- true of every real deployment path this repo documents:
'docker run -d' (Docker's log driver presents a pipe), a plain
'docker run' without -d, and systemd's journal capture (also a pipe).
Confirmed directly, not from the existing gotcha note alone: a real
'docker run -d' container sat for 20+ seconds with 'docker logs'
showing nothing at all -- not even the startup line -- while the
process was confirmed alive and running (ps aux inside the container).
'docker stop' then killed it via SIGTERM (Godot has no SIGTERM hook)
without ever flushing that buffered output, losing it permanently
rather than merely delaying it.

This affects the already-shipped community server path today, not
just the not-yet-built Agones fleet path multiplayer-next.md's task
8.28 gotcha originally flagged this for -- SERVER.md's Docker AND
native-systemd instructions both route through this exact launcher
script, and journald's capture has the same non-TTY-pipe buffering
problem docker logs does.

Wrap the exec in 'stdbuf -oL -eL' (LD_PRELOAD-based line buffering,
touches no binary) when available, falling back to the unwrapped exec
otherwise so a minimal image without GNU coreutils still starts.
Re-verified the same failing scenario against the actual launcher
script in a real image: the startup line now appears within 3s of a
genuinely detached 'docker run -d'. Re-ran the full make verify-phase6
gate end to end afterward to confirm no regression: both arenas
rotated, both clients observed both goals, clean teardown.
2026-09-01 13:50:09 +01:00
Josh Creek 52a96de8d6 feat: protect game fleet during voluntary disruption 2026-08-31 22:01:18 +01:00
Josh Creek 72d4a604c2 feat: spread game fleet across on-demand zones 2026-08-31 21:59:51 +01:00
Josh Creek 90126a4be7 feat: add fleet autoscaling baseline 2026-08-31 21:58:42 +01:00
Josh Creek 0551fb0b1f feat: add regional Agones fleet overlays 2026-08-31 21:51:37 +01:00
Josh Creek 88b5ffedb2 feat: add Kubernetes multiplayer security baseline 2026-08-31 21:21:55 +01:00
Josh Creek f2b72394de feat(server): complete phase 6 local verification 2026-08-21 18:38:30 +01:00