The assignment's own root is a place, and the manifest's maps are placed

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.
This commit is contained in:
2026-09-26 18:18:13 +02:00
parent cd2481dcd8
commit 2e3b13c0f8
4 changed files with 240 additions and 25 deletions
+15
View File
@@ -224,6 +224,21 @@ func (r Resolution) Compose(with Rendering) (Composed, error) {
} }
func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[string]any, error) { func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[string]any, error) {
// Every manifest is placed first (novox/hq ADR 0112): the maps naming where its bindings,
// credentials and contributions land are resolved against this node's directories, so every
// reader below — the binding files, the sealed secrets, the grant paths a contribution
// names — sees a concrete place and none learns the vocabulary.
// Into a fresh slice, never the caller's: one resolution may compose for many nodes, and a
// slice element written in place would carry the first node's places into the second's.
placed := make([]Manifest, len(r.Modules))
for i, m := range r.Modules {
var err error
if placed[i], err = placedManifest(m, with); err != nil {
return nil, err
}
}
r.Modules = placed
// Where each provision's credentials land, so a contribution can name the file rather than // Where each provision's credentials land, so a contribution can name the file rather than
// carry a value the mesh does not have. // carry a value the mesh does not have.
directories := map[string]string{} directories := map[string]string{}
+120 -9
View File
@@ -39,6 +39,11 @@ func dataRoot(with Rendering) string {
} }
// dirsFor is every placed directory of a module, id → the path it resolves to on this node. // dirsFor is every placed directory of a module, id → the path it resolves to on this node.
//
// A pathless directory saying `"place": "."` is the assignment's own root, <root>/<module> —
// to-be 27's one directory per assignment, which every other placed thing sits beneath. At most
// one makes sense; nothing enforces one, because two ids resolving to one path is a mistake the
// module's own files make visible immediately.
func dirsFor(m Manifest, with Rendering) map[string]string { func dirsFor(m Manifest, with Rendering) map[string]string {
dirs := map[string]string{} dirs := map[string]string{}
for _, r := range m.Resources { for _, r := range m.Resources {
@@ -50,20 +55,25 @@ func dirsFor(m Manifest, with Rendering) map[string]string {
dirs[id] = strings.TrimRight(path, "/") dirs[id] = strings.TrimRight(path, "/")
continue continue
} }
if place, said := r["place"].(string); said && place == "." {
dirs[id] = dataRoot(with) + "/" + m.Module
continue
}
dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id
} }
return dirs return dirs
} }
// dirInto places a resource: a pathless directory is given the path the mesh resolved for it, // placedOrAbsolute says a path is usable where the mesh needs one: absolute already, or
// and every ${dir:…} the resource carries — in its path, its content, its mounts, its // beginning with a placed reference — resolution makes it absolute before anything reads it.
// environment and its env-files — becomes that path. // (unknownDirRefs is what checks the reference names a real directory.)
// func placedOrAbsolute(path string) bool {
// A reference naming no directory of this module is refused. Left as written, the literal return strings.HasPrefix(path, "/") ||
// `${dir:x}` would reach the machine as a path, and the runtime would create and mount a (strings.HasPrefix(path, "${dir:") && dirRef.MatchString(path))
// directory called `${dir:x}` — real, wrong, and named after the mistake. }
func dirInto(resource map[string]any, dirs map[string]string, module string) error {
fill := func(s string) (string, error) { // dirFill resolves every ${dir:…} in one string, or refuses a reference naming no directory.
func dirFill(s string, dirs map[string]string, module string) (string, error) {
var missing error var missing error
out := dirRef.ReplaceAllStringFunc(s, func(ref string) string { out := dirRef.ReplaceAllStringFunc(s, func(ref string) string {
id := dirRef.FindStringSubmatch(ref)[1] id := dirRef.FindStringSubmatch(ref)[1]
@@ -79,11 +89,72 @@ func dirInto(resource map[string]any, dirs map[string]string, module string) err
return out, missing return out, missing
} }
// placedManifest is the manifest with every path the mesh resolves already resolved: the maps
// naming where bindings, credentials and contributions land are filled against this node's
// placed directories, so everything downstream — the generated binding files, the sealed
// secrets, the grant directories — reads a concrete place and learns nothing new.
func placedManifest(m Manifest, with Rendering) (Manifest, error) {
dirs := dirsFor(m, with)
fillMap := func(in map[string]string) (map[string]string, error) {
if len(in) == 0 {
return in, nil
}
out := make(map[string]string, len(in))
for key, value := range in {
filled, err := dirFill(value, dirs, m.Module)
if err != nil {
return nil, err
}
out[key] = filled
}
return out, nil
}
var err error
if m.Receives, err = fillMap(m.Receives); err != nil {
return m, err
}
if m.Binds, err = fillMap(m.Binds); err != nil {
return m, err
}
if m.Secrets, err = fillMap(m.Secrets); err != nil {
return m, err
}
if m.OwnSecrets, err = fillMap(m.OwnSecrets); err != nil {
return m, err
}
if m.Grants, err = fillMap(m.Grants); err != nil {
return m, err
}
if len(m.SecretsMany) > 0 {
many := make(map[string]map[string]string, len(m.SecretsMany))
for to, locals := range m.SecretsMany {
if many[to], err = fillMap(locals); err != nil {
return m, err
}
}
m.SecretsMany = many
}
return m, nil
}
// dirInto places a resource: a pathless directory is given the path the mesh resolved for it,
// and every ${dir:…} the resource carries — in its path, its content, its mounts, its
// environment and its env-files — becomes that path.
//
// A reference naming no directory of this module is refused. Left as written, the literal
// `${dir:x}` would reach the machine as a path, and the runtime would create and mount a
// directory called `${dir:x}` — real, wrong, and named after the mistake.
func dirInto(resource map[string]any, dirs map[string]string, module string) error {
fill := func(s string) (string, error) { return dirFill(s, dirs, module) }
if fmt.Sprint(resource["type"]) == "directory" { if fmt.Sprint(resource["type"]) == "directory" {
id := fmt.Sprint(resource["id"]) id := fmt.Sprint(resource["id"])
if path, stated := resource["path"].(string); !stated || path == "" { if path, stated := resource["path"].(string); !stated || path == "" {
resource["path"] = dirs[id] resource["path"] = dirs[id]
} }
// Said in the catalogue, not on the machine: the host parses strictly and knows no
// such field — resolved, the place IS the path.
delete(resource, "place")
} }
var err error var err error
@@ -157,6 +228,28 @@ func (m Manifest) unknownDirRefs() []string {
return ids return ids
} }
var problems []string var problems []string
for _, r := range m.Resources {
place, said := r["place"].(string)
if !said {
continue
}
if fmt.Sprint(r["type"]) != "directory" {
problems = append(problems, fmt.Sprintf(
"%s says place on %v, which is not a directory — only a directory is placed",
m.Module, r["id"]))
continue
}
if path, stated := r["path"].(string); stated && path != "" {
problems = append(problems, fmt.Sprintf(
"%s states both path and place on %v — a stated path IS the placement",
m.Module, r["id"]))
}
if place != "." {
problems = append(problems, fmt.Sprintf(
"%s says place %q on %v, and the only place is %q — the assignment's own root",
m.Module, place, r["id"], "."))
}
}
seen := map[string]bool{} seen := map[string]bool{}
refuse := func(id string, where any) { refuse := func(id string, where any) {
if declared[id] || seen[id] { if declared[id] || seen[id] {
@@ -197,6 +290,24 @@ func (m Manifest) unknownDirRefs() []string {
} }
} }
} }
maps := map[string]map[string]string{
"receives": m.Receives, "binds": m.Binds, "secrets": m.Secrets,
"own-secrets": m.OwnSecrets, "grants": m.Grants,
}
for field, entries := range maps {
for _, value := range entries {
for _, id := range referenced(value) {
refuse(id, field)
}
}
}
for to, locals := range m.SecretsMany {
for _, value := range locals {
for _, id := range referenced(value) {
refuse(id, "secrets."+to)
}
}
}
sort.Strings(problems) sort.Strings(problems)
return problems return problems
} }
+89
View File
@@ -132,6 +132,95 @@ func TestAReferenceToNoDirectoryRefusesAtCompositionToo(t *testing.T) {
} }
} }
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 { func shallowCopy(resource map[string]any) map[string]any {
copied := map[string]any{} copied := map[string]any{}
for k, v := range resource { for k, v := range resource {
+10 -10
View File
@@ -1049,9 +1049,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
} }
} }
for to, where := range m.Binds { for to, where := range m.Binds {
if !strings.HasPrefix(where, "/") { if !placedOrAbsolute(where) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s binds %q at %q, which is not an absolute path", m.Module, to, where)) "%s binds %q at %q, which is neither an absolute path nor a placed one", m.Module, to, where))
} }
var wanted bool var wanted bool
for _, w := range m.Wants() { for _, w := range m.Wants() {
@@ -1183,9 +1183,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
} }
} }
for name, where := range m.OwnSecrets { for name, where := range m.OwnSecrets {
if !strings.HasPrefix(where, "/") { if !placedOrAbsolute(where) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s needs %q at %q, which is not an absolute path", m.Module, name, where)) "%s needs %q at %q, which is neither an absolute path nor a placed one", m.Module, name, where))
} }
if name == "" { if name == "" {
problems = append(problems, m.Module+" needs a secret with no name") problems = append(problems, m.Module+" needs a secret with no name")
@@ -1200,9 +1200,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
} }
} }
for _, f := range m.SecretFiles(to) { for _, f := range m.SecretFiles(to) {
if !strings.HasPrefix(f.Path, "/") { if !placedOrAbsolute(f.Path) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s keeps the credential for %q at %q, which is not an absolute path", "%s keeps the credential for %q at %q, which is neither an absolute path nor a placed one",
m.Module, SecretLocal(to, f.Local), f.Path)) m.Module, SecretLocal(to, f.Local), f.Path))
} }
if f.Local != "" && !name.MatchString(f.Local) { if f.Local != "" && !name.MatchString(f.Local) {
@@ -1252,9 +1252,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
} }
} }
for to, where := range m.Grants { for to, where := range m.Grants {
if !strings.HasPrefix(where, "/") { if !placedOrAbsolute(where) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s grants %q into %q, which is not an absolute path", m.Module, to, where)) "%s grants %q into %q, which is neither an absolute path nor a placed one", m.Module, to, where))
} }
var offered bool var offered bool
for _, o := range m.Offers() { for _, o := range m.Offers() {
@@ -1275,9 +1275,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
if !name.MatchString(to) { if !name.MatchString(to) {
problems = append(problems, fmt.Sprintf("%q is not a usable name to receive", to)) problems = append(problems, fmt.Sprintf("%q is not a usable name to receive", to))
} }
if !strings.HasPrefix(where, "/") { if !placedOrAbsolute(where) {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s receives %q at %q, which is not an absolute path", m.Module, to, where)) "%s receives %q at %q, which is neither an absolute path nor a placed one", m.Module, to, where))
} }
var offered bool var offered bool
for _, o := range m.Offers() { for _, o := range m.Offers() {