diff --git a/cmd/mesh-controller/gate.go b/cmd/mesh-controller/gate.go index b4dd98e2..04df137f 100644 --- a/cmd/mesh-controller/gate.go +++ b/cmd/mesh-controller/gate.go @@ -154,6 +154,10 @@ type gateFacts struct { // groupsAdded is, per module, whether the move judged puts an account in a group its previous build did // not (issue 318 review): the only move whose wait for a new login is excused. groupsAdded map[string]bool + // accessesChanged is, per module, whether the move judged changes its accesses (novox/hq ADR 0325): an access the + // engine cannot judge this look (unknown), or entries it left while a disk was away, of a module whose move leaves + // its accesses as they were is no fault of the move. A module absent here is taken to change them: judged as before. + accessesChanged map[string]bool // waits is what the controller holds to check a module's wait for the operator (novox/hq ADR 0283): the // manifest of each module's build judged, and the secrets given on each machine. waits operatorWaitFacts @@ -528,6 +532,7 @@ func judgeMoves(ctx context.Context, open *stores, g *inventory.PlanGate, pairs return "", err } facts.groupsAdded = movesAddingGroups(ctx, open.inventory, g, pairs, shelf) + facts.accessesChanged = movesChangingAccesses(ctx, open.inventory, g, pairs, shelf) facts.waits = gateWaitFacts(ctx, open.inventory, g, pairs, shelf, facts.health) facts.sent = g.Sent facts.commits, facts.sentBuilds = judgedCommits(g, pairs), map[string]map[string]string{} @@ -783,6 +788,50 @@ func joinSaid(a, b string) string { return a + "; " + b } +// movesChangingAccesses is, per module a gate judges, whether its move changes its accesses — one added, dropped, +// or given another use — read from the builds' manifests (novox/hq ADR 0325). A move not known changes them: an +// access it cannot judge then holds the gate to its bound, and never passes unseen. +func movesChangingAccesses(ctx context.Context, inv *inventory.Inventory, g *inventory.PlanGate, pairs []judged, + shelf map[string]catalogue.Manifest) map[string]bool { + out := map[string]bool{} + for _, j := range pairs { + if _, done := out[j.module]; done { + continue + } + from, to := g.From, g.To + for _, c := range g.Carried { + if c.Module == j.module { + from, to = c.From, c.To + break + } + } + target, found, err := inv.ManifestAt(ctx, j.module, to) + if err != nil || !found { + target = shelf[j.module] + } + before, had, err := inv.ManifestAt(ctx, j.module, from) + if from == "" || err != nil || !had { + out[j.module] = true + continue + } + out[j.module] = !sameAccesses(before.Accesses, target.Accesses) + } + return out +} + +// sameAccesses says two manifests declare the same accesses, at the same use, in any order. +func sameAccesses(a, b []catalogue.Access) bool { + key := func(as []catalogue.Access) string { + var out []string + for _, x := range as { + out = append(out, x.ID+"\x00"+x.Path+"\x00"+x.At()) + } + sort.Strings(out) + return strings.Join(out, "\x01") + } + return key(a) == key(b) +} + // movesAddingGroups is, per module a gate judges, whether its move puts an account in a group the build it // moved from did not: read from the builds' manifests. A module whose move is not known adds none. func movesAddingGroups(ctx context.Context, inv *inventory.Inventory, g *inventory.PlanGate, pairs []judged, diff --git a/cmd/mesh-controller/kept_folders_test.go b/cmd/mesh-controller/kept_folders_test.go index b4b80343..317649a0 100644 --- a/cmd/mesh-controller/kept_folders_test.go +++ b/cmd/mesh-controller/kept_folders_test.go @@ -421,3 +421,41 @@ func TestEntriesLeftWhileADiskWasAwayDoNotHoldADeliveryThatDidNotBringThem(t *te t.Fatalf("entries left by this send read %v; want judged unhealthy", h) } } + +// An access the engine reports unknown (its filesystem could not be named this look) carries no wait: of a module +// whose move leaves its accesses as they were it is pre-existing and not counted; of one whose move changes them, or +// whose move is not known, it is not yet healthy — held to the gate's bound, never waited on for ever. Entries left +// while a disk was away follow the same rule whenever they were stated. +func TestAnUnknownAccessAndLeftEntriesAreJudgedByWhetherTheMoveChangesAccesses(t *testing.T) { + now := time.Now() + since := now.Add(-time.Minute) + unknown := inventory.ResourceHealth{Module: "radarr", Resource: "radarr.access-movies", Kind: link.KindAccess, + Target: moviesAt, State: link.StateUnknown, Since: now} + left := inventory.ResourceHealth{Module: "radarr", Resource: "radarr.access-gone~entries", Kind: link.KindKeptEntries, + Target: moviesAt, State: link.StateUnhealthy, Since: now} + facts := func(changes map[string]bool, rs ...inventory.ResourceHealth) gateFacts { + return gateFacts{now: now, waits: accessFacts(), groupsAdded: map[string]bool{"radarr": false}, + accessesChanged: changes, health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now, + Resources: rs}}} + } + unchanged := map[string]bool{"radarr": false} + if h, why := moduleHealthWord("radarr", "ace", since, facts(unchanged, unknown)); h != healthGood { + t.Errorf("an unknown access of a move that leaves accesses as they were reads %v %q; want not counted", h, why) + } + for name, changes := range map[string]map[string]bool{"changes them": {"radarr": true}, "not known": nil} { + if h, _ := moduleHealthWord("radarr", "ace", since, facts(changes, unknown)); h != healthNotYet { + t.Errorf("an unknown access of a move that %s reads %v; want not yet, held to the bound", name, h) + } + } + if h, why := moduleHealthWord("radarr", "ace", since, facts(unchanged, left)); h != healthPerson || + !strings.Contains(why, "access-gone~entries") { + t.Errorf("entries left, a move that leaves accesses as they were: %v %q; want carried", h, why) + } + if h, _ := moduleHealthWord("radarr", "ace", since, facts(map[string]bool{"radarr": true}, left)); h != healthNotYet { + t.Errorf("entries left since the send by a move that changes accesses read %v", h) + } + if !sameAccesses(radarrManifest.Accesses, []catalogue.Access{{ID: "movies", Path: "/services/media/movies", + Mode: catalogue.AccessReadWrite}}) || sameAccesses(radarrManifest.Accesses, plexManifest.Accesses) { + t.Error("sameAccesses does not tell a changed access from an unchanged one") + } +} diff --git a/cmd/mesh-controller/module_health.go b/cmd/mesh-controller/module_health.go index a3500630..45079385 100644 --- a/cmd/mesh-controller/module_health.go +++ b/cmd/mesh-controller/module_health.go @@ -673,14 +673,23 @@ func moduleHealthWord(module, machine string, since time.Time, f gateFacts) (hea found = append(found, r.Resource) continue } - // **The mesh's entries left on a folder whose disk was away, stated before this send, are a condition this - // build found** (novox/hq ADR 0325 §8): they may name an access no longer declared, they wait for the disk, - // and a delivery that did not bring them is not held for them — said in the verdict, as a directory used as - // found is. Stated since the send, the send brought them, and they are judged as unhealthy. - if r.Kind == link.KindKeptEntries && r.State == link.StateUnhealthy && r.Since.Before(since) { + // **The mesh's entries left on a folder whose disk was away are a condition this build found** when its move + // leaves the module's accesses as they were, or they were stated before this send (novox/hq ADR 0325 §8): they + // may name an access no longer declared, they wait for the disk, and a delivery that did not bring them is not + // held for them — said in the verdict, as a directory used as found is. Otherwise they are judged unhealthy. + changed, known := f.accessesChanged[module] + changes := !known || changed + if r.Kind == link.KindKeptEntries && r.State == link.StateUnhealthy && (!changes || r.Since.Before(since)) { leftover = append(leftover, r.Resource) continue } + // **An access the engine could not judge this look** (its filesystem could not be named — a hung mount) carries + // no wait. Of a module whose move leaves its accesses as they were it is a condition the move found, not + // counted; of one whose move changes them it is not yet healthy, held to the gate's bound like anything still + // unsettled, so the gate fails safe at the bound and never waits for ever. + if r.Kind == link.KindAccess && r.State == link.StateUnknown && !changes { + continue + } switch r.State { case link.StateHealthy: case link.StateStarting: diff --git a/internal/link/protocol.go b/internal/link/protocol.go index 961aa828..e3357bac 100644 --- a/internal/link/protocol.go +++ b/internal/link/protocol.go @@ -613,6 +613,11 @@ func (w AccessWait) WaitsForDisk() bool { return strings.HasPrefix(strings.TrimSpace(w.What), "mount the disk") } +// KindAccess is an access's verdict (novox/hq ADR 0325; mesh-host internal/link KindAccess): healthy while each user +// has its right, waiting while the mesh may not keep its folder yet or its disk is not there, unhealthy otherwise — +// and unknown when its filesystem could not be named this look (a hung mount), carrying no wait. +const KindAccess = "access" + // KindKeptEntries is the mesh's entries on a shared folder that nobody wants and that could not be taken out yet // because its disk was away (novox/hq ADR 0325 §8; mesh-host internal/link): unhealthy, said under the access that // last named it (`~entries`), which may no longer be declared. The node-engine's own.