Commit Graph

219 Commits

Author SHA1 Message Date
Josh Creek 9a72dd5eab feat(multiplayer): expose allocator quota metrics 2026-09-01 18:43:55 +01:00
Josh Creek 72e8d27633 feat(multiplayer): add shared allocation quota 2026-09-01 18:38:06 +01:00
Josh Creek 55706ba9ea test(multiplayer): cover matcher load boundary 2026-09-01 18:32:40 +01:00
Josh Creek 71935e61b5 test(multiplayer): add chaos recovery smoke 2026-09-01 18:30:09 +01:00
Josh Creek 181a928c87 feat(multiplayer): add regional allocation budget 2026-09-01 18:25:25 +01:00
Josh Creek c4c2ada1f6 test(multiplayer): add api load gate 2026-09-01 18:23:22 +01:00
Josh Creek 76c1c3d600 fix(multiplayer): publish stalled allocation recovery 2026-09-01 18:19:28 +01:00
Josh Creek 7c044b7094 test(multiplayer): harden agones allocation gate 2026-09-01 18:16:54 +01:00
Josh Creek 3317574bf2 fix(multiplayer): fence recovered allocation tuples 2026-09-01 18:06:56 +01:00
Josh Creek bf396afcaf fix(multiplayer): recover ambiguous agones allocations 2026-09-01 18:05:43 +01:00
Josh Creek b72a7cf843 test(multiplayer): cover compose allocator binding 2026-09-01 18:02:05 +01:00
Josh Creek 063dff463d test(multiplayer): drive compose proposal orchestration 2026-09-01 17:58:37 +01:00
Josh Creek 00d6b1edd3 test(multiplayer): cover compose queue lifecycle 2026-09-01 17:56:47 +01:00
Josh Creek b33f681ffa test(multiplayer): exercise compose supervisor drain 2026-09-01 17:54:16 +01:00
Josh Creek b1783abdce test(multiplayer): isolate agones lifecycle smoke 2026-09-01 17:52:02 +01:00
Josh Creek 88eb510dc3 test(multiplayer): add allocated compose smoke flow 2026-09-01 17:50:52 +01:00
Josh Creek 8129bb2571 fix(multiplayer): correct allocated fleet entrypoint 2026-09-01 17:47:15 +01:00
Josh Creek 7170400f49 test(multiplayer): verify observability manifests locally 2026-09-01 17:34:15 +01:00
Josh Creek 0bf33e7fe8 feat(multiplayer): export queryable API latency histograms 2026-09-01 17:21:13 +01:00
Josh Creek d0c96422b2 fix(multiplayer): harden observability redaction 2026-09-01 17:15:44 +01:00
Josh Creek f1b8366531 feat(multiplayer): export bounded API metrics 2026-09-01 17:06:38 +01:00
Josh Creek 9ae01ecc5a fix(multiplayer): propagate allocated playlist 2026-09-01 17:00:37 +01:00
Josh Creek 8bd1455a2c feat(multiplayer): propagate allocated launch configuration 2026-09-01 16:58:09 +01:00
Josh Creek 315c524c42 feat(multiplayer): propagate allocated launch configuration 2026-09-01 16:57:27 +01:00
Josh Creek 75cb8faac4 feat(multiplayer): acknowledge supervisor shutdown 2026-09-01 16:54:40 +01:00
Josh Creek cc12260225 feat(multiplayer): expose server shutdown acknowledgement 2026-09-01 16:50:26 +01:00
Josh Creek 8c2dc66c7c fix(multiplayer): release innocent no-show participants 2026-09-01 16:39:46 +01:00
Josh Creek d15d16d593 feat(multiplayer): publish allocation state events 2026-09-01 16:30:35 +01:00
Josh Creek ec82367c50 fix(multiplayer): close initial connect sweep rows 2026-09-01 16:28:08 +01:00
Josh Creek a6d2bdf8bd feat(multiplayer): sweep initial connect outcomes 2026-09-01 16:27:15 +01:00
Josh Creek bae7458668 feat(multiplayer): apply initial connect outcomes durably 2026-09-01 16:26:06 +01:00
Josh Creek b7642ad2be feat(multiplayer): model initial connect outcomes 2026-09-01 16:21:40 +01:00
Josh Creek a439a1059b fix(multiplayer): fence roster topology at persistence 2026-09-01 16:20:01 +01:00
Josh Creek 5812386676 feat(observability): log authenticated read routes 2026-09-01 16:14:20 +01:00
Josh Creek 12b9712a8d test(multiplayer): lock allocated launch overrides 2026-09-01 16:09:57 +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 68e76b5feb feat(multiplayer): deliver allocated server rosters 2026-09-01 15:54:07 +01:00
Josh Creek 863cf61f1a test(multiplayer): verify result websocket fanout 2026-09-01 15:47:08 +01:00
Josh Creek 802e5fc96f test(multiplayer): fence conflicting result races 2026-09-01 15:44:47 +01:00
Josh Creek f3b56f70e3 feat(multiplayer): dispatch completed result events 2026-09-01 15:43:32 +01:00
Josh Creek 9ec707674a test(multiplayer): verify allocated supervisor registration 2026-09-01 15:39:28 +01:00
Josh Creek 207ab47866 test(multiplayer): verify allocator worker lifecycle 2026-09-01 15:34:51 +01:00
Josh Creek 04b5d3b29d feat(multiplayer): verify ranked profile delivery 2026-09-01 15:29:49 +01:00
Josh Creek d081a72b9a feat(multiplayer): wire ranked profile runtime policy 2026-09-01 15:28:37 +01:00
Josh Creek 79a6092b28 feat(multiplayer): wire production proposal outbox delivery 2026-09-01 15:21:49 +01:00
Josh Creek aa93aeec95 test(multiplayer): verify two-player proposal round trip 2026-09-01 15:17:53 +01:00
Josh Creek 544f76c502 feat(multiplayer): deliver signed workload tokens via Agones allocation annotation
Closes the remaining gap the previous two commits left open: WorkloadVerify
itself worked, but nothing minted a real token at allocation time or handed
it to a running pod, so it had no real caller yet.

