package inventory import ( "context" "errors" "strings" "testing" "github.com/novox/mesh-control/internal/catalogue" ) func manifest(name string, provides, requires []string) catalogue.Manifest { return catalogue.Manifest{Module: name, Provides: catalogue.Offers(provides...), Requires: requires} } func TestAModuleRoundTripsWholeAndUnshredded(t *testing.T) { // The manifest is held as it was given. Every field of it is read together when a node is // resolved, and a manifest that gains a field should not need a migration before it can be // stored — the module system is the thing most likely to grow. inv := fresh(t) m := catalogue.Manifest{ Module: "xorg", Provides: catalogue.Offers("display-server"), Capabilities: []string{"seat"}, Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}, Resources: []map[string]any{{"id": "conf", "type": "file", "path": "/etc/X11/x.conf"}}, } if err := inv.RegisterModule(t.Context(), m, Source{}); err != nil { t.Fatal(err) } shelf, err := inv.Catalogue(t.Context()) if err != nil { t.Fatal(err) } back, ok := shelf["xorg"] if !ok { t.Fatal("the module was not in the catalogue") } if len(back.Claims) != 1 || back.Claims[0].Name != "the-seat" { t.Errorf("the claims did not survive: %+v", back.Claims) } if len(back.Resources) != 1 || back.Resources[0]["path"] != "/etc/X11/x.conf" { t.Errorf("the resources did not survive: %+v", back.Resources) } } func TestRegisteringAgainReplacesTheManifest(t *testing.T) { // A manifest changing is the ordinary case — a module gains a requirement, a claim, a // resource. What matters is that the change is what the next resolution sees. inv := fresh(t) if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil { t.Fatal(err) } if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil), Source{}); err != nil { t.Fatal(err) } shelf, err := inv.Catalogue(t.Context()) if err != nil { t.Fatal(err) } if len(shelf) != 1 { t.Fatalf("registering twice made %d modules", len(shelf)) } if len(shelf["thing"].Provides) != 1 { t.Error("the second manifest did not replace the first") } } func TestAModuleAMachineIsRunningCannotBeForgotten(t *testing.T) { // Not a fault. It means a machine is running that module now, and removing the record would // leave the mesh unable to describe what is on it. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil { t.Fatal(err) } if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil { t.Fatal(err) } err := inv.ForgetModule(t.Context(), "thing") if !errors.Is(err, ErrStillAssigned) { t.Fatalf("a module in use was forgotten: %v", err) } if err := inv.Unassign(t.Context(), "laptop", "thing"); err != nil { t.Fatal(err) } if err := inv.ForgetModule(t.Context(), "thing"); err != nil { t.Errorf("an unassigned module could not be forgotten: %v", err) } } func TestRemovingANodeTakesItsAssignments(t *testing.T) { // The asymmetry with modules above, and it is deliberate: a node that is gone cannot be // running anything, so its assignments are meaningless rather than dangerous. inv := fresh(t) node, err := inv.AddNode(t.Context(), "laptop") if err != nil { t.Fatal(err) } if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil { t.Fatal(err) } if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil { t.Fatal(err) } if _, err := inv.store.Pool().Exec(t.Context(), `delete from node where id = $1`, node.ID); err != nil { t.Fatal(err) } var left int if err := inv.store.Pool().QueryRow(t.Context(), `select count(*) from assignment`).Scan(&left); err != nil { t.Fatal(err) } if left != 0 { t.Errorf("%d assignment(s) outlived the node they were on", left) } // And the module itself survives, because other nodes may be running it. shelf, err := inv.Catalogue(t.Context()) if err != nil { t.Fatal(err) } if len(shelf) != 1 { t.Error("removing a node took a module with it") } } func TestAssigningAModuleTheMeshDoesNotKnowIsRefused(t *testing.T) { // Said as "no module of that name" rather than as a foreign key. A person mistyping a module // name should be told that, not shown a constraint. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } err := inv.Assign(t.Context(), "laptop", "not-a-module") if !errors.Is(err, ErrNoSuchModule) { t.Fatalf("assigning an unknown module gave %v", err) } } func TestAssigningTwiceIsNotAnError(t *testing.T) { // It is a statement of what should be true, and it already is. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil { t.Fatal(err) } for i := 0; i < 3; i++ { if err := inv.Assign(t.Context(), "laptop", "thing"); err != nil { t.Fatalf("assigning again failed: %v", err) } } assigned, err := inv.Assigned(t.Context(), "laptop") if err != nil { t.Fatal(err) } if len(assigned) != 1 { t.Errorf("assigned three times and got %v", assigned) } } func TestANodeThatNeverReportedHasNoCapabilities(t *testing.T) { // Not "everything". A node that has never spoken will refuse anything needing a capability, // which is wrong but visible — where assuming it can do everything would assign work it // cannot do and find out on the machine. inv := fresh(t) if _, err := inv.AddNode(t.Context(), "laptop"); err != nil { t.Fatal(err) } caps, err := inv.ProfileOf(t.Context(), "laptop") if err != nil { t.Fatal(err) } if len(caps) != 0 { t.Errorf("a node that never reported has capabilities: %v", caps) } } func TestOnlyPresentCapabilitiesCount(t *testing.T) { // A profile lists what was looked for and whether it was found. A capability that was looked // for and absent is the same as one nobody looked for, as far as what may run here goes — // and reading the list without the verdict would let a module onto a machine that reported // "no". inv := fresh(t) node, err := inv.AddNode(t.Context(), "laptop") if err != nil { t.Fatal(err) } if err := inv.RecordProfile(t.Context(), node.ID, map[string]any{ "capabilities": []any{ map[string]any{"name": "seat", "present": true}, map[string]any{"name": "firewall", "present": false}, }, }); err != nil { t.Fatal(err) } caps, err := inv.ProfileOf(t.Context(), "laptop") if err != nil { t.Fatal(err) } if !caps["seat"] { t.Error("a capability the node reported as present is missing") } if caps["firewall"] { t.Error("a capability the node reported as ABSENT was counted as present") } } func TestAModuleWithNoSourceIsNeverBehind(t *testing.T) { // It was handed over directly, which is how a one-off arrives and how every module got here // before provenance existed. Saying "out of date" about it would be inventing a comparison // against nothing. inv := fresh(t) if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{}); err != nil { t.Fatal(err) } from, err := inv.SourceOf(t.Context(), "thing") if err != nil { t.Fatal(err) } if !from.Current() { t.Error("a module with no source was reported as behind") } behind, err := inv.Behind(t.Context()) if err != nil { t.Fatal(err) } if len(behind) != 0 { t.Errorf("a module with no source is in the behind list: %v", behind) } } func TestASourceThatMovedMakesTheModuleBehind(t *testing.T) { inv := fresh(t) if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{Repository: "novox/thing", Ref: "main", BuiltFrom: "aaaa1111"}); err != nil { t.Fatal(err) } from, err := inv.SourceOf(t.Context(), "thing") if err != nil { t.Fatal(err) } if !from.Current() { t.Fatal("a module built from the only commit its source has is behind") } if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil { t.Fatal(err) } from, err = inv.SourceOf(t.Context(), "thing") if err != nil { t.Fatal(err) } if from.Current() { t.Error("the source moved and the module still reports as current") } } func TestBeingBehindNamesTheMachinesRunningTheOldOne(t *testing.T) { // The question somebody actually has. A module being out of date is a fact about the // catalogue; machines running last week's version is the thing with consequences. inv := fresh(t) for _, n := range []string{"laptop", "workstation"} { if _, err := inv.AddNode(t.Context(), n); err != nil { t.Fatal(err) } } if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil { t.Fatal(err) } for _, n := range []string{"laptop", "workstation"} { if err := inv.Assign(t.Context(), n, "thing"); err != nil { t.Fatal(err) } } if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil { t.Fatal(err) } behind, err := inv.Behind(t.Context()) if err != nil { t.Fatal(err) } if len(behind["thing"]) != 2 { t.Errorf("running on %v; both machines have the old one", behind["thing"]) } } func TestRebuildingCatchesUp(t *testing.T) { inv := fresh(t) if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil { t.Fatal(err) } if err := inv.SourceMoved(t.Context(), "thing", "bbbb2222"); err != nil { t.Fatal(err) } if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"a-thing"}, nil), Source{Repository: "novox/thing", BuiltFrom: "bbbb2222"}); err != nil { t.Fatal(err) } from, err := inv.SourceOf(t.Context(), "thing") if err != nil { t.Fatal(err) } if !from.Current() { t.Errorf("built from the commit the source has and still behind: %+v", from) } } func TestHandingOverAManifestDoesNotEraseWhereItComesFrom(t *testing.T) { // Fixing something in a hurry is legitimate. Silently forgetting where the module normally // comes from is not: it is the only thing that would say, afterwards, that a machine is // running something nobody can rebuild. inv := fresh(t) if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{Repository: "novox/thing", Ref: "main", BuiltFrom: "aaaa1111"}); err != nil { t.Fatal(err) } if err := inv.RegisterModule(t.Context(), manifest("thing", []string{"patched"}, nil), Source{}); err != nil { t.Fatal(err) } from, err := inv.SourceOf(t.Context(), "thing") if err != nil { t.Fatal(err) } if from.Repository != "novox/thing" { t.Errorf("handing over a manifest erased the source: %+v", from) } } func TestASourceNobodyHasCheckedIsNotBehind(t *testing.T) { // A module built from a commit, where nothing has yet told the mesh whether that source has // moved. It is not behind — nobody has looked. Reporting it as behind would put every module // on the list the moment provenance was recorded, which makes the list say nothing. inv := fresh(t) if err := inv.RegisterModule(t.Context(), manifest("thing", nil, nil), Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}); err != nil { t.Fatal(err) } from, err := inv.SourceOf(t.Context(), "thing") if err != nil { t.Fatal(err) } // Registering sets the head to what was built, so the two agree until something says // otherwise. Either way it must not read as behind. if !from.Current() { t.Errorf("a source nobody has checked reports as behind: %+v", from) } // And with the head genuinely unknown, which is what a module registered before provenance // existed looks like after somebody adds a source to it. if (Source{Repository: "novox/thing", BuiltFrom: "aaaa1111"}).Current() == false { t.Error("a module with no known head reports as behind") } } func TestAPinSurvivesAndCanBeChanged(t *testing.T) { inv := fresh(t) ctx := context.Background() for _, n := range []string{"user", "first", "second"} { if _, err := inv.AddNode(ctx, n); err != nil { t.Fatal(err) } } if err := inv.PinProvision(ctx, "user", "postgres-database", "first"); err != nil { t.Fatal(err) } // Changing the answer replaces it rather than adding a second, or a machine would be told to // use two databases and nothing would say which. if err := inv.PinProvision(ctx, "user", "postgres-database", "second"); err != nil { t.Fatal(err) } pins, err := inv.PinsFor(ctx, "user") if err != nil { t.Fatal(err) } if len(pins) != 1 || pins["postgres-database"] != "second" { t.Fatalf("got %v", pins) } if err := inv.UnpinProvision(ctx, "user", "postgres-database"); err != nil { t.Fatal(err) } if pins, _ := inv.PinsFor(ctx, "user"); len(pins) != 0 { t.Fatalf("the choice outlived being removed: %v", pins) } // Removing something that was never said is a mistake worth reporting, not a silent success. if err := inv.UnpinProvision(ctx, "user", "postgres-database"); err == nil { t.Fatal("unpinning something nobody pinned reported success") } } func TestAPinGoesWhenTheProviderLeavesTheMesh(t *testing.T) { // Otherwise a machine is pointed at something that no longer exists and reported as // configured, which is the failure mode this whole project keeps refusing. inv := fresh(t) ctx := context.Background() for _, n := range []string{"consumer", "provider"} { if _, err := inv.AddNode(ctx, n); err != nil { t.Fatal(err) } } if err := inv.PinProvision(ctx, "consumer", "postgres-database", "provider"); err != nil { t.Fatal(err) } if _, err := inv.store.Pool().Exec(ctx, `delete from node where name = 'provider'`); err != nil { t.Fatal(err) } // Counted in the table, not read through PinsFor. PinsFor joins on the provider, so a pin // left behind by a departed node is invisible through it whether or not it was cleaned up — // which made the first version of this test pass with the cascade removed. rows, err := inv.pinRows(ctx, "consumer") if err != nil { t.Fatal(err) } if rows != 0 { t.Fatalf("a choice outlived the machine it named: %d row(s) left", rows) } } func TestTheCatalogueSaysWhereEachModuleCameFromAndWhoRunsIt(t *testing.T) { // The provenance was recorded from the first build and nothing showed it, which made "is this // current?" a question you could only answer by reading the database. inv := fresh(t) ctx := context.Background() for _, n := range []string{"workstation", "laptop"} { if _, err := inv.AddNode(ctx, n); err != nil { t.Fatal(err) } } if err := inv.RegisterModule(ctx, manifest("shell", []string{"login-shell"}, nil), Source{Repository: "https://forge.invalid/shell.git", BuiltFrom: "aaa", Head: "aaa"}); err != nil { t.Fatal(err) } if err := inv.RegisterModule(ctx, manifest("byhand", nil, nil), Source{}); err != nil { t.Fatal(err) } if err := inv.Provide(ctx, manifest("networking", nil, []string{"login-shell"})); err != nil { t.Fatal(err) } for _, n := range []string{"workstation", "laptop"} { if err := inv.Assign(ctx, n, "shell"); err != nil { t.Fatal(err) } } entries, err := inv.Catalogued(ctx) if err != nil { t.Fatal(err) } by := map[string]Entry{} for _, e := range entries { by[e.Manifest.Module] = e } if len(by) != 3 { t.Fatalf("the catalogue has %d modules", len(by)) } // Sorted, and both nodes, so a person reading it twice sees the same thing. if got := strings.Join(by["shell"].On, ","); got != "laptop,workstation" { t.Fatalf("shell runs on %q", got) } if by["shell"].Source.Repository != "https://forge.invalid/shell.git" { t.Fatalf("shell came from %q", by["shell"].Source.Repository) } if by["shell"].Provided { t.Fatal("a module built from a repository was reported as shipped with the control plane") } // A module nobody runs is in the catalogue: the catalogue is what EXISTS, and what runs is a // different question the same row answers. if len(by["byhand"].On) != 0 { t.Fatalf("byhand runs on %v", by["byhand"].On) } // Handed over by hand is its own state. Nothing can rebuild it, and showing it as a // repository would send somebody looking for one. if by["byhand"].Source.Repository != "" || by["byhand"].Provided { t.Fatalf("byhand: %+v", by["byhand"]) } if !by["networking"].Provided { t.Fatal("a module the control plane ships was not marked as such") } if by["networking"].Source.Repository != "" { // It is not a repository, and showing it as one would have somebody go looking for it. t.Fatalf("networking claims to come from %q", by["networking"].Source.Repository) } } func TestACatalogueEntryKnowsWhetherItIsBehind(t *testing.T) { inv := fresh(t) ctx := context.Background() if err := inv.RegisterModule(ctx, manifest("shell", nil, nil), Source{Repository: "https://forge.invalid/shell.git", BuiltFrom: "aaa", Head: "aaa"}); err != nil { t.Fatal(err) } if err := inv.SourceMoved(ctx, "shell", "bbb"); err != nil { t.Fatal(err) } entries, err := inv.Catalogued(ctx) if err != nil { t.Fatal(err) } if entries[0].Source.Current() { t.Fatal("a module whose source moved reported itself current") } } // A capability's detail is half of what the machine said, and it was being thrown away. // // novox/hq ADR 0009: a capability's presence gates an assignment and its detail carries a value — // `seat: card1-DP-1`. So *can this run here* and *what should it be configured as* are one fact // read two ways, and keeping only the first read makes the second unanswerable. func TestWhatAMachineSaidAboutItselfIsKeptWhole(t *testing.T) { inv := ForTest(t) ctx := t.Context() node, err := inv.AddNode(ctx, "workstation") if err != nil { t.Fatal(err) } if err := inv.RecordProfile(ctx, node.ID, map[string]any{"capabilities": []map[string]any{ {"name": "seat", "present": true, "detail": "card1-DP-1, card1-HDMI-A-1"}, {"name": "container-runtime", "present": false, "detail": "docker is not installed"}, }}); err != nil { t.Fatal(err) } held, err := inv.Profile(ctx, "workstation") if err != nil { t.Fatal(err) } if len(held) != 2 { t.Fatalf("the machine reported two things and the mesh kept %d", len(held)) } found := map[string]Capability{} for _, c := range held { found[c.Name] = c } if found["seat"].Detail != "card1-DP-1, card1-HDMI-A-1" { t.Fatalf("the value the machine reported was discarded: %+v", found["seat"]) } // And the absent one keeps its reason, which is the case a person most needs explaining: // "this machine has no container runtime" is the answer and "docker is not installed" is why. if found["container-runtime"].Present { t.Fatal("something the machine said it does not have was recorded as present") } if found["container-runtime"].Detail != "docker is not installed" { t.Fatalf("the reason a capability is absent was discarded: %+v", found["container-runtime"]) } // The gating read is unchanged, and takes only what is present. gates, err := inv.ProfileOf(ctx, "workstation") if err != nil { t.Fatal(err) } if !gates["seat"] || gates["container-runtime"] { t.Fatalf("the gating read disagrees with what the machine said: %+v", gates) } } // Never reported is not the same as reported nothing: one machine has not run the host, the other // ran it and can do nothing, and the remedies are different. func TestNeverReportedAndReportedNothingAreDifferentProfiles(t *testing.T) { inv := ForTest(t) ctx := t.Context() silent, err := inv.AddNode(ctx, "silent") if err != nil { t.Fatal(err) } empty, err := inv.AddNode(ctx, "empty") if err != nil { t.Fatal(err) } if err := inv.RecordProfile(ctx, empty.ID, map[string]any{"capabilities": []map[string]any{}}); err != nil { t.Fatal(err) } _ = silent never, err := inv.Profile(ctx, "silent") if err != nil { t.Fatal(err) } if never != nil { t.Fatalf("a machine that never reported looks like one that reported nothing: %+v", never) } nothing, err := inv.Profile(ctx, "empty") if err != nil { t.Fatal(err) } if nothing == nil { t.Fatal("a machine that reported nothing looks like one that never reported") } }