mesh/delivery-group group feat/sdk1 rejected: composed together: the checker's verdict at the members' heads (428d048dd50a): unit: git.novox.be/novox/mesh-sdk/…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-sdk/go/process.TestStatePutWritesAKeyWhole: a Put through a short blackout: 0, mesh-mail's state send…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
182 lines
6.8 KiB
Go
182 lines
6.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// PlacesSetting is the settings key that places a module's declared directories, by id.
|
|
const PlacesSetting = "places"
|
|
|
|
// AccessesSetting is the settings key that says where a module's accesses are on this node, by id.
|
|
const AccessesSetting = "accesses"
|
|
|
|
// placementKeys are the keys of a layer that are placement, not config.
|
|
var placementKeys = map[string]bool{PlacesSetting: true, AccessesSetting: true, NetworksSetting: true,
|
|
EndpointsSetting: true, ExposeSetting: true, ReachSetting: true, PortsSetting: true}
|
|
|
|
// PlacementKeys are the keys of a layer that are placement, not config, sorted: what a settings row must
|
|
// no longer hold once the move of placement has run (issue 736).
|
|
func PlacementKeys() []string {
|
|
keys := make([]string, 0, len(placementKeys))
|
|
for k := range placementKeys {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// Whole is the layer as a person says it: its values and its placement in one map, a new map.
|
|
func (l Layer) Whole() map[string]any {
|
|
out := make(map[string]any, len(l.Values)+len(l.Placement))
|
|
for k, v := range l.Values {
|
|
out[k] = v
|
|
}
|
|
for k, v := range l.Placement {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Placement is what an assignment says about one of a module's directories.
|
|
type Placement struct {
|
|
// Path is where the directory is on this machine. Absolute.
|
|
Path string
|
|
// Owner is "uid:gid" when the assignment overrides the manifest's — the predecessor's data is
|
|
// owned by whoever it ran as, and that is one machine's fact.
|
|
Owner string
|
|
}
|
|
|
|
var ownerShape = regexp.MustCompile(`^[0-9]+:[0-9]+$`)
|
|
|
|
// Where no placement and no access may be, from any route, the controller's terminal too (novox/hq issue 339).
|
|
//
|
|
// A placed directory is created and owned by the node-engine as root, with the owner the setting names, and
|
|
// whatever the module writes into it is written as root; an access is mounted into the module's container,
|
|
// which may run as root. A place at /etc owned by an account a caller names hands that account the machine,
|
|
// and an access at / mounts the machine's root into a container. So the machine's own trees are refused here,
|
|
// before anything is kept or composed, and the node-engine refuses them again where it applies.
|
|
//
|
|
// systemTrees are refused at and below: the machine's system, the kernel's, the boot loader's, root's home,
|
|
// what lives only while the machine runs, the spool (cron's tables are there), the container runtimes' data
|
|
// (every container's filesystem), /opt, and the node-engine's and the mesh's own state. Any home's .ssh is
|
|
// refused as well, wherever the home is. systemRoots are
|
|
// refused at, and wherever a path holds one (an ancestor of /var/lib holds it): each is the parent of every
|
|
// module's or every person's directories, and owning it is owning all of them.
|
|
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"}
|
|
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 ""
|
|
}
|
|
|
|
// Places reads where this node places the module's directories, by directory id.
|
|
//
|
|
// It refuses an id the module declares no directory for, a path that is not absolute, and an
|
|
// owner that is not uid:gid. A directory the assignment does not mention keeps the manifest's
|
|
// stated path or the node's default layout.
|
|
func Places(m Manifest, layers []Layer) (map[string]Placement, error) {
|
|
declared := map[string]bool{}
|
|
for _, r := range m.Resources {
|
|
if fmt.Sprint(r["type"]) == "directory" {
|
|
declared[fmt.Sprint(r["id"])] = true
|
|
}
|
|
}
|
|
out := map[string]Placement{}
|
|
for _, layer := range layers {
|
|
raw, ok := layer.Placement[PlacesSetting]
|
|
if !ok {
|
|
continue
|
|
}
|
|
blocks, ok := raw.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s: %s is a { directory: path | { path, owner } } map, and %q set it to something else",
|
|
m.Module, PlacesSetting, layer.From)
|
|
}
|
|
for id, body := range blocks {
|
|
if !declared[id] {
|
|
return nil, fmt.Errorf(
|
|
"%s places the directory %q, which it does not declare — the setting reaches "+
|
|
"nothing. It declares %s", m.Module, id, orNothing(namesOfDirs(directoriesOf(m))))
|
|
}
|
|
p := out[id]
|
|
switch v := body.(type) {
|
|
case string:
|
|
p.Path = strings.TrimSpace(v)
|
|
case map[string]any:
|
|
if path, said := v["path"]; said {
|
|
text, _ := path.(string)
|
|
p.Path = strings.TrimSpace(text)
|
|
}
|
|
if owner, said := v["owner"]; said {
|
|
text, _ := owner.(string)
|
|
if !ownerShape.MatchString(strings.TrimSpace(text)) {
|
|
return nil, fmt.Errorf("%s places %q with owner %v; an owner is uid:gid, numeric",
|
|
m.Module, id, owner)
|
|
}
|
|
p.Owner = strings.TrimSpace(text)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("%s places %q with %v; a placement is a path, or { path, owner }",
|
|
m.Module, id, body)
|
|
}
|
|
if p.Path == "" {
|
|
return nil, fmt.Errorf("%s places %q without a path", m.Module, id)
|
|
}
|
|
if !strings.HasPrefix(p.Path, "/") {
|
|
return nil, fmt.Errorf("%s places %q at %q, which is not an absolute path", m.Module, id, p.Path)
|
|
}
|
|
p.Path = filepath.Clean(p.Path)
|
|
if why := systemPath(p.Path); why != "" {
|
|
return nil, fmt.Errorf("%s places %q at %s: %s, and the node-engine creates and owns a placed "+
|
|
"directory as root, with the owner the setting names — no placement is ever there "+
|
|
"(novox/hq issue 339)", m.Module, id, p.Path, why)
|
|
}
|
|
out[id] = p
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func directoriesOf(m Manifest) map[string]string {
|
|
dirs := map[string]string{}
|
|
for _, r := range m.Resources {
|
|
if fmt.Sprint(r["type"]) == "directory" {
|
|
dirs[fmt.Sprint(r["id"])] = ""
|
|
}
|
|
}
|
|
return dirs
|
|
}
|