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.
78 lines
2.8 KiB
Go
78 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
)
|
|
|
|
// TestResolveWorldDir pins the two world layouts the reaper must find, and the
|
|
// fail-closed miss. The stock local-path arm is derived from the live PVC's
|
|
// volumeName — a name-based guess (glob) could tar a stale deleted PV's bytes and
|
|
// then delete the current world, which is why it is read from the API instead.
|
|
func TestResolveWorldDir(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
|
|
// Arrange a world under the documented <root>/<pvc> layout.
|
|
named := filepath.Join(root, "world-named-0")
|
|
if err := os.MkdirAll(named, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Arrange a second world the way k3s local-path stores it.
|
|
pvDir := filepath.Join(root, "pvc-11111111-2222-3333-4444-555555555555_minecraft_world-live-0")
|
|
if err := os.MkdirAll(pvDir, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
claim := &corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "world-live-0", Namespace: "minecraft"},
|
|
Spec: corev1.PersistentVolumeClaimSpec{
|
|
VolumeName: "pvc-11111111-2222-3333-4444-555555555555",
|
|
},
|
|
}
|
|
cl := fake.NewClientBuilder().WithScheme(haltScheme(t)).WithObjects(claim).Build()
|
|
resolve := resolveWorldDir(ctx, cl, "minecraft", root)
|
|
|
|
t.Run("documented name layout wins", func(t *testing.T) {
|
|
got, err := resolve("world-named-0")
|
|
if err != nil || got != named {
|
|
t.Fatalf("resolve = (%q, %v), want (%q, nil)", got, err, named)
|
|
}
|
|
})
|
|
|
|
t.Run("stock local-path layout resolves exactly", func(t *testing.T) {
|
|
got, err := resolve("world-live-0")
|
|
if err != nil || got != pvDir {
|
|
t.Fatalf("resolve = (%q, %v), want (%q, nil)", got, err, pvDir)
|
|
}
|
|
})
|
|
|
|
t.Run("neither layout present falls back to the documented path", func(t *testing.T) {
|
|
// The claim exists but its directory does not: return the documented path so
|
|
// the archive walk fails there, and the reaper preserves the world.
|
|
missing := &corev1.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "world-gone-0", Namespace: "minecraft"},
|
|
Spec: corev1.PersistentVolumeClaimSpec{VolumeName: "pvc-99999999-0000-0000-0000-000000000000"},
|
|
}
|
|
cl := fake.NewClientBuilder().WithScheme(haltScheme(t)).WithObjects(missing).Build()
|
|
got, err := resolveWorldDir(ctx, cl, "minecraft", root)("world-gone-0")
|
|
if err != nil || got != filepath.Join(root, "world-gone-0") {
|
|
t.Fatalf("resolve = (%q, %v), want (%q, nil)", got, err, filepath.Join(root, "world-gone-0"))
|
|
}
|
|
})
|
|
|
|
t.Run("unknown pvc is an error, not a guess", func(t *testing.T) {
|
|
_, err := resolve("world-unknown-0")
|
|
if err == nil || !strings.Contains(err.Error(), "resolve world PVC world-unknown-0") {
|
|
t.Fatalf("err = %v, want a resolve-world-PVC error", err)
|
|
}
|
|
})
|
|
}
|