R-492: delete the always-empty cfg.Paths.HDDPath (field, env binding, every fallback)
Readers (report builder, health check, /api/system, web primaryHDDPath, metrics collector, AutoDiscoverStoragePaths' fallback parameter) now use the storage registry only. An old controller.yaml still carrying paths.hdd_path keeps loading (non-strict YAML), pinned by TestR492_OldConfigWithHDDPathStillLoads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1171,9 +1171,9 @@ func (r *Router) triggerSync(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
|
||||
func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
// R-490 / R-465: the same default-storage-path fallback every other reader of the always-empty
|
||||
// cfg.Paths.HDDPath has; without it hdd_configured was false on every box.
|
||||
hddPath := r.cfg.Paths.HDDPath
|
||||
// R-490 / R-465 / R-492: the default storage path is the only source (the always-empty global
|
||||
// paths.hdd_path was deleted in R-492); without it hdd_configured was false on every box.
|
||||
hddPath := ""
|
||||
if r.sett != nil {
|
||||
if p := r.sett.GetDefaultStoragePath(); p != "" {
|
||||
hddPath = p
|
||||
|
||||
@@ -164,7 +164,6 @@ type PathsConfig struct {
|
||||
StacksDir string `yaml:"stacks_dir"`
|
||||
DataDir string `yaml:"data_dir"`
|
||||
SystemDataPath string `yaml:"system_data_path"`
|
||||
HDDPath string `yaml:"hdd_path"`
|
||||
}
|
||||
|
||||
type WebConfig struct {
|
||||
@@ -450,7 +449,6 @@ func applyEnvOverrides(cfg *Config) {
|
||||
envStr("FELHOM_WEB_LISTEN", &cfg.Web.Listen)
|
||||
envStr("FELHOM_WEB_PASSWORD_HASH", &cfg.Web.PasswordHash)
|
||||
envStr("FELHOM_PATHS_STACKS_DIR", &cfg.Paths.StacksDir)
|
||||
envStr("FELHOM_PATHS_HDD_PATH", &cfg.Paths.HDDPath)
|
||||
envStr("FELHOM_LOGGING_LEVEL", &cfg.Logging.Level)
|
||||
envStr("FELHOM_MONITORING_SYSTEM_HEALTH_INTERVAL", &cfg.Monitoring.SystemHealthInterval)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-492 — the global paths.hdd_path was deleted (it was empty on every box and every reader already
|
||||
// fell back to the storage registry). The consequence that matters is the OLD box: a controller.yaml
|
||||
// written before the deletion that still carries `hdd_path` (empty or not) must keep loading, with
|
||||
// every other field intact — a config that fails to parse is a controller that does not start.
|
||||
// RED-PROOF (REPORT): switch loadAndParse to a strict decoder (yaml KnownFields) and this fails.
|
||||
func TestR492_OldConfigWithHDDPathStillLoads(t *testing.T) {
|
||||
for _, v := range []string{`""`, `/mnt/legacy_hdd`} {
|
||||
y := minimalYAML(bcryptHash) + "paths:\n stacks_dir: /opt/stacks\n hdd_path: " + v + "\n"
|
||||
cfg, err := LoadFromBytes([]byte(y))
|
||||
if err != nil {
|
||||
t.Fatalf("hdd_path=%s: an old config no longer loads: %v", v, err)
|
||||
}
|
||||
if cfg.Paths.StacksDir != filepath.FromSlash("/opt/stacks") && cfg.Paths.StacksDir != "/opt/stacks" {
|
||||
t.Errorf("hdd_path=%s: neighbouring field lost: stacks_dir=%q", v, cfg.Paths.StacksDir)
|
||||
}
|
||||
if cfg.Customer.ID != "demo" {
|
||||
t.Errorf("hdd_path=%s: customer.id lost: %q", v, cfg.Customer.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func RunHealthCheck(cfg *config.Config, cpuCollector *system.CPUCollector, stora
|
||||
|
||||
debug := cfg.Logging.Level == "debug" && logger != nil
|
||||
|
||||
hddPath := cfg.Paths.HDDPath
|
||||
hddPath := "" // R-492: the global paths.hdd_path is gone; the registry is the only source
|
||||
if len(storagePaths) > 0 {
|
||||
hddPath = storagePaths[0].Path
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func BuildReport(
|
||||
|
||||
// System info
|
||||
staticInfo := metrics.GetStaticInfo()
|
||||
hddPath := cfg.Paths.HDDPath
|
||||
hddPath := "" // R-492: the global paths.hdd_path is gone; the registry is the only source
|
||||
if len(storagePaths) > 0 {
|
||||
hddPath = storagePaths[0].Path
|
||||
}
|
||||
|
||||
@@ -1738,7 +1738,6 @@ func (s *Settings) SetStorageLabel(path, label string) error {
|
||||
// already in the registry. It is ADDITIVE: pre-existing entries are never removed,
|
||||
// modified, or reactivated.
|
||||
// - discoveredPaths are pre-scanned HDD_PATH values from deployed apps' app.yaml.
|
||||
// - fallbackHDDPath is the legacy controller.yaml paths.hdd_path (may be empty).
|
||||
//
|
||||
// Invariants:
|
||||
// - A path already present in the registry IN ANY STATE (including a Decommissioned
|
||||
@@ -1747,12 +1746,12 @@ func (s *Settings) SetStorageLabel(path, label string) error {
|
||||
// - IsDefault is never flipped on an existing entry. A newly-discovered path becomes
|
||||
// default ONLY if the registry currently has no default at all (and then only the
|
||||
// first such new path).
|
||||
func (s *Settings) AutoDiscoverStoragePaths(discoveredPaths []string, fallbackHDDPath string, logger *log.Logger) {
|
||||
func (s *Settings) AutoDiscoverStoragePaths(discoveredPaths []string, logger *log.Logger) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.debug {
|
||||
s.log.Printf("[DEBUG] [settings] AutoDiscoverStoragePaths discovered=%v fallback=%q existing=%d", discoveredPaths, fallbackHDDPath, len(s.StoragePaths))
|
||||
s.log.Printf("[DEBUG] [settings] AutoDiscoverStoragePaths discovered=%v existing=%d", discoveredPaths, len(s.StoragePaths))
|
||||
}
|
||||
|
||||
// Index existing paths (in ANY state) and whether a default already exists.
|
||||
@@ -1765,7 +1764,7 @@ func (s *Settings) AutoDiscoverStoragePaths(discoveredPaths []string, fallbackHD
|
||||
}
|
||||
}
|
||||
|
||||
// Build the de-duplicated, cleaned candidate list (discovered first, then fallback).
|
||||
// Build the de-duplicated, cleaned candidate list.
|
||||
seen := make(map[string]bool)
|
||||
var ordered []string
|
||||
for _, p := range discoveredPaths {
|
||||
@@ -1775,13 +1774,6 @@ func (s *Settings) AutoDiscoverStoragePaths(discoveredPaths []string, fallbackHD
|
||||
ordered = append(ordered, cleaned)
|
||||
}
|
||||
}
|
||||
if fallbackHDDPath != "" {
|
||||
cleaned := filepath.Clean(fallbackHDDPath)
|
||||
if cleaned != "" && cleaned != "." && !seen[cleaned] {
|
||||
seen[cleaned] = true
|
||||
ordered = append(ordered, cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
added := 0
|
||||
for _, path := range ordered {
|
||||
|
||||
@@ -30,7 +30,6 @@ func TestAutoDiscoverStoragePaths_Additive(t *testing.T) {
|
||||
name string
|
||||
existing []StoragePath
|
||||
discovered []string
|
||||
fallback string
|
||||
wantPaths []string // expected Path set after discovery (order-insensitive)
|
||||
wantNewPath string // a path expected to be newly added (may be "")
|
||||
// assertions run against the resulting registry
|
||||
@@ -101,17 +100,6 @@ func TestAutoDiscoverStoragePaths_Additive(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fallback path registered when missing",
|
||||
existing: []StoragePath{
|
||||
{Path: "/mnt/felhom-usb", Label: "x", IsDefault: true, Schedulable: true, AddedAt: "2026-01-01T00:00:00Z"},
|
||||
},
|
||||
discovered: nil,
|
||||
fallback: "/mnt/legacy_hdd",
|
||||
wantPaths: []string{"/mnt/felhom-usb", "/mnt/legacy_hdd"},
|
||||
wantNewPath: "/mnt/legacy_hdd",
|
||||
check: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -119,7 +107,7 @@ func TestAutoDiscoverStoragePaths_Additive(t *testing.T) {
|
||||
s := newTestSettings(t, cloneStoragePaths(tc.existing))
|
||||
before := cloneStoragePaths(tc.existing)
|
||||
|
||||
s.AutoDiscoverStoragePaths(tc.discovered, tc.fallback, logger)
|
||||
s.AutoDiscoverStoragePaths(tc.discovered, logger)
|
||||
|
||||
// Normalize with filepath.Clean so comparisons hold on both Linux (the deploy
|
||||
// target) and Windows (the dev machine), where Clean uses backslashes.
|
||||
@@ -158,7 +146,7 @@ func TestAutoDiscoverStoragePaths_DecommissionedNotReactivated(t *testing.T) {
|
||||
before := cloneStoragePaths(existing)
|
||||
|
||||
// A deployed app still points at the decommissioned drive.
|
||||
s.AutoDiscoverStoragePaths([]string{"/mnt/old_hdd", "/mnt/felhom-usb"}, "", logger)
|
||||
s.AutoDiscoverStoragePaths([]string{"/mnt/old_hdd", "/mnt/felhom-usb"}, logger)
|
||||
|
||||
if len(s.StoragePaths) != 2 {
|
||||
t.Fatalf("path count changed: got %d want 2 (%v)", len(s.StoragePaths), pathList(s))
|
||||
|
||||
@@ -399,15 +399,13 @@ func TestDeleteStack_R442_RemovesDeclaredDriveData(t *testing.T) {
|
||||
}
|
||||
|
||||
// GetStackHDDData (the modal's source) reads the per-app record too: with HDD_PATH recorded it
|
||||
// lists the folder; the global config stays empty throughout.
|
||||
// lists the folder; there is no global config value to fall back to (deleted in R-492).
|
||||
func TestGetStackHDDData_R442_ReadsPerAppRecord(t *testing.T) {
|
||||
drive := t.TempDir()
|
||||
m, _, _ := newR442Manager(t, "app", driveCompose, driveAppYAML(drive), drive)
|
||||
data := filepath.Join(drive, "appdata", "app")
|
||||
plantFile(t, filepath.Join(data, "one.bin"))
|
||||
if m.cfg.Paths.HDDPath != "" {
|
||||
t.Fatal("fixture error: the global must stay empty to prove the per-app read")
|
||||
}
|
||||
// (R-492: the global paths.hdd_path no longer exists, so the per-app record is the only source.)
|
||||
resp, err := m.GetStackHDDData("app")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -982,12 +982,10 @@ func (s *Server) findStackBySubdomain(subdomain string) (*stacks.Stack, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// primaryHDDPath returns the default storage path, or the legacy config value.
|
||||
// primaryHDDPath returns the default storage path ("" when none is registered — R-492 deleted the
|
||||
// always-empty legacy config value it used to fall back to).
|
||||
func (s *Server) primaryHDDPath() string {
|
||||
if p := s.settings.GetDefaultStoragePath(); p != "" {
|
||||
return p
|
||||
}
|
||||
return s.cfg.Paths.HDDPath
|
||||
return s.settings.GetDefaultStoragePath()
|
||||
}
|
||||
|
||||
func (s *Server) render(w http.ResponseWriter, name string, data interface{}) {
|
||||
|
||||
Reference in New Issue
Block a user