controller v0.239.0: any backup tier lets an app update (R-475)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
Operator ruling 2026-09-13. The update precondition walks Tier 2, Tier 1 (own recovery unit, "helyi") and Tier 3 (off-site, 15 s bound; unreachable counts as absent with a WARN) and leans on the first FRESH copy; the backup_max_age rule applies to whichever tier is chosen. No copy anywhere: back up first. Refused only when nothing exists and no backup can be taken. RunAppBackupNow tolerates a Tier-2 failure (WARN) and marks the captured unit proven current. The hold names the tier (második meghajtó / saját meghajtó / távoli mentés) and the date; pre-v0.239.0 holds keep their text. A successful off-site restore now lifts an update hold. The backups page still uses Tier2UnitRestorePoint unchanged. Scenarios G-M tested; red-proofs M, L, the tail and the off-site clear in felhom.eu documentation/audits/rulings-r472-r475-2026-09-13/.
This commit is contained in:
@@ -24,8 +24,9 @@ import (
|
||||
// every path here refuses, or fails at the pin (the app's catalog template is absent on purpose).
|
||||
|
||||
type apiFakeGuards struct {
|
||||
b *backup.Manager
|
||||
rp stacks.UpdateRestorePoint
|
||||
b *backup.Manager
|
||||
points []stacks.UpdateRestorePoint
|
||||
cannotBackUp bool
|
||||
// blindToHolds makes the manager-side preflight NOT see holds, so a test can prove the ROUTER's
|
||||
// own hold check refuses — the two layers are each pinned separately (the preflight's by
|
||||
// TestSlice4_D_CheapRefusals/held). Without it, removing either layer passes inertly, because the
|
||||
@@ -40,14 +41,20 @@ func (g *apiFakeGuards) HoldFor(n string) (bool, string) {
|
||||
return g.b.RestoreHoldFor(n)
|
||||
}
|
||||
func (g *apiFakeGuards) Busy(string) (bool, string) { return false, "" }
|
||||
func (g *apiFakeGuards) RestorePoint(string) (stacks.UpdateRestorePoint, error) {
|
||||
return g.rp, nil
|
||||
func (g *apiFakeGuards) RestorePoints(_ context.Context, _ string, accept func(stacks.UpdateRestorePoint) bool) (stacks.UpdateRestorePoint, bool, []stacks.UpdateRestorePoint) {
|
||||
for _, p := range g.points {
|
||||
if accept == nil || accept(p) {
|
||||
return p, true, g.points
|
||||
}
|
||||
}
|
||||
return stacks.UpdateRestorePoint{}, false, g.points
|
||||
}
|
||||
func (g *apiFakeGuards) CanBackUp(string) (bool, string) { return !g.cannotBackUp, "fake: no drive" }
|
||||
func (g *apiFakeGuards) BackupNow(context.Context, string) error { return nil }
|
||||
func (g *apiFakeGuards) SafetyDump(context.Context, string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (g *apiFakeGuards) HoldAfterFailedUpdate(string, time.Time, time.Time) error { return nil }
|
||||
func (g *apiFakeGuards) HoldAfterFailedUpdate(string, time.Time, stacks.UpdateRestorePoint) error { return nil }
|
||||
|
||||
const slice4AppYAML = "deployed: true\nenv: {}\npinned_images:\n app: nginx:1.27\n"
|
||||
|
||||
@@ -82,7 +89,7 @@ func newSlice4Router(t *testing.T) (*Router, *settings.Settings, *apiFakeGuards,
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := backup.NewManager(cfg, sett, lg)
|
||||
g := &apiFakeGuards{b: b, rp: stacks.UpdateRestorePoint{Restorable: true, Proven: true, ProvenAt: time.Now().Add(-time.Hour)}}
|
||||
g := &apiFakeGuards{b: b, points: []stacks.UpdateRestorePoint{{Tier: stacks.UpdateTierSecondDrive, ProvenAt: time.Now().Add(-time.Hour)}}}
|
||||
m.SetUpdateGuards(g)
|
||||
return &Router{cfg: cfg, stackMgr: m, backupMgr: b, logger: lg}, sett, g, dir
|
||||
}
|
||||
@@ -105,7 +112,7 @@ func postUpdate(t *testing.T, r *Router) (int, apiResponse) {
|
||||
// message — which is what proves the router line is the one doing it.
|
||||
func TestR439_UpdateOfAHeldAppIsRefused(t *testing.T) {
|
||||
r, sett, g, dir := newSlice4Router(t)
|
||||
g.rp = stacks.UpdateRestorePoint{} // the preflight's own refusal would say "no backup" — not the hold
|
||||
g.points, g.cannotBackUp = nil, true // the preflight's own refusal would say "no backup" — not the hold
|
||||
g.blindToHolds = true // only the router's line can produce the hold's sentence
|
||||
if err := sett.SetRestoreHold(settings.RestoreHold{Stack: "app", At: "2026-09-13T08:00:00Z", Reason: settings.HoldReasonUpdateFailed, CopyDate: "2026-09-13T01:30:00Z"}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -124,7 +131,7 @@ func TestR439_UpdateOfAHeldAppIsRefused(t *testing.T) {
|
||||
|
||||
func TestSlice4_Router_NoBackupIs409AndRecordsNothing(t *testing.T) {
|
||||
r, _, g, dir := newSlice4Router(t)
|
||||
g.rp = stacks.UpdateRestorePoint{Restorable: false}
|
||||
g.points, g.cannotBackUp = nil, true
|
||||
before, _ := os.ReadFile(filepath.Join(dir, "app.yaml"))
|
||||
code, resp := postUpdate(t, r)
|
||||
if code != http.StatusConflict || resp.Error != fmt.Sprintf(stacks.MsgUpdateNoBackupFmt, "app") {
|
||||
|
||||
Reference in New Issue
Block a user