diff --git a/deploy/k8s/base/control-plane-deployment.yaml b/deploy/k8s/base/control-plane-deployment.yaml index 5a88635c..5e8012cd 100644 --- a/deploy/k8s/base/control-plane-deployment.yaml +++ b/deploy/k8s/base/control-plane-deployment.yaml @@ -2,6 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: control-plane + namespace: cosmic-clash labels: app.kubernetes.io/name: control-plane spec: @@ -56,4 +57,3 @@ spec: secretKeyRef: name: cosmic-clash-steam key: publisher-key - diff --git a/deploy/k8s/base/fleet.yaml b/deploy/k8s/base/fleet.yaml new file mode 100644 index 00000000..a4b84bfe --- /dev/null +++ b/deploy/k8s/base/fleet.yaml @@ -0,0 +1,55 @@ +apiVersion: agones.dev/v1 +kind: Fleet +metadata: + name: cosmic-clash-game + namespace: cosmic-clash + labels: + app.kubernetes.io/name: game-fleet +spec: + replicas: 2 + strategy: + type: RollingUpdate + template: + metadata: + labels: + app.kubernetes.io/name: game-server + cosmic-clash.io/region: EU + cosmic-clash.io/build: build-1 + cosmic-clash.io/protocol: "1" + cosmic-clash.io/transport: enet + spec: + ports: + - name: game + containerPort: 7777 + protocol: UDP + health: + disabled: false + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 3 + template: + spec: + serviceAccountName: match-server + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + seccompProfile: + type: RuntimeDefault + containers: + - name: game-server + image: ghcr.io/cosmic-clash/game-server@sha256:0000000000000000000000000000000000000000000000000000000000000000 + args: ["--port=7777"] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1 + memory: 512Mi diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml index 8cdd1a18..03d335e3 100644 --- a/deploy/k8s/base/kustomization.yaml +++ b/deploy/k8s/base/kustomization.yaml @@ -1,10 +1,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: cosmic-clash resources: - namespace.yaml - service-accounts.yaml - rbac.yaml - network-policies.yaml - control-plane-deployment.yaml - + - fleet.yaml diff --git a/deploy/k8s/base/network-policies.yaml b/deploy/k8s/base/network-policies.yaml index 3188323e..f69148cd 100644 --- a/deploy/k8s/base/network-policies.yaml +++ b/deploy/k8s/base/network-policies.yaml @@ -2,6 +2,7 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress-egress + namespace: cosmic-clash spec: podSelector: {} policyTypes: [Ingress, Egress] @@ -10,6 +11,7 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: control-plane-allowed-flows + namespace: cosmic-clash spec: podSelector: matchLabels: @@ -64,4 +66,3 @@ spec: podSelector: matchLabels: k8s-app: kube-dns - diff --git a/deploy/k8s/overlays/eu/kustomization.yaml b/deploy/k8s/overlays/eu/kustomization.yaml new file mode 100644 index 00000000..c8a40d32 --- /dev/null +++ b/deploy/k8s/overlays/eu/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../../base +patches: + - path: region.yaml diff --git a/deploy/k8s/overlays/eu/region.yaml b/deploy/k8s/overlays/eu/region.yaml new file mode 100644 index 00000000..cb227bb6 --- /dev/null +++ b/deploy/k8s/overlays/eu/region.yaml @@ -0,0 +1,10 @@ +apiVersion: agones.dev/v1 +kind: Fleet +metadata: + name: cosmic-clash-game + namespace: cosmic-clash +spec: + template: + metadata: + labels: + cosmic-clash.io/region: EU diff --git a/deploy/k8s/overlays/na/kustomization.yaml b/deploy/k8s/overlays/na/kustomization.yaml new file mode 100644 index 00000000..c8a40d32 --- /dev/null +++ b/deploy/k8s/overlays/na/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../../base +patches: + - path: region.yaml diff --git a/deploy/k8s/overlays/na/region.yaml b/deploy/k8s/overlays/na/region.yaml new file mode 100644 index 00000000..33014752 --- /dev/null +++ b/deploy/k8s/overlays/na/region.yaml @@ -0,0 +1,10 @@ +apiVersion: agones.dev/v1 +kind: Fleet +metadata: + name: cosmic-clash-game + namespace: cosmic-clash +spec: + template: + metadata: + labels: + cosmic-clash.io/region: NA diff --git a/multiplayer-next.md b/multiplayer-next.md index d3390fe4..435054e2 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -111,8 +111,11 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md). ## Phase 8 — Agones and regional server capacity -- [ ] Add portable EU/NA Agones Fleets with provider edge/network/secret and - Valve-approved SDR POP/certificate/public-UDP overlays. +- [ ] **IN PROGRESS:** Add portable EU/NA Agones Fleets with provider + edge/network/secret and Valve-approved SDR POP/certificate/public-UDP + overlays. A restricted provider-neutral Fleet base and distinct EU/NA + Kustomize overlays now exist; live rendering and provider/Valve overlays + remain. - [ ] Add the local-safe Agones adapter and separate process-ready (listen then Ready) from assignment-ready (Allocated manifest verified and registered). The Go supervisor now validates dynamic address/port data and gates Ready on diff --git a/multiplayer-todo.md b/multiplayer-todo.md index bdcc3deb..627cf8e4 100644 --- a/multiplayer-todo.md +++ b/multiplayer-todo.md @@ -1208,7 +1208,7 @@ the local/CI/community transport, not a silent production fallback. | # | Task | Acceptance | |---|---|---| -| 8.26 `[D:8.1,8.6,8.12]` | Portable Helm/Kustomize Fleets per build/EU/NA region; isolate provider edge/network/DNS/secret and SDR POP/cert/public-UDP overlays | Two provider fixtures render; labels select region/build/protocol/transport; each fixture documents Valve approval and externally reachable UDP mapping | +| 8.26 `[D:8.1,8.6,8.12]` | **IN PROGRESS.** Provider-neutral Kustomize base now defines a restricted Agones Fleet with region/build/protocol/transport labels and UDP game port, plus distinct EU/NA overlays; the base avoids rewriting cross-namespace Agones RBAC | `deploy/k8s/base/fleet.yaml`, `overlays/eu`, `overlays/na` and `server/security/test_fleet_manifests.py` cover labels, replica floor, UDP declaration, pod hardening, overlay distinction and RBAC namespace safety; live Kustomize/Agones rendering, second-provider fixtures, edge/network/DNS/secret and SDR POP/cert/public-UDP overlays remain | | 8.27 `[D:8.26]` | **IN PROGRESS.** Go supervisor package provides local-safe Agones REST discovery, validates assigned address/port data, injects dynamic `SDR_LISTEN_PORT`/`SDR_IP`, performs explicit process-ready probing and Ready transition; direct mode bypasses Agones | `server/supervisor/` covers allocated/direct startup, invalid endpoint rejection, dynamic endpoint/Ready ordering and authenticated drain; Godot Agones adapter, metadata watch, Health/annotation/Shutdown and emulator integration remain | | 8.28 `[D:8.6,8.27]` | **IN PROGRESS.** Supervisor separates explicit process-ready from Agones Ready and never scrapes stdout; allocated mode refuses to mark Ready without a configured readiness probe | `server/supervisor/` tests prove Ready follows the probe and direct mode remains functional; Godot readiness endpoint, detached-container and Health-reclaim integration remain | | 8.29 `[D:8.26,8.27]` | **IN PROGRESS.** Supervisor discovers and validates the Agones endpoint, propagates the actual dynamic `--port`, and exports `SDR_LISTEN_PORT`/`SDR_IP` only for Hosted-SDR while preserving an isolated ENet path | `server/supervisor/` tests cover invalid address/port rejection, dynamic port argument/env propagation and SDR-vs-ENet separation; real Agones dynamic/passthrough mapping, POP/cert/firewall/NAT and multi-match fixture remain | diff --git a/server/security/test_fleet_manifests.py b/server/security/test_fleet_manifests.py new file mode 100644 index 00000000..3ac30c21 --- /dev/null +++ b/server/security/test_fleet_manifests.py @@ -0,0 +1,40 @@ +from pathlib import Path +import unittest + + +BASE = Path(__file__).parents[2] / "deploy" / "k8s" + + +class FleetManifestTest(unittest.TestCase): + def read(self, path): + return (BASE / path).read_text() + + def test_base_fleet_selects_compatible_game_servers(self): + fleet = self.read("base/fleet.yaml") + for label in ( + "cosmic-clash.io/region: EU", "cosmic-clash.io/build: build-1", + 'cosmic-clash.io/protocol: "1"', "cosmic-clash.io/transport: enet", + "protocol: UDP", "containerPort: 7777", "replicas: 2", + ): + self.assertIn(label, fleet) + for hardening in ("runAsNonRoot: true", "automountServiceAccountToken: false", "readOnlyRootFilesystem: true", "allowPrivilegeEscalation: false"): + self.assertIn(hardening, fleet) + + def test_eu_and_na_overlays_are_distinct_and_namespaced(self): + eu = self.read("overlays/eu/region.yaml") + na = self.read("overlays/na/region.yaml") + self.assertIn("cosmic-clash.io/region: EU", eu) + self.assertIn("cosmic-clash.io/region: NA", na) + self.assertNotEqual(eu, na) + for document in (eu, na): + self.assertIn("namespace: cosmic-clash", document) + + def test_kustomization_does_not_rewrite_cross_namespace_agones_rbac(self): + base = self.read("base/kustomization.yaml") + rbac = self.read("base/rbac.yaml") + self.assertNotIn("namespace: cosmic-clash", base) + self.assertIn("namespace: agones-system", rbac) + + +if __name__ == "__main__": + unittest.main()