package compose import ( "errors" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/overlay" ) // storeManifests is mesh-controller's bindings_test.go storeManifests (cmd/mesh-controller at 6c2a8d62): a store holding the // mesh-store seat, a resolver holding mesh-dns-resolver, a network that requires wildcard resolution, and a board that // requires a database. func storeManifests() []catalogue.Manifest { return []catalogue.Manifest{ {Module: "store", Version: "1", Provides: []catalogue.Offer{{Name: "postgres-database", Scope: catalogue.ScopeMesh}}, Claims: []catalogue.Claim{{Name: "mesh-store", Scope: catalogue.ScopeMesh}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}, Grants: map[string]string{"postgres-database": "/var/lib/mesh/store/grants"}}, {Module: "resolver", Version: "1", Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}}, Claims: []catalogue.Claim{{Name: "mesh-dns-resolver", Scope: catalogue.ScopeMesh}}}, {Module: "network", Version: "1", Requires: []string{"wildcard-resolution"}}, {Module: "board", Version: "1", Requires: []string{"postgres-database"}}, } } func need(t *testing.T, plan catalogue.Resolution, consumer, provision string) catalogue.Needed { t.Helper() for _, n := range plan.Needs { if n.For == consumer && n.Name == provision { return n } } t.Fatalf("no %s for %s: %+v", provision, consumer, plan.Needs) return catalogue.Needed{} } // theStoreMesh is the mesh of mesh-controller's bindings_integration_test.go // TestTheIncidentAConsumerStaysBesideItsDataUntilAPersonPinsIt: the anchor's store and resolver hold the mesh's seats on // record; the laptop runs its own of each, and a network and a board. func theStoreMesh(t *testing.T) Input { t.Helper() in := aMesh(t) for _, m := range storeManifests() { in.Catalogue[m.Module] = m } anchor, laptop := nodeOf(t, &in, "anchor"), nodeOf(t, &in, "laptop") anchor.Assigned = append(anchor.Assigned, "store", "resolver") laptop.Assigned = append(laptop.Assigned, "store", "resolver", "network", "board") in.Holdings = []catalogue.Held{ {Claim: "mesh-store", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "store", Site: "here"}, {Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "resolver", Site: "here"}, } return in } // 1362/AC7 and AC4 (bindings_integration_test.go): a consumer of a provision that keeps its data stays beside its data, one that // does not follows the seat; the laptop's store taken away, the consumer bound there on record is refused, not moved to the // anchor's empty one; a person's pin moves it. func TestAConsumerStaysBesideItsDataUntilAPersonPinsIt(t *testing.T) { in := theStoreMesh(t) got, err := Resolve(in, "laptop") if err != nil { t.Fatal(err) } if n := need(t, got.Plan, "board", "postgres-database"); n.From != "laptop" || n.Module != "store" || !n.KeepsData { t.Fatalf("the consumer was bound to %s/%s (keeps data: %v); its data is beside it", n.From, n.Module, n.KeepsData) } if n := need(t, got.Plan, "network", "wildcard-resolution"); n.From != "anchor" || n.KeepsData { t.Fatalf("the resolver was bound to %s (keeps data: %v); its seat is held on anchor (issue 258)", n.From, n.KeepsData) } // Sent, and recorded: the binding to data. The laptop's store taken away: refused, not moved to the anchor's empty one. laptop := nodeOf(t, &in, "laptop") laptop.Bound = map[string]map[string]catalogue.Chosen{"board": {"postgres-database": {Node: "laptop", Module: "store"}}} laptop.Assigned = []string{overlay.Name, "resolver", "network", "board"} _, err = Resolve(in, "laptop") if err == nil || !IsUnresolvable(err) || !strings.Contains(err.Error(), "board on laptop is bound to laptop/store") || !strings.Contains(err.Error(), "pin laptop postgres-database anchor store") { t.Fatalf("the consumer's store went and it was answered elsewhere: %v", err) } // A person pins the anchor's: it moves, and the resolution says where it was bound (Kept is empty, as it moves). laptop.Pins = map[string]catalogue.Chosen{"postgres-database": {Node: "anchor", Module: "store"}} got, err = Resolve(in, "laptop") if err != nil { t.Fatal(err) } if n := need(t, got.Plan, "board", "postgres-database"); n.From != "anchor" { t.Fatalf("pinned to the anchor and bound to %s", n.From) } } // 1362/AC4: a data-keeping consumer bound on record to a provider elsewhere is kept on that provider, and the resolution says // so (Resolution.Kept) when the mesh would answer it otherwise. func TestADataKeepingConsumerBoundElsewhereIsKeptThereAndSaid(t *testing.T) { in := theStoreMesh(t) laptop := nodeOf(t, &in, "laptop") laptop.Assigned = []string{overlay.Name, "resolver", "network", "board"} // no store of its own laptop.Bound = map[string]map[string]catalogue.Chosen{"board": {"postgres-database": {Node: "anchor", Module: "store"}}} got, err := Resolve(in, "laptop") if err != nil { t.Fatal(err) } if n := need(t, got.Plan, "board", "postgres-database"); n.From != "anchor" || !n.KeepsData { t.Fatalf("the consumer is bound to %s (keeps data: %v), not where it was sent", n.From, n.KeepsData) } if len(got.Plan.Kept) != 0 { t.Errorf("a binding that was kept where it is is said as a kept move: %+v", got.Plan.Kept) } // And bound somewhere else on record while the mesh would answer from the anchor: kept, and said. laptop.Bound = map[string]map[string]catalogue.Chosen{"board": {"postgres-database": {Node: "home", Module: "store"}}} home := Node{Name: "home", Place: Place{Site: "there", Address: "10.77.0.7"}, Assigned: []string{overlay.Name, "store"}, Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}} in.Nodes = append(in.Nodes, home) got, err = Resolve(in, "laptop") if err != nil { t.Fatal(err) } if n := need(t, got.Plan, "board", "postgres-database"); n.From != "home" { t.Errorf("the consumer was moved from where it was sent: %s", n.From) } } // A provider module whose offer says only a person gives its credential (ADR 0337), and a consumer of it. func outsideObjects() catalogue.Manifest { m := objects() m.Provides[0].Credential = &catalogue.OfferCredential{IssuedBy: catalogue.IssuedOutside} return m } func theObjectsMesh(t *testing.T, provider catalogue.Manifest) Input { t.Helper() in := aMesh(t) in.Catalogue[provider.Module], in.Catalogue["files"] = provider, files() nodeOf(t, &in, "anchor").Assigned = append(nodeOf(t, &in, "anchor").Assigned, provider.Module) nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "files") nodeOf(t, &in, "anchor").SealingKey = "anchor-key" nodeOf(t, &in, "laptop").SealingKey = "laptop-key" return in } func theFilesNeed(t *testing.T, got Resolved) catalogue.Needed { t.Helper() return need(t, got.Plan, "files", "s3-bucket") } // 1362/AC1: a pair credential the Input names as existing with no value, and one it names not at all, are composed with a // stand-in and named foreseen, and nothing is made; one with a value is composed with it and foreseen is empty. func TestAPairCredentialNotHeldIsComposedWithAStandInAndNamedForeseen(t *testing.T) { mk := func() Input { return theObjectsMesh(t, objects()) } for name, creds := range map[string][]Credential{ "no entry": nil, "held with no value": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialHeld}}, "to be made": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialWouldMake}}, } { in := mk() in.Credentials = creds got, err := Resolve(in, "laptop") if err != nil { t.Fatalf("%s: %v", name, err) } if n := theFilesNeed(t, got); n.Sealed != catalogue.ForeseenSealed { t.Errorf("%s: the credential is %q, not the stand-in", name, n.Sealed) } if len(got.Foreseen) != 1 || got.Foreseen[0] != "files on laptop needs s3-bucket from anchor" { t.Errorf("%s: foreseen is %v", name, got.Foreseen) } } in := mk() in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialHeld, Sealed: "sealed-to-laptop"}} got, err := Resolve(in, "laptop") if err != nil { t.Fatal(err) } if n := theFilesNeed(t, got); n.Sealed != "sealed-to-laptop" || len(got.Foreseen) != 0 { t.Errorf("a held credential: %q foreseen %v", n.Sealed, got.Foreseen) } unchanged(t, mk, func(in Input) { if _, err := Resolve(in, "laptop"); err != nil { t.Fatal(err) } }) } // 1362/AC2: a credential only a person gives and nobody has is not given and not made: the consumer still resolves without it // (novox/hq ADR 0337 clause 4), said with the controller's words; one given is composed with it. func TestACredentialOnlyAPersonGivesIsNotMadeAndTheConsumerStillResolves(t *testing.T) { for name, creds := range map[string][]Credential{ "no entry": nil, "to be made (never, for one issued outside)": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialWouldMake}}, "not given": {{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialNotGiven, Why: "the value held was made by the mesh, which objects's software would refuse"}}, } { in := theObjectsMesh(t, outsideObjects()) in.Credentials = creds got, err := Resolve(in, "laptop") if err != nil { t.Fatalf("%s: %v", name, err) } if n := theFilesNeed(t, got); n.Sealed != "" { t.Errorf("%s: a value stands in for what nobody gave: %q", name, n.Sealed) } if len(got.Foreseen) != 0 || len(got.NotGiven) != 1 || !strings.Contains(got.NotGiven[0], "files on laptop holds no s3-bucket from anchor: ") { t.Errorf("%s: foreseen %v not given %v", name, got.Foreseen, got.NotGiven) } if len(got.Plan.Modules) == 0 { t.Errorf("%s: the machine did not resolve", name) } } in := theObjectsMesh(t, outsideObjects()) in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialHeld, Sealed: "given-and-sealed"}} got, err := Resolve(in, "laptop") if err != nil || theFilesNeed(t, got).Sealed != "given-and-sealed" || len(got.NotGiven) != 0 { t.Fatalf("a value a person gave: %+v %v", got.NotGiven, err) } } // 1362/AC3: a credential the send would refuse, a given value sealed to a key a machine no longer has, refuses the resolution with // today's words, and is not a node whose set does not compose. func TestACredentialTheSendWouldRefuseRefusesTheResolutionInTheSendsWords(t *testing.T) { why := "files's \"s3-bucket\" credential from anchor was accepted from a person, and a sealing key at one end has changed since. " + "The mesh cannot re-seal a value it does not hold: accept it again with `secret accept laptop files s3-bucket --provider anchor`" in := theObjectsMesh(t, objects()) in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialRefused, Why: why}} _, err := Resolve(in, "laptop") want := "files on laptop needs s3-bucket from anchor and no credential could be made for it: " + why if err == nil || err.Error() != want { t.Fatalf("got %v, want %q", err, want) } if IsUnresolvable(err) { t.Error("a refused credential is a node whose set does not compose") } } // A record-answered need is filled with the licence key sealed to this holder, and the manager holder is handed the manager node's // public sealing key (novox/hq ADR 0050) and no other. func TestARecordAnsweredNeedGetsItsKeyAndTheManagerItsPublicKey(t *testing.T) { in := aMesh(t) in.Catalogue["assistant"] = catalogue.Manifest{Module: "assistant", Version: "1", Requires: []string{LicenceProvision}} laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "assistant") laptop.SealingKey = "laptop-key" in.Licences = []Licence{{Name: "personal", Serves: map[string]any{"model": "x"}, Holders: []Holder{{Node: "laptop", Module: "assistant"}}, Keys: map[Holder]string{{Node: "laptop", Module: "assistant"}: "key-sealed-to-laptop"}}} got, err := Resolve(in, "laptop") if err != nil { t.Fatal(err) } n := need(t, got.Plan, "assistant", LicenceProvision) if !n.ByRecord || n.Sealed != "key-sealed-to-laptop" || n.Manager || n.Serves["manager_public_key"] != nil { t.Fatalf("the record-answered need is %+v", n) } in.Licences[0].Manager = Holder{Node: "laptop", Module: "assistant"} got, err = Resolve(in, "laptop") if err != nil { t.Fatal(err) } n = need(t, got.Plan, "assistant", LicenceProvision) if !n.Manager || n.Serves["manager_public_key"] != "laptop-key" || n.Serves["model"] != "x" { t.Fatalf("the manager is %+v", n) } if len(got.Foreseen) != 0 || len(got.NotGiven) != 0 { t.Errorf("a record-answered need needs no pair credential: %v %v", got.Foreseen, got.NotGiven) } } // 1362/AC5: a node whose own set does not compose returns an error callers can tell from an unreadable Input. func TestANodeWhoseOwnSetDoesNotComposeIsToldApartFromAnUnreadableInput(t *testing.T) { in := aMesh(t) in.Catalogue["rival-one"] = catalogue.Manifest{Module: "rival-one", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}} in.Catalogue["rival-two"] = catalogue.Manifest{Module: "rival-two", Version: "1", Claims: []catalogue.Claim{{Name: "the-seat", Scope: catalogue.ScopeNode}}} laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "rival-one", "rival-two") if _, err := Resolve(in, "laptop"); err == nil || !IsUnresolvable(err) { t.Fatalf("a set that does not compose: %v", err) } if _, err := Resolve(in, "nowhere"); err == nil || IsUnresolvable(err) || !errors.Is(err, ErrNoSuchNode) { t.Fatalf("a node the mesh does not have: %v", err) } in.Catalogue = nil if _, err := Resolve(in, "anchor"); err == nil || IsUnresolvable(err) { t.Fatalf("an Input without the catalogue: %v", err) } } // 1362/AC6: settings are returned for every resolved module that has layers in the Input, including one pulled in by another. func TestSettingsAreReturnedForEveryResolvedModuleIncludingOneThatWasPulledIn(t *testing.T) { in := aMesh(t) in.Catalogue["lib"] = catalogue.Manifest{Module: "lib", Version: "1", Provides: catalogue.Offers("blob")} in.Catalogue["app"] = catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"blob"}} laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "app") laptop.Settings = map[string][]catalogue.Layer{ "lib": {{From: "mesh", Values: map[string]any{"x": 1}}}, "app": {{From: "laptop", Values: map[string]any{"y": 2}}}, "other": {{From: "laptop", Values: map[string]any{"z": 3}}}, // not resolved here: no settings come of it "empty": nil, } got, err := Resolve(in, "laptop") if err != nil { t.Fatal(err) } pulled := false for _, m := range got.Plan.Modules { pulled = pulled || m.Module == "lib" } if !pulled { t.Fatalf("lib was not pulled in: %+v", got.Plan.Modules) } if len(got.Settings) != 2 || got.Settings["lib"][0].Values["x"] != 1 || got.Settings["app"][0].Values["y"] != 2 { t.Errorf("settings are %+v", got.Settings) } // The settings returned are copies: changing them does not change the Input. got.Settings["app"][0].From = "changed" if laptop.Settings["app"][0].From != "laptop" { t.Error("the settings returned share the Input's layers") } }