Skip to content
Open
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
13 changes: 11 additions & 2 deletions agent/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ func main() {
transport = rcon.New(rconHost, rconPort, rcon.PasswordFromFile(rconPassFile))
}

flushOnly := rconEnabled && capSpec != nil && capSpec.BackupFlush == "minecraft-save-all" && gameName == "minecraft-java" && capSpec.Quiesce == nil
commands := rcon.NewCommands(transport)
rconClient := transport
if quiesceSpec != nil {
if quiesceSpec != nil || flushOnly {
rconClient = commands
}
r := chi.NewRouter()
Expand Down Expand Up @@ -219,9 +220,17 @@ func main() {
for _, version := range []string{"v1", "v2"} {
r.Route("/"+version+"/pods/{uid}", func(protected chi.Router) {
protected.Use(authCheck.Middleware, target.Guard(envOr("GAMEPLANE_POD_UID", "")), middleware.Timeout(30*time.Second))
protected.Get("/identity", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
protected.Get("/identity", func(w http.ResponseWriter, _ *http.Request) {
if version == "v2" && flushOnly {
w.Header().Set("Gameplane-Backup-Mode", "flush-only")
}
w.WriteHeader(http.StatusNoContent)
})
if version == "v2" {
quiesce.MountVerified(protected, commands, gameName, quiesceSpec)
if flushOnly {
quiesce.MountMinecraftFlush(protected, commands)
}
} else {
quiesce.Mount(protected, commands, gameName, quiesceSpec)
}
Expand Down
13 changes: 7 additions & 6 deletions agent/internal/caps/caps.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import (

// Spec is the root capabilities document.
type Spec struct {
Players *PlayerActions `json:"players,omitempty"`
Quiesce *Quiesce `json:"quiesce,omitempty"`
Lifecycle *Lifecycle `json:"lifecycle,omitempty"`
Actions []ServerAction `json:"actions,omitempty"`
Status *Status `json:"status,omitempty"`
Mods *Mods `json:"mods,omitempty"`
BackupFlush string `json:"backupFlush,omitempty"`
Players *PlayerActions `json:"players,omitempty"`
Quiesce *Quiesce `json:"quiesce,omitempty"`
Lifecycle *Lifecycle `json:"lifecycle,omitempty"`
Actions []ServerAction `json:"actions,omitempty"`
Status *Status `json:"status,omitempty"`
Mods *Mods `json:"mods,omitempty"`
}

// Lifecycle declares command sequences the agent runs over RCON to drive
Expand Down
16 changes: 16 additions & 0 deletions agent/internal/quiesce/quiesce.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ func mount(r chi.Router, rc *rcon.Commands, game string, q Quiescer, verified bo
handle("/unquiesce", "resumed", false, q.Unquiesce)
}

// MountMinecraftFlush cannot pause or resume saving. Even a lost response
// leaves the game free to write; the caller can repeat the same checkpoint.
func MountMinecraftFlush(r chi.Router, rc *rcon.Commands) {
r.Post("/flush", func(w http.ResponseWriter, req *http.Request) {
err := rc.Sequence(req.Context(), func(transport rcon.Executor) error {
_, err := (minecraftReplies{transport}).Exec("save-all flush")
return err
})
if err != nil {
httpjson.Write(w, http.StatusBadGateway, response{Reason: "checkpoint was not acknowledged"})
return
}
httpjson.Write(w, http.StatusOK, response{Outcome: "flushed"})
})
}

type minecraftReplies struct{ Rcon }

// Vanilla Java 1.21.4 en_us commands.save.* messages. RCON can concatenate
Expand Down
21 changes: 21 additions & 0 deletions agent/internal/quiesce/verified_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ func TestMinecraftPositiveCommandResponses(t *testing.T) {
}
}

func TestMinecraftCheckpointLeavesSavingEnabled(t *testing.T) {
for _, tc := range []struct {
reply string
code int
outcome string
}{
{"Saved the game", http.StatusOK, "flushed"},
{"Saving the game (this may take a moment!)", http.StatusBadGateway, ""},
} {
rc := &fakeRcon{respond: func(string) (string, error) { return tc.reply, nil }}
router := chi.NewRouter()
MountMinecraftFlush(router, rcon.NewCommands(rc))
w := httptest.NewRecorder()
router.ServeHTTP(w, httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/flush", nil))
got := decodeResponse(t, w.Body.Bytes())
if w.Code != tc.code || got.Quiesced || got.Outcome != tc.outcome || !reflect.DeepEqual(rc.calls(), []string{"save-all flush"}) {
t.Fatalf("checkpoint reply %q: HTTP %d, %+v, commands %v", tc.reply, w.Code, got, rc.calls())
}
}
}

func TestVerifiedSequenceBlocksCleanupUntilRollbackFinishes(t *testing.T) {
reached, release := make(chan struct{}), make(chan struct{})
var once sync.Once
Expand Down
24 changes: 23 additions & 1 deletion agent/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,30 @@ paths:
get:
summary: Verify the pod UID and availability of the verified backup protocol
responses:
"204": { description: Matching pod UID }
"204":
description: Matching pod UID
headers:
Gameplane-Backup-Mode:
description: Present for a checkpoint that leaves writes enabled
schema: { type: string, enum: [flush-only] }
"404": { description: Missing or mismatched pod UID }
/v2/pods/{uid}/flush:
parameters: [*podUID]
post:
summary: Checkpoint the running game while writes stay enabled
responses:
"200":
description: The game acknowledged its checkpoint
content:
application/json:
schema:
type: object
required: [outcome, quiesced]
properties:
outcome: { type: string, enum: [flushed] }
quiesced: { type: boolean, enum: [false] }
"404": { description: Missing adapter or mismatched pod UID }
"502": { description: The game did not acknowledge the checkpoint }
/v2/pods/{uid}/quiesce:
parameters: [*podUID]
post:
Expand Down
5 changes: 5 additions & 0 deletions agent/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ The agent is a per-pod HTTP/HTTPS sidecar that runs inside every game pod to exp

## Responsibilities

The v2 identity response advertises `Gameplane-Backup-Mode: flush-only`
for a configured Minecraft Java checkpoint adapter. Its pod-bound
`POST /v2/pods/{uid}/flush` checks the game's acknowledgment of
`save-all flush` and keeps automatic saving enabled.

- **Request authentication**: Verify incoming requests via mTLS (preferred) or shared-secret bearer token.
- **Console & RCON**: Duplex WebSocket forwarding user commands to the game via RCON and echoing responses; supports Valve/Source, Telnet, WebSocket, BattlEye, Satisfactory, and Palworld protocols.
- **File I/O**: List, read, download, upload, write, mkdir, delete within the `/data` volume; reject path traversal and any symlink traversal; every operation runs through directory descriptors rooted at the data directory, so it cannot be redirected by a concurrent filesystem change.
Expand Down
5 changes: 5 additions & 0 deletions changelog/unreleased/907-backup-consistency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Backup consistency reporting

Backup records distinguish an acknowledged save with writes still enabled, a verified write
freeze and a stopped capture. Historical backups with unverified hooks remain restorable and
report unknown consistency. [PR #907](https://github.com/GameplanePanel/Gameplane/pull/907)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: backups.gameplane.local
spec:
Expand Down Expand Up @@ -166,8 +166,9 @@ spec:
type: string
quiesceOutcome:
description: |-
QuiesceOutcome is confirmed, already-stopped, unsupported, not-requested,
agent-unavailable, or unknown, independent of subsequent copy success.
QuiesceOutcome records verified-freeze, already-stopped, flush-only or
a weaker observation independently of copy success. Historical confirmed
records lack pod-bound proof of a write freeze.
type: string
recordedAt:
format: date-time
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: backupschedules.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: clusters.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: gameservers.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: gametemplates.gameplane.local
spec:
Expand Down Expand Up @@ -291,6 +291,12 @@ spec:
> 0)
maxItems: 32
type: array
backupFlush:
description: BackupFlush requests an acknowledged checkpoint without
disabling writes.
enum:
- minecraft-save-all
type: string
lifecycle:
description: |-
Lifecycle configures graceful lifecycle transitions the agent drives
Expand Down Expand Up @@ -1016,6 +1022,9 @@ spec:
type: array
type: object
type: object
x-kubernetes-validations:
- message: choose a flush-only checkpoint or a write-freeze declaration
rule: '!(has(self.backupFlush) && has(self.quiesce))'
categories:
description: |-
Categories group this game in the dashboard's catalog and Create
Expand Down Expand Up @@ -2549,6 +2558,9 @@ spec:
rule: '!has(self.capabilities) || !has(self.capabilities.mods) || !has(self.capabilities.mods.loaders)
|| size(self.capabilities.mods.loaders) == 0 || (has(self.versions)
&& size(self.versions) > 0)'
- message: backupFlush requires the Minecraft Java adapter
rule: '!has(self.capabilities) || !has(self.capabilities.backupFlush)
|| self.game == ''minecraft-java'''
status:
description: |-
GameTemplateStatus is intentionally minimal — templates are mostly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: modules.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: modulesources.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: networkcaptures.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: restores.gameplane.local
spec:
Expand Down
7 changes: 4 additions & 3 deletions charts/gameplane/crd-manifests/gameplane.local_backups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: backups.gameplane.local
spec:
Expand Down Expand Up @@ -166,8 +166,9 @@ spec:
type: string
quiesceOutcome:
description: |-
QuiesceOutcome is confirmed, already-stopped, unsupported, not-requested,
agent-unavailable, or unknown, independent of subsequent copy success.
QuiesceOutcome records verified-freeze, already-stopped, flush-only or
a weaker observation independently of copy success. Historical confirmed
records lack pod-bound proof of a write freeze.
type: string
recordedAt:
format: date-time
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: backupschedules.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: clusters.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: gameservers.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: gametemplates.gameplane.local
spec:
Expand Down Expand Up @@ -291,6 +291,12 @@ spec:
> 0)
maxItems: 32
type: array
backupFlush:
description: BackupFlush requests an acknowledged checkpoint without
disabling writes.
enum:
- minecraft-save-all
type: string
lifecycle:
description: |-
Lifecycle configures graceful lifecycle transitions the agent drives
Expand Down Expand Up @@ -1016,6 +1022,9 @@ spec:
type: array
type: object
type: object
x-kubernetes-validations:
- message: choose a flush-only checkpoint or a write-freeze declaration
rule: '!(has(self.backupFlush) && has(self.quiesce))'
categories:
description: |-
Categories group this game in the dashboard's catalog and Create
Expand Down Expand Up @@ -2549,6 +2558,9 @@ spec:
rule: '!has(self.capabilities) || !has(self.capabilities.mods) || !has(self.capabilities.mods.loaders)
|| size(self.capabilities.mods.loaders) == 0 || (has(self.versions)
&& size(self.versions) > 0)'
- message: backupFlush requires the Minecraft Java adapter
rule: '!has(self.capabilities) || !has(self.capabilities.backupFlush)
|| self.game == ''minecraft-java'''
status:
description: |-
GameTemplateStatus is intentionally minimal — templates are mostly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: modules.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: modulesources.gameplane.local
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
gameplane.local/crd-bundle-sha256: 3b2e34cfb50a4b2312502522937d45442c012cb313195cd2a556f69af01f3a96
gameplane.local/crd-bundle-sha256: d3efd1281c2997c24937b847986ca018a49591589bbb105d8ed74fa1259003c8
controller-gen.kubebuilder.io/version: v0.22.0
name: networkcaptures.gameplane.local
spec:
Expand Down
Loading
Loading