diff --git a/pkg/sentinel/collector.go b/pkg/sentinel/collector.go index 42be66f..4875aee 100644 --- a/pkg/sentinel/collector.go +++ b/pkg/sentinel/collector.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "os/exec" + "path/filepath" "strings" "metis/pkg/facts" @@ -182,8 +183,60 @@ func bindHealthy(target, source string) bool { if target == "" || source == "" { return false } + + targetReal := realPath(target) + sourceReal := realPath(source) + if sourceReal != "" && pathWithin(targetReal, sourceReal) { + return true + } + mountSource, _, mounted := mountInfo(target) - return mounted && strings.TrimSpace(mountSource) == source + if !mounted { + return false + } + if strings.TrimSpace(mountSource) == source { + return true + } + + rootSource, _, rootMounted := mountInfo(source) + if !rootMounted { + return false + } + device, subpath := splitFindmntSource(mountSource) + return device != "" && device == rootSource && subpath == filepath.Clean(target) +} + +func realPath(path string) string { + path = strings.TrimSpace(path) + if path == "" { + return "" + } + out, err := commandOutput("readlink", "-f", path) + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + +func pathWithin(path, root string) bool { + path = filepath.Clean(strings.TrimSpace(path)) + root = filepath.Clean(strings.TrimSpace(root)) + if path == "" || root == "" || path == "." || root == "." { + return false + } + return path == root || strings.HasPrefix(path, root+string(os.PathSeparator)) +} + +func splitFindmntSource(source string) (string, string) { + source = strings.TrimSpace(source) + if source == "" { + return "", "" + } + start := strings.Index(source, "[") + if start == -1 || !strings.HasSuffix(source, "]") { + return source, "" + } + return source[:start], source[start+1 : len(source)-1] } func resolveDeviceByUUID(uuid string) string { diff --git a/pkg/sentinel/collector_test.go b/pkg/sentinel/collector_test.go index 7f594cd..12cba08 100644 --- a/pkg/sentinel/collector_test.go +++ b/pkg/sentinel/collector_test.go @@ -120,6 +120,82 @@ esac`, } } +func TestBindHealthyAcceptsScratchSymlinksAndDeviceSubpaths(t *testing.T) { + dir := fakeCollectorCommands(t, map[string]string{ + "readlink": `case "${2:-}" in + /mnt/scratch) printf '/mnt/scratch\n' ;; + /var/log/pods) printf '/mnt/scratch/var/log/pods\n' ;; + *) exit 1 ;; +esac`, + "findmnt": `target="" +for ((i=1; i<=$#; i++)); do + if [[ "${!i}" == "-T" ]]; then + j=$((i + 1)) + target="${!j}" + break + fi +done +case "${target}" in + /mnt/scratch) printf 'SOURCE="/dev/sdz1" TARGET="/mnt/scratch" FSTYPE="ext4"\n' ;; + /var/tmp) printf 'SOURCE="/dev/sdz1[/var/tmp]" TARGET="/var/tmp" FSTYPE="ext4"\n' ;; + /other) printf 'SOURCE="/dev/sdz1[/unexpected]" TARGET="/other" FSTYPE="ext4"\n' ;; + *) exit 1 ;; +esac`, + }) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + + if !bindHealthy("/var/log/pods", "/mnt/scratch") { + t.Fatal("scratch-backed symlink target should be healthy") + } + if !bindHealthy("/var/tmp", "/mnt/scratch") { + t.Fatal("device subpath bind target should be healthy") + } + if bindHealthy("/other", "/mnt/scratch") { + t.Fatal("unrelated device subpath should be unhealthy") + } + if device, subpath := splitFindmntSource(`/dev/sdz1[/var/tmp]`); device != "/dev/sdz1" || subpath != "/var/tmp" { + t.Fatalf("splitFindmntSource device/subpath = %q %q", device, subpath) + } + if device, subpath := splitFindmntSource("/dev/sdz1"); device != "/dev/sdz1" || subpath != "" { + t.Fatalf("plain splitFindmntSource device/subpath = %q %q", device, subpath) + } + if device, subpath := splitFindmntSource(""); device != "" || subpath != "" { + t.Fatalf("empty splitFindmntSource device/subpath = %q %q", device, subpath) + } + if got := realPath(""); got != "" { + t.Fatalf("empty realPath = %q", got) + } + if got := realPath("/missing"); got != "" { + t.Fatalf("missing realPath = %q", got) + } + if pathWithin("", "/mnt/scratch") || pathWithin("/mnt/scratch/file", "") { + t.Fatal("empty pathWithin inputs should be false") + } +} + +func TestBindHealthyRejectsDeviceSubpathWithoutRootMount(t *testing.T) { + dir := fakeCollectorCommands(t, map[string]string{ + "readlink": `exit 1`, + "findmnt": `target="" +for ((i=1; i<=$#; i++)); do + if [[ "${!i}" == "-T" ]]; then + j=$((i + 1)) + target="${!j}" + break + fi +done +case "${target}" in + /var/tmp) printf 'SOURCE="/dev/sdz1[/var/tmp]" TARGET="/var/tmp" FSTYPE="ext4"\n' ;; + *) exit 1 ;; +esac`, + }) + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + + if bindHealthy("/var/tmp", "/mnt/scratch") { + t.Fatal("device subpath should be unhealthy when root scratch mount is unknown") + } +} + func TestCollectUSBScratchDefaultsAndParserEdges(t *testing.T) { dir := fakeCollectorCommands(t, map[string]string{ "cat": `printf '%s\n' '{"usb_scratch":{"mountpoint":"/mnt/scratch"}}'`,