fix(install): backups exist on a default install; retention resolves real world dirs (#6)

Three faces of one gap, all on the supported install path:

- Backup/restore answered 503 out of the box: nothing ever rendered the
  archive PVC, so FELIS_BACKUP_PVC was unset. The bundle now renders the
  PVC (Minecraft namespace, RWO 10Gi, cluster default class) and
  'felis manifests' names it by default (--backup-pvc= is the explicit
  no-store shape); bootstrap passes it through so the generated felis.toml
  [archive] local_path and the jobs' mount path come from one variable.
- Retention was unreachable: bootstrap never passed the reaper flags. It
  now forwards FELIS_WORLDS_HOST_PATH/FELIS_ARCHIVE_LOCAL_PATH, so one
  env enables the daily CronJob; unset keeps today's fail-safe (no reaper,
  nothing deleted).
- Even when enabled it could not find a world on a stock install:
  resolveWorldDir now also resolves the exact local-path directory
  <pv-name>_<ns>_<pvc-name> read from the live PVC's volumeName (never a
  glob, so a stale deleted PV's bytes can't be archived in place of the
  current world). Reaper Role gains persistentvolumeclaims:get (weaker
  than the delete it already held).

README (zh/en) stops promising automatic/scheduled backups and states
retention is opt-in. bootstrap_test covers the env->flag contract.
This commit is contained in:
Lemon-miaow committed 2026-09-22 20:48:07 +08:00
1 parent ff7c57cf9c
commit fd33fd05e1
14 files changed
+394 -82

No files matched your search

+33 -8
View File
@@ -77,6 +77,10 @@ func TestManifestsRendersBundle(t *testing.T) {
"10.0.0.5/32",
// The felis image flows through to the Deployments.
"registry.felis.svc:5000/felis:v1",
// Backup works out of the box: the archive PVC renders and the api gets
// the env that wires the backup/restore executors to it.
"name: felis-backups",
"name: FELIS_BACKUP_PVC",
} {
if !strings.Contains(text, want) {
t.Errorf("rendered bundle missing %q", want)
@@ -95,15 +99,19 @@ func TestManifestsRendersBundle(t *testing.T) {
}
}
// TestManifestsReaperRequiresTrio proves --worlds-host-path is a fail-loud opt-in:
// asking for the reaper without the backup PVC and its mount path (which must equal
// [archive] local_path) is rejected rather than silently dropping retention.
func TestManifestsReaperRequiresTrio(t *testing.T) {
// TestManifestsReaperRequiresStorage proves --worlds-host-path is a fail-loud
// opt-in: asking for the reaper without a writable archive store (the backup PVC,
// which defaults to felis-backups but can be emptied) and its mount path (which
// must equal [archive] local_path) is rejected rather than silently dropping
// retention or deleting worlds it could not archive first.
func TestManifestsReaperRequiresStorage(t *testing.T) {
base := []string{"manifests", "--felis-image", "reg/felis:test", "--velocity-cidr", "10.0.0.5/32", "--worlds-host-path", "/var/lib/felis/worlds"}
for _, extra := range [][]string{
{}, // neither backup-pvc nor archive-local-path
{"--backup-pvc", "felis-backups"}, // missing archive-local-path
{"--archive-local-path", "/backups"}, // missing backup-pvc
{}, // missing archive-local-path (backup-pvc defaults)
{"--backup-pvc", "other"}, // still missing archive-local-path
// A reaper with no archive store would have nowhere to write the archive
// it must verify before deleting a world; emptying the PVC is rejected.
{"--archive-local-path", "/backups", "--backup-pvc="},
} {
var out, errBuf bytes.Buffer
code := run(append(append([]string{}, base...), extra...), &out, &errBuf)
@@ -119,6 +127,23 @@ func TestManifestsReaperRequiresTrio(t *testing.T) {
}
}
// TestManifestsBackupPVCOptOut proves --backup-pvc= renders a bundle with no
// archive store at all: no PVC and no FELIS_BACKUP_PVC env, so backup/restore
// answer 503 instead of pointing Jobs at a claim nobody provisions.
func TestManifestsBackupPVCOptOut(t *testing.T) {
var out, errBuf bytes.Buffer
code := run([]string{"manifests", "--felis-image", "reg/felis:test",
"--velocity-cidr", "10.0.0.5/32", "--backup-pvc="}, &out, &errBuf)
if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, errBuf.String())
}
for _, absent := range []string{"felis-backups", "FELIS_BACKUP_PVC"} {
if strings.Contains(out.String(), absent) {
t.Errorf("--backup-pvc= bundle must not contain %q", absent)
}
}
}
// TestManifestsRendersReaper proves the happy path with the full retention trio:
// a batch/v1 CronJob is emitted, named felis-reaper, mounting the backup PVC at the
// supplied archive path.
@@ -150,7 +175,7 @@ func TestManifestsRendersReaper(t *testing.T) {
// this generator cannot verify (else a misarranged hostPath silently no-ops
// retention): the <path>/<pvc> arrangement-dependency and the multi-node
// nodeSelector hazard.
for _, want := range []string{"local-path-provisioner", "nodeSelector"} {
for _, want := range []string{"local-path", "nodeSelector"} {
if !strings.Contains(errBuf.String(), want) {
t.Errorf("reaper render must warn operators about %q on stderr, got %q", want, errBuf.String())
}