Add POST /api/v1/servers/{name}/backup: an owner or admin snapshots a
stopped server's world into the archive store on demand, recorded as a
first-class world_backups row (reason `manual`) — restorable by the
existing restore path and expired by the reaper's retention pass, so it
never leaks as an orphan archive. This is the break-glass "Sync" op,
resolved as immediate/on-demand backup.
felis-api cannot archive in-process (the world PVC is RWO, held by the
operator StatefulSet), so the work hands off to a one-shot Kubernetes Job
(new internal/backupjob) that mounts the world PVC read-only and the
backup PVC read-write, plus the felis config Secret so it self-records
its row atomically like the reaper. The Pod mirrors restore's weak-SA
isolation (SA token un-mounted, non-root, read-only rootfs, drop ALL);
the one reviewed departure is that config-Secret mount, frozen by
jobspec_test.go. Handler answers 202 backing_up; gated on the server
being Stopped (RWO world PVC), owner-or-admin, and FELIS_IMAGE +
FELIS_BACKUP_PVC being wired (else 503 backup_unavailable).
Each request mints a unique Job name (backup-<server>-<rand>) so a repeat
on-demand backup produces a fresh archive rather than colliding with a
just-finished Job still inside its TTL window and silently no-op'ing the
retry.
67 lines
2.1 KiB
Go
67 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
const usage = `felis — Kubernetes-native Minecraft server orchestration
|
|
|
|
Usage:
|
|
felis <command> [flags]
|
|
|
|
Commands:
|
|
migrate up Apply embedded database migrations under an advisory lock
|
|
operator Run the MinecraftServer controller-manager
|
|
api Run the felis-api HTTP server
|
|
reaper Run the world reaper / backup batch
|
|
restore Extract a world archive into a world volume (internal Job entrypoint)
|
|
backup Archive a world into the backup store and record it (internal Job entrypoint)
|
|
manifests Render the control-plane RBAC + NetworkPolicy install bundle as YAML
|
|
apply Create a MinecraftServer CRD (direct K8s write; use -f server.json)
|
|
setup Run host bootstrap + first-run setup console (TUI; requires root/sudo)
|
|
breakGlass Open the local break-glass emergency console (TUI; requires root/sudo)
|
|
|
|
Run "felis <command> -h" for command-specific flags.
|
|
`
|
|
|
|
// run dispatches a subcommand. It is separate from main so the router is
|
|
// testable without spawning a process.
|
|
func run(args []string, stdout, stderr io.Writer) int {
|
|
if len(args) == 0 {
|
|
fmt.Fprint(stderr, usage)
|
|
return 2
|
|
}
|
|
cmd, rest := args[0], args[1:]
|
|
switch cmd {
|
|
case "migrate":
|
|
return cmdMigrate(rest, stdout, stderr)
|
|
case "operator":
|
|
return cmdOperator(rest, stdout, stderr)
|
|
case "api":
|
|
return cmdAPI(rest, stdout, stderr)
|
|
case "reaper":
|
|
return cmdReaper(rest, stdout, stderr)
|
|
case "restore":
|
|
return cmdRestore(rest, stdout, stderr)
|
|
case "backup":
|
|
return cmdBackup(rest, stdout, stderr)
|
|
case "manifests":
|
|
return cmdManifests(rest, stdout, stderr)
|
|
case "apply":
|
|
return cmdApply(rest, stdout, stderr)
|
|
case "setup":
|
|
return cmdSetup(rest, stdout, stderr)
|
|
case "breakGlass":
|
|
return cmdBreakGlass(rest, stdout, stderr)
|
|
case "bootstrap-assets":
|
|
return cmdBootstrapAssets(rest, stdout, stderr)
|
|
case "-h", "--help", "help":
|
|
fmt.Fprint(stdout, usage)
|
|
return 0
|
|
default:
|
|
fmt.Fprintf(stderr, "felis: unknown command %q\n\n%s", cmd, usage)
|
|
return 2
|
|
}
|
|
}
|