Take the catalogue's composition rules into the node-engine
ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// asSaid is the layers with placement folded back into the values, for what reads a layer whole as it
|
||||
// did while placement was kept in it: today's mergeable file, which takes every key (ADR 0326 §3), and
|
||||
// the judgement that no value holds a line end.
|
||||
func asSaid(layers []mcat.Layer) []mcat.Layer {
|
||||
out := make([]mcat.Layer, len(layers))
|
||||
for i, l := range layers {
|
||||
out[i] = l
|
||||
if len(l.Placement) > 0 {
|
||||
out[i] = mcat.Layer{From: l.From, Values: l.Whole(), Default: l.Default}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var systemTrees = []string{"/etc", "/usr", "/boot", "/root", "/run", "/var/run", "/var/lock", "/proc", "/sys",
|
||||
"/dev", "/bin", "/sbin", "/lib", "/lib32", "/lib64", "/var/lib/mesh", "/var/lib/mesh-host", "/var/spool",
|
||||
"/var/lib/docker", "/var/lib/containers", "/var/lib/containerd", "/opt"}
|
||||
|
||||
var systemRoots = []string{"/", "/var", "/var/lib", "/var/cache", "/var/log", "/var/tmp", "/home",
|
||||
"/mnt", "/media", "/srv", "/tmp", "/storage", "/data", "/services"}
|
||||
|
||||
// systemPath says why a clean absolute path is the machine's own and never a placement's or an access's, or "".
|
||||
func systemPath(path string) string {
|
||||
// Any home's keys, wherever the home is: a .ssh directory is its account's, and the keys and the list of who
|
||||
// may log in as it are in there.
|
||||
for _, part := range strings.Split(path, "/") {
|
||||
if part == ".ssh" {
|
||||
return path + " is an account's .ssh, which holds its keys and who may log in as it"
|
||||
}
|
||||
}
|
||||
for _, tree := range systemTrees {
|
||||
if path == tree || strings.HasPrefix(path, tree+"/") {
|
||||
return path + " is in " + tree + ", the machine's own or the mesh's state"
|
||||
}
|
||||
if strings.HasPrefix(tree, path+"/") || path == "/" {
|
||||
return path + " holds " + tree + ", the machine's own or the mesh's state"
|
||||
}
|
||||
}
|
||||
for _, root := range systemRoots {
|
||||
if path == root {
|
||||
return path + " is the parent of every module's or every person's directories"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// engineModule is the node-engine's own module, the one that places in engineTrees.
|
||||
const engineModule = "mesh-host"
|
||||
|
||||
// accessRef is how a module names one of its accesses: ${access:<id>}.
|
||||
var accessRef = regexp.MustCompile(`\$\{access:([a-z0-9][a-z0-9-]*)\}`)
|
||||
|
||||
// AccessPlaces reads where this node keeps the operator's data the module accesses, by access id.
|
||||
//
|
||||
// It refuses an id the module declares no access under, and a path that is not absolute. An
|
||||
// access declared by path alone cannot be placed — it has no name to place it by.
|
||||
func AccessPlaces(m mcat.Manifest, layers []mcat.Layer) (map[string]string, error) {
|
||||
declared := map[string]bool{}
|
||||
for _, a := range m.Accesses {
|
||||
if a.ID != "" {
|
||||
declared[a.ID] = true
|
||||
}
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, layer := range layers {
|
||||
raw, ok := layer.Placement[mcat.AccessesSetting]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
blocks, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: %s is a { access: path } map, and %q set it to something else",
|
||||
m.Module, mcat.AccessesSetting, layer.From)
|
||||
}
|
||||
for id, body := range blocks {
|
||||
if !declared[id] {
|
||||
return nil, fmt.Errorf(
|
||||
"%s places the access %q, which it does not declare — the setting reaches "+
|
||||
"nothing. It declares %s", m.Module, id, orNothing(namesOfAccesses(m)))
|
||||
}
|
||||
path, _ := body.(string)
|
||||
path = strings.TrimSpace(path)
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return nil, fmt.Errorf("%s places the access %q at %v, which is not an absolute path",
|
||||
m.Module, id, body)
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if why := systemPath(path); why != "" {
|
||||
return nil, fmt.Errorf("%s places the access %q at %s: %s, and an access is mounted into the "+
|
||||
"module's container — no access is ever there (novox/hq issue 339)", m.Module, id, path, why)
|
||||
}
|
||||
out[id] = path
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// placedAccess is one access with the path it resolves to on this node.
|
||||
type placedAccess struct {
|
||||
ID string
|
||||
Path string
|
||||
Mode string
|
||||
}
|
||||
|
||||
// accessesFor is every access of a module with its path on this node: the assignment's where it
|
||||
// placed one, the definition's where it carries a default, and refused where neither says — an
|
||||
// access that resolves to nowhere would reach the machine as a mount of nothing.
|
||||
func accessesFor(m mcat.Manifest, layers []mcat.Layer) ([]placedAccess, map[string]string, error) {
|
||||
placed, err := AccessPlaces(m, layers)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var out []placedAccess
|
||||
byID := map[string]string{}
|
||||
for _, a := range m.Accesses {
|
||||
path := a.Path
|
||||
if a.ID != "" {
|
||||
if at, said := placed[a.ID]; said {
|
||||
path = at
|
||||
}
|
||||
}
|
||||
if path == "" {
|
||||
return nil, nil, fmt.Errorf(
|
||||
"%s accesses %q, and nothing says where that is on this node — the definition "+
|
||||
"carries no path (it must not, novox/hq ADR 0112) and the assignment places "+
|
||||
"none. Set %s: {%q: \"/where/it/is\"}",
|
||||
m.Module, a.ID, mcat.AccessesSetting, a.ID)
|
||||
}
|
||||
out = append(out, placedAccess{ID: a.ID, Path: path, Mode: a.At()})
|
||||
if a.ID != "" {
|
||||
byID[a.ID] = path
|
||||
}
|
||||
}
|
||||
return out, byID, nil
|
||||
}
|
||||
|
||||
// accessFill resolves every ${access:…} in one string, or refuses a reference naming no access.
|
||||
func accessFill(s string, accesses map[string]string, module string) (string, error) {
|
||||
var missing error
|
||||
out := accessRef.ReplaceAllStringFunc(s, func(ref string) string {
|
||||
id := accessRef.FindStringSubmatch(ref)[1]
|
||||
path, has := accesses[id]
|
||||
if !has {
|
||||
missing = fmt.Errorf(
|
||||
"%s says ${access:%s}, and %s declares no access %q. It declares %s",
|
||||
module, id, module, id, orNothing(namesOfAccessIDs(accesses)))
|
||||
return ref
|
||||
}
|
||||
return path
|
||||
})
|
||||
return out, missing
|
||||
}
|
||||
|
||||
// accessInto fills every ${access:…} a resource carries — in its path, its content, its mounts,
|
||||
// its environment and its env-files — with the path this node resolved for it. The same walk as
|
||||
// dirInto, for the same reason: a literal `${access:x}` reaching the machine would be mounted as
|
||||
// a directory called that.
|
||||
func accessInto(resource map[string]any, accesses map[string]string, module string) error {
|
||||
if !mentionsAccess(resource) {
|
||||
return nil
|
||||
}
|
||||
fill := func(s string) (string, error) { return accessFill(s, accesses, module) }
|
||||
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
|
||||
}
|
||||
}
|
||||
for _, field := range []string{"volumes", "env-file"} {
|
||||
list, ok := resource[field].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
filled := make([]any, len(list))
|
||||
for i, v := range list {
|
||||
filled[i] = v
|
||||
if s, ok := v.(string); ok {
|
||||
if filled[i], err = fill(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
resource[field] = 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 s, ok := v.(string); ok {
|
||||
if filled[key], err = fill(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
resource["env"] = filled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mentionsAccess(resource map[string]any) bool {
|
||||
for _, field := range []string{"path", "content"} {
|
||||
if s, ok := resource[field].(string); ok && accessRef.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, field := range []string{"volumes", "env-file"} {
|
||||
if list, ok := resource[field].([]any); ok {
|
||||
for _, v := range list {
|
||||
if s, ok := v.(string); ok && accessRef.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if env, ok := resource["env"].(map[string]any); ok {
|
||||
for _, v := range env {
|
||||
if s, ok := v.(string); ok && accessRef.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ownerInto gives a placed directory the owner the assignment said, where it said one. The
|
||||
// manifest's owner is what the image expects on any machine; the assignment's is what this
|
||||
// machine's data already is.
|
||||
func ownerInto(resource map[string]any, placed map[string]mcat.Placement) {
|
||||
if fmt.Sprint(resource["type"]) != "directory" {
|
||||
return
|
||||
}
|
||||
if p, ok := placed[fmt.Sprint(resource["id"])]; ok && p.Owner != "" {
|
||||
resource["owner"] = p.Owner
|
||||
}
|
||||
}
|
||||
|
||||
func namesOfAccesses(m mcat.Manifest) []string {
|
||||
var names []string
|
||||
for _, a := range m.Accesses {
|
||||
if a.ID != "" {
|
||||
names = append(names, fmt.Sprintf("%q", a.ID))
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func namesOfAccessIDs(accesses map[string]string) []string {
|
||||
var names []string
|
||||
for id := range accesses {
|
||||
names = append(names, fmt.Sprintf("%q", id))
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
Reference in New Issue
Block a user