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:
@@ -1330,6 +1330,71 @@ func (s *Settings) IsNetworkStoragePath(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RefuseAsAppNamespace reports whether `path` must be REFUSED as an app's data namespace (its
|
||||
// HDD_PATH), and why. It is the single predicate every placement surface consults — R-108.
|
||||
//
|
||||
// WHY AN APP NAMESPACE MAY NOT LIVE ON A NAS (operator ruling, 2026-07-30). An app's namespace root is
|
||||
// also where its backups go: `namespaceRoot(drivePath)` returns a non-system drive path AS-IS, so the
|
||||
// app's recovery unit lands at `<path>/backups/primary/<stack>/` (appbackup.RecoveryUnitPath). For a
|
||||
// network share that directory would sit inside the share ROOT — which FileBrowser binds whole, with
|
||||
// `download: true`, and MUST keep binding whole: the `:rslave` share-root bind is load-bearing for
|
||||
// automount wake/idle propagation into the running container (R-67), and scoping it is impossible
|
||||
// besides — apps on a share store at `<share>/<app>`, there is no `userdata/` layer, and creating one
|
||||
// would write Felhom's convention onto a customer's own NAS, which R-67 forbids outright.
|
||||
//
|
||||
// So the browsing surface cannot be narrowed and the backup tree must therefore never be placed under
|
||||
// it. Tier 2 already refuses network targets for exactly this class of reason (F-6C-1); this closes the
|
||||
// PRIMARY namespace, which was the remaining way a `backups/` tree could appear inside a share-root
|
||||
// bind. That is the precondition D5 was waiting on.
|
||||
//
|
||||
// FAIL CLOSED, and the two non-obvious cases are why this is a function and not an `IsNetwork()` call:
|
||||
//
|
||||
// - `NetworkMountRoot` holds BOTH kinds in-guest (`/mnt/felhom-drives/hdd_1` is a local drive,
|
||||
// `/mnt/felhom-drives/Felhom-Share` is a NAS), so a path prefix CANNOT classify. `Kind` is the only
|
||||
// discriminator, and it exists only on a REGISTERED path.
|
||||
// - therefore an UNREGISTERED path under `NetworkMountRoot` is un-classifiable, and un-classifiable
|
||||
// must refuse. Allowing it would be a fallback to "probably a drive" on the one surface that
|
||||
// accepts an arbitrary caller-supplied path (the deploy POST validates only `os.Stat` existence).
|
||||
// Every NAS share is registered under this root by construction (see NetworkMountRoot), so refusing
|
||||
// the unregistered case makes the network set completely covered without touching drives.
|
||||
//
|
||||
// An empty path is ALLOWED: it means the app is system/SSD-resident and has no external namespace at
|
||||
// all. A nil receiver refuses — we cannot consult the registry, so we cannot tell.
|
||||
func (s *Settings) RefuseAsAppNamespace(path string) (bool, string) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return false, "" // SSD-resident: no external namespace to place
|
||||
}
|
||||
if s == nil {
|
||||
return true, refuseAppNamespaceUndeterminable
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, sp := range s.StoragePaths {
|
||||
if path == sp.Path || strings.HasPrefix(path, sp.Path+"/") {
|
||||
if sp.IsNetwork() {
|
||||
return true, refuseAppNamespaceNetwork
|
||||
}
|
||||
return false, "" // a registered DRIVE — the supported case, unchanged
|
||||
}
|
||||
}
|
||||
// Not registered. Under the shared mount root its kind is undeterminable → refuse (see above).
|
||||
if path == NetworkMountRoot || strings.HasPrefix(path, NetworkMountRoot+"/") {
|
||||
return true, refuseAppNamespaceUndeterminable
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// Refusal reasons for RefuseAsAppNamespace. Hungarian, adult tone, no emoji — these reach the customer
|
||||
// through the deploy/migrate error surfaces. They name the storage class and what to do instead, never
|
||||
// an internal path or field name.
|
||||
const (
|
||||
refuseAppNamespaceNetwork = "Hálózati tárhelyen (NAS) nem futtatható alkalmazás adatkönyvtára — " +
|
||||
"a NAS megosztás tallózásra és médiatárolásra használható. Válasszon csatlakoztatott meghajtót."
|
||||
refuseAppNamespaceUndeterminable = "A megadott tárhely nem azonosítható regisztrált meghajtóként, " +
|
||||
"ezért alkalmazás adatkönyvtáraként nem használható. Válasszon a listából csatlakoztatott meghajtót."
|
||||
)
|
||||
|
||||
// IsStoragePathSchedulable returns whether a path belongs to a registered,
|
||||
// schedulable (active) storage path. Returns false if the path is unknown,
|
||||
// disconnected, decommissioned, or inactive.
|
||||
|
||||
Reference in New Issue
Block a user