From 2f1047f5b12b5ce8f6749e38997e3a78ada1d8b4 Mon Sep 17 00:00:00 2001 From: Baylem Date: Wed, 14 Oct 2026 19:29:43 -0400 Subject: [PATCH 1/4] fix(chart): gate bundled CRDs for standalone installs Signed-off-by: Baylem --- .github/workflows/ci.yaml | 10 ++-- CHANGELOG.md | 7 +++ Makefile | 2 +- charts/gameplane/Chart.yaml | 6 ++ .../charts/gameplane-crds/Chart.yaml | 5 ++ .../crds/gameplane.local_backups.yaml | 0 .../crds/gameplane.local_backupschedules.yaml | 0 .../crds/gameplane.local_clusters.yaml | 0 .../crds/gameplane.local_gameservers.yaml | 0 .../crds/gameplane.local_gametemplates.yaml | 0 .../crds/gameplane.local_modules.yaml | 0 .../crds/gameplane.local_modulesources.yaml | 0 .../crds/gameplane.local_networkcaptures.yaml | 0 .../crds/gameplane.local_restores.yaml | 0 .../examples/standalone-panel-values.yaml | 4 +- charts/gameplane/templates/NOTES.txt | 4 +- charts/gameplane/values.yaml | 10 +++- charts/tests/check_gateway.py | 7 ++- charts/tests/check_standalone.py | 58 +++++++++++++++++-- docs/install.md | 17 ++++-- docs/standalone-panel.md | 16 ++--- hack/gen-module-schema.py | 2 +- hack/sync-chart-crds.sh | 6 +- test/e2e/helm_install_e2e_test.go | 4 +- 24 files changed, 121 insertions(+), 37 deletions(-) create mode 100644 charts/gameplane/charts/gameplane-crds/Chart.yaml rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_backups.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_backupschedules.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_clusters.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_gameservers.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_gametemplates.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_modules.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_modulesources.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_networkcaptures.yaml (100%) rename charts/gameplane/{ => charts/gameplane-crds}/crds/gameplane.local_restores.yaml (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6bf55774c..d0b6c22f5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -205,7 +205,7 @@ jobs: - 'Makefile' - 'hack/sync-chart-crds.sh' - 'operator/config/**' - - 'charts/gameplane/crds/**' + - 'charts/gameplane/charts/gameplane-crds/**' - 'charts/gameplane/crd-manifests/**' - 'hack/check-codegen-drift_test.sh' ci: @@ -1267,7 +1267,7 @@ jobs: trap 'cp /tmp/values.yaml.orig charts/gameplane/values.yaml' EXIT cp charts/gameplane/values.yaml /tmp/values.yaml.orig cp charts/gameplane/testdata/upgrade-from-v0.2.0-beta.8-values.yaml charts/gameplane/values.yaml - helm template gameplane charts/gameplane > /tmp/rendered-reuse-values.yaml + helm template gameplane charts/gameplane --include-crds > /tmp/rendered-reuse-values.yaml python3 - <<'PY' import yaml docs = [d for d in yaml.safe_load_all(open('/tmp/rendered-reuse-values.yaml')) if d] @@ -1275,6 +1275,8 @@ jobs: required = {('Deployment', 'gameplane-api'), ('Deployment', 'gameplane-web'), ('PersistentVolumeClaim', 'gameplane-api-data')} assert required <= resources, f"reuse-values removed existing resources: {required - resources}" + crds = {name for kind, name in resources if kind == 'CustomResourceDefinition'} + assert len(crds) == 9, f"reuse-values disabled the bundled CRDs: {crds}" assert ('Deployment', 'gameplane-gateway') not in resources assert ('NetworkPolicy', 'allow-api-to-agent') in resources assert ('NetworkPolicy', 'allow-api-to-capture') not in resources, "legacy values without capture enabled its ingress" @@ -1310,7 +1312,7 @@ jobs: # The CRD-apply hook serves CRDs from crd-manifests/ (crds/ isn't # readable by Helm .Files). make manifests keeps both in sync; guard # against a drift that would apply stale CRDs on upgrade. - run: diff -r charts/gameplane/crds charts/gameplane/crd-manifests + run: diff -r charts/gameplane/charts/gameplane-crds/crds charts/gameplane/crd-manifests - name: chart CRDs carry the current bundle-hash stamp (F-218) # crd-apply-hook.yaml fires on pre-install only when the live CRDs' # gameplane.local/crd-bundle-sha256 differs from the chart's. A stale @@ -1320,7 +1322,7 @@ jobs: # difference. run: | hack/sync-chart-crds.sh - git diff --exit-code -- charts/gameplane/crds charts/gameplane/crd-manifests + git diff --exit-code -- charts/gameplane/charts/gameplane-crds/crds charts/gameplane/crd-manifests - name: chart cosign key matches signing/cosign.pub # values.yaml embeds a copy of the signing public key so the operator # can verify official module bundles. Nothing regenerates it, so a key diff --git a/CHANGELOG.md b/CHANGELOG.md index 399d0a86c..558a7e034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ reaches `1.0.0`. Pre-1.0 minor versions may contain breaking changes. ## [Unreleased] +### Fixed + +- **Standalone Helm CRD isolation:** the central panel profile excludes bundled + native CRDs and their apply hook/RBAC without requiring `--skip-crds`. + Default operator installations keep Helm's native CRD lifecycle; existing + CRDs and their custom resources are retained when a profile omits them. + ### Upgrade Notes - **Standalone panel/API:** run the central panel with Docker Compose or the diff --git a/Makefile b/Makefile index 02f0df3fe..25e06e883 100644 --- a/Makefile +++ b/Makefile @@ -331,7 +331,7 @@ manifests: ## Regenerate CRDs + RBAC manifests (and sync chart CRD copies) paths=./... \ output:crd:artifacts:config=config/crd \ output:rbac:artifacts:config=config/rbac - # Copy the CRDs into the chart's crds/ and crd-manifests/ (the same CRDs + # Copy the CRDs into the conditional CRD subchart and crd-manifests/ (the same CRDs # where Helm's .Files can read them -- it can't read the special crds/ # dir -- so the crds.autoApply hook can apply them), stamping each copy # with a bundle hash the hook compares against the live CRDs to tell diff --git a/charts/gameplane/Chart.yaml b/charts/gameplane/Chart.yaml index 779bbed1d..ba38b4ccb 100644 --- a/charts/gameplane/Chart.yaml +++ b/charts/gameplane/Chart.yaml @@ -15,3 +15,9 @@ keywords: - control-panel maintainers: - name: Gameplane contributors +dependencies: + # Vendored locally so installs never fetch another chart. Helm resolves this + # condition before installing native CRDs, including on a fresh cluster. + - name: gameplane-crds + version: 0.3.0 + condition: operator.enabled diff --git a/charts/gameplane/charts/gameplane-crds/Chart.yaml b/charts/gameplane/charts/gameplane-crds/Chart.yaml new file mode 100644 index 000000000..cd9616bb8 --- /dev/null +++ b/charts/gameplane/charts/gameplane-crds/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: gameplane-crds +description: Native CRDs bundled with the Gameplane operator. +type: application +version: 0.3.0 diff --git a/charts/gameplane/crds/gameplane.local_backups.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backups.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_backups.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backups.yaml diff --git a/charts/gameplane/crds/gameplane.local_backupschedules.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backupschedules.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_backupschedules.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_backupschedules.yaml diff --git a/charts/gameplane/crds/gameplane.local_clusters.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_clusters.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_clusters.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_clusters.yaml diff --git a/charts/gameplane/crds/gameplane.local_gameservers.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gameservers.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_gameservers.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gameservers.yaml diff --git a/charts/gameplane/crds/gameplane.local_gametemplates.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gametemplates.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_gametemplates.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_gametemplates.yaml diff --git a/charts/gameplane/crds/gameplane.local_modules.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modules.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_modules.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modules.yaml diff --git a/charts/gameplane/crds/gameplane.local_modulesources.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modulesources.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_modulesources.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_modulesources.yaml diff --git a/charts/gameplane/crds/gameplane.local_networkcaptures.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_networkcaptures.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_networkcaptures.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_networkcaptures.yaml diff --git a/charts/gameplane/crds/gameplane.local_restores.yaml b/charts/gameplane/charts/gameplane-crds/crds/gameplane.local_restores.yaml similarity index 100% rename from charts/gameplane/crds/gameplane.local_restores.yaml rename to charts/gameplane/charts/gameplane-crds/crds/gameplane.local_restores.yaml diff --git a/charts/gameplane/examples/standalone-panel-values.yaml b/charts/gameplane/examples/standalone-panel-values.yaml index 12472e1be..2bbb8505d 100644 --- a/charts/gameplane/examples/standalone-panel-values.yaml +++ b/charts/gameplane/examples/standalone-panel-values.yaml @@ -1,5 +1,5 @@ -# Fresh central panel install; use helm install/upgrade --skip-crds. -# Helm processes crds/ before values/templates, so values alone cannot skip CRDs. +# Fresh central panel install. operator.enabled=false excludes the bundled +# native CRD subchart; api.standalone=true also skips the CRD apply hook/RBAC. operator: enabled: false api: diff --git a/charts/gameplane/templates/NOTES.txt b/charts/gameplane/templates/NOTES.txt index 5d86d3263..6f5d24139 100644 --- a/charts/gameplane/templates/NOTES.txt +++ b/charts/gameplane/templates/NOTES.txt @@ -12,8 +12,8 @@ Back up the database and matching encryption key separately. SQLite uses the API data PVC; generated keys use the separate API key PVC mounted at /keys. If using api.panelKey.existingSecret, protect and back up that Secret instead. Restrict access to key backups; do not include them in database backup jobs. -Always pass --skip-crds when installing this profile; Helm's native crds/ -installation happens before chart values can disable resources. +operator.enabled=false excludes bundled native CRDs. Standalone mode also +skips the CRD apply hook and its RBAC; no --skip-crds argument is required. {{- end }} {{- $receiverDeployed := and (eq (include "gameplane.apiEnabled" .) "true") $r.enabled (not .Values.api.telemetry.endpoint) }} {{- $persistence := $r.persistence | default dict }} diff --git a/charts/gameplane/values.yaml b/charts/gameplane/values.yaml index 2b02286df..a89c42b6b 100644 --- a/charts/gameplane/values.yaml +++ b/charts/gameplane/values.yaml @@ -22,7 +22,10 @@ image: pullPolicy: IfNotPresent pullSecrets: [] -# CRD lifecycle. Helm installs the files under crds/ only on the FIRST install +# Native CRDs live in the bundled gameplane-crds subchart, enabled by +# operator.enabled. Dependency gating happens before Helm installs CRDs, so +# operator.enabled=false omits them from a standalone central panel install. +# CRD lifecycle. Helm installs the files under the subchart's crds/ on first install # and NEVER updates them on `helm upgrade` (a documented Helm limitation). This # hook closes that gap: a pre-UPGRADE Job runs `kubectl apply --server-side` # over the CRDs, so a plain `helm upgrade` always brings the CRD schema up to @@ -47,7 +50,7 @@ crds: pullPolicy: IfNotPresent operator: - # Disable all operator resources for a standalone central panel. + # Disable the operator and bundled native CRDs for a standalone central panel. enabled: true replicas: 1 logLevel: info # zap log level: debug|info|error (warn not supported by zap's --zap-log-level) @@ -126,7 +129,8 @@ api: # integrations on remote operator+gateway installs. Agent mTLS is preserved. enabled: true # Central panel only: SQL-backed settings and credentials, no local cluster. - # Requires operator.enabled=false and helm install --skip-crds. + # Requires operator.enabled=false, which excludes bundled native CRDs. + # Standalone mode also skips the CRD apply hook and its RBAC. standalone: false replicas: 1 # Keep at 1. SQLite is single-writer on a RWO volume; with db.driver=postgres the user-management lock and audit hash chain are still per-process logLevel: info # API log verbosity (debug|info|warn|error), passed as GAMEPLANE_LOG_LEVEL diff --git a/charts/tests/check_gateway.py b/charts/tests/check_gateway.py index 711783343..1126ceedd 100644 --- a/charts/tests/check_gateway.py +++ b/charts/tests/check_gateway.py @@ -9,13 +9,14 @@ CHART = ROOT / "charts" / "gameplane" -def render(values=None, failure=None, upgrade=False, namespace="gameplane-system", release="gameplane"): +def render(values=None, failure=None, upgrade=False, namespace="gameplane-system", release="gameplane", include_crds=False, chart=CHART): with tempfile.TemporaryDirectory() as directory: values_path = Path(directory) / "values.yaml" values_path.write_text(yaml.safe_dump(values or {}), encoding="utf-8") result = subprocess.run( - ["helm", "template", release, str(CHART), "--namespace", namespace, "--values", str(values_path)] - + (["--is-upgrade"] if upgrade else []), + ["helm", "template", release, str(chart), "--namespace", namespace, "--values", str(values_path)] + + (["--is-upgrade"] if upgrade else []) + + (["--include-crds"] if include_crds else []), capture_output=True, text=True, check=False, ) if failure: diff --git a/charts/tests/check_standalone.py b/charts/tests/check_standalone.py index 21fed688b..eb6613758 100644 --- a/charts/tests/check_standalone.py +++ b/charts/tests/check_standalone.py @@ -1,13 +1,48 @@ """CI-only checks for combined, standalone central, and remote deployment roles.""" from pathlib import Path +import subprocess +import tempfile import yaml -from check_gateway import one, render +from check_gateway import CHART, one, render + + +EXPECTED_CRDS = { + "gameservers.gameplane.local", "gametemplates.gameplane.local", + "backups.gameplane.local", "backupschedules.gameplane.local", + "restores.gameplane.local", "modules.gameplane.local", + "modulesources.gameplane.local", "clusters.gameplane.local", + "networkcaptures.gameplane.local", +} + + +def assert_crds(objects, expected): + names = [obj["metadata"]["name"] for obj in objects if obj["kind"] == "CustomResourceDefinition"] + assert set(names) == expected and len(names) == len(expected), names + + +def check_packaged_crds(example): + # Package only vendored files. Dependency downloads/builds would hide a + # missing bundled CRD chart and require network access for installation. + with tempfile.TemporaryDirectory() as directory: + result = subprocess.run(["helm", "package", str(CHART), "--destination", directory], + capture_output=True, text=True, check=False) + assert result.returncode == 0, result.stderr + packages = list(Path(directory).glob("gameplane-*.tgz")) + assert len(packages) == 1, packages + for upgrade in (False, True): + for values in ({}, {"api": {"enabled": False}}): + assert_crds(render(values, chart=packages[0], include_crds=True, upgrade=upgrade), EXPECTED_CRDS) + for values in ({"operator": {"enabled": False}, "api": {"standalone": True}}, example): + objects = render(values, chart=packages[0], include_crds=True, upgrade=upgrade) + assert_crds(objects, set()) + assert not any("crd-apply" in obj["metadata"]["name"] for obj in objects), objects def main(): - defaults = render() + defaults = render(include_crds=True) + assert_crds(defaults, EXPECTED_CRDS) one(defaults, "Deployment", "gameplane-operator") one(defaults, "Deployment", "gameplane-api") one(defaults, "ModuleSource", "default") @@ -22,7 +57,8 @@ def main(): "mcpServer": {"enabled": True}, "capture": {"enabled": True}, } for upgrade in (False, True): - objects = render(values, upgrade=upgrade) + objects = render(values, upgrade=upgrade, include_crds=True) + assert_crds(objects, set()) for obj in objects: assert obj["kind"] not in {"ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding", "Namespace", "ModuleSource", "NetworkPolicy", "PodMonitor", "PrometheusRule", "Job"}, obj assert "operator" not in obj["metadata"]["name"], obj @@ -87,11 +123,14 @@ def main(): args = one(objects, "Deployment", "gameplane-api")["spec"]["template"]["spec"]["containers"][0]["args"] assert "--remote-allowed-cidrs=10.20.0.0/16,fd12:3456::/48" in args - remote = render({"api": {"enabled": False}}) + remote = render({"api": {"enabled": False}}, include_crds=True) + assert_crds(remote, EXPECTED_CRDS) one(remote, "Deployment", "gameplane-operator") one(remote, "Secret", "gameplane-agent-ca") assert not any(obj["metadata"]["name"] in {"gameplane-api", "gameplane-web"} for obj in remote) - disabled = render({"operator": {"enabled": False}}) + disabled = render({"operator": {"enabled": False}}, include_crds=True) + assert_crds(disabled, set()) + one(disabled, "Job", "gameplane-crd-apply") assert not any("operator" in obj["metadata"]["name"] for obj in disabled) one(disabled, "Deployment", "gameplane-api") @@ -99,7 +138,14 @@ def main(): render({"operator": {"enabled": False}, "api": {"enabled": False, "standalone": True}}, failure="api.standalone requires api.enabled=true") render({"operator": {"enabled": False}, "api": {"standalone": True, "replicas": 2}}, failure="api.standalone requires api.replicas=1") example = yaml.safe_load((Path(__file__).parents[1] / "gameplane/examples/standalone-panel-values.yaml").read_text()) - one(render(example), "Deployment", "gameplane-api") + for upgrade in (False, True): + objects = render(example, include_crds=True, upgrade=upgrade) + assert_crds(objects, set()) + one(objects, "Deployment", "gameplane-api") + assert not any("crd-apply" in obj["metadata"]["name"] for obj in objects), objects + assert_crds(render(include_crds=True, upgrade=upgrade), EXPECTED_CRDS) + assert_crds(render({"api": {"enabled": False}}, include_crds=True, upgrade=upgrade), EXPECTED_CRDS) + check_packaged_crds(example) if __name__ == "__main__": diff --git a/docs/install.md b/docs/install.md index 835cc2e7b..e45f8c27b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -111,9 +111,9 @@ Open `https://` and log in. Top-level knobs (see `values.yaml` for the full list): - `image.registry` / `image.tag` — container image pinning -- `operator.enabled` — enable the workload operator (default `true`). Set to `false` for a standalone central panel. +- `operator.enabled` — enable the workload operator and bundled native CRDs (default `true`). Set to `false` for a standalone central panel; the CRD subchart is excluded before Helm installs CRDs. - `api.enabled` — enable the central API and dashboard (default `true`). Set to `false` on a remote operator/gateway installation. -- `api.standalone` — store management records and encrypted credentials in SQL without a local cluster (default `false`). Requires `operator.enabled=false`, one API replica, and `--skip-crds` when installing the panel. See the [standalone guide](standalone-panel.md). +- `api.standalone` — store management records and encrypted credentials in SQL without a local cluster (default `false`). Requires `operator.enabled=false` and one API replica. The standalone profile omits native CRDs and the CRD apply hook/RBAC without an additional CLI flag. See the [standalone guide](standalone-panel.md). - `operator.replicas` — leader-elected, safe at 2+ - `operator.configInitImage` / `operator.resticImage` — the two images the operator injects into workloads it creates: the config-init container on game pods @@ -769,9 +769,18 @@ remote workloads running. Keep cookie jars and kubeconfig files private. ### Helm CRD caveat This section applies to combined and remote workload installations. A standalone -central Helm release skips CRDs and their apply hook; follow its +central Helm release uses `operator.enabled=false` to exclude the bundled +`gameplane-crds` subchart and `api.standalone=true` to skip the CRD apply hook and +its RBAC. No `--skip-crds` argument is needed; follow its [panel profile](standalone-panel.md#central-panel-on-kubernetes). +With the default `operator.enabled=true`, the subchart supplies CRDs through +Helm's native `crds/` phase before the release's custom resources are installed. +CRDs already present on the cluster remain in place when switching to a profile +that omits them; this setting does not delete CRDs or their custom resources. +Combined API-only installs retain the existing apply hook when +`api.standalone=false` and `crds.autoApply.enabled=true`. + **`helm upgrade` never updates CRDs.** That is a documented Helm limitation, not a Gameplane one: files under a chart's `crds/` directory are installed on first install and ignored on every upgrade thereafter. @@ -788,7 +797,7 @@ hook: ```sh # only when crds.autoApply.enabled=false -kubectl apply --server-side -f charts/gameplane/crds/ +kubectl apply --server-side -f charts/gameplane/crd-manifests/ ``` The hook fires on pre-upgrade always. On `helm install` it fires only when diff --git a/docs/standalone-panel.md b/docs/standalone-panel.md index b6884b4bf..c405b996b 100644 --- a/docs/standalone-panel.md +++ b/docs/standalone-panel.md @@ -201,7 +201,7 @@ games or granting the API Kubernetes access. From this checkout: ```sh helm upgrade --install gameplane charts/gameplane \ - --namespace gameplane-system --create-namespace --skip-crds \ + --namespace gameplane-system --create-namespace \ --values charts/gameplane/examples/standalone-panel-values.yaml ``` @@ -212,9 +212,10 @@ and TLS. Expose the web Service through your HTTPS ingress or use a local port forward. Bootstrap the admin with `/api bootstrap-admin` in the API pod as in the [combined installation guide](install.md#first-time-setup). -Keep `--skip-crds` on installation commands. Helm installs files in `crds/` -before evaluating templates; setting values alone cannot suppress those CRDs. -Standalone mode skips the CRD apply hook automatically. It creates no operator, +`operator.enabled=false` excludes the bundled native CRD subchart before Helm's +CRD installation phase. `api.standalone=true` also skips the CRD apply hook and +its RBAC automatically, so this profile needs no `--skip-crds` argument. It +leaves any CRDs already on the cluster in place. It creates no operator, agent certificates, game namespace, module source, game network policies, operator monitors, or API Kubernetes RBAC. Its API pod disables service account token mounting. SQLite uses a database PVC; PostgreSQL creates no local database @@ -251,8 +252,9 @@ helm --kubeconfig remote-admin.yaml upgrade --install gameplane charts/gameplane --set image.registry= --set image.tag= ``` -The remote install needs its CRDs, so keep `--skip-crds` off this command. The -operator creates agents alongside game servers. Enable the optional +The remote install keeps `operator.enabled=true`, so Helm installs the bundled +CRDs through its native installation phase. The operator creates agents alongside +game servers. Enable the optional [private gateway](gateway-install.md) for agent operations such as console, files, and player management. Without a gateway, Kubernetes resource operations remain available, but agent-backed operations are unavailable. @@ -416,7 +418,7 @@ versions using their Helm values. Existing games continue running during a panel restart; a remote operator upgrade may roll game pods. For a panel-only Helm release, repeat the installation command with the updated -chart, matching images, `--skip-crds`, and your standalone values file. Keep +chart, matching images, and your standalone values file. Keep `api.replicas=1` and the existing key storage, including when using PostgreSQL. ## Troubleshooting diff --git a/hack/gen-module-schema.py b/hack/gen-module-schema.py index 36f14eae0..0ba07fb40 100644 --- a/hack/gen-module-schema.py +++ b/hack/gen-module-schema.py @@ -21,7 +21,7 @@ import yaml REPO = Path(__file__).resolve().parent.parent -CRD = REPO / "charts/gameplane/crds/gameplane.local_gametemplates.yaml" +CRD = REPO / "charts/gameplane/crd-manifests/gameplane.local_gametemplates.yaml" OUT = REPO / "modules/.schema/gametemplate.schema.json" diff --git a/hack/sync-chart-crds.sh b/hack/sync-chart-crds.sh index bde6bbc43..98ba9ff18 100755 --- a/hack/sync-chart-crds.sh +++ b/hack/sync-chart-crds.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# sync-chart-crds.sh: copy the controller-gen CRDs into the Helm chart's crds/ -# and crd-manifests/ directories, stamping every copy with a bundle hash. +# sync-chart-crds.sh: copy the controller-gen CRDs into the conditional CRD +# subchart and the parent's crd-manifests/, stamping each with a bundle hash. # # Purpose: # The chart's crds.autoApply hook (templates/crd-apply-hook.yaml) must fire @@ -58,7 +58,7 @@ for f in "${files[@]}"; do exit 1 fi base=$(basename "$f") - for dir in charts/gameplane/crds charts/gameplane/crd-manifests; do + for dir in charts/gameplane/charts/gameplane-crds/crds charts/gameplane/crd-manifests; do awk -v line=" ${annotation}: ${hash}" ' { print } /^ annotations:$/ && !done { print line; done = 1 } diff --git a/test/e2e/helm_install_e2e_test.go b/test/e2e/helm_install_e2e_test.go index 48127adda..ffa62e6aa 100644 --- a/test/e2e/helm_install_e2e_test.go +++ b/test/e2e/helm_install_e2e_test.go @@ -69,7 +69,7 @@ func TestHelmInstall_AllPodsReady(t *testing.T) { // TestHelmInstall_AllCRDsPresent — every Gameplane CRD declared by the // chart is reachable via discovery. Catches a missing CRD YAML in -// `charts/gameplane/crds/` from a future refactor. +// `charts/gameplane/charts/gameplane-crds/crds/` from a future refactor. func TestHelmInstall_AllCRDsPresent(t *testing.T) { t.Parallel() @@ -82,6 +82,8 @@ func TestHelmInstall_AllCRDsPresent(t *testing.T) { "restores.gameplane.local", "modules.gameplane.local", "modulesources.gameplane.local", + "clusters.gameplane.local", + "networkcaptures.gameplane.local", } for _, name := range want { ok, err := envInstance.CRDExists(ctx, name) From 564220c37d04e1e6a79d6ebb8108a0bcb942d80b Mon Sep 17 00:00:00 2001 From: Baylem Date: Wed, 14 Oct 2026 19:47:40 -0400 Subject: [PATCH 2/4] docs(chart): describe CRD render check contracts Signed-off-by: Baylem --- charts/tests/check_gateway.py | 5 +++++ charts/tests/check_standalone.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/charts/tests/check_gateway.py b/charts/tests/check_gateway.py index 1126ceedd..83f174ed6 100644 --- a/charts/tests/check_gateway.py +++ b/charts/tests/check_gateway.py @@ -10,6 +10,11 @@ def render(values=None, failure=None, upgrade=False, namespace="gameplane-system", release="gameplane", include_crds=False, chart=CHART): + """Return rendered objects, or assert the expected Helm validation failure. + + Accept a source chart or packaged archive; native CRDs are opt-in to match + Helm template's default behavior. + """ with tempfile.TemporaryDirectory() as directory: values_path = Path(directory) / "values.yaml" values_path.write_text(yaml.safe_dump(values or {}), encoding="utf-8") diff --git a/charts/tests/check_standalone.py b/charts/tests/check_standalone.py index eb6613758..4caea9cc6 100644 --- a/charts/tests/check_standalone.py +++ b/charts/tests/check_standalone.py @@ -18,11 +18,13 @@ def assert_crds(objects, expected): + """Require exactly the expected CRD names, with no duplicate definitions.""" names = [obj["metadata"]["name"] for obj in objects if obj["kind"] == "CustomResourceDefinition"] assert set(names) == expected and len(names) == len(expected), names def check_packaged_crds(example): + """Verify vendored chart archives preserve CRD gating for each install role.""" # Package only vendored files. Dependency downloads/builds would hide a # missing bundled CRD chart and require network access for installation. with tempfile.TemporaryDirectory() as directory: @@ -41,6 +43,7 @@ def check_packaged_crds(example): def main(): + """Check deployment roles, standalone key custody, and source/archive CRDs.""" defaults = render(include_crds=True) assert_crds(defaults, EXPECTED_CRDS) one(defaults, "Deployment", "gameplane-operator") From 37436a55fcda67ad9239381273640fef4a249273 Mon Sep 17 00:00:00 2001 From: Baylem Date: Fri, 16 Oct 2026 01:06:21 -0400 Subject: [PATCH 3/4] docs(chart): update CRD generation and lifecycle references Signed-off-by: Baylem --- .coderabbit.yaml | 3 ++- gameproto/specs.md | 2 +- operator/specs.md | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 0ffe2cba2..3d701a8d1 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -95,7 +95,8 @@ reviews: - operator/api/v1alpha1/zz_generated.deepcopy.go (regenerated) - operator/config/crd/*.yaml (regenerated) - operator/config/rbac/*.yaml (regenerated) - - charts/gameplane/crds/*.yaml (regenerated and synced) + - charts/gameplane/charts/gameplane-crds/crds/*.yaml (regenerated and synced) + - charts/gameplane/crd-manifests/*.yaml (regenerated hook copy) If a CRD type changed but these regenerated files are absent, the PR is incomplete. Run: make generate && make manifests diff --git a/gameproto/specs.md b/gameproto/specs.md index f291ecf71..b32b78886 100644 --- a/gameproto/specs.md +++ b/gameproto/specs.md @@ -276,7 +276,7 @@ To add a new game protocol (e.g., "factorio"): - Unit tests for Classify() and response builders - Comparison/equivalence tests if migrating from an existing implementation -5. **Register the protocol with the operator's API too.** The steps above only make the sentinel able to classify the new protocol; a `GameTemplate`'s `wakeProtocol` field is a Kubernetes CRD enum (`operator/api/v1alpha1/gametemplate_types.go`, `+kubebuilder:validation:Enum=...`) that must list the new protocol name before any `GameTemplate` can select it. Add the name to that enum, then run `make generate && make manifests` and commit the regenerated `zz_generated.deepcopy.go`, `operator/config/crd/*.yaml`, and `charts/gameplane/crds/*.yaml` (an enum change does not touch RBAC). Without this step, `kubectl apply` on a template using the new `wakeProtocol` value is rejected with `Unsupported value`, even though the sentinel recognizes it. +5. **Register the protocol with the operator's API too.** The steps above only make the sentinel able to classify the new protocol; a `GameTemplate`'s `wakeProtocol` field is a Kubernetes CRD enum (`operator/api/v1alpha1/gametemplate_types.go`, `+kubebuilder:validation:Enum=...`) that must list the new protocol name before any `GameTemplate` can select it. Add the name to that enum, then run `make generate && make manifests` and commit the regenerated `zz_generated.deepcopy.go`, `operator/config/crd/*.yaml`, `charts/gameplane/charts/gameplane-crds/crds/*.yaml`, and `charts/gameplane/crd-manifests/*.yaml` (an enum change does not touch RBAC). Without this step, `kubectl apply` on a template using the new `wakeProtocol` value is rejected with `Unsupported value`, even though the sentinel recognizes it. 6. **That's it for shared Go code.** No further changes to sentinel/main.go, gameproto.go, or any other shared package are needed beyond the CRD enum update above. diff --git a/operator/specs.md b/operator/specs.md index 52e45e7c0..73c477517 100644 --- a/operator/specs.md +++ b/operator/specs.md @@ -422,7 +422,8 @@ Regenerates and commits atomically: - `operator/api/v1alpha1/zz_generated.deepcopy.go` — struct deepcopy methods (includes CaptureConfiguration, CaptureStatus, NetworkCapture, NetworkCaptureList, NetworkCaptureSpec, NetworkCaptureStatus deepcopy functions). - `operator/config/crd/gameplane.local_*.yaml` — 9 CRD manifests (includes gameplane.local_networkcaptures.yaml; gameplane.local_gameservers.yaml extended with capture spec/status schemas). - `operator/config/rbac/*.yaml` — ServiceAccount, Roles, RoleBindings, ClusterRoles, ClusterRoleBindings (includes pods/ephemeralcontainers and networkcaptures CRUD permissions). -- `charts/gameplane/crds/*.yaml` — copy of CRDs for Helm integration (Helm `crds/` directory + pre-upgrade hook for `kubectl apply --server-side`). +- `charts/gameplane/charts/gameplane-crds/crds/*.yaml` — native Helm CRDs in the bundled subchart, gated by `operator.enabled`. +- `charts/gameplane/crd-manifests/*.yaml` — CRD copies used by the pre-upgrade hook for `kubectl apply --server-side`. Forgetting codegen leaves the YAML out of sync with types — CI's `make manifests` verify gate will catch it, but envtest runs will fail mysteriously first. @@ -434,7 +435,7 @@ Forgetting codegen leaves the YAML out of sync with types — CI's `make manifes 3. **Codegen is mandatory after CRD type edits.** Generated deepcopy + YAML must ship in the same commit as type changes. -4. **CRDs are owned by the control plane, not Helm.** Helm's `crds/` is applied only on first install; updates come from a pre-upgrade hook running `kubectl apply --server-side --server-side-apply-manager=gameplane` on every `helm upgrade`, which also fires on a `helm install` over leftover CRDs whose `gameplane.local/crd-bundle-sha256` stamp (written by `make manifests` via `hack/sync-chart-crds.sh`) differs from the chart's (F-218). CRDs are never owned or deleted by Helm. +4. **CRDs use Helm's native lifecycle.** The bundled CRD subchart is enabled by `operator.enabled`. Helm 3 installs missing native CRDs and skips existing ones; Helm 4 server-side applies native CRDs with field manager `helm`. When `crds.autoApply.enabled=true` and `api.standalone=false`, the pre-upgrade hook updates CRDs using `kubectl apply --server-side --force-conflicts --field-manager=helm`. The hook also runs on installs with leftover CRDs whose `gameplane.local/crd-bundle-sha256` stamp (written by `make manifests` via `hack/sync-chart-crds.sh`) differs from the chart's (F-218). Native CRDs remain outside Helm's release-owned resources and survive uninstall. 5. **Agent mTLS is optional but recommended.** Operator boots without `--agent-ca-bundle`/`--agent-client-cert`/`--agent-client-key` (client.Disabled=true); Agent methods silently no-op. Production installs should supply all three. @@ -536,4 +537,4 @@ CI run: `make cover` generates merged profile (unit + envtest), `make cover-ratc - **`go.mod` / `go.sum`** — dependency lock files. - **CRD YAML:** `operator/config/crd/gameplane.local_*.yaml` (generated from types). - **RBAC YAML:** `operator/config/rbac/*.yaml` (generated, defines operator ServiceAccount + permissions). -- **Helm integration:** `charts/gameplane/crds/` + `charts/gameplane/templates/crd-apply-hook.yaml` (pre-upgrade CRD sync). +- **Helm integration:** `charts/gameplane/charts/gameplane-crds/crds/` (native CRDs) + `charts/gameplane/crd-manifests/` and `charts/gameplane/templates/crd-apply-hook.yaml` (pre-upgrade CRD sync). From c0caa489de6c2c79fa2153db7dbed8315a2da9b7 Mon Sep 17 00:00:00 2001 From: Baylem Date: Fri, 16 Oct 2026 01:18:46 -0400 Subject: [PATCH 4/4] docs(chart): correct existing agent CRD paths Signed-off-by: Baylem --- .claude/agents/crd-engineer.md | 11 +++++++---- CLAUDE.md | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.claude/agents/crd-engineer.md b/.claude/agents/crd-engineer.md index a3b5fbf29..e516c9025 100644 --- a/.claude/agents/crd-engineer.md +++ b/.claude/agents/crd-engineer.md @@ -43,15 +43,17 @@ make generate # regenerates operator/api/v1alpha1/zz_generated.deepcopy.go make manifests # regenerates operator/config/crd/*.yaml, operator/config/rbac/*.yaml ``` -**Critical:** The chart also syncs these. After `make manifests`, verify both locations are updated: +**Critical:** The chart also syncs these. After `make manifests`, verify all three locations are updated: - `operator/config/crd/` (YAML source) -- `charts/gameplane/crds/` (chart copy for pre-upgrade hook) +- `charts/gameplane/charts/gameplane-crds/crds/` (native Helm CRDs, gated by `operator.enabled`) +- `charts/gameplane/crd-manifests/` (chart copy for pre-upgrade hook) Do NOT manually edit any generated file. Never touch: - `operator/api/v1alpha1/zz_generated.deepcopy.go` - `operator/config/crd/*.yaml` - `operator/config/rbac/*.yaml` -- `charts/gameplane/crds/*.yaml` +- `charts/gameplane/charts/gameplane-crds/crds/*.yaml` +- `charts/gameplane/crd-manifests/*.yaml` ### 3. Update the Reconciler @@ -96,7 +98,8 @@ git add operator/api/v1alpha1/_types.go \ operator/api/v1alpha1/zz_generated.deepcopy.go \ operator/config/crd/*.yaml \ operator/config/rbac/*.yaml \ - charts/gameplane/crds/*.yaml \ + charts/gameplane/charts/gameplane-crds/crds/*.yaml \ + charts/gameplane/crd-manifests/*.yaml \ operator/internal/controller/_controller.go \ operator/internal/controller/_envtest_test.go \ web/src/types.ts [web/src/routes/.tsx] # Replace with specific route files actually modified (e.g., web/src/routes/Dashboard.tsx web/src/routes/Config.tsx) diff --git a/CLAUDE.md b/CLAUDE.md index 4ad438ad5..7dbfb6f58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,7 @@ After cloning: `git submodule update --init` (`modules/` required for `make dev- - **Dev:** `make dev-up` (Kind + OCI registry :5001 + Helm), `make web-dev` (Vite, proxies in-cluster API), `make dev-load` (load built images; run `make images` first), `make dev-install` (Helm upgrade), `make dev-down`. - **Build:** `make build` (all Go + web), `make build-go` (15 modules in GO_MODULES, all but test/e2e), `make build-web` (`npm ci && npm run build`), `make images`. -- **Codegen:** `make generate` (deepcopy), `make manifests` (CRD/RBAC YAML, synced to `charts/gameplane/crds/`), `make modules-push`, `make tidy`. +- **Codegen:** `make generate` (deepcopy), `make manifests` (CRD/RBAC YAML, synced to `charts/gameplane/charts/gameplane-crds/crds/` and `charts/gameplane/crd-manifests/`), `make modules-push`, `make tidy`. - **Tests (CI only — never run locally, see Rule 8):** `make test`, `make test-integration` (envtest operator+api), `make test-e2e` (~10–20 min), `make test-e2e-bucket BUCKET=(operator|api-auth|api-roles|api-rbac|api-agent|api-mods|ratelimit|bot-fast|bot-heavy|multicluster|upgrade)`. - **E2E conventions:** register new tests in `test/e2e/buckets.sh`; `t.Parallel()` + unique names; guard shared resources (`ociPushMu` for module pushes, `ensureResticRepo(t)` for shared backup repos). Rate limits per cluster: IP burst 10 (5/min), user burst 6 (3/min) — cap an API bucket at ~7 admin logins. - **Lint:** `make lint` = gofmt, go vet, golangci-lint, ESLint, `check-specs`, `check-doc-versions`, `check-links`. @@ -85,7 +85,7 @@ After cloning: `git submodule update --init` (`modules/` required for `make dev- 4. **Fix, don't silence:** no `//nolint`, `eslint-disable`, or loosened configs. Allowed: `_test.go` exempt from `errcheck`/`gosec`/`unparam`; `operator/internal/controller/` exempt from revive `exported:`. 5. **TypeScript:** strict; no unjustified `any` (explain in a comment if unavoidable); every promise `await`ed or `void`-prefixed. 6. **Go errors:** wrap with `%w`. -7. **CRD sync:** after editing `operator/api/v1alpha1/*_types.go` run `make generate && make manifests`; commit deepcopy, `operator/config/{crd,rbac}/*.yaml`, `charts/gameplane/crds/*.yaml`/`crd-manifests/` in the same changeset. +7. **CRD sync:** after editing `operator/api/v1alpha1/*_types.go` run `make generate && make manifests`; commit deepcopy, `operator/config/{crd,rbac}/*.yaml`, `charts/gameplane/charts/gameplane-crds/crds/*.yaml`, and `charts/gameplane/crd-manifests/*.yaml` in the same changeset. 8. **Verification:** locally only compile checks (`go build ./...`, `npx tsc --noEmit`); never test/lint suites (`make test|lint|cover`, `go test`, `npm test`, envtest, E2E). Push a feature branch; CI is the sole verification authority. 9. **Kubernetes primitives first** (StatefulSet, Service, PVC, Job, ConfigMap, Secret, CRDs) before custom abstractions. 10. **Operator authority:** business logic lives in reconcilers (`operator/internal/controller/`); the API is a UX gateway and never bypasses reconciliation.