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.
This commit is contained in:
2026-10-12 19:45:13 +02:00
parent d12bba0109
commit 8b2d65072c
12 changed files with 354 additions and 21 deletions
+10 -2
View File
@@ -60,6 +60,10 @@ func newAccessJudge() *accessJudge {
func (j *accessJudge) set(d *declaration.Declaration, held map[string]bool) {
var as []judgedAccess
if d != nil {
// A tool runner's account that came with no number is read by name, as the apply read it (ADR 0325 §4).
ctx, cancel := context.WithTimeout(context.Background(), lookBound)
apply.ResolveRunners(ctx, d)
cancel()
users := map[string]map[uint32]store.KeptUser{}
taken := map[string]bool{}
for _, r := range d.Resources {
@@ -161,11 +165,11 @@ func judgeAccess(a judgedAccess) verdict {
return verdict{state: link.StateWaiting, reason: "it holds data the mesh has not taken on the filesystem " +
"it is on now, and waits for the operator's word (novox/hq ADR 0325 §7, §8)",
waits: []link.AccessWait{{Kind: link.WaitKindAccess, Access: a.name, Part: a.module,
What: apply.KeepWords(a.name), Placed: v.Placed}}}
What: apply.KeepWords(a.name), Placed: v.Placed, Reason: link.WaitKeep, ACL: v.ACL}}}
case apply.VerdictWaitDisk:
return verdict{state: link.StateWaiting, reason: "the disk that holds it is not there (novox/hq ADR 0325 §9)",
waits: []link.AccessWait{{Kind: link.WaitKindAccess, Access: a.name, Part: a.module,
What: apply.DiskWords(a.name)}}}
What: apply.DiskWords(a.name), Reason: link.WaitDisk}}}
case apply.VerdictRoot:
return verdict{state: link.StateUnhealthy, reason: apply.RootWords(v.Why)}
case apply.VerdictUnknown:
@@ -288,6 +292,10 @@ func (j *accessJudge) look(now time.Time) ([]link.ResourceHealth, bool) {
out := make([]link.ResourceHealth, 0, len(as))
for i, a := range as {
r := read[i]
// Every kept or waiting access lists its users, the tool runner's account with the uid read here (§4).
if r.entries == nil && len(a.users) > 0 && !a.leftover {
r.entries = entriesOf(a.users)
}
kind := link.KindAccess
if a.leftover {
kind = link.KindKeptEntries
+40 -1
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
@@ -8,6 +9,7 @@ import (
"testing"
"time"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/link"
"github.com/novox/mesh-host/internal/store"
@@ -48,7 +50,7 @@ func TestAnAccessTheMeshMayNotKeepYetIsSaidAsAWaitOfItsOwnKind(t *testing.T) {
}
raw, _ := json.Marshal(v.waits[0])
want := `{"kind":"access","access":"movies","part":"radarr","what":"let the mesh keep the shared folder movies",` +
`"placed":"` + v.waits[0].Placed + `"}`
`"placed":"` + v.waits[0].Placed + `","reason":"keep"}`
if string(raw) != want || !strings.HasPrefix(v.waits[0].Placed, "fs-") {
t.Fatalf("the wait reads %s", raw)
}
@@ -76,3 +78,40 @@ func TestAnOlderControllersAccessesSayNothing(t *testing.T) {
t.Fatalf("an older controller's accesses are judged: %+v", vs)
}
}
// The waits' reasons and the tool runner's uid read here are said without words to parse (ADR 0325 §4, §7, §9).
func TestTheWaitsSayTheirReasonAndTheRunnersUid(t *testing.T) {
if link.ACLNeedsEnabling != apply.ACLNeedsEnabling || link.ACLUnsupported != apply.ACLUnsupported {
t.Fatal("the engine's ACL words are not the link's")
}
dir := t.TempDir()
was := apply.AccountIDs
apply.AccountIDs = func(context.Context, string) (int, int, bool, error) { return 61003, 1000, true, nil }
defer func() { apply.AccountIDs = was }()
d, err := declaration.Parse([]byte(`{"declaration":1,"resources":[{"id":"importer.books","type":"access",` +
`"path":"` + filepath.Join(dir, "no", "such") + `","mode":"read-write","users":[{"identity":"nox",` +
`"use":"read-write","from":"tool-runner"}]}]}`))
if err != nil {
t.Fatal(err)
}
j := newAccessJudge()
j.set(d, nil)
vs, _ := j.look(time.Now())
if len(vs) != 1 || len(vs[0].Entries) != 1 || vs[0].Entries[0].UID != 61003 {
t.Fatalf("the tool runner's uid read here is not said: %+v", vs)
}
// A folder whose disk is not there: its mount target named, not mounted.
disk := filepath.Join(dir, "disk")
if err := os.Mkdir(disk, 0o755); err != nil {
t.Fatal(err)
}
wasTargets := apply.MountTargets
apply.MountTargets = func(context.Context) []string { return []string{disk} }
defer func() { apply.MountTargets = wasTargets }()
v := judgeAccess(judgedAccess{module: "radarr", id: "radarr.movies", name: "movies",
path: filepath.Join(disk, "movies"), users: []store.KeptUser{{Identity: "mesh-radarr", UID: 61001,
Use: "read-write", From: "container"}}})
if len(v.waits) != 1 || v.waits[0].Reason != link.WaitDisk || link.WaitDisk != "disk" || link.WaitKeep != "keep" {
t.Fatalf("a disk wait does not say its reason: %+v", v.waits)
}
}