fix(api): refuse backup/restore before a missing world volume
A server whose world PVC does not exist yet (never started) or no longer exists (the world was already reaped) accepted the backup/restore POST, answered 202, and the Job sat Pending on the missing claim until its deadline with nothing recorded anywhere — a silent no-op from the operator's seat. The live drill on the reaped `resolvecheck` world reproduced exactly that. Both handlers now read the world PVC (Cluster.WorldVolumeExists, over the same naming.WorldPVCName the Jobs mount) and answer a specific 409 no_world_volume with "start it once to create it, then retry". The felis-api Role gains the matching get-only PVC grant — the first live run surfaced the missing RBAC as a 403 behind a 500, so the fix ships with it. Live (auditfix38): resolvecheck -> 409 no_world_volume on both faces; test-one (which has a world) still backs up through the new gate end to end.
This commit is contained in:
8 files changed
+127
-3
No files matched your search
@@ -134,6 +134,22 @@ func TestBackupNow(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no world volume -> 409 no_world_volume, no backup", func(t *testing.T) {
|
||||
// A never-started (or reaped) server has no world PVC: the Job would hang
|
||||
// Pending on the missing claim with nothing recorded, so the gate must
|
||||
// refuse before the backuper is reached.
|
||||
api, _, cl, backuper := mk()
|
||||
cl.noWorld["survival"] = true
|
||||
api.External = staticExternal{p: owner}
|
||||
w := do(api.ExternalHandler(), "POST", path, "", nil)
|
||||
if w.Code != http.StatusConflict || decodeErr(t, w) != "no_world_volume" {
|
||||
t.Fatalf("code = %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
if backuper.calls != 0 {
|
||||
t.Fatal("a world-less server must not reach the backuper")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil Backuper -> 503 backup_unavailable", func(t *testing.T) {
|
||||
api, _, _, _ := mk()
|
||||
api.Backuper = nil
|
||||
@@ -240,6 +256,20 @@ func TestInternalBackup(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no world volume -> 409 no_world_volume, no backup", func(t *testing.T) {
|
||||
// The break-glass face shares enqueueBackup, so the world-volume gate must
|
||||
// hold here too — this is the face the TUI's Sync picker drives.
|
||||
api, _, cl, backuper := mk()
|
||||
cl.noWorld["survival"] = true
|
||||
w := do(api.InternalHandler(), "POST", path, "", jsonHeader)
|
||||
if w.Code != http.StatusConflict || decodeErr(t, w) != "no_world_volume" {
|
||||
t.Fatalf("code = %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
if backuper.calls != 0 {
|
||||
t.Fatal("a world-less server must not reach the backuper")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil Backuper -> 503 backup_unavailable", func(t *testing.T) {
|
||||
api, _, _, _ := mk()
|
||||
api.Backuper = nil
|
||||
|
||||
Reference in new issue
Block a user