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:
+126
-15
@@ -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.
|
||||
//
|
||||
// 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 {
|
||||
dirs := map[string]string{}
|
||||
for _, r := range m.Resources {
|
||||
@@ -50,11 +55,88 @@ func dirsFor(m Manifest, with Rendering) map[string]string {
|
||||
dirs[id] = strings.TrimRight(path, "/")
|
||||
continue
|
||||
}
|
||||
if place, said := r["place"].(string); said && place == "." {
|
||||
dirs[id] = dataRoot(with) + "/" + m.Module
|
||||
continue
|
||||
}
|
||||
dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// placedOrAbsolute says a path is usable where the mesh needs one: absolute already, or
|
||||
// beginning with a placed reference — resolution makes it absolute before anything reads it.
|
||||
// (unknownDirRefs is what checks the reference names a real directory.)
|
||||
func placedOrAbsolute(path string) bool {
|
||||
return strings.HasPrefix(path, "/") ||
|
||||
(strings.HasPrefix(path, "${dir:") && dirRef.MatchString(path))
|
||||
}
|
||||
|
||||
// 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
|
||||
out := dirRef.ReplaceAllStringFunc(s, func(ref string) string {
|
||||
id := dirRef.FindStringSubmatch(ref)[1]
|
||||
path, has := dirs[id]
|
||||
if !has {
|
||||
missing = fmt.Errorf(
|
||||
"%s says ${dir:%s}, and %s declares no directory %q. It declares %s",
|
||||
module, id, module, id, orNothing(namesOfDirs(dirs)))
|
||||
return ref
|
||||
}
|
||||
return path
|
||||
})
|
||||
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.
|
||||
@@ -63,27 +145,16 @@ func dirsFor(m Manifest, with Rendering) map[string]string {
|
||||
// `${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) {
|
||||
var missing error
|
||||
out := dirRef.ReplaceAllStringFunc(s, func(ref string) string {
|
||||
id := dirRef.FindStringSubmatch(ref)[1]
|
||||
path, has := dirs[id]
|
||||
if !has {
|
||||
missing = fmt.Errorf(
|
||||
"%s says ${dir:%s}, and %s declares no directory %q. It declares %s",
|
||||
module, id, module, id, orNothing(namesOfDirs(dirs)))
|
||||
return ref
|
||||
}
|
||||
return path
|
||||
})
|
||||
return out, missing
|
||||
}
|
||||
fill := func(s string) (string, error) { return dirFill(s, dirs, module) }
|
||||
|
||||
if fmt.Sprint(resource["type"]) == "directory" {
|
||||
id := fmt.Sprint(resource["id"])
|
||||
if path, stated := resource["path"].(string); !stated || path == "" {
|
||||
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
|
||||
@@ -157,6 +228,28 @@ func (m Manifest) unknownDirRefs() []string {
|
||||
return ids
|
||||
}
|
||||
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{}
|
||||
refuse := func(id string, where any) {
|
||||
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)
|
||||
return problems
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user