Files
mesh-host/internal/apply/keep.go
T
jschoubben aad33dfe5c Give the controller the contract it can fill (hq ADR 0325, issue 567)
The controller cannot know the operator's account's uid, names an access by its
declared id (which holds a slash), and should not parse words. A tool runner's
account may now come by name alone and is read here (refused when absent or root's,
its uid said in the access's listing); the take act matches the declared id exactly;
an access wait says its reason (keep or disk) and, for a filesystem that refuses
ACLs, whether the take act can enable them.
2026-10-12 19:51:09 +02:00

1015 lines
38 KiB
Go

package apply
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"github.com/novox/mesh-host/internal/accounts"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/store"
"github.com/novox/mesh-host/internal/system"
)
// A shared folder is kept usable through access entries (novox/hq ADR 0325, issue 567).
//
// Several modules use one folder of the operator's — a library one imports into, one renames in, one plays from —
// each as its own identity. The engine keeps, for every placed folder, one ACL entry per identity that uses it,
// on the folder and as its default for everything made under it, and changes nothing else of the path (§5).
//
// **When it may keep a folder.** Only one that is the mesh's to keep: taken on the operator's warrant (§8), or
// holding no data yet — empty when first seen, or made by the engine (§9). A folder that holds data and was never
// taken is stated waiting, with a wait of kind access, until the operator lets the mesh keep it (§7); one whose disk
// is not there waits for the disk (§9); root's data on its placed filesystem is never kept, and is unhealthy (§12).
//
// **What every apply does.** It judges the folder (assessFolder), makes a missing one where its data lives, and
// sets the users' entries on the folder itself. What lies under a taken folder is the walks' (ReconcileKept): a
// user added has its entries added to everything under it, a user dropped has exactly its own removed, outside
// the apply and bounded. The watch and the sweep keep what is made later usable (§6, §10).
// keptRecords is where the engine keeps what it knows of each kept or seen folder: the file beside the state in
// the engine, memory in a test.
type keptRecords interface {
All() (map[string]store.KeptFolder, error)
Update(path string, change func(f *store.KeptFolder, had bool) bool) error
}
// KeptFolders is the engine's record of the folders it keeps. The engine sets the file beside its state.
var KeptFolders keptRecords = NewKeptMemory()
// keptMemory is a keptRecords held in memory.
type keptMemory struct {
mu sync.Mutex
m map[string]store.KeptFolder
}
// NewKeptMemory is an empty record held in memory: a test's, or a one-shot apply's.
func NewKeptMemory() keptRecords { return &keptMemory{m: map[string]store.KeptFolder{}} }
func (k *keptMemory) All() (map[string]store.KeptFolder, error) {
k.mu.Lock()
defer k.mu.Unlock()
out := map[string]store.KeptFolder{}
for p, f := range k.m {
out[p] = f
}
return out, nil
}
func (k *keptMemory) Update(path string, change func(f *store.KeptFolder, had bool) bool) error {
k.mu.Lock()
defer k.mu.Unlock()
f, had := k.m[path]
f.Path = path
if change(&f, had) {
k.m[path] = f
} else {
delete(k.m, path)
}
return nil
}
// keptRecord is the record of path, and whether there is one. A record that cannot be read is none: the folder
// is judged afresh, which waits rather than keeps.
func keptRecord(path string) (store.KeptFolder, bool) {
all, err := KeptFolders.All()
if err != nil {
return store.KeptFolder{}, false
}
f, had := all[path]
return f, had
}
// folderWant is what every access naming one folder asks of it: the accesses, each uid at the widest use any of
// them gives it (ADR 0325 §4: the tool runner's account stands for every module whose tools use it), and whether
// the controller says it was taken.
type folderWant struct {
path string
ids []string
users map[uint32]store.KeptUser
taken bool
}
// foldersOf is every declared access's folder, by its clean path. A folder the guard refuses is not read (issue
// 339).
func foldersOf(resources []declaration.Resource, refused map[string]error) map[string]*folderWant {
out := map[string]*folderWant{}
for _, r := range resources {
a, ok := r.(*declaration.Access)
if !ok {
continue
}
path := filepath.Clean(a.Path)
if refused[path] != nil {
continue
}
f := out[path]
if f == nil {
f = &folderWant{path: path, users: map[uint32]store.KeptUser{}}
out[path] = f
}
f.ids = append(f.ids, a.ID)
f.taken = f.taken || a.Taken
for _, u := range a.Users {
had, seen := f.users[uint32(u.UID)]
if seen && (had.Use == declaration.AccessReadWrite || u.Use != declaration.AccessReadWrite) {
continue
}
f.users[uint32(u.UID)] = store.KeptUser{Identity: u.Identity, UID: uint32(u.UID), Use: u.Use, From: u.From}
}
}
return out
}
// list is the users, by uid.
func (f *folderWant) list() []store.KeptUser {
if f == nil {
return nil
}
out := make([]store.KeptUser, 0, len(f.users))
for _, u := range f.users {
out = append(out, u)
}
sort.Slice(out, func(i, j int) bool { return out[i].UID < out[j].UID })
return out
}
// usersOf is a list of users as each uid and whether it writes.
func usersOf(us []store.KeptUser) map[uint32]bool {
out := map[uint32]bool{}
for _, u := range us {
out[u.UID] = out[u.UID] || u.Use == declaration.AccessReadWrite
}
return out
}
// sameUsers is whether two lists give the same uids the same use.
func sameUsers(a, b []store.KeptUser) bool {
x, y := usersOf(a), usersOf(b)
if len(x) != len(y) {
return false
}
for uid, rw := range x {
if w, ok := y[uid]; !ok || w != rw {
return false
}
}
return true
}
// accessName is the module and its own id for an access, from the resource id `<module>.<id>`.
func accessName(id string) (module, name string) {
if m, ok := moduleOf(id); ok {
return m, id[len(m)+1:]
}
return "", id
}
// KeepWords and DiskWords are what an access wait says the operator gives (ADR 0325 §7): never a path.
func KeepWords(name string) string { return "let the mesh keep the shared folder " + name }
func DiskWords(name string) string { return "mount the disk that holds " + name }
// The judgements of a folder.
const (
// folderKeep is a folder the mesh keeps: taken, or holding no data yet.
folderKeep = "keep"
// folderMake is a folder absent under a parent on its placed filesystem, not root's: made, then kept.
folderMake = "make"
// folderWaitKeep is a folder that holds data and was never taken: it waits for the operator's word.
folderWaitKeep = "wait-keep"
// folderWaitDisk is a folder whose disk is not there.
folderWaitDisk = "wait-disk"
// folderRoot is root's data on its placed filesystem: never kept.
folderRoot = "root"
// folderUnknown is a folder whose filesystem could not be named: left as it is until it is.
folderUnknown = "unknown"
)
// folderVerdict is the judgement of one folder, with what it read.
type folderVerdict struct {
kind string
// how is why a folder kept is the mesh's: store.KeptByWarrant, store.KeptEmpty or store.KeptMade.
how string
// mount is the mount point that holds what is there.
mount string
// why is a root verdict's or a broken folder's reason; err a folder that is not one (a file, a link).
why string
err error
// moved is that the folder's recorded mount is mounted and another holds it: it is judged afresh.
moved bool
// unnamed is a folder whose filesystem cannot be named: never kept, nor taken.
unnamed bool
}
// assessFolder judges a folder as an apply, a look and every walk do (ADR 0325 §7, §9, §12): its disk first, then
// whose it is, then whether the mesh may keep it. Its words carry no path: they reach the operator's channel.
func assessFolder(ctx context.Context, path string, rec store.KeptFolder, taken bool) folderVerdict {
others := recordedMounts(path)
info, err := lstatWithin(path)
if errors.Is(err, os.ErrNotExist) {
m, err := NearestAncestor(path)
if err != nil {
return folderVerdict{err: err}
}
if !m.Info.IsDir() {
return folderVerdict{err: errors.New("the folder is not there, and its nearest existing parent is not " +
"a folder (a file or a link); nothing is made under it")}
}
pl := placedOn(ctx, path, m.Ancestor, rec.Mount, others)
if pl.Away {
return folderVerdict{kind: folderWaitDisk, mount: pl.Mount}
}
uid, gid, ok := statOwner(m.Info)
if !ok {
return folderVerdict{err: errors.New("the owner of the folder's nearest existing parent cannot be read " +
"on this machine")}
}
if why := rootOwned(uint32(uid), uint32(gid)); why != "" {
return folderVerdict{kind: folderRoot, mount: pl.Mount, why: "it is not there, and its nearest " +
"existing parent is " + why}
}
return folderVerdict{kind: folderMake, mount: pl.Mount, how: store.KeptMade, moved: pl.Moved}
}
if err != nil {
return folderVerdict{err: errors.New("the folder cannot be read: " + pathless(err))}
}
if !info.IsDir() {
return folderVerdict{err: errors.New("the folder is not a directory, and an access is a shared directory")}
}
pl := placedOn(ctx, path, path, rec.Mount, others)
if pl.Away {
return folderVerdict{kind: folderWaitDisk, mount: pl.Mount}
}
// Another filesystem at the same mount point (another pool, another disk) is a move too. A record from an earlier
// build names its filesystem another way, and is not compared: the next keep names it this way.
here := PlacedID(pl.Mount)
// **A filesystem that cannot be named now is unknown, never a move** (a name read that did not answer, a
// filesystem with no UUID, id or source): the folder holds what it is — nothing walked, nothing newly kept,
// nothing reset — and is judged again when it is named. One never kept is so never kept: it fails closed.
if here == "" {
return folderVerdict{kind: folderUnknown, mount: pl.Mount, unnamed: true,
why: "its filesystem could not be named this time (no UUID, filesystem id or source answered)"}
}
moved := pl.Moved || (rec.Fs != "" && rec.Mount == pl.Mount && rec.Placed != here)
facts, err := readFolder(path)
if err != nil {
return folderVerdict{err: errors.New("the folder cannot be read: " + pathless(err))}
}
if why := rootOwned(facts.UID, facts.GID); why != "" {
return folderVerdict{kind: folderRoot, mount: pl.Mount, why: "it is " + why}
}
// The controller's word that the folder was taken holds for the filesystem the warrant was carried out on: a
// folder moved to another is not taken, and asks again (§8).
takenHere := taken && (rec.TakenOn == here || (rec.TakenOn == "" && !moved))
switch {
case rec.Kept && !moved:
return folderVerdict{kind: folderKeep, mount: pl.Mount, how: rec.How}
case takenHere:
return folderVerdict{kind: folderKeep, mount: pl.Mount, how: store.KeptByWarrant, moved: moved}
case facts.Empty:
return folderVerdict{kind: folderKeep, mount: pl.Mount, how: store.KeptEmpty, moved: moved}
}
// What is there now holds data the mesh never took — a disk mounted over a folder kept while it was empty
// below it included (a move): it waits for the operator's word.
return folderVerdict{kind: folderWaitKeep, mount: pl.Mount, moved: moved}
}
// movedRecord is f judged afresh on the filesystem now at mount: the warrant's filesystem is kept so the
// controller's word is not read for the new one, and nothing else of what was kept there holds. What the mesh
// added on the old filesystem is left to be taken out once it holds the folder again (Left) only where it was
// walked onto data under a warrant: a folder kept because it was empty, or made, carries entries on that empty
// folder alone, hidden under the mount point and granting nothing on data, and they are forgotten (said once, by
// the apply that saw the move).
func movedRecord(f store.KeptFolder, mount string) store.KeptFolder {
left := f.Left
if f.How == store.KeptByWarrant && len(f.Users) > 0 && f.Mount != "" {
left = addLeft(left, f.Mount, f.Placed, f.Fs, f.Users)
}
placed, fs := placeOf(mount)
return store.KeptFolder{Path: f.Path, Mount: mount, Placed: placed, Fs: fs, Want: f.Want, IDs: f.IDs,
Act: f.Act, TakenOn: f.TakenOn, Left: left}
}
// placeOf is the name of the filesystem at mount, and what it was made from.
func placeOf(mount string) (placed, fs string) {
fs = filesystemKey(mount)
return placedFrom(mount, fs), fs
}
// addLeft adds users' entries left on mount, once per mount and uid.
func addLeft(left []store.KeptLeft, mount, placed, fs string, users []store.KeptUser) []store.KeptLeft {
for i := range left {
if left[i].Mount == mount && left[i].Placed == placed {
have := usersOf(left[i].Users)
for _, u := range users {
if _, had := have[u.UID]; !had {
left[i].Users = append(left[i].Users, u)
}
}
return left
}
}
return append(left, store.KeptLeft{Mount: mount, Placed: placed, Fs: fs, Since: time.Now().UTC(),
Users: append([]store.KeptUser(nil), users...)})
}
// PlacedID names the filesystem at mount for a wait, with no path in it: the controller asks again when the
// folder's filesystem is not the one a warrant was given for. Empty when the filesystem cannot be named, and a folder
// on it is then never kept (it fails closed, and waits).
func PlacedID(mount string) string { return placedFrom(mount, filesystemKey(mount)) }
// placedFrom is PlacedID from a filesystem key already read.
func placedFrom(mount, key string) string {
if mount == "" || key == "" {
return ""
}
sum := sha256.Sum256([]byte(mount + "\x00" + key))
return "fs-" + hex.EncodeToString(sum[:6])
}
// filesystemKey is what names the filesystem at mount across reboots, in this order:
//
// - a filesystem on a block device: its UUID, found by matching the mount's device against the devices in
// /dev/disk/by-uuid (never blkid). xfs's statfs id follows the device number, so the UUID comes first;
// - else the kernel's filesystem id (statfs f_fsid) when it is not zero: ZFS's from its dataset, btrfs's from its
// UUID;
// - else mountinfo's source with the filesystem's type: an NFS export's server:/export, a FUSE filesystem's own
// source string (both report f_fsid 0).
//
// Never the mount point and type alone, and never a device's name, which a reboot may renumber. Empty when none of
// these answers within statBound: then nothing is kept there. Read once per mount at a time.
func filesystemKey(mount string) string {
if mount == "" {
return ""
}
v, ok := boundedOnce("fs "+mount, func() any { return readFilesystemKey(mount) })
if !ok {
return ""
}
return v.(string)
}
func readFilesystemKey(mount string) string {
if u := MountUUID(mount); u != "" {
return "uuid:" + u
}
if id := MountFsid(mount); strings.Trim(id, "0") != "" {
return "fsid:" + id
}
if source, fstype := mountSourceOf(mount); source != "" && source != "none" {
return "source:" + fstype + ":" + source
}
return ""
}
// statBound is how long a read of a folder may take before it is said not to answer: a hard network mount whose
// server is down blocks a stat for ever, and one access must never hold the apply.
var statBound = 5 * time.Second
// errNoAnswer is a folder whose read did not return within statBound.
var errNoAnswer = errors.New("it does not answer: a read of it did not return within its bound (a mount that is down)")
// lstatWithin is os.Lstat within statBound, and statWithin os.Stat. A read that hangs is left to return on its own.
func lstatWithin(path string) (os.FileInfo, error) { return within("lstat", os.Lstat, path) }
func statWithin(path string) (os.FileInfo, error) { return within("stat", os.Stat, path) }
// flight is one read in flight, shared by everyone who asks while it is: a read that hangs on a dead mount holds
// one goroutine per path, never one per apply or look.
type flight struct {
done chan struct{}
v any
}
var inFlight = struct {
sync.Mutex
reads map[string]*flight
}{reads: map[string]*flight{}}
// boundedOnce is read's answer within statBound, sharing a read of key already in flight; false when it did not
// answer in time.
func boundedOnce(key string, read func() any) (any, bool) {
inFlight.Lock()
f, running := inFlight.reads[key]
if !running {
f = &flight{done: make(chan struct{})}
inFlight.reads[key] = f
go func() {
f.v = read()
inFlight.Lock()
delete(inFlight.reads, key)
inFlight.Unlock()
close(f.done)
}()
}
inFlight.Unlock()
select {
case <-f.done:
return f.v, true
case <-time.After(statBound):
return nil, false
}
}
func within(op string, stat func(string) (os.FileInfo, error), path string) (os.FileInfo, error) {
type read struct {
info os.FileInfo
err error
}
v, ok := boundedOnce(op+" "+path, func() any {
info, err := stat(path)
return read{info, err}
})
if !ok {
return nil, errNoAnswer
}
r := v.(read)
return r.info, r.err
}
// absPath is an absolute path in words: what pathless takes out.
var absPath = regexp.MustCompile(`(^|[\s"'(=])/[^\s"',;:)]*`)
// Pathless is an error in words with no path (ADR 0234 §6): a path error's operation and cause, and any other
// path said as "a path".
func Pathless(err error) string { return pathless(err) }
func pathless(err error) string {
var pe *os.PathError
if errors.As(err, &pe) {
return pe.Op + ": " + pe.Err.Error()
}
return absPath.ReplaceAllString(err.Error(), "${1}a path")
}
// recordedMounts is every mount point recorded for another kept folder: where a disk was once seen.
func recordedMounts(except string) []string {
all, err := KeptFolders.All()
if err != nil {
return nil
}
var out []string
for p, f := range all {
if p != except && f.Mount != "" && f.Mount != "/" {
out = append(out, f.Mount)
}
}
return out
}
// RootWords is the reason root's data is stated unhealthy with (ADR 0325 §12): never a path.
func RootWords(why string) string {
return why + ", and root's data is never kept: the operator places the data elsewhere or gives it an owner " +
"(novox/hq ADR 0325 §12)"
}
// applyAccess is one access, applied (ADR 0325): its folder judged, made when it is missing where its data lives,
// and its users' entries kept on it when the mesh may keep it; waiting otherwise, and failing for root's data.
// Nothing is changed on the folder but the entries the mesh adds and the mask that shows them.
func applyAccess(ctx context.Context, sys system.System, r *declaration.Access, run Runner, previous store.Applied,
in inputs) (Outcome, error) {
// A tool runner's account that is not here, or is root's, refuses its access (§4, §11).
if err := in.runners[r.ID]; err != nil {
return begin(r), err
}
// What ADR 0312's grant put an account in is taken back first: no account is in a group for data (§5).
gave, left, said := takeBackGranted(ctx, sys, previous, run, in.groups, in.handed)
out, err := applyAccessFolder(ctx, r, in)
if len(left) > 0 {
out.user, out.groups = previous.User, left
}
if said != "" {
out.Detail = joinDetail(out.Detail, said)
if gave && out.Action == "unchanged" {
// Updated, so what names the access under restart-on — the tool runner — holds the groups it has now.
out.Action = "updated"
}
}
return out, err
}
// applyAccessFolder is applyAccess's folder.
func applyAccessFolder(ctx context.Context, r *declaration.Access, in inputs) (Outcome, error) {
out := begin(r)
path := filepath.Clean(r.Path)
want := in.folders[path]
if !keeps(want, in.identities) {
return legacyAccess(r)
}
users := want.list()
_, name := accessName(r.ID)
rec, _ := keptRecord(path)
v := assessFolder(ctx, path, rec, want != nil && want.taken)
if v.err != nil {
return out, v.err
}
if v.moved {
// What is at the path now is not what was kept (a disk mounted over a folder kept while it was empty
// below it): judged afresh, the record of what was kept there no longer holds.
_ = KeptFolders.Update(path, func(f *store.KeptFolder, had bool) bool {
*f = movedRecord(*f, v.mount)
return had
})
}
note := ""
if v.moved {
note = "; what is at it now is on another filesystem than what the mesh kept there, so it was judged afresh"
if rec.How != store.KeptByWarrant {
note += "; the entries on the empty folder kept below are forgotten: hidden under the mount, they grant " +
"nothing on data"
}
}
if len(r.Grant) > 0 {
note += "; its grant is no longer acted on: the mesh puts no account in a group for data (novox/hq ADR 0325 §5)"
}
switch v.kind {
case folderUnknown:
out.Action = "waiting"
out.Detail = v.why + ": the folder is left exactly as it is, and judged again on the next apply" + note
return out, nil
case folderWaitDisk:
out.Action = "waiting"
out.Detail = "the disk that holds it is not there: nothing is made, and nothing mounts it until it is " +
"(novox/hq ADR 0325 §9); waits for the operator to " + DiskWords(name) + note
return out, nil
case folderRoot:
if len(users) == 0 {
out.Action, out.Detail = "unchanged", "present or not, nothing is kept: no identity uses it"+note
return out, nil
}
return out, errors.New(RootWords(v.why) + "; nothing was made or changed")
case folderMake:
made, err := CreatePath(path)
if err != nil {
return out, err
}
out.Action = "created"
out.Detail = fmt.Sprintf("made %d level(s) as its nearest existing parent is: owner, group and mode "+
"(novox/hq ADR 0325 §9)", made)
if len(users) == 0 {
out.Detail += note
return out, nil
}
case folderWaitKeep:
if len(users) == 0 {
out.Action, out.Detail = "unchanged", "present; nothing is kept: no identity uses it"+note
return out, nil
}
_ = KeptFolders.Update(path, func(f *store.KeptFolder, _ bool) bool {
f.Mount, f.Want, f.IDs = v.mount, users, want.ids
f.Placed, f.Fs = placeOf(v.mount)
return true
})
out.Action = "waiting"
out.Detail = "it holds data the mesh has not taken: nothing is changed on it until the operator's warrant " +
"(novox/hq ADR 0325 §7, §8); waits for the operator to " + KeepWords(name) + note
return out, nil
}
if len(users) == 0 {
// Nobody uses it any more: what the mesh added goes on the next walk.
dropping := false
_ = KeptFolders.Update(path, func(f *store.KeptFolder, had bool) bool {
f.Want = nil
dropping = len(f.Users) > 0
return had
})
if out.Action == "" {
out.Action, out.Detail = "unchanged", "present; nothing is kept: no identity uses it"
}
if dropping {
out.Detail += "; the entries the mesh added are taken out by the next walk"
}
out.Detail += note
return out, nil
}
changed, err := keepTop(path, usersOf(users))
if errors.Is(err, ErrNoACL) {
_ = KeptFolders.Update(path, func(f *store.KeptFolder, _ bool) bool {
f.Mount, f.Want = v.mount, users
f.Placed, f.Fs = placeOf(v.mount)
return true
})
out.Action = "waiting"
out.Detail = "its filesystem allows no access entries here: the take act lets it, on the operator's word " +
"(novox/hq ADR 0325 §8); waits for the operator to " + KeepWords(name) + note
return out, nil
}
if err != nil {
return out, err
}
var pending string
_ = KeptFolders.Update(path, func(f *store.KeptFolder, had bool) bool {
first := !f.Kept
f.Kept, f.Mount, f.Want, f.IDs = true, v.mount, users, want.ids
f.Placed, f.Fs = placeOf(v.mount)
if v.how == store.KeptByWarrant && f.TakenOn == "" {
// The controller says it was taken, and this engine has no record of where: here, now.
f.TakenOn = PlacedID(v.mount)
}
if first {
f.How, f.At = v.how, time.Now().UTC()
// A folder that held nothing has nothing under it to walk; one the controller says was taken
// without this engine's record is walked whole.
if v.how == store.KeptEmpty || v.how == store.KeptMade {
f.Users = users
}
}
if !sameUsers(f.Users, f.Want) {
pending = "; the entries under it follow on the next walk"
}
return true
})
if out.Action == "" {
out.Action = "unchanged"
if changed {
out.Action = "updated"
}
}
out.Detail = joinDetail(out.Detail, "kept for "+usersWords(users)+": its own entries and default, nothing "+
"else of it changed (novox/hq ADR 0325 §5)"+pending+note)
return out, nil
}
// takeBackGranted takes an account out of every group ADR 0312's grant put it in for an access, as removeUser takes
// back a user's (ADR 0252 §3): unless another declared resource still asks for it, which is then handed the group.
// Whether any was taken back, the groups still to take back — one whose leaving failed stays recorded, so the next
// apply tries again — and what was said; nothing for an access that granted nothing.
func takeBackGranted(ctx context.Context, sys system.System, previous store.Applied, run Runner, wanted Wanted,
handed Handed) (bool, []string, string) {
if previous.User == "" || len(previous.Groups) == 0 {
return false, nil, ""
}
account := previous.User
in, err := system.GroupsOf(ctx, system.Runner(run), account)
if err != nil {
return false, previous.Groups, fmt.Sprintf("the groups ADR 0312's grant put %s in (%s) were not taken "+
"back, and are tried again: %v", account, strings.Join(previous.Groups, ", "), err)
}
already := setOf(in)
gave := false
var left, said []string
for _, g := range previous.Groups {
switch {
case !already[g]:
continue
case len(wanted.othersAsk(account, g, previous.ID)) > 0:
said = append(said, fmt.Sprintf("left in %s, which %s still asks for", g,
strings.Join(wanted.othersAsk(account, g, previous.ID), ", ")))
handed.hand(account, g)
continue
}
gone, why := leaveGroup(ctx, sys, account, g, run)
if !gone {
left = append(left, g)
why += "; tried again on the next apply"
}
gave = gave || gone
said = append(said, why)
}
return gave, left, "the group grant ADR 0312 made is taken back: " + account + " " + strings.Join(said, "; ")
}
// usersWords names users for a person: "mesh-radarr (read-write), the tool runner's nox (read)".
func usersWords(users []store.KeptUser) string {
words := make([]string, 0, len(users))
for _, u := range users {
who := u.Identity
if u.From == declaration.FromToolRunner {
who = "the tool runner's " + who
}
words = append(words, who+" ("+u.Use+")")
}
return strings.Join(words, ", ")
}
// forgetUndeclared marks every recorded folder no declared access names any more: nobody wants its entries, and
// the next walk takes out exactly what the mesh added, then forgets it.
func forgetUndeclared(folders map[string]*folderWant) {
all, err := KeptFolders.All()
if err != nil {
return
}
for path := range all {
if folders[path] != nil {
continue
}
_ = KeptFolders.Update(path, func(f *store.KeptFolder, _ bool) bool {
f.Want = nil
// A folder never kept, or kept with nobody's entries left anywhere, is forgotten now.
return (f.Kept && len(f.Users) > 0) || len(f.Left) > 0
})
}
}
// MissingPath is an accessed folder that is not there, and its nearest existing ancestor.
type MissingPath struct {
Ancestor string
// Levels are the names to make below Ancestor, outermost first.
Levels []string
// Info is the ancestor as lstat reads it: a link is not a directory.
Info os.FileInfo
}
// NearestAncestor reads, for a path that is not there, its nearest existing ancestor by lstat. An error for a
// path that is there, one not stated exactly, or an ancestor that cannot be read.
func NearestAncestor(path string) (MissingPath, error) {
if err := keptPath(path); err != nil {
return MissingPath{}, err
}
if _, err := lstatWithin(path); err == nil || !errors.Is(err, os.ErrNotExist) {
return MissingPath{}, errors.New("the folder is there, or cannot be read")
}
var levels []string
for p := path; p != "/"; p = filepath.Dir(p) {
levels = append([]string{filepath.Base(p)}, levels...)
parent := filepath.Dir(p)
info, err := lstatWithin(parent)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return MissingPath{}, errors.New("a parent of the folder cannot be read: " + pathless(err))
}
return MissingPath{Ancestor: parent, Levels: levels, Info: info}, nil
}
return MissingPath{}, errors.New("no part of the folder's path is there")
}
// accessPaths is every declared access's folder with its id: what no container mounts while it is not there.
//
// Only accesses the mesh keeps (Keeps): an older controller's access holds no container, as before ADR 0325.
func accessPaths(resources []declaration.Resource, refused map[string]error, identities bool) map[string]string {
folders := foldersOf(resources, refused)
out := map[string]string{}
for _, r := range resources {
a, ok := r.(*declaration.Access)
if !ok {
continue
}
if path := filepath.Clean(a.Path); refused[path] == nil && keeps(folders[path], identities) {
out[path] = a.ID
}
}
return out
}
// keeps is whether ADR 0325's keeping applies to a folder: some access naming it carries users, or the
// declaration carries identities — a controller that speaks link.IdentityContract. An older controller's access
// (no users, no identities) is applied exactly as before: mounted when present, refused when absent, nothing kept,
// nothing made, nothing judged (ADR 0051 §7).
func keeps(f *folderWant, identities bool) bool {
return identities || (f != nil && len(f.users) > 0)
}
// Keeps is keeps for a whole declaration's access at path, for the engine's looks.
func Keeps(d *declaration.Declaration, path string) bool {
if d == nil {
return false
}
return keeps(foldersOf(d.Resources, nil)[filepath.Clean(path)], len(d.Identities) > 0)
}
// mountsMissing is the access a container waits for: one whose folder it mounts, or mounts below, is not there
// now — its disk away, or not made yet. The runtime would make it as root (04-ISSUES/026).
func mountsMissing(ctx context.Context, c *declaration.Container, accesses map[string]string) string {
for _, v := range c.Volumes {
src := mountSource(v)
if !strings.HasPrefix(src, "/") {
continue
}
for path, id := range accesses {
if !atOrBelow(filepath.Clean(src), path) {
continue
}
if _, err := statWithin(path); errors.Is(err, os.ErrNotExist) || errors.Is(err, errNoAnswer) {
return id
}
// There, and its disk not: what is at the path is the empty mount point on the filesystem below, and
// a container's writes would land there (§9).
rec, _ := keptRecord(path)
if placedOn(ctx, path, path, rec.Mount, recordedMounts(path)).Away {
return id
}
}
}
return ""
}
// WaitingOnAccess is every container of the declaration that waits for an accessed folder not there, read now:
// not created by the apply, so not judged alive or dead — the access says why.
func WaitingOnAccess(d *declaration.Declaration) map[string]bool {
out := map[string]bool{}
if d == nil {
return out
}
accesses := accessPaths(d.Resources, refusedPlaces(d.Resources), len(d.Identities) > 0)
if len(accesses) == 0 {
return out
}
for _, r := range d.Resources {
if c, ok := r.(*declaration.Container); ok && mountsMissing(context.Background(), c, accesses) != "" {
out[c.ID] = true
}
}
return out
}
// The judgements of an access as a look says them.
const (
VerdictKeep = "keep"
VerdictMake = "make"
VerdictWaitKeep = "wait-keep"
VerdictWaitDisk = "wait-disk"
VerdictRoot = "root"
VerdictBroken = "broken"
// VerdictUnknown is a folder whose filesystem could not be named this look: neither kept nor faulted.
VerdictUnknown = "unknown"
// VerdictNobody is a folder no identity uses: nothing is kept, and only a missing disk is said.
VerdictNobody = "nobody"
)
// What a keep wait says of its folder's filesystem and ACLs: the same words as link's ACLNeedsEnabling and
// ACLUnsupported, which the engine says them as.
const (
ACLNeedsEnabling = "needs-enabling"
ACLUnsupported = "unsupported"
)
// AccessJudgement is a look's judgement of an access's folder, and why, in words; Placed names the filesystem at
// it for a wait, with no path (PlacedID); ACL is, on a keep wait, ACLNeedsEnabling or ACLUnsupported
// when its filesystem refuses access entries now.
type AccessJudgement struct {
Kind, Why, Placed, ACL string
}
// AccessVerdict judges an access's folder for a look, read only (ADR 0325 §7, §9, §12): taken is whether the
// controller says it was taken, used whether any identity uses it. A folder the mesh could keep that no apply has
// kept — its filesystem allowing no ACLs — waits as one not taken does.
func AccessVerdict(ctx context.Context, path string, taken, used bool) AccessJudgement {
rec, _ := keptRecord(path)
v := assessFolder(ctx, path, rec, taken)
switch {
case v.err != nil:
return AccessJudgement{Kind: VerdictBroken, Why: pathless(v.err)}
case v.kind == folderWaitDisk:
return AccessJudgement{Kind: VerdictWaitDisk}
case v.kind == folderUnknown:
return AccessJudgement{Kind: VerdictUnknown, Why: v.why}
case !used:
return AccessJudgement{Kind: VerdictNobody}
case v.kind == folderRoot:
return AccessJudgement{Kind: VerdictRoot, Why: v.why}
case v.kind == folderMake:
return AccessJudgement{Kind: VerdictMake}
case v.kind == folderWaitKeep, v.kind == folderKeep && !rec.Kept && !taken:
return AccessJudgement{Kind: VerdictWaitKeep, Placed: PlacedID(v.mount), ACL: aclProbe(path, v.mount)}
}
return AccessJudgement{Kind: VerdictKeep}
}
// Leftover is the mesh's entries that could not be taken out yet: under the accesses that last named the folder,
// why, in words with no path.
type Leftover struct {
Module, Access, Name, Path, Why string
}
// Leftovers is every folder the mesh still has entries on that nobody wants (ADR 0325 §8): a folder no access names
// any more whose disk is away, so its drop walk waits; and entries left on a filesystem no longer at a folder. Each
// is said until its drop walk has run.
func Leftovers() []Leftover {
all, err := KeptFolders.All()
if err != nil {
return nil
}
var out []Leftover
for path, f := range all {
module, name, access := recordName(path, f)
dropping := f.Kept && len(f.Want) == 0 && len(f.Users) > 0 && f.Act != nil && f.Act.State == "waiting"
switch {
case dropping:
// Why the walk could not run, as it recorded it: its disk not there, or its filesystem not named.
why := strings.TrimSuffix(strings.TrimPrefix(f.Act.Why, "not walked: "), "; walked once it is")
if why == "" {
why = "the walk could not run"
}
out = append(out, Leftover{Module: module, Access: access, Name: name, Path: path, Why: "the mesh's entries for " +
name + " could not be removed: " + why + "; they are removed once it can (novox/hq ADR 0325 §8)"})
case len(f.Left) > 0:
out = append(out, Leftover{Module: module, Access: access, Name: name, Path: path, Why: "the mesh's entries for " +
name + " on a filesystem no longer at its folder could not be removed: they are removed once that " +
"filesystem is back (novox/hq ADR 0325 §8)"})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Access < out[j].Access })
return out
}
// recordName is the module, the name and the access a kept folder's record is said under: the accesses that last
// named it, or for a record written before it kept them, a name with no path — the module of a container identity
// among its users, and a folder named by a digest of its path.
func recordName(path string, f store.KeptFolder) (module, name, access string) {
if len(f.IDs) > 0 {
module, name = accessName(f.IDs[0])
return module, name, f.IDs[0]
}
sum := sha256.Sum256([]byte(path))
name = "folder-" + hex.EncodeToString(sum[:4])
for _, u := range append(append([]store.KeptUser(nil), f.Users...), f.Want...) {
if m, ok := strings.CutPrefix(u.Identity, declaration.IdentityPrefix); ok && u.From == declaration.FromContainer {
return m, name, m + "." + name
}
}
return "", name, name
}
// legacyAccess is an access from a controller older than ADR 0325, applied exactly as before it (ADR 0051 §7).
//
// **Absent is refused, not created.** A bind mount whose source does not exist is made for you by the container
// runtime, as root, with whatever mode it picks — which is exactly the silent wrong-ownership 04-ISSUES/026
// records. So the host checks first and says plainly that the operator must provide the path, rather than
// conjuring a directory it does not own and cannot give the right owner. Nothing is written, so this never reports
// a change: the machine did not move, the host merely confirmed a fact about it.
func legacyAccess(r *declaration.Access) (Outcome, error) {
out := begin(r)
info, err := os.Stat(r.Path)
if errors.Is(err, os.ErrNotExist) {
return out, fmt.Errorf(
"%s is not there, and the mesh does not own it — the operator must provide it. It is "+
"shared, pre-existing data (novox/hq ADR 0051): the host mounts it and creates "+
"nothing, so a missing one is said here rather than made as root by the container "+
"runtime", r.Path)
}
if err != nil {
return out, err
}
if !info.IsDir() {
return out, fmt.Errorf(
"%s is not a directory, and an access is a shared directory the operator provides",
r.Path)
}
mode := declaration.AccessRead
if r.Mode != "" {
mode = r.Mode
}
out.Action = "unchanged"
out.Detail = "operator-owned; present, " + mode + ", nothing managed"
return out, nil
}
// AccountIDs reads an account's uid and primary gid by name from the machine's user database (getent, through the
// account judge's reader). A variable so a test can stand in for accounts it cannot make.
var AccountIDs = func(ctx context.Context, name string) (uid, gid int, found bool, err error) {
return accounts.Exec{Run: accounts.Runner(ExecRunner)}.IDs(ctx, name)
}
// ResolveRunners gives every tool runner's account that came with no uid its number, read by name on this machine
// (ADR 0325 §4): the controller cannot know the operator's account's. One that is not here, or is root's — uid 0 or
// a root group (§11) — is taken out of the access's users, and its access is refused, naming why, by id. Changes
// the declaration in place; a user that already has its number is left as it is.
func ResolveRunners(ctx context.Context, d *declaration.Declaration) map[string]error {
refused := map[string]error{}
if d == nil {
return refused
}
for _, r := range d.Resources {
a, ok := r.(*declaration.Access)
if !ok {
continue
}
kept := a.Users[:0]
for _, u := range a.Users {
if u.From != declaration.FromToolRunner || u.UID != 0 {
kept = append(kept, u)
continue
}
uid, gid, found, err := AccountIDs(ctx, u.Identity)
switch {
case err != nil:
refused[a.ID] = fmt.Errorf("the tool runner's account %s could not be read: %w", u.Identity, err)
continue
case !found:
refused[a.ID] = fmt.Errorf("the tool runner's account %s is not on this machine, so no entry can "+
"be kept for it (novox/hq ADR 0325 §4)", u.Identity)
continue
}
if err := rootIdentity(uid, gid); err != nil {
refused[a.ID] = fmt.Errorf("the tool runner's account %s is never given an entry: %w", u.Identity, err)
continue
}
u.UID = uid
kept = append(kept, u)
}
a.Users = kept
}
return refused
}