controller v0.241.0: a bind-data app leans on off-site before its own unit; the hold names what the copy holds (R-479)
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
Operator ruling 2026-09-13. An app with classified binds walks second drive -> off-site -> own unit (its unit holds no files); volume apps keep 2 -> 1 -> 3. RestoreHold.CopyHolds records what the chosen copy holds and the sentence ends with it; older holds keep their tier-only sentence. Tests on both halves; red-proof: a layout-blind order fails the bind case.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"fmt"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-479 (operator ruling 2026-09-13) — for an app whose data is bind-mounted files, the tiers are
|
||||
// walked second drive → off-site → own unit, and the hold sentence says what the chosen copy holds.
|
||||
|
||||
func r479Manager(t *testing.T, bindApp bool) *Manager {
|
||||
t.Helper()
|
||||
m, _ := newOffboxManager(t)
|
||||
prov := &offbox3aProvider{hdd: map[string]string{}, binds: map[string][]ClassifiedBind{}, has: map[string]bool{}, deployed: map[string]bool{"app": true}}
|
||||
if bindApp {
|
||||
prov.hdd["app"] = t.TempDir()
|
||||
prov.binds["app"] = []ClassifiedBind{{ComposeBind: appbackup.ComposeBind{RelPath: "appdata/app"}, Class: ClassMandatory}}
|
||||
prov.has["app"] = true
|
||||
}
|
||||
m.SetStackProvider(prov)
|
||||
return m
|
||||
}
|
||||
|
||||
// COMPANION RED-PROOF (REPORT.md): make UpdateTierOrderFor always return updateTierOrder — the
|
||||
// bind-app case then picks the own unit ahead of off-site and this fails.
|
||||
func TestR479_BindDataAppWalksSecondDriveOffsiteThenOwnUnit(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
bind bool
|
||||
want string
|
||||
}{{true, "[2 3 1]"}, {false, "[2 1 3]"}} {
|
||||
m := r479Manager(t, tc.bind)
|
||||
if got := fmt.Sprint(m.UpdateTierOrderFor("app")); got != tc.want {
|
||||
t.Errorf("bind=%v: order %s, want %s", tc.bind, got, tc.want)
|
||||
}
|
||||
if m.DataOutsideUnit("app") != tc.bind {
|
||||
t.Errorf("bind=%v: DataOutsideUnit must follow the classified binds", tc.bind)
|
||||
}
|
||||
}
|
||||
// The consequence, not only the mechanism: with Tier 2 absent and BOTH the unit and off-site
|
||||
// holding a copy, a bind-data app leans on OFF-SITE; a unit app on its own unit.
|
||||
for _, tc := range []struct {
|
||||
bind bool
|
||||
want int
|
||||
}{{true, UpdateTierOffsite}, {false, UpdateTierLocal}} {
|
||||
m := r479Manager(t, tc.bind)
|
||||
m.updateTier2PointFn = noTier2
|
||||
m.updateTier1PointsFn = tier1At(r475T0.Add(-time.Hour))
|
||||
m.updateOffsiteTimesFn = func(context.Context) (map[string]time.Time, error) {
|
||||
return map[string]time.Time{"app": r475T0.Add(-2 * time.Hour)}, nil
|
||||
}
|
||||
p, ok, _ := m.UpdateRestorePoints(context.Background(), "app", nil)
|
||||
if !ok || p.Tier != tc.want {
|
||||
t.Errorf("bind=%v: chose tier %d, want %d", tc.bind, p.Tier, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestR479_HoldSentenceNamesWhatTheCopyHolds(t *testing.T) {
|
||||
at := time.Date(2026, 9, 13, 8, 0, 0, 0, time.UTC)
|
||||
copyAt := time.Date(2026, 9, 13, 1, 30, 0, 0, time.UTC)
|
||||
m := r479Manager(t, true)
|
||||
m.logger = log.New(io.Discard, "", 0)
|
||||
holds := m.UpdateCopyHolds("app", UpdateTierLocal)
|
||||
if !strings.HasPrefix(holds, "csak a beállításokat") || !strings.HasSuffix(holds, "a fájlokat nem") {
|
||||
t.Fatalf("a bind-data app's own unit holds settings and the database only, got %q", holds)
|
||||
}
|
||||
if h := m.UpdateCopyHolds("app", UpdateTierOffsite); !strings.Contains(h, "a fájlokat") || strings.Contains(h, "csak") {
|
||||
t.Errorf("off-site holds the files too, got %q", h)
|
||||
}
|
||||
if err := m.HoldAfterFailedUpdateHolding("app", at, copyAt, UpdateTierLocal, holds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, why := m.RestoreHoldFor("app")
|
||||
want := fmt.Sprintf(UpdateHoldFmt, "app", "2026-09-13 10:00", "saját meghajtó", "2026-09-13 03:30", holds)
|
||||
if why != want {
|
||||
t.Errorf("hold text =\n%q\nwant\n%q", why, want)
|
||||
}
|
||||
if !strings.HasSuffix(why, "a fájlokat nem.") {
|
||||
t.Errorf("the sentence must END with what the copy does not hold, got %q", why)
|
||||
}
|
||||
// A hold recorded WITHOUT the phrase (v0.239.0–v0.240.0) keeps the tier-only sentence.
|
||||
if err := m.HoldAfterFailedUpdate("app2", at, copyAt, UpdateTierSecondDrive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, why2 := m.RestoreHoldFor("app2")
|
||||
if why2 != fmt.Sprintf(UpdateHoldTierFmt, "app2", "2026-09-13 10:00", "második meghajtó", "2026-09-13 03:30") {
|
||||
t.Errorf("tier-only hold text = %q", why2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user