Judge every path the controller resolves for a directory or file where the plan is composed, so the merge gate refuses a directory in Docker's data (issue 496)
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheResolverAndWhatAsksItComposeOnOneMachine (0.02s)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheResolverAndWhatAsksItComposeOnOneMachine (0.02s)
mesh/delivery superseded: a newer head of the same pull request
This commit is contained in:
@@ -257,7 +257,10 @@ func dirInto(resource map[string]any, dirs map[string]string, module string) err
|
|||||||
}
|
}
|
||||||
resource["env-file"] = filled
|
resource["env-file"] = filled
|
||||||
}
|
}
|
||||||
return nil
|
// **Placed, then judged** (novox/hq issue 496): the path a directory or file resolves to — the default
|
||||||
|
// layout, a place, a stated path, one beneath a ${dir:…} — is judged here, where every plan and the merge
|
||||||
|
// gate compose it, so a module whose directory lands in Docker's data is refused before it reaches a machine.
|
||||||
|
return judgeResolved(resource, module)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unknownDirRefs is every ${dir:…} in the definition that names no directory the definition
|
// unknownDirRefs is every ${dir:…} in the definition that names no directory the definition
|
||||||
|
|||||||
@@ -92,6 +92,81 @@ func systemPath(path string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Where no directory or file the controller resolves for a module may be, from any route (novox/hq issue 496).
|
||||||
|
//
|
||||||
|
// mesh-catalog #205 gave docker's `state` directory `"place": "."`, which resolves to <root>/docker: /var/lib/docker,
|
||||||
|
// every container's filesystem. systemPath judged only the `places` and `accesses` settings, so the default layout
|
||||||
|
// and a definition's own paths reached the merge gate unjudged; it composed every machine and passed, and only the
|
||||||
|
// node-engine refused the directory, at apply, failing the walk. So every path a directory or file resolves to is
|
||||||
|
// judged where the plan is composed, which is also where the gate composes.
|
||||||
|
//
|
||||||
|
// Not by systemPath's whole list: a module writes the machine's configuration in /etc and /usr by design (units,
|
||||||
|
// jails, a launcher), keeps run directories in /run, an account's keys in its .ssh, and the mesh places its own files
|
||||||
|
// for every module in /var/lib/mesh. Those are the node-engine's to judge with what only the machine knows (owners,
|
||||||
|
// links, where the runtimes really keep their data). Refused here is what no module ever writes, whoever owns it:
|
||||||
|
// the trees in neverWritten, the node-engine's own trees but by its own module, and a path that is one of the
|
||||||
|
// machine's roots or holds one. Each is refused by the node-engine too (its placement guard, issue 339), so nothing
|
||||||
|
// that applies today is refused here.
|
||||||
|
var (
|
||||||
|
neverWritten = []string{"/boot", "/root", "/proc", "/sys", "/dev", "/bin", "/sbin", "/lib", "/lib32", "/lib64",
|
||||||
|
"/var/spool", "/var/lib/docker", "/var/lib/containers", "/var/lib/containerd", "/opt"}
|
||||||
|
engineTrees = []string{"/var/lib/mesh-host", "/usr/lib/nox-mesh-host"}
|
||||||
|
// engineModule is the node-engine's own module, the one that places in engineTrees.
|
||||||
|
engineModule = "mesh-host"
|
||||||
|
// usrRoots are the roots below /usr that hold every program and library: never a module's directory itself.
|
||||||
|
usrRoots = []string{"/usr/bin", "/usr/lib", "/usr/lib64", "/usr/local", "/usr/local/bin", "/usr/local/lib",
|
||||||
|
"/usr/local/sbin", "/usr/sbin", "/usr/share"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// resolvedSystemPath says why a path resolved for one of module's directories or files is the machine's own, or "".
|
||||||
|
func resolvedSystemPath(path, module string) string {
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
in := func(tree string) bool { return path == tree || strings.HasPrefix(path, tree+"/") }
|
||||||
|
for _, tree := range neverWritten {
|
||||||
|
if in(tree) {
|
||||||
|
return path + " is in " + tree + ", which no module writes in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if module != engineModule {
|
||||||
|
for _, tree := range engineTrees {
|
||||||
|
if in(tree) {
|
||||||
|
return path + " is in " + tree + ", the node-engine's own, which only its module places in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, roots := range [][]string{systemRoots, systemTrees, usrRoots} {
|
||||||
|
for _, root := range roots {
|
||||||
|
if path == root {
|
||||||
|
return path + " is one of the machine's roots, the parent of everything in it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tree := range append(append([]string(nil), systemTrees...), neverWritten...) {
|
||||||
|
if strings.HasPrefix(tree, path+"/") {
|
||||||
|
return path + " holds " + tree + ", the machine's own or the mesh's state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// judgeResolved refuses a directory or file whose resolved path resolvedSystemPath refuses (novox/hq issue 496).
|
||||||
|
func judgeResolved(resource map[string]any, module string) error {
|
||||||
|
kind := fmt.Sprint(resource["type"])
|
||||||
|
if kind != "directory" && kind != "file" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
path, _ := resource["path"].(string)
|
||||||
|
if why := resolvedSystemPath(path, module); why != "" {
|
||||||
|
return fmt.Errorf("%s's %s %q resolves to %s: %s. The node-engine refuses it at apply, so it is refused "+
|
||||||
|
"where the plan is composed, and the merge gate with it (novox/hq issue 496)",
|
||||||
|
module, kind, fmt.Sprint(resource["id"]), filepath.Clean(path), why)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// accessRef is how a module names one of its accesses: ${access:<id>}.
|
// accessRef is how a module names one of its accesses: ${access:<id>}.
|
||||||
var accessRef = regexp.MustCompile(`\$\{access:([a-z0-9][a-z0-9-]*)\}`)
|
var accessRef = regexp.MustCompile(`\$\{access:([a-z0-9][a-z0-9-]*)\}`)
|
||||||
|
|
||||||
@@ -284,6 +359,10 @@ func accessInto(resource map[string]any, accesses map[string]string, module stri
|
|||||||
if resource["path"], err = fill(path); err != nil {
|
if resource["path"], err = fill(path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Judged again once the access is in it: dirInto judged it while it still named one (novox/hq issue 496).
|
||||||
|
if err := judgeResolved(resource, module); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if content, ok := resource["content"].(string); ok {
|
if content, ok := resource["content"].(string); ok {
|
||||||
if resource["content"], err = fill(content); err != nil {
|
if resource["content"], err = fill(content); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
// Every path the controller resolves for a module's directories and files is judged where the plan is
|
||||||
|
// composed (novox/hq issue 496). mesh-catalog #205 gave docker's `state` directory `"place": "."`, which
|
||||||
|
// resolves to <root>/docker — /var/lib/docker, every container's filesystem. The merge gate composed every
|
||||||
|
// machine and passed it; only the node-engine refused it, at apply, and failed the walk. The gate composes
|
||||||
|
// through Declaration, so a refusal here is a refusal at the gate, for every module.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func composedOn(m Manifest, with Rendering) ([]map[string]any, error) {
|
||||||
|
return Resolution{Node: "anchor", Modules: []Manifest{m}}.Declaration(with)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathOf(t *testing.T, resources []map[string]any, id string) string {
|
||||||
|
t.Helper()
|
||||||
|
for _, r := range resources {
|
||||||
|
if r["id"] == id {
|
||||||
|
return r["path"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("no resource %s in %v", id, resources)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestADirectoryPlacedInDockersDataIsRefusedAtComposition(t *testing.T) {
|
||||||
|
// The #205 shape, as it was merged.
|
||||||
|
m := Manifest{Module: "docker", Version: "1", Resources: []map[string]any{
|
||||||
|
{"id": "state", "type": "directory", "place": "."},
|
||||||
|
}}
|
||||||
|
_, err := composedOn(m, Rendering{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("a directory resolving to /var/lib/docker composed; the node-engine refuses it at apply")
|
||||||
|
}
|
||||||
|
for _, said := range []string{"docker", `"state"`, "/var/lib/docker", "issue 496"} {
|
||||||
|
if !strings.Contains(err.Error(), said) {
|
||||||
|
t.Errorf("the refusal names the module, the resource, the path and why; %q is missing from %q", said, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheMeshsPlaceForDockerComposes(t *testing.T) {
|
||||||
|
// The fix #205 needed: the mesh's own directory for the module, <root>/mesh/docker.
|
||||||
|
m := Manifest{Module: "docker", Version: "1", Resources: []map[string]any{
|
||||||
|
{"id": "state", "type": "directory", "place": "mesh"},
|
||||||
|
{"id": "marker", "type": "file", "path": "${dir:state}/applied", "content": "x"},
|
||||||
|
}}
|
||||||
|
resources, err := composedOn(m, Rendering{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("place %q is the mesh's tree, which the mesh writes for every module: %v", "mesh", err)
|
||||||
|
}
|
||||||
|
if got := pathOf(t, resources, "docker.state"); got != "/var/lib/mesh/docker" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEveryResolvedPathIsJudged(t *testing.T) {
|
||||||
|
refused := map[string]Manifest{
|
||||||
|
"a default directory under a data root inside containerd's data": {Module: "images", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "cache", "type": "directory"}}},
|
||||||
|
"a stated directory in docker's data": {Module: "sidecar", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "volumes", "type": "directory", "path": "/var/lib/docker/volumes/x"}}},
|
||||||
|
"a file beneath a placed directory that climbs out of it": {Module: "docker", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "state", "type": "directory", "place": "mesh"},
|
||||||
|
{"id": "f", "type": "file", "path": "${dir:state}/../../containers/x", "content": "x"}}},
|
||||||
|
"a file in /boot": {Module: "grub", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "cfg", "type": "file", "path": "/boot/grub/custom.cfg", "content": "x"}}},
|
||||||
|
"a directory that is the data root itself": {Module: "lib", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "all", "type": "directory", "place": "."}}},
|
||||||
|
"a directory in the node-engine's own tree, by another module": {Module: "intruder", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "d", "type": "directory", "path": "/var/lib/mesh-host/x"}}},
|
||||||
|
}
|
||||||
|
roots := map[string]Rendering{
|
||||||
|
"a default directory under a data root inside containerd's data": {DataRoot: "/var/lib/containerd"},
|
||||||
|
"a directory that is the data root itself": {DataRoot: "/var"},
|
||||||
|
}
|
||||||
|
for name, m := range refused {
|
||||||
|
if _, err := composedOn(m, roots[name]); err == nil || !strings.Contains(err.Error(), "issue 496") {
|
||||||
|
t.Errorf("%s: composed or refused for another reason: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the mesh's own modules place today, on every machine (read from each machine's live plan): the
|
||||||
|
// machine's configuration, the mesh's tree, the node-engine's own, run directories and an account's keys.
|
||||||
|
passes := map[string]Manifest{
|
||||||
|
"a unit in /etc": {Module: "power", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "d", "type": "directory", "path": "/etc/systemd/system/x.service.d"},
|
||||||
|
{"id": "u", "type": "file", "path": "/etc/systemd/system/x.service.d/a.conf", "content": "x"}}},
|
||||||
|
"a run directory": {Module: "fail2ban", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "run-dir", "type": "directory", "path": "/var/run/fail2ban"}}},
|
||||||
|
"a program in /usr/local": {Module: "claude-code", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "start", "type": "file", "path": "/usr/local/bin/claude-agent", "content": "x"}}},
|
||||||
|
"the node-engine's launcher, by the node-engine": {Module: "mesh-host", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "launcher", "type": "file", "path": "/usr/lib/nox-mesh-host/launch", "content": "x"}}},
|
||||||
|
"an account's .ssh": {Module: "ssh-client", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "ssh-dir", "type": "directory", "path": "/home/someone/.ssh"},
|
||||||
|
{"id": "config", "type": "file", "path": "/home/someone/.ssh/config", "content": "x"}}},
|
||||||
|
"a module's own root": {Module: "mailu", Version: "1",
|
||||||
|
Resources: []map[string]any{{"id": "state", "type": "directory", "place": "."}}},
|
||||||
|
}
|
||||||
|
for name, m := range passes {
|
||||||
|
if _, err := composedOn(m, Rendering{}); err != nil {
|
||||||
|
t.Errorf("%s: refused: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user