fix: preserve snapshot symlinks and guard incomplete upgrade copies

This commit is contained in:
2026-10-18 10:12:53 +08:00
parent 1c244e135f
commit ed72985e7b
8 changed files with 192 additions and 45 deletions
+48 -1
View File
@@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"strings"
"syscall"
"testing"
)
@@ -122,6 +123,12 @@ func TestSnapshotFullRestore(t *testing.T) {
for _, running := range []bool{false, true} {
t.Run(fmt.Sprint(running), func(t *testing.T) {
a, i, home, id := snapshotRestoreFixture(t, running, "")
links := map[string]string{"absolute": "/workspace/example", "dangling": "missing", "loop": "loop", "relative": "data"}
for name, target := range links {
if err := os.Symlink(target, filepath.Join(filepath.Dir(home), id, name)); err != nil {
t.Fatal(err)
}
}
if err := a.recordSnapshot(i, id, i.Version, i.ImageID); err != nil {
t.Fatal(err)
}
@@ -140,6 +147,12 @@ func TestSnapshotFullRestore(t *testing.T) {
}
}
current, _ := a.s.GetInstance(i.ID)
for name, target := range links {
got, err := os.Readlink(filepath.Join(home, name))
if err != nil || got != target {
t.Fatalf("restored link %s: %q %v", name, got, err)
}
}
want := "exited"
if running {
want = "running"
@@ -169,6 +182,40 @@ func TestSnapshotRestoreRollback(t *testing.T) {
}
}
func TestIncompleteSnapshotNotRestorable(t *testing.T) {
a, i, _, id := snapshotRestoreFixture(t, false, "")
if _, err := a.s.DB.Exec("CREATE TABLE snapshot_copy_state (instance_id TEXT, id TEXT, ready INTEGER)"); err != nil {
t.Fatal(err)
}
if _, err := a.s.DB.Exec("INSERT INTO snapshot_copy_state VALUES(?,?,0)", i.ID, id); err != nil {
t.Fatal(err)
}
w := configCall(a, i, "GET", "snapshots", "")
var list []snapshotInfo
if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &list) != nil || len(list) != 1 || list[0].Restorable || list[0].RestoreReason != "snapshot copy incomplete" {
t.Fatal(w.Code, w.Body.String())
}
w = configCall(a, i, "POST", "snapshots", snapshotRestoreBody(id, true))
if w.Code != 409 || !strings.Contains(w.Body.String(), "snapshot copy incomplete") {
t.Fatal(w.Code, w.Body.String())
}
if err := a.recordSnapshot(i, id, i.Version, i.ImageID); err != nil {
t.Fatal(err)
}
if _, err := a.s.DB.Exec("UPDATE snapshot_copy_state SET ready=1"); err != nil {
t.Fatal(err)
}
if _, _, reason := a.snapshotCompatibility(i, id); reason != "" {
t.Fatal(reason)
}
if _, err := a.s.DB.Exec("ALTER TABLE snapshot_copy_state RENAME COLUMN ready TO unavailable"); err != nil {
t.Fatal(err)
}
if _, _, reason := a.snapshotCompatibility(i, id); reason != "snapshot completion metadata unavailable" {
t.Fatal(reason)
}
}
func TestSnapshotRestoreGuards(t *testing.T) {
for _, test := range []string{"unknown", "mismatch", "password", "confirm", "reader", "quota", "error", "unsafe", "foreign", "oversized"} {
t.Run(test, func(t *testing.T) {
@@ -196,7 +243,7 @@ func TestSnapshotRestoreGuards(t *testing.T) {
case "error":
a.s.SetInstance(i.ID, "error")
case "unsafe":
if err := os.Symlink("/etc/passwd", filepath.Join(filepath.Dir(home), id, "unsafe")); err != nil {
if err := syscall.Mkfifo(filepath.Join(filepath.Dir(home), id, "unsafe"), 0600); err != nil {
t.Fatal(err)
}
want = 400