128 lines
7.6 KiB
Go
128 lines
7.6 KiB
Go
package catalogue
|
|
|
|
// A definition's directories and files are judged at their resolved paths when the definition is (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. ParseManifest is what `module check`, registration and the
|
|
// merge gate's reading of a changed repository all run, so a refusal here is a refusal at each of them.
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestADirectoryPlacedInDockersDataIsRefusedWhereTheDefinitionIsJudged(t *testing.T) {
|
|
// The #205 shape, as it was merged.
|
|
_, err := ParseManifest([]byte(`{"module": "docker", "version": "1",
|
|
"resources": [{"id": "state", "type": "directory", "place": "."}]}`))
|
|
if err == nil {
|
|
t.Fatal("a directory resolving to /var/lib/docker was accepted; 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 TestTheMeshsPlaceForDockerIsAccepted(t *testing.T) {
|
|
// The fix #205 needed: the mesh's own directory for the module, <root>/mesh/docker.
|
|
m, err := ParseManifest([]byte(`{"module": "docker", "version": "1", "resources": [
|
|
{"id": "state", "type": "directory", "place": "mesh"},
|
|
{"id": "marker", "type": "file", "path": "${dir:state}/applied", "content": "x"}]}`))
|
|
if err != nil {
|
|
t.Fatalf("place %q is in the mesh's tree, where the mesh writes for every module: %v", "mesh", err)
|
|
}
|
|
if got := dirsFor(m, Rendering{}); got["state"] != "/var/lib/mesh/docker" {
|
|
t.Fatalf("got %v", got)
|
|
}
|
|
}
|
|
|
|
// The node-engine's rules, for directories and — as the engine judges them since novox/hq issue 495 — files.
|
|
func TestADefinitionIsRefusedWhereTheNodeEngineRefusesItsPaths(t *testing.T) {
|
|
refused := map[string]string{
|
|
"a stated directory in docker's data": `{"module": "sidecar", "version": "1", "resources": [
|
|
{"id": "volumes", "type": "directory", "path": "/var/lib/docker/volumes/x"}]}`,
|
|
"a file in containerd's data": `{"module": "images", "version": "1", "resources": [
|
|
{"id": "f", "type": "file", "path": "/var/lib/containerd/x", "content": "x"}]}`,
|
|
"a file beneath a placed directory that climbs out of it": `{"module": "docker", "version": "1", "resources": [
|
|
{"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": [
|
|
{"id": "cfg", "type": "file", "path": "/boot/grub/custom.cfg", "content": "x"}]}`,
|
|
"a directory that is the mesh's whole tree": `{"module": "mesh", "version": "1", "resources": [
|
|
{"id": "all", "type": "directory", "place": "."}]}`,
|
|
"a directory that holds /etc": `{"module": "x", "version": "1", "resources": [
|
|
{"id": "d", "type": "directory", "path": "/"}]}`,
|
|
"a directory in the node-engine's own tree, by another module": `{"module": "intruder", "version": "1",
|
|
"resources": [{"id": "d", "type": "directory", "path": "/var/lib/mesh-host/x"}]}`,
|
|
}
|
|
for name, raw := range refused {
|
|
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "issue 496") {
|
|
t.Errorf("%s: accepted, or refused for another reason: %v", name, err)
|
|
}
|
|
}
|
|
|
|
// What the engine applies, the mesh's own modules' paths among them (read from every machine's live plan):
|
|
// the machine's configuration, run directories, programs below /usr/local, the node-engine's own trees by its
|
|
// own module, an account's keys, a module's own root, and — not on the engine's lists, so not refused here —
|
|
// below /lib and at /storage, /data, /services and /var/lock.
|
|
accepted := map[string]string{
|
|
"a unit in /etc": `{"module": "power", "version": "1", "resources": [
|
|
{"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": [
|
|
{"id": "run-dir", "type": "directory", "path": "/var/run/fail2ban"}]}`,
|
|
"a program in /usr/local/bin": `{"module": "claude-code", "version": "1", "resources": [
|
|
{"id": "start", "type": "file", "path": "/usr/local/bin/claude-agent", "content": "x"}]}`,
|
|
"the node-engine's launcher and state, by the node-engine": `{"module": "mesh-host", "version": "1", "resources": [
|
|
{"id": "launcher", "type": "file", "path": "/usr/lib/nox-mesh-host/launch", "content": "x"},
|
|
{"id": "state", "type": "directory", "path": "/var/lib/mesh-host"}]}`,
|
|
"an account's .ssh": `{"module": "ssh-client", "version": "1", "resources": [
|
|
{"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": [
|
|
{"id": "state", "type": "directory", "place": "."}]}`,
|
|
"a file below /lib": `{"module": "udev", "version": "1", "resources": [
|
|
{"id": "rule", "type": "file", "path": "/lib/udev/rules.d/99-x.rules", "content": "x"}]}`,
|
|
"directories at /storage, /data, /services and /var/lock": `{"module": "roots", "version": "1", "resources": [
|
|
{"id": "a", "type": "directory", "path": "/storage"}, {"id": "b", "type": "directory", "path": "/data"},
|
|
{"id": "c", "type": "directory", "path": "/services"}, {"id": "d", "type": "directory", "path": "/var/lock"}]}`,
|
|
}
|
|
for name, raw := range accepted {
|
|
if _, err := ParseManifest([]byte(raw)); err != nil {
|
|
t.Errorf("%s: refused: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPathThroughAnAccessIsJudgedWithTheAccessFilledIn(t *testing.T) {
|
|
// A file's path may name an access; the access's default path is the definition's, so it is judged with it.
|
|
_, err := ParseManifest([]byte(`{"module": "backup", "version": "1",
|
|
"accesses": [{"id": "images", "path": "/var/lib/docker/volumes"}],
|
|
"resources": [{"id": "marker", "type": "file", "path": "${access:images}/marker", "content": "x"}]}`))
|
|
if err == nil || !strings.Contains(err.Error(), "/var/lib/docker/volumes/marker") ||
|
|
!strings.Contains(err.Error(), "issue 496") {
|
|
t.Fatalf("a file reaching Docker's data through an access's default path was accepted: %v", err)
|
|
}
|
|
// An access the definition gives no path is placed by a setting, which Places and AccessPlaces judge, and on
|
|
// the machine by the node-engine: nothing here to resolve it against, so nothing is refused for it.
|
|
if _, err := ParseManifest([]byte(`{"module": "backup", "version": "1",
|
|
"accesses": [{"id": "images"}],
|
|
"resources": [{"id": "marker", "type": "file", "path": "${access:images}/marker", "content": "x"}]}`)); err != nil {
|
|
t.Fatalf("an access placed only by a setting cannot be judged at the definition: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestComposingAMachineIsNeverStoppedByOneModulesPath(t *testing.T) {
|
|
// A module registered from outside the catalogue never passes the gate. Refusing its path while a machine's
|
|
// declaration is composed would fail the whole declaration and freeze every module on that machine; the
|
|
// node-engine fails only the one resource. So composition leaves it to the engine.
|
|
m := Manifest{Module: "docker", Version: "1", Resources: []map[string]any{
|
|
{"id": "state", "type": "directory", "place": "."},
|
|
}}
|
|
if _, err := (Resolution{Node: "anchor", Modules: []Manifest{m}}).Declaration(Rendering{}); err != nil {
|
|
t.Fatalf("the machine's declaration failed for one module's path: %v", err)
|
|
}
|
|
}
|