Slice two of ADR 0112. A pathless directory saying place "." is the
assignment's one directory, <root>/<module> — to-be 27's shape — and
place never reaches the host, which parses strictly. The maps naming
where bindings, credentials and contributions land (binds, secrets,
own-secrets, receives, grants) fill against the placed directories at
composition, into fresh maps and a fresh module slice, because one
resolution composes for many nodes. The five absolute-path checks on
those maps accept a placed reference — resolution makes it absolute
before anything reads it — while certificate, operator-keeps and
accesses paths stay absolute-only: those are the operator's or another
vocabulary's. unknownDirRefs scans the maps too, and validates place
itself: only on a directory, only ".", never beside a stated path.
Found by the foundation tests validating the sibling catalogue: the
first conversion's blanket replace turned /var/lib/gitea/database.json
into ${dir:data}base.json — which resolves to the right path by pure
string concatenation. Production was saved by a coincidence; the
catalogue cleanup that follows spells it ${dir:state}/database.json.
231 lines
8.5 KiB
Go
231 lines
8.5 KiB
Go
package catalogue
|
|
|
|
// A directory the mesh places (novox/hq ADR 0112). These tests pin the contract: a pathless
|
|
// directory resolves under the node's root, ${dir:…} names it from every field a host path can
|
|
// live in, a stated path is the adopted-data placement and wins, an unknown reference refuses at
|
|
// the manifest, and filling for one node never leaks into the next composition.
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func placedModule() Manifest {
|
|
return Manifest{
|
|
Module: "photos",
|
|
Resources: []map[string]any{
|
|
{"id": "data", "type": "directory", "mode": "0700"},
|
|
{"id": "server-env", "type": "file", "path": "${dir:data}/server.env",
|
|
"content": "STORE=${dir:data}/objects\n"},
|
|
{"id": "server", "type": "container", "name": "photos-server",
|
|
"volumes": []any{"${dir:data}:/data"},
|
|
"env": map[string]any{"DATA": "${dir:data}/objects"},
|
|
"env-file": []any{"${dir:data}/server.env"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestAPathlessDirectoryResolvesUnderTheNodesRoot(t *testing.T) {
|
|
m := placedModule()
|
|
dirs := dirsFor(m, Rendering{})
|
|
if dirs["data"] != "/var/lib/photos/data" {
|
|
t.Fatalf("the default root is /var/lib and the shape is <root>/<module>/<id>; got %q", dirs["data"])
|
|
}
|
|
dirs = dirsFor(m, Rendering{DataRoot: "/tank/nox/"})
|
|
if dirs["data"] != "/tank/nox/photos/data" {
|
|
t.Fatalf("a node's own root is honoured, trailing slash and all; got %q", dirs["data"])
|
|
}
|
|
}
|
|
|
|
func TestDirReferencesBecomeThePlaceInEveryField(t *testing.T) {
|
|
m := placedModule()
|
|
dirs := dirsFor(m, Rendering{})
|
|
|
|
directory := shallowCopy(m.Resources[0])
|
|
if err := dirInto(directory, dirs, m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if directory["path"] != "/var/lib/photos/data" {
|
|
t.Fatalf("a pathless directory receives its resolved path; got %v", directory["path"])
|
|
}
|
|
|
|
file := shallowCopy(m.Resources[1])
|
|
if err := dirInto(file, dirs, m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if file["path"] != "/var/lib/photos/data/server.env" {
|
|
t.Fatalf("a file's path names the place; got %v", file["path"])
|
|
}
|
|
if file["content"] != "STORE=/var/lib/photos/data/objects\n" {
|
|
t.Fatalf("a file's content names the place; got %v", file["content"])
|
|
}
|
|
|
|
container := shallowCopy(m.Resources[2])
|
|
if err := dirInto(container, dirs, m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if container["volumes"].([]any)[0] != "/var/lib/photos/data:/data" {
|
|
t.Fatalf("a mount names the place; got %v", container["volumes"])
|
|
}
|
|
if container["env"].(map[string]any)["DATA"] != "/var/lib/photos/data/objects" {
|
|
t.Fatalf("an environment value names the place; got %v", container["env"])
|
|
}
|
|
if container["env-file"].([]any)[0] != "/var/lib/photos/data/server.env" {
|
|
t.Fatalf("an env-file names the place; got %v", container["env-file"])
|
|
}
|
|
}
|
|
|
|
func TestAStatedPathIsThePlacementAndStillAnswersByName(t *testing.T) {
|
|
m := placedModule()
|
|
// The adopted-machine case: data that must sit where the predecessor already put it.
|
|
m.Resources[0]["path"] = "/services/mssql/data/"
|
|
dirs := dirsFor(m, Rendering{})
|
|
if dirs["data"] != "/services/mssql/data" {
|
|
t.Fatalf("a stated path wins over the root, trimmed; got %q", dirs["data"])
|
|
}
|
|
container := shallowCopy(m.Resources[2])
|
|
if err := dirInto(container, dirs, m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if container["volumes"].([]any)[0] != "/services/mssql/data:/data" {
|
|
t.Fatalf("references follow the placement; got %v", container["volumes"])
|
|
}
|
|
}
|
|
|
|
func TestFillingForOneNodeLeaksIntoNoOther(t *testing.T) {
|
|
m := placedModule()
|
|
first := shallowCopy(m.Resources[2])
|
|
if err := dirInto(first, dirsFor(m, Rendering{DataRoot: "/first"}), m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second := shallowCopy(m.Resources[2])
|
|
if err := dirInto(second, dirsFor(m, Rendering{DataRoot: "/second"}), m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := second["volumes"].([]any)[0]; got != "/second/photos/data:/data" {
|
|
t.Fatalf("the second composition must see the manifest, not the first fill; got %v", got)
|
|
}
|
|
if m.Resources[2]["volumes"].([]any)[0] != "${dir:data}:/data" {
|
|
t.Fatalf("the manifest itself stays a template; got %v", m.Resources[2]["volumes"])
|
|
}
|
|
}
|
|
|
|
func TestAReferenceToNoDirectoryRefusesAtTheManifest(t *testing.T) {
|
|
m := placedModule()
|
|
m.Resources[2]["volumes"] = []any{"${dir:date}:/data"} // a typo, the likely shape
|
|
problems := m.unknownDirRefs()
|
|
if len(problems) != 1 || !strings.Contains(problems[0], `${dir:date}`) {
|
|
t.Fatalf("a reference naming no directory is a manifest problem; got %v", problems)
|
|
}
|
|
if got := placedModule().unknownDirRefs(); len(got) != 0 {
|
|
t.Fatalf("a correct definition has none; got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAReferenceToNoDirectoryRefusesAtCompositionToo(t *testing.T) {
|
|
m := placedModule()
|
|
container := shallowCopy(m.Resources[2])
|
|
container["env"] = map[string]any{"DATA": "${dir:date}"}
|
|
err := dirInto(container, dirsFor(m, Rendering{}), m.Module)
|
|
if err == nil || !strings.Contains(err.Error(), `"date"`) || !strings.Contains(err.Error(), `"data"`) {
|
|
t.Fatalf("the refusal names the mistake and what exists; got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheAssignmentsOwnRootIsAPlace(t *testing.T) {
|
|
m := Manifest{Module: "mailu", Resources: []map[string]any{
|
|
{"id": "state", "type": "directory", "place": ".", "mode": "0700"},
|
|
{"id": "data-mail", "type": "directory"},
|
|
}}
|
|
dirs := dirsFor(m, Rendering{})
|
|
if dirs["state"] != "/var/lib/mailu" {
|
|
t.Fatalf("place %q is the assignment's root; got %q", ".", dirs["state"])
|
|
}
|
|
if dirs["data-mail"] != "/var/lib/mailu/data-mail" {
|
|
t.Fatalf("everything else sits beneath it; got %q", dirs["data-mail"])
|
|
}
|
|
root := shallowCopy(m.Resources[0])
|
|
if err := dirInto(root, dirs, m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if root["path"] != "/var/lib/mailu" {
|
|
t.Fatalf("the root receives its path; got %v", root["path"])
|
|
}
|
|
if _, still := root["place"]; still {
|
|
t.Fatal("place must never reach the host, which parses strictly")
|
|
}
|
|
}
|
|
|
|
func TestTheManifestsMapsArePlaced(t *testing.T) {
|
|
m := Manifest{
|
|
Module: "photos",
|
|
Resources: []map[string]any{
|
|
{"id": "state", "type": "directory", "place": "."},
|
|
},
|
|
Binds: map[string]string{"route": "${dir:state}/route.json"},
|
|
Secrets: map[string]string{"mongodb-database": "${dir:state}/database.secret"},
|
|
OwnSecrets: map[string]string{"admin-key": "${dir:state}/admin-key.secret"},
|
|
Receives: map[string]string{"route": "${dir:state}/grants/mesh.json"},
|
|
}
|
|
placed, err := placedManifest(m, Rendering{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if placed.Binds["route"] != "/var/lib/photos/route.json" {
|
|
t.Fatalf("binds are placed; got %v", placed.Binds)
|
|
}
|
|
if placed.Secrets["mongodb-database"] != "/var/lib/photos/database.secret" {
|
|
t.Fatalf("secrets are placed; got %v", placed.Secrets)
|
|
}
|
|
if placed.OwnSecrets["admin-key"] != "/var/lib/photos/admin-key.secret" {
|
|
t.Fatalf("own-secrets are placed; got %v", placed.OwnSecrets)
|
|
}
|
|
if placed.Receives["route"] != "/var/lib/photos/grants/mesh.json" {
|
|
t.Fatalf("receives are placed; got %v", placed.Receives)
|
|
}
|
|
if m.Binds["route"] != "${dir:state}/route.json" {
|
|
t.Fatalf("the manifest itself stays a template; got %v", m.Binds)
|
|
}
|
|
}
|
|
|
|
func TestAMapReferenceToNoDirectoryRefusesAtTheManifest(t *testing.T) {
|
|
m := Manifest{
|
|
Module: "photos",
|
|
Resources: []map[string]any{{"id": "state", "type": "directory", "place": "."}},
|
|
Binds: map[string]string{"route": "${dir:stat}/route.json"},
|
|
}
|
|
problems := m.unknownDirRefs()
|
|
if len(problems) != 1 || !strings.Contains(problems[0], `${dir:stat}`) {
|
|
t.Fatalf("a map naming no directory is a manifest problem; got %v", problems)
|
|
}
|
|
}
|
|
|
|
func TestPlaceIsValidatedAtTheManifest(t *testing.T) {
|
|
both := Manifest{Module: "x", Resources: []map[string]any{
|
|
{"id": "d", "type": "directory", "place": ".", "path": "/somewhere"},
|
|
}}
|
|
if got := both.unknownDirRefs(); len(got) != 1 || !strings.Contains(got[0], "both path and place") {
|
|
t.Fatalf("path beside place refuses; got %v", got)
|
|
}
|
|
elsewhere := Manifest{Module: "x", Resources: []map[string]any{
|
|
{"id": "f", "type": "file", "place": ".", "path": "/somewhere", "content": ""},
|
|
}}
|
|
if got := elsewhere.unknownDirRefs(); len(got) != 1 || !strings.Contains(got[0], "not a directory") {
|
|
t.Fatalf("place on a file refuses; got %v", got)
|
|
}
|
|
wrong := Manifest{Module: "x", Resources: []map[string]any{
|
|
{"id": "d", "type": "directory", "place": "sub/dir"},
|
|
}}
|
|
if got := wrong.unknownDirRefs(); len(got) != 1 || !strings.Contains(got[0], `the only place is "."`) {
|
|
t.Fatalf("a place that is not the root refuses; got %v", got)
|
|
}
|
|
}
|
|
|
|
func shallowCopy(resource map[string]any) map[string]any {
|
|
copied := map[string]any{}
|
|
for k, v := range resource {
|
|
copied[k] = v
|
|
}
|
|
return copied
|
|
}
|