agones.Client gains WorkloadSecret/WorkloadTokenTTL. When set, Allocate
mints a signed workload token for the allocation (allocation_id is known at
request-construction time, before Agones has picked a server -- see the
previous commit for why that's the only identifier the token can bind) and
requests it as a third cosmic-clash.io/workload-token annotation, alongside
the existing match-id/allocation-id ones. Left unset (the default), Allocate
requests no such annotation, so a deployment not yet using this path is
unaffected. cmd/allocator wires it from a new --workload-secret /
COSMIC_CLASH_WORKLOAD_SECRET flag (must match cmd/control-plane's own), with
a startup warning if left unset.

supervisor.Supervisor.workloadToken() resolves the bearer credential for
control-plane registration: an explicitly configured --workload-token-path
always wins (kept for a future Kubernetes-projected-JWT WorkloadVerify path,
not yet wired server-side), otherwise it falls back to the
cosmic-clash.io/workload-token annotation on the allocated GameServer --
the same annotation-fallback pattern matchID already used for
cosmic-clash.io/match-id. WorkloadTokenPath is accordingly no longer
required at construction time when ControlPlaneURL is set.

Verified: new agones test proves the annotation is requested (and parses/
verifies against the same secret, naming the right allocation) when
WorkloadSecret is configured, and that it's absent when it isn't; new
supervisor tests prove the annotation-sourced token is what's actually sent
as the Authorization bearer, and that Start fails closed with neither a
configured path nor an annotation present. Full
`go build ./... && go vet ./... && gofmt -l . && go test ./... -race` and
`go test -tags integration ./... -race` both clean.
2026-09-01 14:57:45 +01:00
Josh Creek d588898f5d fix(multiplayer): bind signed workload tokens to allocation_id only
The just-landed signed workload token embedded (allocation_id, match_id,
server_id) as claims. That doesn't actually work for its intended delivery
channel: the token is meant to be requested as a GameServerAllocation
annotation in the SAME request that asks Agones to pick a server, so at mint
time the allocator knows allocation_id (it generates it) but not yet which
server_id Agones will return -- server_id only exists in Agones's response,
after the annotation request has already been sent. Embedding it was simply
not possible for the real caller this was built for; only the (allocator ->
signed_token) unit tests and hand-constructed integration tests happened to
supply it directly, masking the gap.

Fixes it by having the token bind only allocation_id (the one identifier
actually known at mint time) plus expiry. match_id/server_id are resolved at
verify time from the durable allocations table via the new
store.AllocationBindingByAllocationID, keyed by allocation_id -- which the
allocator already records immediately after Agones responds. This is
strictly stronger, not just a workaround: a caller can no longer claim any
match/server pairing at all, even one that happens to be internally
consistent -- the binding returned is entirely durable-record-derived.

Verified: server/workload's unit tests updated for the new two-field claim
shape; server/api's Postgres integration suite gains
TestWorkloadVerifierFromSignedTokenNeverTrustsCallerSuppliedBinding (two
distinct real allocations each resolve to their own, and only their own,
match/server pairing) replacing the now-inapplicable mismatched-triple test.
Full `go build ./... && go vet ./... && gofmt -l . && go test ./... -race`
and `go test -tags integration ./... -race` both clean; the api integration
suite re-run 3x clean against a live postgres:17-alpine container.
2026-09-01 14:54:38 +01:00
Josh Creek 520613aab0 feat(multiplayer): implement WorkloadVerify without a Kubernetes trust boundary
WorkloadVerify (api.Service.WorkloadVerify) was permanently unwired: both
/v1/servers/{id}/register and /v1/servers/{id}/result always 503, because
the only design considered so far was verifying a Kubernetes-projected
service-account JWT (server/workload/jwt.go), which needs a live cluster's
TokenReview/JWKS endpoint to validate against safely -- something this
sandbox cannot do without guessing at a trust boundary.

The API layer doesn't actually require that specific mechanism. serverMutation
only compares WorkloadBinding.ServerID and .MatchID (server/api/service.go);
AdvanceServerRegistration only uses .MatchID/.ServerID/.AllocationID. Nothing
downstream needs Namespace/ServiceAcct/PodUID/GameServerUID populated.

This adds a self-contained alternative: a short-lived, HMAC-signed token the
control plane mints and verifies with a secret only it holds (server/workload/
signed_token.go), the same trust model domain.SessionStore already uses for
player sessions elsewhere in this codebase. It needs no cluster to verify --
signature + expiry is fully self-contained and unit-testable.

The design's soundness rests on the delivery channel, not the crypto: the
token is meant to reach the allocated GameServer via the same Agones
GameServerAllocation annotation channel allocation.go already uses for
match-id/allocation-id, readable only by that pod's own local SDK sidecar. A
caller presenting this token has already proven, via that channel, that it is
the pod Agones allocated. (Wiring the actual annotation delivery -- extending
agones.Client.Allocate and the supervisor's token source -- is a separate,
follow-up change; this commit lands the verification core it depends on.)

store.AllocationBindingStillValid adds defense-in-depth on top of signature
and expiry: it cross-checks the token's claims against the durable
allocations table (append-only, never leaves 'ALLOCATED'), so a validly-signed
token naming an allocation that was never recorded -- or a real allocation id
paired with a mismatched match/server -- is still rejected.

api.WorkloadVerifierFromSignedToken wires the two together and is now plugged
into cmd/control-plane (new --workload-secret / COSMIC_CLASH_WORKLOAD_SECRET
flag; a startup warning is logged if it's left unset, since the route then
stays 503 exactly as before) and cmd/testkit-api (fixed test secret, since
that binary is test-only already).

Verified: new unit tests in server/workload (signature tamper, wrong secret,
expiry boundary, malformed input) and a new Postgres integration suite in
server/api (real allocation row, real signed token, acceptance / unknown-
allocation rejection / mismatched-triple rejection / the previously-503
Service.WorkloadVerify field itself) -- both run clean with -race across
multiple passes against a live postgres:17-alpine container. Full
`go build ./... && go vet ./... && gofmt -l . && go test ./... -race` and
`go test -tags integration ./... -race` both clean.
2026-09-01 14:51:21 +01:00