From b33f681ffa87302bb1808eb1108b772eac3bbe88 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:54:16 +0100 Subject: [PATCH] test(multiplayer): exercise compose supervisor drain --- compose.allocated-smoke.yml | 30 ++++++++++++++++ scripts/verify_allocated_compose.sh | 42 ++++++++++++++++++++--- server/security/test_compose_manifests.py | 3 ++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/compose.allocated-smoke.yml b/compose.allocated-smoke.yml index 523d8a78..73b550ed 100644 --- a/compose.allocated-smoke.yml +++ b/compose.allocated-smoke.yml @@ -24,3 +24,33 @@ services: condition: service_healthy ports: - "18080:8080" + + game-server: + build: + context: . + target: game-server + command: + - --drain-url=http://127.0.0.1:7780/drain + - --drain-token-env=COSMIC_CLASH_DRAIN_TOKEN + - --drain-grace=10s + - -- + - /opt/cosmic-clash/CosmicClashServer.x86_64 + - --port=31001 + - --allocated-mode + - --match-id=compose-match + - --server-id=compose-server + - --playlist-version=casual + - --playlist=casual + - --client-build=build-1 + - --assignment-expiry-unix=4102444800 + - --server-image-digest=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - --transport=enet + - --region=EU + - --join-authorisations-file=/run/cosmic-clash/join-roster.json + - --join-authorisations-key-file=/run/secrets/cosmic-clash/join-signing-key + - --readiness-port=7780 + environment: + COSMIC_CLASH_DRAIN_TOKEN: compose-drain-token + volumes: + - ${COMPOSE_SMOKE_DIR:-/tmp/cosmic-clash-allocated-smoke}/join-roster.json:/run/cosmic-clash/join-roster.json:ro + - ${COMPOSE_SMOKE_DIR:-/tmp/cosmic-clash-allocated-smoke}/join-signing-key:/run/secrets/cosmic-clash/join-signing-key:ro diff --git a/scripts/verify_allocated_compose.sh b/scripts/verify_allocated_compose.sh index 729316d0..a4e676f1 100755 --- a/scripts/verify_allocated_compose.sh +++ b/scripts/verify_allocated_compose.sh @@ -8,6 +8,7 @@ compose_file="$root_dir/compose.allocated-smoke.yml" project="${COMPOSE_PROJECT_NAME:-cosmic-clash-allocated-smoke}" api_url="http://127.0.0.1:18080" secret="compose-workload-secret" +smoke_dir="${COMPOSE_SMOKE_DIR:-/tmp/cosmic-clash-allocated-smoke}" compose=(docker compose -p "$project" -f "$compose_file") cleanup() { @@ -20,9 +21,36 @@ trap cleanup EXIT command -v docker >/dev/null 2>&1 || { echo "Docker is required for 8.48" >&2; exit 2; } docker info >/dev/null 2>&1 || { echo "A running Docker daemon is required for 8.48" >&2; exit 2; } +mkdir -p "$smoke_dir" +python3 - "$smoke_dir" <<'PY' +import base64, hashlib, hmac, json, pathlib, sys, time + +directory = pathlib.Path(sys.argv[1]) +key = b"compose-join-signing-key" +expires = "2099-12-31T00:00:00Z" +fields = ["compose-match", "compose-server", "compose-player", "compose-steam", "0", "0", "v1", "1", expires] +canonical = b"\0".join(field.encode() for field in fields) +signature = base64.urlsafe_b64encode(hmac.new(key, canonical, hashlib.sha256).digest()).rstrip(b"=").decode() +envelope = {"Authorisation": {"MatchID": fields[0], "ServerID": fields[1], "PlayerID": fields[2], "SteamID": fields[3], "Slot": 0, "Team": 0, "Protocol": fields[6], "Generation": 1, "ExpiresAt": expires}, "Signature": signature} +(directory / "join-signing-key").write_bytes(key) +(directory / "join-roster.json").write_text(json.dumps([base64.urlsafe_b64encode(json.dumps(envelope, separators=(",", ":")).encode()).rstrip(b"=").decode()]) + "\n") +PY + "${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true "${compose[@]}" up -d --build +for attempt in $(seq 1 60); do + if "${compose[@]}" logs game-server 2>/dev/null | grep -q '"event":"server_started"'; then + break + fi + if [[ "$attempt" == 60 ]]; then + "${compose[@]}" logs >&2 + echo "allocated Compose game server did not become ready" >&2 + exit 1 + fi + sleep 1 +done + for attempt in $(seq 1 60); do if curl -fsS "$api_url/healthz" >/dev/null 2>&1; then break @@ -81,9 +109,15 @@ curl -fsS -o /dev/null -w '%{http_code}' \ -H 'Content-Type: application/json' -d '{"reason":"server_draining"}' | grep -qx 204 "${compose[@]}" exec -T database psql -At -U cosmic_clash_test -d cosmic_clash_test -c "SELECT count(*) FROM audit_events WHERE action = 'SERVER_SHUTDOWN' AND aggregate_id = 'compose-match'" | grep -qx 1 -"${compose[@]}" stop -t 10 control-plane >/dev/null -if "${compose[@]}" ps --status running --services | grep -qx control-plane; then - echo "control-plane did not stop cleanly" >&2 + +"${compose[@]}" stop -t 12 game-server >/dev/null +if "${compose[@]}" ps --status running --services | grep -qx game-server; then + echo "allocated game-server did not stop after supervisor drain" >&2 exit 1 fi -echo "8.48 PASS: allocated Compose HTTP result/retry/shutdown flow completed" +if ! "${compose[@]}" logs game-server | grep -q '"event":"server_draining"'; then + echo "allocated game-server did not record a drain request" >&2 + exit 1 +fi +"${compose[@]}" stop -t 10 control-plane >/dev/null +echo "8.48 PASS: allocated Compose HTTP result/retry/shutdown and supervisor drain completed" diff --git a/server/security/test_compose_manifests.py b/server/security/test_compose_manifests.py index a17af047..3fade56f 100644 --- a/server/security/test_compose_manifests.py +++ b/server/security/test_compose_manifests.py @@ -10,6 +10,9 @@ class ComposeManifestTest(unittest.TestCase): allocated = (ROOT / "compose.allocated-smoke.yml").read_text() legacy = (ROOT / "compose.phase6-smoke.yml").read_text() self.assertIn("target: testkit-api", allocated) + self.assertIn("target: game-server", allocated) + self.assertIn("--drain-url=http://127.0.0.1:7780/drain", allocated) + self.assertIn("--allocated-mode", allocated) self.assertIn('"18080:8080"', allocated) self.assertNotIn("compose.phase6-smoke.yml", allocated) self.assertNotIn("18080:8080", legacy)