366 lines
12 KiB
Go
366 lines
12 KiB
Go
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 — `<root>/<module>/<id>`, 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:<id>}`, 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:<id>}`.** 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 `<root>/mesh/<module>`. A directory
|
|
// saying `"place": "mesh"` is that place; the definition names the files beneath it by
|
|
// `${dir:<id>}` 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, <root>/<module> — 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, <root>/mesh/<module>.
|
|
placeMesh = "mesh"
|
|
)
|
|
|
|
// dirRef is how a module names one of its placed directories: ${dir:<id>}.
|
|
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, <root>/<module>; one
|
|
// saying `"place": "mesh"` is the mesh's directory for the module, <root>/mesh/<module>; one
|
|
// saying neither is <root>/<module>/<id>. 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
|
|
}
|
|
|
|
// 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 != placeOwn && place != placeMesh {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s says place %q on %v, and the places are %q — the assignment's own root — and "+
|
|
"%q — where the mesh keeps what it writes for the module",
|
|
m.Module, place, r["id"], placeOwn, placeMesh))
|
|
}
|
|
}
|
|
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
|
|
}
|