package catalogue import ( "fmt" "regexp" "sort" "strings" ) // A directory the mesh places (novox/hq ADR 0112, to-be 27, issue 119). // // **A module definition names no host path.** A directory resource may omit `path`; the mesh // resolves where it lands when the declaration is composed — `//`, the root a // node's own setting with /var/lib as the default. From then on the module's own files, mounts // and environment name the place as `${dir:}`, the same shape as `${bound:…}` and // `${secret:…}`: a fact the module asks for by name and never states. // // **A directory that states a path keeps it, and still answers `${dir:}`.** That is the // placement for an adopted machine: data that must sit where the predecessor already put it is // declared with the path as the exception it is, and everything else in the module names it by // id — so moving it later is one line, not a search. // // **Resolved here, not on the machine.** The host receives concrete paths exactly as it always // has; nothing new reaches it and it learns no field. Which also means a resolved path changing // is a spec change like any other — and the spec comparison must see it (novox/hq issue 126). // defaultDataRoot is where module data lands when a node states no root of its own. const defaultDataRoot = "/var/lib" // dirRef is how a module names one of its placed directories: ${dir:}. var dirRef = regexp.MustCompile(`\$\{dir:([a-z0-9][a-z0-9-]*)\}`) // dataRoot is the root this node keeps placed directories under. func dataRoot(with Rendering) string { if root := strings.TrimRight(strings.TrimSpace(with.DataRoot), "/"); root != "" { return root } return defaultDataRoot } // 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, / — // 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 { if fmt.Sprint(r["type"]) != "directory" { continue } id := fmt.Sprint(r["id"]) if path, stated := r["path"].(string); stated && path != "" { 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. // // 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" { 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 if path, ok := resource["path"].(string); ok { if resource["path"], err = fill(path); err != nil { return err } } if content, ok := resource["content"].(string); ok { if resource["content"], err = fill(content); err != nil { return err } } // Nested values are rebuilt, never written into: the resource is a shallow copy of the // manifest's own map, and the manifest is composed once per node — a fill written in place // would leave the first node's paths inside every later composition. if volumes, ok := resource["volumes"].([]any); ok { filled := make([]any, len(volumes)) for i, v := range volumes { filled[i] = v if mount, ok := v.(string); ok { if filled[i], err = fill(mount); err != nil { return err } } } resource["volumes"] = filled } if env, ok := resource["env"].(map[string]any); ok { filled := make(map[string]any, len(env)) for key, v := range env { filled[key] = v if value, ok := v.(string); ok { if filled[key], err = fill(value); err != nil { return err } } } resource["env"] = filled } if files, ok := resource["env-file"].([]any); ok { filled := make([]any, len(files)) for i, v := range files { filled[i] = v if path, ok := v.(string); ok { if filled[i], err = fill(path); err != nil { return err } } } resource["env-file"] = filled } return nil } // unknownDirRefs is every ${dir:…} in the definition that names no directory the definition // declares — refused where the author is, not at composition on some later day (the same // near-versus-far reasoning as the host's strict parse). func (m Manifest) unknownDirRefs() []string { declared := map[string]bool{} for _, r := range m.Resources { if fmt.Sprint(r["type"]) == "directory" { declared[fmt.Sprint(r["id"])] = true } } referenced := func(s string) []string { var ids []string for _, match := range dirRef.FindAllStringSubmatch(s, -1) { ids = append(ids, match[1]) } 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] { return } seen[id] = true problems = append(problems, fmt.Sprintf( "%s says ${dir:%s} in %v, and declares no directory %q — a reference the mesh "+ "cannot place would reach the machine as a literal path", m.Module, id, where, id)) } for _, r := range m.Resources { for _, field := range []string{"path", "content"} { if s, ok := r[field].(string); ok { for _, id := range referenced(s) { refuse(id, r["id"]) } } } for _, field := range []string{"volumes", "env-file"} { if list, ok := r[field].([]any); ok { for _, v := range list { if s, ok := v.(string); ok { for _, id := range referenced(s) { refuse(id, r["id"]) } } } } } if env, ok := r["env"].(map[string]any); ok { for _, v := range env { if s, ok := v.(string); ok { for _, id := range referenced(s) { refuse(id, r["id"]) } } } } } 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 } func namesOfDirs(dirs map[string]string) []string { var names []string for id := range dirs { names = append(names, fmt.Sprintf("%q", id)) } sort.Strings(names) return names }