Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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/...

Expand All @@ -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)
Expand Down
64 changes: 64 additions & 0 deletions docs/module-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 6 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
Loading