A held module is composed exactly as before it joined, at the build its machine runs, so its port, resources and contributions stay. A joining module's directories belong to its user and its own group, and its tools' directories are refused closed to that group; the runner joins it where the engine can say so. A take records the root and is taken only when the engine states the act done. Libraries' paths and verdicts count modules pulled in by a requirement, and a unit takes the identity.
546 lines
24 KiB
Go
546 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// novox/hq ADR 0328 §7, §8 and §9 on the controller's side: a library's wait is excused only for a defined library
|
|
// that reaches the machine and is not yet taken, or whose disk is not there; the gate passes it as a wait for a person
|
|
// and the operator reads it as needs-operator in words with no path; the operator is asked once per library, never
|
|
// about one taken; and the library is taken only on the warrant, where it still waits.
|
|
|
|
var radarrManifest = catalogue.Manifest{Module: "radarr", Version: "1",
|
|
Libraries: []catalogue.LibraryUse{{ID: "movies", Library: "media", Path: "movies", Use: catalogue.AccessReadWrite}}}
|
|
|
|
var plexManifest = catalogue.Manifest{Module: "plex", Version: "1",
|
|
Libraries: []catalogue.LibraryUse{{ID: "films", Library: "media", Path: "movies", Use: catalogue.AccessRead}}}
|
|
|
|
var mediaLib = catalogue.Library{Name: "media", Home: "ace", Root: "/storage/media", Reach: []string{"novox"}}
|
|
|
|
func keepWait(access string) inventory.Wait {
|
|
return inventory.Wait{Part: "radarr", Access: access, Reason: link.WaitKeep,
|
|
What: "let the mesh keep the library media"}
|
|
}
|
|
|
|
func placedWait(access, placed string) inventory.Wait {
|
|
w := keepWait(access)
|
|
w.Placed = placed
|
|
return w
|
|
}
|
|
|
|
func diskWait(access string) inventory.Wait {
|
|
return inventory.Wait{Part: "radarr", Access: access, Disk: true, Reason: link.WaitDisk,
|
|
What: "mount the disk that holds media"}
|
|
}
|
|
|
|
// libraryFacts is the facts with media defined, and each library given taken on ace on fs-000000000001.
|
|
func libraryFactsWith(taken ...string) operatorWaitFacts {
|
|
t := map[string]inventory.TakenLibrary{}
|
|
for _, l := range taken {
|
|
t[l] = inventory.TakenLibrary{Library: l, Home: "ace", Root: "/storage/media", Placed: "fs-000000000001", Done: true}
|
|
}
|
|
return operatorWaitFacts{
|
|
manifests: map[string]catalogue.Manifest{"radarr": radarrManifest, "plex": plexManifest},
|
|
given: map[string]map[string]time.Time{"radarr@ace": {}, "plex@ace": {}},
|
|
libraries: map[string]catalogue.Library{"media": mediaLib},
|
|
taken: t,
|
|
librariesRead: true,
|
|
}
|
|
}
|
|
|
|
// Rows 8 and 9: excused for a defined library reaching the machine and not yet taken — by the module's entry or the
|
|
// library's own wait — and for one whose disk is not there, taken or not; judged unhealthy when undefined, not
|
|
// reaching the machine, already taken on the filesystem it is on, for an access that is not a library, or for a
|
|
// reason the controller does not know.
|
|
func TestALibraryWaitIsExcusedOnlyForALibraryNotYetTaken(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
machine string
|
|
w inventory.Wait
|
|
f operatorWaitFacts
|
|
says string
|
|
}{
|
|
{"the module's entry, not taken", "ace", keepWait("movies"), libraryFactsWith(), ""},
|
|
{"the library's own wait, not taken", "ace", keepWait("library:media"), libraryFactsWith(), ""},
|
|
{"on a machine it reaches", "novox", keepWait("movies"), libraryFactsWith(), ""},
|
|
{"its disk not there", "ace", diskWait("movies"), libraryFactsWith(), ""},
|
|
{"its disk not there, taken before", "ace", diskWait("movies"), libraryFactsWith("media"), ""},
|
|
{"taken already", "ace", keepWait("movies"), libraryFactsWith("media"), "taken already"},
|
|
{"taken on the filesystem it is on", "ace", placedWait("library:media", "fs-000000000001"),
|
|
libraryFactsWith("media"), "taken already"},
|
|
{"taken on another filesystem: it moved", "ace", placedWait("library:media", "fs-0000000000ff"),
|
|
libraryFactsWith("media"), ""},
|
|
{"taken at another root: its root changed", "ace", keepWait("movies"), func() operatorWaitFacts {
|
|
f := libraryFactsWith("media")
|
|
f.taken["media"] = inventory.TakenLibrary{Library: "media", Home: "ace", Root: "/storage/old-media", Done: true,
|
|
Placed: "fs-000000000001"}
|
|
return f
|
|
}(), ""},
|
|
{"a machine it does not reach", "far", keepWait("movies"), libraryFactsWith(), "does not reach far"},
|
|
{"undefined", "ace", keepWait("library:books"), libraryFactsWith(), "not defined"},
|
|
{"not a library", "ace", keepWait("downloads"), libraryFactsWith(), "only a library waits"},
|
|
{"a reason not known", "ace", inventory.Wait{Part: "radarr", Access: "movies", Reason: "later", What: "w"},
|
|
libraryFactsWith(), "take it or for its disk"},
|
|
{"libraries unread", "ace", keepWait("movies"), operatorWaitFacts{manifests: libraryFactsWith().manifests},
|
|
"could not be read"},
|
|
} {
|
|
err := checkWait("radarr", c.machine, c.w, c.f)
|
|
switch {
|
|
case c.says == "" && err != nil:
|
|
t.Errorf("%s: refused: %v", c.name, err)
|
|
case c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)):
|
|
t.Errorf("%s: %v; want a refusal saying %q", c.name, err, c.says)
|
|
}
|
|
}
|
|
}
|
|
|
|
func libraryWaiting(module string, w inventory.Wait) inventory.ResourceHealth {
|
|
return inventory.ResourceHealth{Module: module, Resource: module + ".app", Kind: "container", Target: module,
|
|
State: link.StateWaiting, Waits: []inventory.Wait{w}}
|
|
}
|
|
|
|
// Rows 8 and 9 at the gate: both excused waits pass the first-node gate as a wait for a person, and the module is said
|
|
// as needs-operator in words with no path; a wait on a library taken already does not pass.
|
|
func TestALibraryWaitPassesTheGateAndAsksTheOperatorWithoutAPath(t *testing.T) {
|
|
now := time.Now()
|
|
for _, w := range []inventory.Wait{keepWait("movies"), diskWait("movies")} {
|
|
w.What += " at /storage/media" // an engine's words carrying a path never reach the operator's
|
|
f := gateFacts{now: now, waits: libraryFactsWith(), groupsAdded: map[string]bool{"radarr": false},
|
|
health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now,
|
|
Resources: []inventory.ResourceHealth{libraryWaiting("radarr", w)}}}}
|
|
h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f)
|
|
if h != healthPerson || !strings.Contains(why, "waits for the operator") || strings.Contains(why, "/") {
|
|
t.Fatalf("an excused library wait reads %v %q; want a wait for a person, with no path", h, why)
|
|
}
|
|
o := needsOperatorObservation("radarr", "ace", []inventory.Wait{w}, nil)
|
|
for _, words := range []string{o.Summary, o.Headline, o.Explanation, o.Needs} {
|
|
if strings.Contains(words, "/") {
|
|
t.Errorf("the operator reads a path: %q", words)
|
|
}
|
|
}
|
|
want := "let the mesh keep the library entry movies when you are asked"
|
|
if w.Disk {
|
|
want = "mount the disk that holds the library entry movies on ace"
|
|
}
|
|
if o.Kind != kindNeedsOperator || !strings.Contains(o.Needs, want) {
|
|
t.Errorf("needs-operator %s asks %q; want %q", o.Kind, o.Needs, want)
|
|
}
|
|
}
|
|
f := gateFacts{now: now, waits: libraryFactsWith("media"), groupsAdded: map[string]bool{"radarr": false},
|
|
health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now,
|
|
Resources: []inventory.ResourceHealth{libraryWaiting("radarr", keepWait("movies"))}}}}
|
|
if h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthNotYet || !strings.Contains(why, "taken") {
|
|
t.Fatalf("a wait on a taken library reads %v %q", h, why)
|
|
}
|
|
}
|
|
|
|
func takeRig(t *testing.T, waiting func() map[string][]waitingFolder) (*askerRig, *[]string) {
|
|
r := newAskerRig(t)
|
|
var taken []string
|
|
libs := map[string]catalogue.Library{"media": mediaLib}
|
|
r.a.folders = func(context.Context) ([]libraryTake, error) { return takesOf(waiting(), libs), nil }
|
|
r.a.machines = func(context.Context) ([]string, error) { return []string{"ace", "novox"}, nil }
|
|
r.a.take = func(_ context.Context, lt libraryTake, askID, by string) (string, error) {
|
|
taken = append(taken, lt.Library+" "+lt.Node+" "+lt.Root+" "+lt.Placed+" "+askID+" "+by)
|
|
return "taking", nil
|
|
}
|
|
return r, &taken
|
|
}
|
|
|
|
func takeAsks(r *askerRig) []asked {
|
|
var out []asked
|
|
for _, a := range r.store {
|
|
if a.Take != nil {
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func openTakes(r *askerRig) []asked {
|
|
var out []asked
|
|
for _, a := range takeAsks(r) {
|
|
if a.State == askOpen {
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Row 7: one ask per library however many modules and machines wait on it, in the clause's words, at approve; none
|
|
// again while it is open; taken back when the library no longer waits.
|
|
func TestTheOperatorIsAskedOncePerLibrary(t *testing.T) {
|
|
current := map[string][]waitingFolder{"media": {
|
|
{module: "radarr", machine: "ace", placed: "fs-000000000001"},
|
|
{module: "plex", machine: "novox"},
|
|
{module: "", machine: "ace", placed: "fs-000000000001"}}}
|
|
r, _ := takeRig(t, func() map[string][]waitingFolder { return current })
|
|
ctx := context.Background()
|
|
for i := 0; i < 3; i++ {
|
|
if err := r.a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
kept := takeAsks(r)
|
|
if len(kept) != 1 {
|
|
t.Fatalf("%d asks for one library", len(kept))
|
|
}
|
|
q := kept[0].Ask
|
|
if err := q.Check(r.now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if q.Highest() != asks.Approve || len(q.Options) != 1 {
|
|
t.Fatalf("the ask offers %+v; want one answer at approve", q.Options)
|
|
}
|
|
// The root may not leave the mesh (the router's content rule): it is said as "its root".
|
|
want := "Let the mesh keep library media at its root on ace: its group becomes mesh-media, the group may read " +
|
|
"and write, folders keep the group; owners and everyone else's rights stay as they are. Waiting on it: plex, radarr."
|
|
if q.Explanation != want {
|
|
t.Errorf("the ask says\n %q\nwant\n %q", q.Explanation, want)
|
|
}
|
|
if kept[0].Take.Node != "ace" || kept[0].Take.Placed != "fs-000000000001" {
|
|
t.Errorf("the take is for %+v; want the home and the filesystem its home names", kept[0].Take)
|
|
}
|
|
current = nil
|
|
if err := r.a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := takeAsks(r); len(got) != 1 || got[0].State != askCancelled {
|
|
t.Fatalf("the ask about a library that no longer waits: %+v", got)
|
|
}
|
|
}
|
|
|
|
// Row 7: nothing is taken without the operator's warrant — a warrant for another asker, an ask that expired, or a
|
|
// record changed after the ask takes nothing; the warrant takes it once, on its home, naming library:<name>.
|
|
func TestALibraryIsTakenOnlyOnTheWarrant(t *testing.T) {
|
|
waits := func() map[string][]waitingFolder {
|
|
return map[string][]waitingFolder{"media": {{module: "radarr", machine: "ace", placed: "fs-000000000001"}}}
|
|
}
|
|
ctx := context.Background()
|
|
r, taken := takeRig(t, waits)
|
|
if err := r.a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := takeAsks(r)[0]
|
|
other := warrantOn(a, "let-the-mesh-keep-it", r.now)
|
|
other.Asker = "someone-else"
|
|
answerWith(t, r, other)
|
|
changed := r.store[a.ID]
|
|
changed.Take = &libraryTake{Library: "media", Node: "ace", Root: "/", Modules: a.Take.Modules, Placed: a.Take.Placed}
|
|
r.store[a.ID] = changed
|
|
answerWith(t, r, warrantOn(a, "let-the-mesh-keep-it", r.now))
|
|
if len(*taken) != 0 || !strings.HasPrefix(r.store[a.ID].Acted, "failed") {
|
|
t.Fatalf("a take whose record changed after the ask: %v, %q", *taken, r.store[a.ID].Acted)
|
|
}
|
|
|
|
r2, taken2 := takeRig(t, waits)
|
|
if err := r2.a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := takeAsks(r2)[0]
|
|
answerWith(t, r2, asks.Warrant{Ask: b.ID, Asker: "mesh-controller", Outcome: asks.OutcomeExpired, At: r2.now})
|
|
if len(*taken2) != 0 {
|
|
t.Fatal("taken on an ask that expired")
|
|
}
|
|
|
|
r3, taken3 := takeRig(t, waits)
|
|
if err := r3.a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := takeAsks(r3)[0]
|
|
w := warrantOn(c, "let-the-mesh-keep-it", r3.now)
|
|
answerWith(t, r3, w)
|
|
answerWith(t, r3, w)
|
|
if len(*taken3) != 1 || !strings.HasPrefix((*taken3)[0], "media ace /storage/media fs-000000000001 "+c.ID) ||
|
|
!strings.Contains((*taken3)[0], "telegram") {
|
|
t.Fatalf("the warrant took %v", *taken3)
|
|
}
|
|
if !strings.HasPrefix(r3.store[c.ID].Acted, "done") {
|
|
t.Fatalf("the take was recorded %q", r3.store[c.ID].Acted)
|
|
}
|
|
}
|
|
|
|
// A warrant for a library on one filesystem is never carried out on another: when it comes and the library waits on
|
|
// another, nothing is sent, the ask is spent, and the operator is asked again for the filesystem it is on now.
|
|
func TestAWarrantForOneFilesystemIsNotCarriedOutOnAnother(t *testing.T) {
|
|
placed := "fs-000000000001"
|
|
r, taken := takeRig(t, func() map[string][]waitingFolder {
|
|
return map[string][]waitingFolder{"media": {{module: "radarr", machine: "ace", placed: placed}}}
|
|
})
|
|
ctx := context.Background()
|
|
if err := r.a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := takeAsks(r)[0]
|
|
placed = "fs-0000000000ff"
|
|
answerWith(t, r, warrantOn(a, "let-the-mesh-keep-it", r.now))
|
|
if len(*taken) != 0 {
|
|
t.Fatalf("a warrant for %s was carried out on %s: %v", a.Take.Placed, placed, *taken)
|
|
}
|
|
if spent := r.store[a.ID]; spent.State == askOpen || !strings.HasPrefix(spent.Acted, "nothing") {
|
|
t.Fatalf("the ask after the library moved: %s, %q", spent.State, spent.Acted)
|
|
}
|
|
if err := r.a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again := openTakes(r); len(again) != 1 || again[0].Take.Placed != placed {
|
|
t.Fatalf("not asked again for the filesystem it is on now: %+v", again)
|
|
}
|
|
}
|
|
|
|
// The take act names library:<name> and the warrant, is signed over its own context, and is recorded only on the
|
|
// engine's word.
|
|
func TestATakeAskNamesTheLibraryAndItsWarrant(t *testing.T) {
|
|
_, key, _ := ed25519.GenerateKey(nil)
|
|
signer := keySigner{key}
|
|
ask := link.AccessActAsk{Node: "ace", Path: "/storage/media", By: "the operator", Access: "library:media"}
|
|
if _, err := link.SignTake(context.Background(), signer, ask); err == nil {
|
|
t.Fatal("a take with no warrant was signed")
|
|
}
|
|
ask.Warrant = "c0123456789abcdef"
|
|
body, err := link.SignTake(context.Background(), signer, ask)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var signed link.SignedHandOver
|
|
if err := json.Unmarshal(body, &signed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ed25519.Verify(key.Public().(ed25519.PublicKey), append([]byte(link.AccessActContext), signed.Ask...), signed.Signature) {
|
|
t.Fatal("the take is not signed over its own context")
|
|
}
|
|
var got map[string]any
|
|
if err := json.Unmarshal(signed.Ask, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["act"] != "take" || got["warrant"] != "c0123456789abcdef" || got["access"] != "library:media" ||
|
|
got["path"] != "/storage/media" {
|
|
t.Fatalf("the ask carries %v", got)
|
|
}
|
|
var asked link.AccessActAsk
|
|
refusing := takeFolderIn(nil, func(_ context.Context, a link.AccessActAsk) (link.HandOverAnswer, error) {
|
|
asked = a
|
|
return link.HandOverAnswer{Refused: "the disk is not there"}, nil
|
|
})
|
|
if _, err := refusing(context.Background(), libraryTake{Library: "media", Node: "ace", Root: "/storage/media"},
|
|
"c01", "the operator"); err == nil {
|
|
t.Fatal("a refused take was answered as done")
|
|
}
|
|
if asked.Access != "library:media" || asked.Node != "ace" || asked.Path != "/storage/media" || asked.Warrant != "c01" {
|
|
t.Fatalf("the engine was asked %+v", asked)
|
|
}
|
|
unreachable := takeFolderIn(nil, func(context.Context, link.AccessActAsk) (link.HandOverAnswer, error) {
|
|
return link.HandOverAnswer{}, errors.New("nothing on ace answers")
|
|
})
|
|
if _, err := unreachable(context.Background(), libraryTake{Library: "media", Node: "ace", Root: "/storage/media"},
|
|
"c01", "the operator"); err == nil {
|
|
t.Fatal("an unanswered take was answered as done")
|
|
}
|
|
}
|
|
|
|
type keySigner struct{ key ed25519.PrivateKey }
|
|
|
|
func (s keySigner) Sign(_ context.Context, b []byte) ([]byte, error) {
|
|
return ed25519.Sign(s.key, b), nil
|
|
}
|
|
|
|
// The envelope carries groups, identities and libraries only when composed — for an engine at link.LibraryContract —
|
|
// and an older engine's body is what it was.
|
|
func TestTheDeclarationSaysItsLibrariesOnlyWhenComposed(t *testing.T) {
|
|
older, err := sendable{Resources: []map[string]any{{"id": "a"}}}.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"groups", "identities", "libraries"} {
|
|
if strings.Contains(string(older), key) {
|
|
t.Fatalf("an older engine's body names %s: %s", key, older)
|
|
}
|
|
}
|
|
newer, err := sendable{Resources: []map[string]any{{"id": "a"}},
|
|
Groups: []catalogue.GroupEntry{{Name: "mesh-media", GID: 50010}},
|
|
Identities: []catalogue.IdentityEntry{{Module: "radarr", Name: "mesh-radarr", UID: 50020, GID: 50010}},
|
|
Libraries: []catalogue.LibraryEntry{{Library: "media", Root: "/storage/media", Group: "mesh-media", GID: 50010,
|
|
Home: true, Paths: []string{"movies"}, Runner: "jochen"}}}.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{`"groups":[{"name":"mesh-media","gid":50010}]`,
|
|
`"identities":[{"module":"radarr","name":"mesh-radarr","uid":50020,"gid":50010}]`,
|
|
`"libraries":[{"library":"media","root":"/storage/media","group":"mesh-media","gid":50010,"home":true,` +
|
|
`"taken":false,"paths":["movies"],"runner":"jochen"}]`} {
|
|
if !strings.Contains(string(newer), want) {
|
|
t.Errorf("the body lacks %s: %s", want, newer)
|
|
}
|
|
}
|
|
if link.LibraryContract != 6 {
|
|
t.Fatalf("the contract the engine states for ADR 0328 is 6, and this controller reads %d", link.LibraryContract)
|
|
}
|
|
}
|
|
|
|
// The gate: an access or library the engine cannot judge this look, or entries left while a disk was away, of a module
|
|
// whose move leaves its accesses and libraries as they were, are not counted; of one whose move changes them — a
|
|
// library added, or joining — or is not known, they hold the gate to its bound.
|
|
func TestAnUnknownLibraryIsJudgedByWhetherTheMoveChangesIt(t *testing.T) {
|
|
now := time.Now()
|
|
since := now.Add(-time.Minute)
|
|
unknown := inventory.ResourceHealth{Module: "radarr", Resource: "radarr.library-movies", Kind: link.KindAccess,
|
|
Target: "/storage/media/movies", State: link.StateUnknown, Since: now}
|
|
left := inventory.ResourceHealth{Module: "radarr", Resource: "radarr.access-gone~entries", Kind: link.KindKeptEntries,
|
|
Target: "/storage/media", State: link.StateUnhealthy, Since: since.Add(-time.Hour)}
|
|
facts := func(changes map[string]bool, rs ...inventory.ResourceHealth) gateFacts {
|
|
return gateFacts{now: now, waits: libraryFactsWith(), 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 library of a move that changes nothing reads %v %q", h, why)
|
|
}
|
|
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 changes nothing: %v %q", h, why)
|
|
}
|
|
for name, changes := range map[string]map[string]bool{"changes them": {"radarr": true}, "not known": nil} {
|
|
for _, r := range []inventory.ResourceHealth{unknown, left} {
|
|
if h, _ := moduleHealthWord("radarr", "ace", since, facts(changes, r)); h != healthNotYet {
|
|
t.Errorf("%s %s of a move that %s reads %v; want held to the bound", r.Kind, r.State, name, h)
|
|
}
|
|
}
|
|
}
|
|
plain := catalogue.Manifest{Module: "radarr"}
|
|
if sameAccessUse(plain, radarrManifest) || !sameAccessUse(radarrManifest, radarrManifest) {
|
|
t.Fatal("a move that adds a library, and joins, reads unchanged — or an unchanged one reads changed")
|
|
}
|
|
}
|
|
|
|
// The engine states a library once, of no module; each module assigned to the machine that uses it is judged and said
|
|
// by a copy of that verdict — its wait excused and passed by the gate as a wait for a person. A library unknown or
|
|
// starting (a take walking under it) of a move that changes no library is not counted.
|
|
func TestALibrarysVerdictIsSaidForTheModulesThatUseIt(t *testing.T) {
|
|
inv := inventory.ForTest(t)
|
|
ctx := t.Context()
|
|
if _, err := inv.AddNode(ctx, "ace"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range []catalogue.Manifest{radarrManifest, plexManifest, {Module: "plain", Version: "1"}} {
|
|
if err := inv.RegisterModule(ctx, m, inventory.Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m.Module == "plex" {
|
|
continue // pulled in by a requirement, not assigned: it is in what the machine was sent (item 4)
|
|
}
|
|
if _, err := inv.Assign(ctx, "ace", m.Module); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
record, err := inv.NodeByName(ctx, "ace")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordSentWith(ctx, record.ID, "d1", map[string]string{"radarr": "c1", "plex": "c2", "plain": "c3"},
|
|
1, 1, inventory.Send{Sequence: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lib := inventory.ResourceHealth{Resource: "library:media", Kind: link.KindLibrary, Target: "/storage/media",
|
|
State: link.StateWaiting, Waits: []inventory.Wait{{Part: "media", Access: "library:media", Reason: link.WaitKeep,
|
|
Placed: "fs-000000000001", What: "let the mesh keep the library media"}}}
|
|
copies := libraryVerdictsFor(ctx, inv, "ace", []inventory.ResourceHealth{lib})
|
|
var modules []string
|
|
for _, c := range copies {
|
|
modules = append(modules, c.Module)
|
|
}
|
|
if strings.Join(modules, ",") != "plex,radarr" {
|
|
t.Fatalf("the library's verdict is said for %v; want plex and radarr, the modules using it", modules)
|
|
}
|
|
paths, err := inv.LibraryPaths(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(paths["media"], ",") != "movies" {
|
|
t.Fatalf("the paths kept of media are %v", paths["media"])
|
|
}
|
|
now := time.Now()
|
|
f := gateFacts{now: now, waits: libraryFactsWith(), groupsAdded: map[string]bool{"radarr": false},
|
|
accessesChanged: map[string]bool{"radarr": false},
|
|
health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now, Resources: copies}}}
|
|
if h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthPerson {
|
|
t.Fatalf("radarr waiting on media reads %v %q; want a wait for a person", h, why)
|
|
}
|
|
for _, state := range []string{link.StateUnknown, link.StateStarting} {
|
|
c := copies[1]
|
|
c.State, c.Waits = state, nil
|
|
f.health["ace"] = inventory.NodeHealth{Node: "ace", HeardAt: now, Resources: []inventory.ResourceHealth{c}}
|
|
if h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthGood {
|
|
t.Errorf("media %s, a move that changes no library: %v %q", state, h, why)
|
|
}
|
|
f.accessesChanged = map[string]bool{"radarr": true}
|
|
if h, _ := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthNotYet {
|
|
t.Errorf("media %s, a move that changes a library: %v; want held to the bound", state, h)
|
|
}
|
|
f.accessesChanged = map[string]bool{"radarr": false}
|
|
}
|
|
}
|
|
|
|
// The review of 7ea6d36e, item 5: the engine saying it started a take does not make the library taken. While the act
|
|
// runs the library still waits and the operator is not asked again; it is taken when the engine states the act done
|
|
// under that warrant.
|
|
func TestALibraryIsTakenOnlyWhenTheEngineSaysTheActIsDone(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
inv := open.inventory
|
|
register(t, open, catalogue.Manifest{Module: catalogue.LibrariesModule, Version: "1"})
|
|
if err := inv.SetSettings(ctx, "", catalogue.LibrariesModule, map[string]any{catalogue.LibrariesSetting: map[string]any{
|
|
"media": map[string]any{"home": "anchor", "root": "/storage/media"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
take := takeFolderIn(open, func(context.Context, link.AccessActAsk) (link.HandOverAnswer, error) {
|
|
return link.HandOverAnswer{Said: "the take of library:media started"}, nil
|
|
})
|
|
lt := libraryTake{Library: "media", Node: "anchor", Root: "/storage/media", Placed: "fs-000000000001"}
|
|
if _, err := take(ctx, lt, "c01", "the operator"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
libs, err := inv.Libraries(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
taken, err := librariesTaken(ctx, inv, libs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if taken["media"] {
|
|
t.Fatal("a take only started made the library taken")
|
|
}
|
|
// While it runs: the wait is still excused, and no new ask is made for it.
|
|
var f operatorWaitFacts
|
|
readWaitFacts(ctx, inv, "anchor", nil, nil, &f)
|
|
if err := checkWait("radarr", "anchor", placedWait("library:media", "fs-000000000001"), f); err != nil {
|
|
t.Fatalf("the wait while the take runs: %v", err)
|
|
}
|
|
if got := withoutTakesUnderWay([]libraryTake{lt}, f.taken); len(got) != 0 {
|
|
t.Fatalf("a library whose take runs is asked about again: %+v", got)
|
|
}
|
|
// The engine states the act done under that warrant.
|
|
act, _ := json.Marshal(map[string]any{"act": "take", "access": "library:media", "warrant": "c01", "state": "done"})
|
|
if err := stateHealth(ctx, inv, nil, "anchor", link.Health{Contract: link.LibraryContract, At: time.Now(),
|
|
Resources: []link.ResourceHealth{{Resource: "library:media", Kind: link.KindLibrary, Target: "/storage/media",
|
|
State: link.StateHealthy, Since: time.Now(), Act: act}}}, time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if taken, err = librariesTaken(ctx, inv, libs); err != nil || !taken["media"] {
|
|
t.Fatalf("the act done did not make the library taken (%v)", err)
|
|
}
|
|
}
|