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. 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 } dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id } return dirs } // 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) { 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 } if fmt.Sprint(resource["type"]) == "directory" { id := fmt.Sprint(resource["id"]) if path, stated := resource["path"].(string); !stated || path == "" { resource["path"] = dirs[id] } } 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 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"]) } } } } } 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 }