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. // // **The mesh's own files for a module are placed too** (novox/hq issue 174). What the mesh writes // *for* a module — its sealed bus credential, its merged configuration, its bindings — is the // mesh's plumbing, not the module's data, and sits under `/mesh/`. A directory // saying `"place": "mesh"` is that place; the definition names the files beneath it by // `${dir:}` and states no path. // // **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" // The two places a pathless directory may name, beside its own id. const ( // placeOwn is the assignment's own root, / — to-be 27's one directory per // assignment, which every other placed thing of the module sits beneath. placeOwn = "." // placeMesh is where the mesh keeps what it writes for the module, /mesh/. placeMesh = "mesh" ) // 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, /; one // saying `"place": "mesh"` is the mesh's directory for the module, /mesh/; one // saying neither is //. At most one of each place makes sense; nothing enforces // one, because two ids resolving to one path is a mistake the module's own files make visible // immediately. // // A stated path may itself begin with a placed reference — `${dir:mesh-state}/state` — and is // filled after the directories it can name are resolved; one level, because a directory beneath // a placed one is the whole of what an adopted layout needs (issue 174's `state` and `out`). func dirsFor(m Manifest, with Rendering) map[string]string { dirs := map[string]string{} var beneath []map[string]any // The assignment's placement wins over both (novox/hq issue 153). Refused elsewhere when // malformed; here an invalid setting simply places nothing. placed, _ := Places(m, with.Settings[m.Module]) for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "directory" { continue } id := fmt.Sprint(r["id"]) if p, said := placed[id]; said { dirs[id] = p.Path continue } if path, stated := r["path"].(string); stated && path != "" { if strings.HasPrefix(path, "${dir:") { beneath = append(beneath, r) continue } dirs[id] = strings.TrimRight(path, "/") continue } switch place, _ := r["place"].(string); place { case placeOwn: dirs[id] = dataRoot(with) + "/" + m.Module case placeMesh: dirs[id] = dataRoot(with) + "/mesh/" + m.Module default: dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id } } for _, r := range beneath { path := strings.TrimRight(r["path"].(string), "/") // A reference to no directory is left as written and refused where the resource is // placed (dirInto), with the message that names what exists. filled, _ := dirFill(path, dirs, m.Module) dirs[fmt.Sprint(r["id"])] = filled } return dirs } // 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 len(m.OwnSecrets) > 0 { own := make(OwnSecrets, len(m.OwnSecrets)) for name, s := range m.OwnSecrets { filled, err := dirFill(s.Path, dirs, m.Module) if err != nil { return m, err } own[name] = OwnSecret{Path: filled, Taken: s.Taken, IssuedBy: s.IssuedBy, Prompt: s.Prompt, Owner: s.Owner} } m.OwnSecrets = own } 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 } 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 }