controller v0.240.0: seven defects from the any-tier proof and the first nightly rotation
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
R-486 (P1): removing an app with its backups KEPT keeps its Tier-2 record, so the second-drive restore is no longer refused over an intact mirror. R-484: postgis/pgvector/timescaledb images are Postgres (logical dumps). R-485: the backup card sizes the recovery unit and the mirror(s). R-480: a held update's sentence leaves the card once the hold is lifted. R-477: the update's off-site lookup is one snapshots call, no stats. R-478: a copy older than this install's deploy does not count. R-474: "delete backups" deletes the unit, the mirror(s) and the prefs. Tests and red-proofs per row; evidence in felhom.eu documentation/audits/v0240-2026-09-13/ and nightly-2026-09-13-adventurelog/.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// R-474 (v0.240.0) — "delete backups" on removal deletes the app's Tier-2 mirror too.
|
||||
//
|
||||
// Measured three times on 2026-09-13: removing an app with `remove_backups:true` deleted only its
|
||||
// db-dumps directory; the recovery unit, the volume tars and the Tier-2 mirror all survived, and a
|
||||
// later reinstall of the same app then leaned on the old install's unit as its "fresh" restore point
|
||||
// (R-478). The unit is inside the app's own backups base and RemoveStack deletes it; the MIRROR lives
|
||||
// on another drive, outside that base, so it is removed here, with its own path check.
|
||||
|
||||
func validMirrorStackName(n string) bool {
|
||||
return n != "" && n != "." && n != ".." && n != SharesPseudoStack && !strings.ContainsAny(n, `/\`)
|
||||
}
|
||||
|
||||
// tier2MirrorRoots are the namespace roots a Tier-2 mirror of this app can live under: the recorded
|
||||
// destination, every registered drive, and the system data path (a mirror outlives a changed target).
|
||||
func (m *Manager) tier2MirrorRoots(stackName string) []string {
|
||||
seen := map[string]bool{}
|
||||
var roots []string
|
||||
add := func(r string) {
|
||||
if r == "" {
|
||||
return
|
||||
}
|
||||
r = filepath.Clean(r)
|
||||
if filepath.IsAbs(r) && !seen[r] {
|
||||
seen[r] = true
|
||||
roots = append(roots, r)
|
||||
}
|
||||
}
|
||||
if m.settings != nil {
|
||||
if cfg := m.settings.GetCrossDriveConfig(stackName); cfg != nil {
|
||||
add(cfg.DestinationPath)
|
||||
}
|
||||
for _, sp := range m.settings.GetStoragePaths() {
|
||||
if sp.Path != "" {
|
||||
add(NamespaceRootFor(sp.Path, m.systemDataPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.systemDataPath != "" {
|
||||
add(NamespaceRootFor(m.systemDataPath, m.systemDataPath))
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
// Tier2MirrorDirsForApp lists the app's Tier-2 mirror directories that exist now. Call it BEFORE the
|
||||
// removal clears the app's cross-drive record, which is one of the places it looks.
|
||||
func (m *Manager) Tier2MirrorDirsForApp(stackName string) []string {
|
||||
if m == nil || !validMirrorStackName(stackName) {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, root := range m.tier2MirrorRoots(stackName) {
|
||||
d := filepath.Join(root, "backups", "secondary", stackName)
|
||||
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// RemoveTier2Mirrors deletes the given mirror directories, each only if it is exactly
|
||||
// <root>/backups/secondary/<stackName> for one of the app's mirror roots — never another app's mirror,
|
||||
// never the shares mirror, never a path that merely cleans to one. Returns "path (size)" per removal.
|
||||
func (m *Manager) RemoveTier2Mirrors(stackName string, dirs []string) []string {
|
||||
if m == nil || !validMirrorStackName(stackName) {
|
||||
return nil
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, root := range m.tier2MirrorRoots(stackName) {
|
||||
allowed[filepath.Join(root, "backups", "secondary", stackName)] = true
|
||||
}
|
||||
var removed []string
|
||||
for _, d := range dirs {
|
||||
if !allowed[d] {
|
||||
m.logger.Printf("[WARN] [backup] remove %s: refusing to delete %q — not this app's Tier-2 mirror", stackName, d)
|
||||
continue
|
||||
}
|
||||
size := humanizeBytes(dirSizeBytes(d))
|
||||
if err := os.RemoveAll(d); err != nil {
|
||||
m.logger.Printf("[ERROR] [backup] remove %s: deleting the Tier-2 mirror %s failed: %v", stackName, d, err)
|
||||
continue
|
||||
}
|
||||
m.logger.Printf("[INFO] [backup] remove %s: Tier-2 mirror deleted: %s (%s)", stackName, d, size)
|
||||
removed = append(removed, fmt.Sprintf("%s (%s)", d, size))
|
||||
}
|
||||
return removed
|
||||
}
|
||||
Reference in New Issue
Block a user