Files
CosmicClash/scripts/verify_kind_agones.sh
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

110 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Disposable integration gate for multiplayer-next.md §8.49. This deliberately
# does not touch an existing cluster: kind creates an isolated cluster and the
# EXIT trap removes only that named cluster.
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$root_dir"
cluster_name="${KIND_CLUSTER_NAME:-cosmic-clash-agones-smoke}"
agones_version="${AGONES_VERSION:-1.49.0}"
game_server_image="${GAME_SERVER_IMAGE:-cosmic-clash-game-server:kind}"
kind_node_image="${KIND_NODE_IMAGE:-kindest/node:v1.33.1}"
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/cosmic-clash-agones.XXXXXX")"
cleanup() {
local status=$?
kind delete cluster --name "$cluster_name" >/dev/null 2>&1 || true
rm -rf "$work_dir"
exit "$status"
}
trap cleanup EXIT
for tool in docker kind kubectl helm; do
command -v "$tool" >/dev/null 2>&1 || {
echo "8.49 requires '$tool'; install Docker, kind, kubectl, and Helm to run the disposable gate" >&2
exit 2
}
done
if ! docker info >/dev/null 2>&1; then
echo "8.49 requires a running Docker daemon" >&2
exit 2
fi
kind delete cluster --name "$cluster_name" >/dev/null 2>&1 || true
if ! docker image inspect "$game_server_image" >/dev/null 2>&1; then
echo "Building $game_server_image from the pinned game-server target"
docker build --target game-server -t "$game_server_image" .
fi
kind create cluster --name "$cluster_name" --image "$kind_node_image" --wait 120s
kind load docker-image "$game_server_image" --name "$cluster_name"
helm repo add agones https://agones.dev/chart/stable >/dev/null
helm repo update >/dev/null
# Agones 1.49 otherwise requests 10,100 MiB of ephemeral storage for its
# extensions pod, which exceeds a default single-node kind cluster before the
# Fleet can be exercised. These are smoke-only bounds; production resource
# sizing remains deployment-owned.
helm upgrade --install agones agones/agones \
--namespace agones-system --create-namespace \
--version "$agones_version" \
--set agones.crds.cleanup.enabled=true \
--set agones.controller.replicas=1 \
--set agones.extensions.replicas=1 \
--set agones.extensions.resources.requests.ephemeral-storage=128Mi \
--set agones.extensions.resources.limits.ephemeral-storage=512Mi \
--set agones.allocator.replicas=1 \
--wait --timeout 5m
kubectl wait --for=condition=available deployment/agones-controller \
-n agones-system --timeout=180s
kubectl wait --for=condition=available deployment/agones-allocator \
-n agones-system --timeout=180s
# The base Fleet intentionally carries a release-time digest placeholder. For
# this isolated run only, replace that exact placeholder with the image loaded
# into kind. No repository manifest is modified and no mutable image is used
# outside the disposable cluster.
#
# This runner is intentionally an Agones lifecycle smoke, not a substitute for
# the production control-plane gate: there is no PostgreSQL/API/roster backend
# in this disposable cluster. Disable only those production-only child paths so
# the real supervisor can validate the assigned endpoint, launch the exported
# server, and call the Agones SDK Ready endpoint.
zero_digest="$(printf '0%.0s' {1..64})"
sed -e "s|ghcr.io/cosmic-clash/game-server@sha256:${zero_digest}|$game_server_image|" \
-e 's|--control-plane-url=http://control-plane.cosmic-clash.svc.cluster.local:8080|--control-plane-url=|' \
-e '/- --roster-path=\/run\/cosmic-clash\/join-roster.json/d' \
-e '/- --allocated-mode$/d' \
deploy/k8s/base/fleet.yaml > "$work_dir/fleet.yaml"
kubectl apply -f deploy/k8s/base/namespace.yaml
kubectl -n cosmic-clash create secret generic cosmic-clash-game-server \
--from-literal=drain-token=kind-smoke-drain-token \
--from-literal=join-signing-keys.json='{"kind-smoke-key":"a2luZC1zbW9rZS1zaWduaW5nLWtleQ=="}' \
--from-literal=join-signing-key-id=kind-smoke-key \
--dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f deploy/k8s/base/service-accounts.yaml
kubectl apply -f "$work_dir/fleet.yaml"
kubectl wait --for=jsonpath='{.status.ready}'=2 \
fleet/cosmic-clash-game -n cosmic-clash --timeout=5m
cat > "$work_dir/allocation.yaml" <<'EOF'
apiVersion: allocation.agones.dev/v1
kind: GameServerAllocation
metadata:
generateName: cosmic-clash-smoke-
namespace: cosmic-clash
spec:
fleet:
name: cosmic-clash-game
EOF
kubectl create -f "$work_dir/allocation.yaml" -o json > "$work_dir/allocation.json"
python3 scripts/verify_agones_allocation_response.py "$work_dir/allocation.json"