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:
14 files changed
+394
-82
No files matched your search
@@ -0,0 +1,77 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user