R-108: network storage may not host an app's data namespace (v0.187.0)
This is D5's precondition and it is now met. An app's namespace root IS its backup root: namespaceRoot returns a non-system drive path as-is, so the recovery unit lands at <HDD_PATH>/backups/primary/<stack>/. On a NAS that sits inside the share, which FileBrowser binds WHOLE — share root, :rslave, download:true. The bind was NOT narrowed, and establishing why inverted the fix. The share-root :rslave bind is load-bearing (a 2026-07-22 probe proved an in-container access through it wakes the idle automount trigger), and scoping is undefinable anyway: apps on a share store at <share>/<app>, there is no userdata/ layer, and creating one would write Felhom convention onto a customer's own NAS, which R-67 forbids. So the browsing surface cannot be narrowed and the backup tree must never be placed under it. Operator ruling: refuse the placement, keep the browse bind. Tier 2 already refuses network targets for this reason (F-6C-1). Nothing stranded: zero apps on network storage across all six hub customers including Peti. R-67's browse capability is byte-identical. FIVE surfaces, not the four the register named — settings.RefuseAsAppNamespace is the single predicate. The deploy POST is the real boundary (it accepts any caller-supplied HDD_PATH; DeployStack validates only os.Stat). Surface 4, handleStorageDecommission mode=migrate, guarded only its SOURCE, so a whole namespace could be decommissioned ONTO a NAS — that one is not in the register. Fails closed: /mnt/felhom-drives holds both kinds, Kind exists only on a registered path, so an unregistered path under that root refuses. Supersedes README's "NAS backup locality — decision A" (v0.118.0). 9 tests, all non-effect (nil stackMgr, so a guard that misses panics rather than passing). 4 red-proofs, each mutation asserted to have landed. Suite rc=0, 27 packages, 0 FAIL. vet rc=0. Template + emoji gates OK.
This commit is contained in:
@@ -90,6 +90,12 @@ type DeployStoragePath struct {
|
||||
settings.StoragePath
|
||||
FreeHuman string // "234.5 GB"
|
||||
FreePercent float64 // 67.5
|
||||
// NotAllowed marks a path that CANNOT host an app's data namespace (R-108: network storage). The
|
||||
// option is rendered PRESENT-but-disabled with NotAllowedNote rather than dropped: a NAS the
|
||||
// customer registered themselves, silently absent from the list they expect it in, reads as a bug
|
||||
// and generates a support question. Present with a reason answers the question in place.
|
||||
NotAllowed bool
|
||||
NotAllowedNote string // short parenthetical for the option label; "" when allowed
|
||||
}
|
||||
|
||||
// StorageAppDetail holds info about an app using a specific storage path.
|
||||
@@ -462,6 +468,12 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
||||
var deployPaths []DeployStoragePath
|
||||
for _, sp := range s.settings.GetSchedulableStoragePaths() {
|
||||
dp := DeployStoragePath{StoragePath: sp}
|
||||
// R-108: mark, do not hide. The server-side refusal in the deploy POST is the boundary; this is
|
||||
// the honest UI over it, and it must not be mistaken for the boundary itself.
|
||||
if refuse, _ := s.settings.RefuseAsAppNamespace(sp.Path); refuse {
|
||||
dp.NotAllowed = true
|
||||
dp.NotAllowedNote = "hálózati tárhely — alkalmazáshoz nem választható"
|
||||
}
|
||||
if di := system.GetDiskUsage(sp.Path); di != nil {
|
||||
dp.FreeHuman = formatFreeSpace(di.AvailGB)
|
||||
if di.TotalGB > 0 {
|
||||
@@ -676,6 +688,13 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
if sp.Path == current || sp.Decommissioned || sp.Disconnected || !sp.Schedulable {
|
||||
continue
|
||||
}
|
||||
// R-108: never OFFER network storage as a migrate target — an app namespace may not live
|
||||
// there. Dropped rather than shown-disabled: unlike the deploy page this list has no
|
||||
// explanatory surface, and a target that cannot be chosen is not a target. The refusal that
|
||||
// MATTERS is server-side in handleStorageMigrateApp; this only keeps the UI honest.
|
||||
if refuse, _ := s.settings.RefuseAsAppNamespace(sp.Path); refuse {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, sp)
|
||||
}
|
||||
data["MigrateTargets"] = targets
|
||||
|
||||
Reference in New Issue
Block a user