Files
CosmicClash/deploy/k8s/base/allocator-deployment.yaml
T
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

130 lines
4.3 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: allocator
namespace: cosmic-clash
labels:
app.kubernetes.io/name: allocator
app.kubernetes.io/component: allocator
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app.kubernetes.io/name: allocator
template:
metadata:
labels:
app.kubernetes.io/name: allocator
app.kubernetes.io/component: allocator
spec:
terminationGracePeriodSeconds: 10
serviceAccountName: allocator
# This role calls Agones CRDs through the Kubernetes API. The client
# rereads the short-lived projected token on every request.
automountServiceAccountToken: true
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: allocator
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/name: allocator
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: allocator
image: ghcr.io/cosmic-clash/allocator@sha256:0000000000000000000000000000000000000000000000000000000000000000
args:
- --dsn=$(COSMIC_CLASH_POSTGRES_DSN)
- --agones-url=https://kubernetes.default.svc
- --agones-namespace=cosmic-clash
- --provider-timeout=10s
- --readiness-max-stale=30s
- --workload-token-ttl=2h
- --metrics-addr=:9091
# Without these the allocator binds allocations but never publishes
# an assignment roster, and no allocated match can become joinable.
# The same key material is mounted into game servers by fleet.yaml.
- --join-authorisations-key-file=/run/secrets/cosmic-clash/join-signing-keys.json
- --join-authorisations-key-id=$(COSMIC_CLASH_JOIN_SIGNING_KEY_ID)
ports:
- name: metrics
containerPort: 9091
volumeMounts:
- name: join-signing-keys
mountPath: /run/secrets/cosmic-clash
readOnly: true
readinessProbe:
httpGet:
path: /readyz
port: metrics
initialDelaySeconds: 2
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1
memory: 512Mi
env:
- name: COSMIC_CLASH_POSTGRES_DSN
valueFrom:
secretKeyRef:
name: cosmic-clash-database
key: dsn
- name: COSMIC_CLASH_WORKLOAD_SECRET
valueFrom:
secretKeyRef:
name: cosmic-clash-workload
key: secret
# Rotation: publish the new key in the Secret everywhere first,
# then move this ID to it, then drop the retired key once no live
# match can still reference it.
- name: COSMIC_CLASH_JOIN_SIGNING_KEY_ID
valueFrom:
secretKeyRef:
name: cosmic-clash-game-server
key: join-signing-key-id
volumes:
- name: join-signing-keys
secret:
secretName: cosmic-clash-game-server
defaultMode: 0400
items:
- key: join-signing-keys.json
path: join-signing-keys.json