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.
This commit is contained in:
Josh Creek
2026-09-05 20:50:01 +01:00
parent 8aa4af3a3a
commit ca70568fad
11 changed files with 159 additions and 40 deletions
+22 -3
View File
@@ -34,6 +34,8 @@ dump_cluster_state() {
for ns in agones-system cosmic-clash; do
echo "=== namespace ${ns}: pods ===" >&2
kubectl -n "$ns" get pods -o wide >&2 2>&1 || true
echo "=== namespace ${ns}: services ===" >&2
kubectl -n "$ns" get services -o wide >&2 2>&1 || true
# Events explain scheduling/image/probe failures that pod status alone
# does not: FailedScheduling, ImagePullBackOff, readiness probe errors.
echo "=== namespace ${ns}: recent events ===" >&2
@@ -98,15 +100,22 @@ fi
kind create cluster --name "$cluster_name" --image "$kind_node_image" --wait 120s
kind load docker-image "$game_server_image" --name "$cluster_name"
# Agones creates its SDK service account and namespaced RBAC in each configured
# GameServer namespace. The namespace must therefore exist before Helm runs.
kubectl apply -f deploy/k8s/base/namespace.yaml
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 both its
# controller and extensions pods, 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.
# cluster before the Fleet can be exercised. Its allocator and ping Services
# also default to LoadBalancer, whose ingress never becomes ready in plain kind.
# These are smoke-only bounds; production sizing and exposure remain
# deployment-owned.
helm upgrade --install agones agones/agones \
--namespace agones-system --create-namespace \
--version "$agones_version" \
--set 'gameservers.namespaces[0]=cosmic-clash' \
--set agones.crds.cleanup.enabled=true \
--set agones.controller.replicas=1 \
--set agones.controller.resources.requests.ephemeral-storage=128Mi \
@@ -115,6 +124,9 @@ helm upgrade --install agones agones/agones \
--set agones.extensions.resources.requests.ephemeral-storage=128Mi \
--set agones.extensions.resources.limits.ephemeral-storage=512Mi \
--set agones.allocator.replicas=1 \
--set agones.allocator.service.serviceType=ClusterIP \
--set agones.ping.http.serviceType=ClusterIP \
--set agones.ping.udp.serviceType=ClusterIP \
--wait --timeout 5m
kubectl wait --for=condition=available deployment/agones-controller \
@@ -122,6 +134,14 @@ kubectl wait --for=condition=available deployment/agones-controller \
kubectl wait --for=condition=available deployment/agones-allocator \
-n agones-system --timeout=180s
# The production Fleet only schedules on explicitly on-demand, zoned nodes.
# Give the disposable node equivalent labels so this gate exercises those
# constraints instead of rewriting them out of the rendered Fleet.
kubectl label nodes --all \
cosmic-clash.io/capacity-type=on-demand \
topology.kubernetes.io/zone=kind-smoke \
--overwrite
# 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
@@ -139,7 +159,6 @@ sed -e "s|ghcr.io/cosmic-clash/game-server@sha256:${zero_digest}|$game_server_im
-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=="}' \