controller v0.68.0: storage lifecycle on intermediary model (H2/H3/M1/M3 + boot-id)

H2 decommission UI button (migrate / anyway); H3 one-click re-enroll of a
decommissioned drive; M1 default reassignment (auto-promote + block-if-none);
M3 migrate re-asserts 2775 setgid on userdata dirs; deterministic guest-reboot
recreate via agent boot_id (replaces the timed sample). Fixes the {path}/{where}
H1 JS bug. Non-hollow tests + companions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 19:26:02 +02:00
parent bc41acf4da
commit 42f69dadda
10 changed files with 293 additions and 46 deletions
+14 -3
View File
@@ -851,9 +851,12 @@ func walkMerge(lg *log.Logger, srcNS, dstNS string, skip map[string]bool, assert
if err := os.MkdirAll(dst, 0o755); err != nil {
return err
}
// #8 (v0.66.0): preserve the SOURCE dir's full mode (incl. setgid) + group, so the userdata
// ownership convention (2775 setgid, gid 1000) survives a whole-drive migration. MkdirAll's
// mode is umask-masked + drops setgid, so re-stamp explicitly from the source.
// M3: userdata dirs ALWAYS get the 2775-setgid/gid-1000 convention RE-ASSERTED (not merely
// source-preserved), so a PRE-EXISTING stale 755 target dir is corrected regardless of the
// source mode. Other dirs keep #8's source-mode preservation.
if isUserdataDir(rel) {
return appbackup.EnsureUserdataDir(dst)
}
return preserveDirOwnership(dst, d)
}
@@ -1018,6 +1021,14 @@ func copyFile(src, dst string) (int64, error) {
// preserveDirOwnership re-stamps a freshly-created target dir with the SOURCE dir's full mode (incl.
// setgid) and group — part of the #8 fix so the userdata convention survives a migration.
// isUserdataDir reports whether a namespace-relative path is the userdata tree (the customer-facing
// shared-content area) — `userdata` itself or anything under it — which must carry the 2775-setgid
// convention. Normalised to forward slashes so it matches on any host.
func isUserdataDir(rel string) bool {
r := filepath.ToSlash(rel)
return r == "userdata" || strings.HasPrefix(r, "userdata/")
}
func preserveDirOwnership(dst string, d fs.DirEntry) error {
info, err := d.Info()
if err != nil {