v0.117.0: consuming-namespace NAS verification + deploy-view truth (RCA fixes 2+4)
statfs fsclass helper (network/autofs/stub/unknown, fail-open); probe not_network_fs assertion (stub can never verify — red-proven); deploy-time stub refusal (idle autofs proceeds — red-proven); distinct stub badge, stub wins over unreachable (unreachable line byte-identical); deployed select shows stored HDD_PATH (red-proven vs IsDefault-only). MinAgent unchanged 0.81.0. Gates green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -156,7 +156,9 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["SettingsWarning"] = s.settings.LoadWarning // non-empty if settings.json was recovered from corruption
|
||||
data["Stacks"] = deployedStacks
|
||||
data["MissingStorage"] = s.missingStorageMap(deployedStacks)
|
||||
data["NetworkWarnings"] = s.networkStorageWarnings(deployedStacks) // NAS unreachable — recoverable warning, not "missing"
|
||||
nw, ns := s.networkStorageWarnings(deployedStacks) // NAS unreachable (recoverable) / guest-side stub (defect)
|
||||
data["NetworkWarnings"] = nw
|
||||
data["NetworkStubs"] = ns
|
||||
data["RunningCount"] = running
|
||||
data["StoppedCount"] = stopped
|
||||
data["TotalCount"] = len(stackList)
|
||||
@@ -204,7 +206,9 @@ func (s *Server) stacksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
allStacks := s.stackMgr.GetStacks()
|
||||
data["Stacks"] = allStacks
|
||||
data["MissingStorage"] = s.missingStorageMap(allStacks)
|
||||
data["NetworkWarnings"] = s.networkStorageWarnings(allStacks) // NAS unreachable — recoverable warning, not "missing"
|
||||
nw, ns := s.networkStorageWarnings(allStacks) // NAS unreachable (recoverable) / guest-side stub (defect)
|
||||
data["NetworkWarnings"] = nw
|
||||
data["NetworkStubs"] = ns
|
||||
|
||||
// Build storage label lookup for deployed apps
|
||||
storageLabels := make(map[string]string) // stack name → storage label
|
||||
@@ -340,6 +344,25 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
||||
deployPaths = append(deployPaths, dp)
|
||||
}
|
||||
data["StoragePaths"] = deployPaths
|
||||
// RCA fix 4: a deployed app's read-only storage select must show the app's STORED HDD_PATH —
|
||||
// never the default drive (the pre-fix render selected by IsDefault only, so the settings view
|
||||
// lied about where the data lives). When the stored path is no longer schedulable, an extra
|
||||
// disabled option names it verbatim rather than silently showing a different storage.
|
||||
data["CurrentHDDPath"] = ""
|
||||
data["CurrentHDDPathMissing"] = false
|
||||
if alreadyDeployed && appCfg != nil {
|
||||
if hdd := appCfg.Env["HDD_PATH"]; hdd != "" {
|
||||
data["CurrentHDDPath"] = hdd
|
||||
inList := false
|
||||
for _, dp := range deployPaths {
|
||||
if dp.Path == hdd {
|
||||
inList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
data["CurrentHDDPathMissing"] = !inList
|
||||
}
|
||||
}
|
||||
|
||||
// Prevention layer (storage-split): surface the Docker-data volume's reserved-buffer state so the
|
||||
// customer sees BEFORE deploying when free space is too low (the API gate also hard-refuses). Only
|
||||
@@ -1376,14 +1399,21 @@ func (s *Server) missingStorageLabel(hddPath string) (string, bool) {
|
||||
return s.settings.GetStorageLabel(hddPath), true // not in registry → its drive is gone
|
||||
}
|
||||
|
||||
// networkStorageWarnings returns stack-name → share label for every deployed app whose HDD_PATH is a NAS
|
||||
// network path the agent currently reports `unreachable`. This is a RECOVERABLE warning ("hálózati
|
||||
// tárhely nem elérhető"), explicitly NOT the drive missing/stop-cascade — the app keeps running and the
|
||||
// badge clears when the NAS returns. Best-effort: an agent error or no network paths → no warnings.
|
||||
func (s *Server) networkStorageWarnings(list []stacks.Stack) map[string]string {
|
||||
out := map[string]string{}
|
||||
// networkStorageWarnings returns two stack-name → share-label maps for deployed apps on NAS
|
||||
// network paths:
|
||||
// - warnings: the agent reports the share `unreachable` — RECOVERABLE ("hálózati tárhely nem
|
||||
// elérhető"), explicitly NOT the drive missing/stop-cascade; clears when the NAS returns.
|
||||
// - stubs: the path is a plain local STUB in the controller's namespace (RCA fix 2 — the
|
||||
// guest-reboot state where apps silently see an empty dir while the agent's host-side view is
|
||||
// healthy). Checked from THIS process (the consuming namespace), independent of the agent.
|
||||
//
|
||||
// Stub wins: a stack never appears in both. An idle autofs trigger is HEALTHY and is never
|
||||
// force-mounted from here (classification reads the fs magic only). Best-effort: an agent error
|
||||
// drops the unreachable leg but the stub leg still runs.
|
||||
func (s *Server) networkStorageWarnings(list []stacks.Stack) (warnings, stubs map[string]string) {
|
||||
warnings, stubs = map[string]string{}, map[string]string{}
|
||||
if s.settings == nil || s.stackMgr == nil {
|
||||
return out
|
||||
return warnings, stubs
|
||||
}
|
||||
netPaths := map[string]settings.StoragePath{}
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
@@ -1392,47 +1422,77 @@ func (s *Server) networkStorageWarnings(list []stacks.Stack) map[string]string {
|
||||
}
|
||||
}
|
||||
if len(netPaths) == 0 {
|
||||
return out
|
||||
}
|
||||
agent, err := s.agentClient()
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
mounts, err := agent.ListNetStorage(ctx)
|
||||
if err != nil {
|
||||
s.logger.Printf("[WARN] [web] network storage health unavailable for warnings: %v", err)
|
||||
return out
|
||||
return warnings, stubs
|
||||
}
|
||||
// Stub leg — the consuming namespace's own verdict (no agent, no force-mount).
|
||||
stubPaths := s.stubNetworkPaths(netPaths)
|
||||
// Unreachable leg — the agent's host-side liveness view (unchanged behavior).
|
||||
unreachable := map[string]string{} // path → label
|
||||
for _, m := range mounts {
|
||||
if !m.Unreachable() { // only `unreachable` is degraded; `idle`/`ok` are benign
|
||||
continue
|
||||
if agent, err := s.agentClient(); err == nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if mounts, err := agent.ListNetStorage(ctx); err == nil {
|
||||
for _, m := range mounts {
|
||||
if !m.Unreachable() { // only `unreachable` is degraded; `idle`/`ok` are benign
|
||||
continue
|
||||
}
|
||||
p := settings.NetworkMountRoot + "/" + m.Name
|
||||
if sp, ok := netPaths[p]; ok {
|
||||
lbl := sp.Label
|
||||
if lbl == "" {
|
||||
lbl = m.Name
|
||||
}
|
||||
unreachable[p] = lbl
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.logger.Printf("[WARN] [web] network storage health unavailable for warnings: %v", err)
|
||||
}
|
||||
p := settings.NetworkMountRoot + "/" + m.Name
|
||||
if sp, ok := netPaths[p]; ok {
|
||||
}
|
||||
return networkStorageWarningsIn(list, s.stackMgr.LoadAppConfigByName, unreachable, stubPaths)
|
||||
}
|
||||
|
||||
// stubNetworkPaths classifies each registered network path in the controller's namespace and
|
||||
// returns path→label for every STUB (RCA fix 2). Idle autofs is healthy and classification never
|
||||
// force-mounts (fs magic read only); unknown (timeout/statfs error) is NOT a stub — fail open.
|
||||
func (s *Server) stubNetworkPaths(netPaths map[string]settings.StoragePath) map[string]string {
|
||||
out := map[string]string{}
|
||||
for p, sp := range netPaths {
|
||||
if s.classifyFSPath(p) == system.FSClassStub {
|
||||
lbl := sp.Label
|
||||
if lbl == "" {
|
||||
lbl = m.Name
|
||||
lbl = strings.TrimPrefix(p, settings.NetworkMountRoot+"/")
|
||||
}
|
||||
unreachable[p] = lbl
|
||||
out[p] = lbl
|
||||
}
|
||||
}
|
||||
if len(unreachable) == 0 {
|
||||
return out
|
||||
return out
|
||||
}
|
||||
|
||||
// networkStorageWarningsIn is the pure per-stack mapping core (the appsUsingPathIn pattern): given
|
||||
// the path→label verdict sets, assign each deployed stack its badge. Stub WINS over unreachable —
|
||||
// a stack never appears in both maps.
|
||||
func networkStorageWarningsIn(list []stacks.Stack, load func(string) *stacks.AppConfig, unreachable, stubPaths map[string]string) (warnings, stubs map[string]string) {
|
||||
warnings, stubs = map[string]string{}, map[string]string{}
|
||||
if len(unreachable) == 0 && len(stubPaths) == 0 {
|
||||
return warnings, stubs
|
||||
}
|
||||
for _, st := range list {
|
||||
if !st.Deployed {
|
||||
continue
|
||||
}
|
||||
if cfg := s.stackMgr.LoadAppConfigByName(st.Name); cfg != nil {
|
||||
if lbl, bad := unreachable[cfg.Env["HDD_PATH"]]; bad {
|
||||
out[st.Name] = lbl
|
||||
if cfg := load(st.Name); cfg != nil {
|
||||
hdd := cfg.Env["HDD_PATH"]
|
||||
if lbl, bad := stubPaths[hdd]; bad {
|
||||
stubs[st.Name] = lbl // stub wins over unreachable
|
||||
continue
|
||||
}
|
||||
if lbl, bad := unreachable[hdd]; bad {
|
||||
warnings[st.Name] = lbl
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
return warnings, stubs
|
||||
}
|
||||
|
||||
// missingStorageMap returns stack-name → storage label for every deployed app whose data drive is
|
||||
|
||||
Reference in New Issue
Block a user