A directory the mesh places: ${dir:<id>} and the pathless directory resource
The first executable slice of ADR 0112 / to-be 27, sized to what the
operator settled tonight: a module definition names no host path for
its own data. A directory resource may omit path; composition resolves
it to <root>/<module>/<id>, the root a node's setting on Rendering with
/var/lib as the default — which reproduces exactly the layout novox
converged to by hand. ${dir:<id>} names the place from a resource's
path, content, mounts, environment and env-files, the same shape as
${bound:…}. A directory that states a path keeps it and still answers
by name — that is the adopted-data placement, mssql its live case.
Resolved in the controller at composition, so the wire format and the
host change not at all; a reference naming no directory refuses at the
manifest and again at composition; nested fills are rebuilt, never
written into the manifest's own maps, because one manifest composes
for many nodes.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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 shallowCopy(resource map[string]any) map[string]any {
|
||||
copied := map[string]any{}
|
||||
for k, v := range resource {
|
||||
copied[k] = v
|
||||
}
|
||||
return copied
|
||||
}
|
||||
Reference in New Issue
Block a user