Apply an older controller's accesses exactly as before (hq ADR 0325)
mesh/merge-gate head c85c2804a010 merged into trunk dd9ab7927d96 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head c85c2804a010 merged into trunk dd9ab7927d96 — fail: its merge-check.sh failed: --- FAIL: TestAnAccessTheMeshMayNotKeepYetIsSaidAsAWait…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head c85c2804a010 merged into trunk dd9ab7927d96 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head c85c2804a010 merged into trunk dd9ab7927d96 — fail: its merge-check.sh failed: --- FAIL: TestAnAccessTheMeshMayNotKeepYetIsSaidAsAWait…
mesh/delivery superseded: a newer head of the same pull request
The engine delivers before the controller half, and today's controller sends no identities, users or taken. Keeping now starts only for an access whose folder has users, or a declaration with identities; every other access is mounted when present and refused when absent as on main, with nothing made, kept, held or judged. A leftover whose walk could not run says why, not always that its disk is away.
This commit is contained in:
@@ -83,6 +83,10 @@ func (j *accessJudge) set(d *declaration.Declaration, held map[string]bool) {
|
||||
if !ok || held[a.ID] || strings.HasPrefix(a.ID, declaration.AdoptionPrefix) {
|
||||
continue
|
||||
}
|
||||
// An older controller's access is judged by nothing, as before ADR 0325: no state is said of it.
|
||||
if !apply.Keeps(d, a.Path) {
|
||||
continue
|
||||
}
|
||||
at := strings.LastIndex(a.ID, ".")
|
||||
if at <= 0 {
|
||||
continue
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
@@ -47,3 +48,23 @@ func TestAnAccessTheMeshMayNotKeepYetIsSaidAsAWaitOfItsOwnKind(t *testing.T) {
|
||||
t.Fatalf("the reason names the path: %s", v.reason)
|
||||
}
|
||||
}
|
||||
|
||||
// An older controller's accesses (no users, no identities) are judged by nothing, as before ADR 0325: the statement
|
||||
// carries no resource for them, waiting, unknown or kept-entries.
|
||||
func TestAnOlderControllersAccessesSayNothing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
d, err := declaration.Parse([]byte(`{"declaration":1,"resources":[
|
||||
{"id":"radarr.library","type":"access","path":"` + filepath.Join(dir, "library") + `","mode":"read-write"},
|
||||
{"id":"plex.movies","type":"access","path":"` + filepath.Join(dir, "absent") + `"}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(dir, "library"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
j := newAccessJudge()
|
||||
j.set(d, nil)
|
||||
if vs, _ := j.look(time.Now()); len(vs) != 0 {
|
||||
t.Fatalf("an older controller's accesses are judged: %+v", vs)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -351,8 +351,10 @@ func ApplyMindingWindows(
|
||||
case declaration.TypeAccess:
|
||||
// The folder is left as it is: what the mesh added to it is taken out by the walk once no access names
|
||||
// it (novox/hq ADR 0325 §8), never by removing the access's record.
|
||||
action, detail = "forgotten", "an operator-owned folder is never the host's to remove; the entries "+
|
||||
"the mesh added go once no access names it"
|
||||
action, detail = "forgotten", "an operator-owned path is never the host's to remove"
|
||||
if _, kept := keptRecord(filepath.Clean(orphan.Target)); kept {
|
||||
detail += "; the entries the mesh added go once no access names it"
|
||||
}
|
||||
gave, left, said := takeBackGranted(ctx, sys, orphan, run, wanted, handed)
|
||||
if said != "" {
|
||||
detail = joinDetail(detail, said)
|
||||
@@ -540,7 +542,8 @@ func ApplyMindingWindows(
|
||||
// What every declared access asks of its folder, several accesses of one folder as one (novox/hq ADR 0325 §5),
|
||||
// and every accessed folder, which no container mounts while it is not there (§9).
|
||||
in.folders = foldersOf(d.Resources, in.refused)
|
||||
in.accesses = accessPaths(d.Resources, in.refused)
|
||||
in.identities = len(d.Identities) > 0
|
||||
in.accesses = accessPaths(d.Resources, in.refused, in.identities)
|
||||
in.agentHomes = rootNeverHomes(d)
|
||||
in.dirsBefore = map[string]bool{}
|
||||
for _, resource := range d.Resources {
|
||||
@@ -2193,8 +2196,10 @@ type inputs struct {
|
||||
dirsBefore map[string]bool
|
||||
// folders is what every declared access asks of its folder, by path (novox/hq ADR 0325 §5); accesses every
|
||||
// accessed folder with its access's id, which no container mounts while it is not there (§9).
|
||||
folders map[string]*folderWant
|
||||
accesses map[string]string
|
||||
folders map[string]*folderWant
|
||||
// identities is that the declaration carries identities: its controller speaks ADR 0325 for every access.
|
||||
identities bool
|
||||
accesses map[string]string
|
||||
// handed is every group a record that went this apply handed to a resource still asking for it.
|
||||
handed Handed
|
||||
}
|
||||
|
||||
+70
-6
@@ -497,6 +497,9 @@ func applyAccessFolder(ctx context.Context, r *declaration.Access, in inputs) (O
|
||||
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)
|
||||
@@ -737,16 +740,39 @@ func NearestAncestor(path string) (MissingPath, error) {
|
||||
}
|
||||
|
||||
// accessPaths is every declared access's folder with its id: what no container mounts while it is not there.
|
||||
func accessPaths(resources []declaration.Resource, refused map[string]error) map[string]string {
|
||||
//
|
||||
// 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 {
|
||||
if a, ok := r.(*declaration.Access); ok && refused[filepath.Clean(a.Path)] == nil {
|
||||
out[filepath.Clean(a.Path)] = a.ID
|
||||
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 {
|
||||
@@ -780,7 +806,7 @@ func WaitingOnAccess(d *declaration.Declaration) map[string]bool {
|
||||
if d == nil {
|
||||
return out
|
||||
}
|
||||
accesses := accessPaths(d.Resources, refusedPlaces(d.Resources))
|
||||
accesses := accessPaths(d.Resources, refusedPlaces(d.Resources), len(d.Identities) > 0)
|
||||
if len(accesses) == 0 {
|
||||
return out
|
||||
}
|
||||
@@ -857,9 +883,13 @@ func Leftovers() []Leftover {
|
||||
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: its disk is not there; they are removed once it is (novox/hq ADR " +
|
||||
"0325 §8)"})
|
||||
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 " +
|
||||
@@ -887,3 +917,37 @@ func recordName(path string, f store.KeptFolder) (module, name, access string) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//go:build linux
|
||||
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// The engine delivers before the controller half (novox/hq ADR 0325): a controller that does not speak
|
||||
// link.IdentityContract sends no identities, no users and no taken, and every access of its declarations is applied
|
||||
// exactly as before this engine — mounted when present, refused when absent (ADR 0051 §7), nothing kept, nothing
|
||||
// made, no container held, nothing judged. These are origin/main's outcomes, word for word.
|
||||
func TestAnOlderControllersAccessesAreAppliedExactlyAsBefore(t *testing.T) {
|
||||
keeping(t, nil, nil)
|
||||
dir := t.TempDir()
|
||||
unwritable := filepath.Join(dir, "library") // its group may not write it
|
||||
empty := filepath.Join(dir, "spool")
|
||||
absent := filepath.Join(dir, "media", "movies")
|
||||
for _, d := range []string{unwritable, empty} {
|
||||
if err := os.Mkdir(d, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(d, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// A root-owned empty folder, read only.
|
||||
rootEmpty := ""
|
||||
for _, c := range []string{"/var/empty", "/srv/http", "/srv/ftp"} {
|
||||
if entries, err := os.ReadDir(c); err == nil && len(entries) == 0 {
|
||||
if uid, _, _ := statOwner(mustLstat(t, c)); uid == 0 {
|
||||
rootEmpty = c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
resources := []string{
|
||||
`{"id":"radarr.library","type":"access","path":"` + unwritable + `","mode":"read-write"}`,
|
||||
`{"id":"sab.spool","type":"access","path":"` + empty + `","mode":"read-write"}`,
|
||||
`{"id":"plex.movies","type":"access","path":"` + absent + `"}`,
|
||||
`{"id":"plex.app","type":"container","name":"plex","image":"` + pinned + `","volumes":["` + absent + `:/movies"]}`,
|
||||
}
|
||||
if rootEmpty != "" {
|
||||
resources = append(resources, `{"id":"web.root","type":"access","path":"`+rootEmpty+`","mode":"read-write"}`)
|
||||
}
|
||||
var started bool
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
if len(args) > 0 && args[0] == "run" {
|
||||
started = true
|
||||
}
|
||||
if len(args) > 0 && args[0] == "container" {
|
||||
if started {
|
||||
return "true\t" + containerSpec(declare(t, resources[3]).Resources[0].(*declaration.Container), inputs{}), nil
|
||||
}
|
||||
return "", errors.New("no such container")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
report, _, err := Apply(context.Background(), archHost(t), declare(t, strings.Join(resources, ",")), store.State{},
|
||||
store.OriginDeclared, run, nil, nil)
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), absent+" is not there, and the mesh does not own it — the operator "+
|
||||
"must provide it") {
|
||||
t.Fatalf("an absent access is not refused as before: %v", err)
|
||||
}
|
||||
for id, mode := range map[string]string{"radarr.library": "read-write", "sab.spool": "read-write"} {
|
||||
o := outcomeOf(report, id)
|
||||
if o.Action != "unchanged" || o.Detail != "operator-owned; present, "+mode+", nothing managed" {
|
||||
t.Errorf("%s: %+v, and before it was unchanged, %q", id, o, "operator-owned; present, "+mode+", nothing managed")
|
||||
}
|
||||
}
|
||||
if rootEmpty != "" {
|
||||
if o := outcomeOf(report, "web.root"); o.Action != "unchanged" {
|
||||
t.Errorf("a root-owned folder was judged: %+v", o)
|
||||
}
|
||||
}
|
||||
if !started {
|
||||
t.Error("a container mounting an absent access was held, and before it was created")
|
||||
}
|
||||
if _, err := os.Stat(absent); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Error("an absent access was made")
|
||||
}
|
||||
for _, d := range []string{unwritable, empty} {
|
||||
if n, _ := unix.Getxattr(d, aclAccessXattr, nil); n > 0 {
|
||||
t.Errorf("an ACL was set on %s", d)
|
||||
}
|
||||
if info := mustLstat(t, d); info.Mode().Perm() != 0o755 {
|
||||
t.Errorf("%s changed mode: %o", d, info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
if all, _ := KeptFolders.All(); len(all) != 0 {
|
||||
t.Errorf("folders were recorded as kept: %v", all)
|
||||
}
|
||||
if len(Leftovers()) != 0 || len(WaitingOnAccess(declare(t, strings.Join(resources, ",")))) != 0 {
|
||||
t.Error("something was said or held for an older controller's access")
|
||||
}
|
||||
}
|
||||
|
||||
func mustLstat(t *testing.T, path string) os.FileInfo {
|
||||
t.Helper()
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// Review (low): a folder no access names, whose filesystem could not be named this look, is not said as its disk
|
||||
// being away.
|
||||
func TestALeftoverSaysWhyItsWalkCouldNotRun(t *testing.T) {
|
||||
uuid, fsid, source := "6f1c0a4e-1b2d-4c3e-8f00-aa11bb22cc33", "0000080100000000", "/dev/sdb1"
|
||||
_, lib, access := poolNamed(t, &uuid, &fsid, &source)
|
||||
takeIt(t, lib, access)
|
||||
other := `{"id":"x.dir","type":"directory","path":"` + filepath.Join(filepath.Dir(filepath.Dir(lib)), "x") + `"}`
|
||||
if _, _, err := applyAccesses(t, store.State{}, other); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uuid, fsid, source = "", "0000000000000000", "" // nothing names it now
|
||||
ReconcileKept(context.Background())
|
||||
left := Leftovers()
|
||||
if len(left) != 1 || strings.Contains(left[0].Why, "disk") || !strings.Contains(left[0].Why, "could not be named") {
|
||||
t.Fatalf("the leftover does not say why its walk could not run: %+v", left)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user