From 758aa707a7debe6e5bf898bbd2bc32289a72e18f Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:50:44 +0200 Subject: [PATCH] docs: document image pinning, add make module-pin Companion to gameplane-module#35, which pins every module image by digest. - docs/module-authoring.md gains a "Pinning images by digest" section: what the rule requires (defaults are ERROR, other version entries may float), why the readable repo:tag@sha256 form is preferred over a bare digest, the `# gameplane:floating` opt-out and when it is legitimate, and why the written digest is the tag/index digest rather than a per-platform one. Also fixes the version-catalog example, which showed unpinned refs the new rule would now reject. - `make module-pin` sits next to module-schema as the other module codegen entry point. - docs/roadmap.md: correct a stale claim. It listed "restore-path coverage against a real restic repository" as outstanding, but TestRestore_RoundTrip has covered exactly that for some time -- writing a marker, backing up, wiping, restoring and asserting the bytes return, against the real in-cluster restic server. Only the human-facing drill runbook is missing. Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- Makefile | 12 +++++++- docs/module-authoring.md | 64 ++++++++++++++++++++++++++++++++++++++++ docs/roadmap.md | 8 +++-- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index b3441a02b..8dc3ec0c0 100644 --- a/Makefile +++ b/Makefile @@ -254,7 +254,7 @@ image-mcp-server: ## Build mcp-server image docker build -t $(REGISTRY)/mcp-server:$(TAG) -f mcp-server/Dockerfile . # -------- codegen -------- -.PHONY: generate manifests module-schema +.PHONY: generate manifests module-schema module-pin generate: ## Run controller-gen deepcopy generators cd operator && go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths=./api/... @@ -275,6 +275,16 @@ module-schema: ## Regenerate the editor JSON Schema for module template.yaml fro python3 hack/gen-module-schema.py @echo "commit modules/.schema/gametemplate.schema.json in the gameplane-module repo + bump the submodule pointer" +module-pin: ## Re-resolve every module image tag to its current digest (rewrites modules/*/template.yaml) + # Module images are pinned by digest so an upstream retag can never change a + # running game's binary. That also means new upstream builds only arrive when + # someone runs this. Each changed digest is a game binary that changes for + # every server on its next restart -- read the diff before committing. + # Entries marked `# gameplane:floating` are skipped on purpose. + # The gameplane-module repo also runs this monthly via refresh-pins.yml. + python3 modules/validate.py --pin + @echo "commit the rewritten modules/*/template.yaml in the gameplane-module repo + bump the submodule pointer" + # -------- local dev cluster (kind) -------- .PHONY: dev-up dev-down dev-load dev-push dev-install dev-up: ## Create/prepare cluster + install Gameplane (CLUSTER=kind|remote) diff --git a/docs/module-authoring.md b/docs/module-authoring.md index c0b7a2d4a..0cd02cd93 100644 --- a/docs/module-authoring.md +++ b/docs/module-authoring.md @@ -395,6 +395,8 @@ switch entries later from **Settings → Version** (the operator re-renders the StatefulSet and restarts the pod on the new entry). ```yaml +# Digests elided for readability — both refs below must actually be pinned +# as repo:tag@sha256:…; see §Pinning images by digest. image: itzg/minecraft-server:java21 # fallback when no version is selected versions: - id: "1.21.4-paper" # what GameServer.spec.version stores (≤ 40 chars) @@ -436,6 +438,68 @@ Semantics: with no mod manager). Use the registry's loader token verbatim (e.g. Modrinth's `paper`/`fabric`/`neoforge`) so browse filtering works. +### Pinning images by digest + +**Every image a user can get without choosing must be pinned to a digest.** +`validate.py` enforces this, and CI fails a module that breaks it. + +```yaml +image: itzg/minecraft-server:java21@sha256:f71555873bdaa6… # required form +``` + +Write the readable `repo:tag@sha256:…` form rather than a bare +`repo@sha256:…`. Kubernetes resolves by the digest either way — the tag is +decorative and carries no guarantee — but it keeps the ref legible in the +dashboard and in `kubectl describe`, and it is what the refresh tooling +re-resolves against. + +*Why:* a floating tag means a pod restart can swap the game binary under a +live world, with no version bump and no changelog. This is not hypothetical. +`passivelemon/terraria-docker:terraria-latest` moved from Terraria 1.4.4.9 to +1.4.5.6, changed the network protocol version, and broke the e2e Terraria bot +— the first anyone knew of it was a red CI job. + +What the rule requires, exactly: + +| Field | Requirement | +|---|---| +| `spec.image` | **must** be pinned (ERROR) — it runs when no version is selected | +| a `default: true` version entry | **must** be pinned (ERROR) — it runs when the user doesn't choose | +| any other version entry | pinned by default; may float if marked (WARN) | + +A non-default entry may deliberately keep moving. Mark its image line and the +pinner will leave it alone: + +```yaml + - id: latest + displayName: "Experimental channel" + image: factoriotools/factorio:latest # gameplane:floating +``` + +Use this only where tracking upstream *is* the entry's purpose and its +`displayName` makes the drift obvious — factorio's "Experimental channel", +terraria's "tModLoader latest preview". Selecting one is then the user's +explicit, labelled choice rather than a silent default. + +Pinning is mechanical; don't hand-write digests: + +```sh +make module-pin # rewrites every modules/*/template.yaml in place +``` + +It resolves each tag through the registry's standard bearer-token flow (any +OCI registry, not just Docker Hub), preserves the templates' comments, and +leaves any ref it cannot resolve untouched rather than guessing. The digest it +writes is the **tag manifest's** digest — for a multi-arch image that is the +index digest covering every published platform, so a pinned module still runs +on both amd64 and arm64. + +Because pinning stops upstream releases from arriving on their own, the +`gameplane-module` repo runs `refresh-pins.yml` monthly to re-resolve every +pin and open a PR. It never auto-merges: each changed digest is a game binary +that changes for every server on its next restart, which is exactly the event +worth a human's attention. + ### Config schema → wizard Declared `configSchema` fields become inputs in the "New server" diff --git a/docs/roadmap.md b/docs/roadmap.md index 512922cbd..9ea5206f4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -94,8 +94,12 @@ production-readiness hardening below — tracked items, not code gaps. ### Production-readiness hardening -- A documented backup/restore drill, and restore-path coverage against a real - restic repository. +- A documented backup/restore **drill** — a runbook an operator can follow. + The *coverage* half of this item is already done and was stale here: + `TestRestore_RoundTrip` (`test/e2e/restore_e2e_test.go`) writes a marker into + a server's PVC, backs it up, wipes it, restores, and asserts the bytes came + back — against the real in-cluster restic-server that `ensureResticRepo` + provisions. What is missing is the human-facing runbook, not the test. - Upgrade testing across at least one minor version, including the CRD-upgrade caveat (Helm never updates CRDs on `helm upgrade`; see [Helm CRD caveat](install.md#helm-crd-caveat)).