From 1acfed4add3e472db53b7d87798da03b2a4f4105 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 11 Oct 2026 18:17:59 +0200 Subject: [PATCH] Test that a refused directory gates its module and a file meets the same guard (issue 495) Both fail on main: the shape seen on ace, a refused directory, a file below it and a step after it, writes the file, runs the step and reports that everything was attempted. --- internal/apply/refused_directory_test.go | 122 +++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 internal/apply/refused_directory_test.go diff --git a/internal/apply/refused_directory_test.go b/internal/apply/refused_directory_test.go new file mode 100644 index 0000000..13065ff --- /dev/null +++ b/internal/apply/refused_directory_test.go @@ -0,0 +1,122 @@ +package apply + +import ( + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/novox/mesh-host/internal/store" +) + +// A refused directory gates its module, and a file is judged by the same guard as a directory (novox/hq +// issue 495). +// +// The shape it was found in, on a server during mesh-catalog #205's walk (issue 452): the docker module +// declared a directory at Docker's data root, a file below it, and a run-once step after both. The engine +// refused the directory as not placed and said nothing was touched, then wrote the file into the very tree +// it had refused and ran the step. + +// aRuntimeRoot makes a temporary directory the container runtime's data root, as its daemon.json says, so +// the guard refuses it as it refused /var/lib/docker. +func aRuntimeRoot(t *testing.T) string { + t.Helper() + dir := t.TempDir() + root := filepath.Join(dir, "docker") + if err := os.MkdirAll(root, 0o755); err != nil { + t.Fatal(err) + } + daemon := filepath.Join(dir, "daemon.json") + if err := os.WriteFile(daemon, []byte(`{"data-root":"`+root+`"}`), 0o644); err != nil { + t.Fatal(err) + } + before := runtimeConfigs + runtimeConfigs = runtimeFiles{daemonJSON: daemon, storageConf: filepath.Join(dir, "none.conf")} + t.Cleanup(func() { + runtimeConfigs = before + runtimeRootsMu.Lock() + runtimeRoots = nil + runtimeRootsMu.Unlock() + }) + return root +} + +func TestARefusedDirectoryGatesItsModule(t *testing.T) { + root := aRuntimeRoot(t) + b := newStepBed(t) + elsewhere := filepath.Join(b.dir, "other.conf") + report, _, err := Apply(context.Background(), archHost(t), declare(t, + `{"id":"mod.state","type":"directory","path":"`+root+`"},`+ + `{"id":"mod.rotation","type":"file","path":"`+root+`/log-rotation.conf","content":"max-size=10m\n"},`+ + `{"id":"mod.check","type":"process","name":"mod-check","source":"`+b.body+`","digest":"`+b.digest+`",`+ + `"run":["./check"],"run-once":true,"restart-on":["mod.rotation"]},`+ + `{"id":"other.conf","type":"file","path":"`+elsewhere+`","content":"kept apart\n"}`), + store.State{}, store.OriginDeclared, b.run, nil, nil) + + var refusal *PlacementRefusedError + if err == nil || !errors.As(err, &refusal) { + t.Fatalf("the refused directory was not reported as refused: %v", err) + } + if _, statErr := os.Stat(filepath.Join(root, "log-rotation.conf")); statErr == nil { + t.Error("a file was written below the directory the engine had just refused") + } + if b.starts != 0 { + t.Errorf("the module's step ran %d time(s) after its directory was refused", b.starts) + } + for _, id := range []string{"mod.rotation", "mod.check"} { + o := outcomeOf(report, id) + if o.Action != "skipped" || !strings.Contains(o.Detail, root) { + t.Errorf("%s is not reported as skipped behind the refused directory: %+v", id, o) + } + } + // The gate reaches the module, not the machine. + if got, readErr := os.ReadFile(elsewhere); readErr != nil || string(got) != "kept apart\n" { + t.Errorf("another module's file was not applied: %q, %v", got, readErr) + } + // And the report says truthfully what was not attempted. + msg := err.Error() + if strings.Contains(msg, "everything was attempted") || !strings.Contains(msg, "the rest of mod") { + t.Errorf("the report does not say the rest of the module was not attempted: %s", msg) + } +} + +// A file is judged as a directory is: refused in a tree nothing is placed in, and below a directory the +// engine refused, whichever module declares it and in whatever order. +func TestAFileIsJudgedByTheSameGuardAsADirectory(t *testing.T) { + root := aRuntimeRoot(t) + _, _, err := applyOnce(t, `{"id":"notes.conf","type":"file","path":"`+root+`/notes.conf","content":"x\n"}`, + nil, store.State{}) + refusedFor339(t, "a file in the container runtime's data", err) + if _, statErr := os.Stat(filepath.Join(root, "notes.conf")); statErr == nil { + t.Error("a file was written into the container runtime's data") + } + + // Below a directory refused for another reason (a home's directory declared root's), declared before it. + dir := t.TempDir() + home := filepath.Join(dir, "home", "alice") + if err := os.MkdirAll(home, 0o755); err != nil { + t.Fatal(err) + } + passwd := filepath.Join(dir, "passwd") + if err := os.WriteFile(passwd, []byte("alice:x:4242:4242::"+home+":/bin/sh\n"), 0o644); err != nil { + t.Fatal(err) + } + before := passwdFile + passwdFile = passwd + t.Cleanup(func() { passwdFile = before }) + if err := os.MkdirAll(filepath.Join(home, "data"), 0o755); err != nil { + t.Fatal(err) + } + report, _, err := applyOnce(t, + `{"id":"notes.conf","type":"file","path":"`+home+`/data/notes.conf","content":"x\n"},`+ + `{"id":"notes.data","type":"directory","path":"`+home+`/data"}`, nil, store.State{}) + refusedFor339(t, "a file below a refused directory", err) + if _, statErr := os.Stat(filepath.Join(home, "data", "notes.conf")); statErr == nil { + t.Error("a file was written below a directory the engine refused") + } + if o := outcomeOf(report, "notes.conf"); o.ID != "" && o.Action != "skipped" { + t.Errorf("the file below a refused directory is reported %q", o.Action) + } +}