package compose import ( "encoding/json" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" ) func resourceIDs(d Declared) []string { var ids []string for _, r := range d.Resources { ids = append(ids, r["type"].(string)+":"+r["id"].(string)) } return ids } func has(ids []string, want string) bool { for _, id := range ids { if id == want { return true } } return false } // aSmallMesh is three nodes, a provider and its consumer, a seat and the private network: aMesh with `ace` running a database and // a resolver holding the mesh's resolver seat on record (the providers and consumer of mesh-controller's facts_test.go // aMeshWithSecrets, the seat of bindings_test.go storeManifests). func aSmallMesh(t *testing.T) Input { t.Helper() in := threeNodes(t) for _, m := range storeManifests() { if m.Module == "resolver" { in.Catalogue[m.Module] = m } } ace := nodeOf(t, &in, "ace") ace.Assigned = append(ace.Assigned, "resolver") in.Holdings = []catalogue.Held{{Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "ace", Module: "resolver", Site: "there"}} return in } // 1298/AC1: each node of a small mesh composes, its body is accepted by the node-engine's validator, and it names the resources the // controller composes for the same case: the private network's files on the nodes that run it, the provider's received file, the // consumer's file. func TestEachNodeOfASmallMeshComposesAndItsBodyIsAcceptedByTheValidator(t *testing.T) { in := aSmallMesh(t) want := map[string][]string{ "anchor": {"package:mesh-wireguard.overlay-tools", "file:mesh-wireguard.overlay-config", "service:mesh-wireguard.overlay-up", "file:objects.received-s3-bucket"}, "laptop": {"package:mesh-wireguard.overlay-tools", "file:mesh-wireguard.overlay-config", "service:mesh-wireguard.overlay-up", "file:files.config"}, "ace": nil, } for node, ids := range want { d, problems, err := Compose(in, node) if err != nil || len(problems) != 0 { t.Fatalf("%s: %v %v", node, err, problems) } for _, id := range ids { if !has(resourceIDs(d), id) { t.Errorf("%s does not name %s: %v", node, id, resourceIDs(d)) } } body, err := d.Body() if err != nil { t.Fatal(err) } var parsed struct { Declaration int `json:"declaration"` Resources []map[string]any `json:"resources"` } if err := json.Unmarshal(body, &parsed); err != nil || parsed.Declaration != 1 || len(parsed.Resources) != len(d.Resources) { t.Errorf("%s: body %s %v", node, body, err) } } // ace runs a resolver and a database and nothing the mesh declares of its own: an empty declaration says it owns nothing. d, _, _ := Compose(in, "ace") if body, _ := d.Body(); !strings.Contains(string(body), `"owns_nothing":true`) { t.Errorf("an empty declaration is not said to be deliberate: %s", body) } } // 1298/AC2 (mesh-controller merge_gate_integration_test.go TestIssue236AManifestTheNodeEngineRefusesFailsThePullRequest, the manifest): // a service that omits its state is accepted by the catalogue's check and refused by the node-engine; Compose returns the validator's // problems in the node-engine's words and no error. func TestAManifestTheNodeEngineRefusesComesBackAsProblemsAndNoError(t *testing.T) { in := aMesh(t) var lemurs catalogue.Manifest if err := json.Unmarshal([]byte(`{"module":"lemurs","version":"1","capabilities":["systemd"], "resources":[{"id":"service","type":"service","unit":"lemurs.service","boot":"enabled"}]}`), &lemurs); err != nil { t.Fatal(err) } in.Catalogue["lemurs"] = lemurs laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "lemurs") d, problems, err := Compose(in, "laptop") if err != nil { t.Fatalf("a refusal of the validator is an error: %v", err) } if len(problems) == 0 || !strings.Contains(strings.Join(problems, "\n"), "lemurs.service") { t.Fatalf("problems are %v", problems) } if !has(resourceIDs(d), "service:lemurs.service") { t.Errorf("the declaration does not carry the resource: %v", resourceIDs(d)) } // And the same, as the validator says it of the body. body, _ := d.Body() if again := strings.Join(problems, "\n"); again == "" || !strings.Contains(string(body), "lemurs.service") { t.Errorf("the body does not name the service: %s", body) } } // 1298/AC3: a node whose own set does not compose returns an error and no declaration, told apart from an unreadable Input. func TestANodeWhoseSetDoesNotComposeReturnsAnErrorAndNoDeclaration(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") d, problems, err := Compose(in, "laptop") if err == nil || !IsUnresolvable(err) || d.Resources != nil || problems != nil { t.Fatalf("a set that does not compose: %v %v %v", d.Resources, problems, err) } if _, _, err := Compose(in, "nowhere"); err == nil || IsUnresolvable(err) { t.Fatalf("a node the Input does not have: %v", err) } } // 1298/AC4: composing the same Input twice gives byte-identical bodies, and the Input is unchanged. func TestComposingTwiceGivesTheSameBytesAndChangesNothing(t *testing.T) { mk := func() Input { in := aSmallMesh(t) nodeOf(t, &in, "anchor").Account = "jochens" nodeOf(t, &in, "laptop").Sequence = 7 return in } in := mk() for _, node := range []string{"anchor", "laptop", "ace"} { a, _, err := Compose(in, node) if err != nil { t.Fatal(err) } b, _, err := Compose(in, node) if err != nil { t.Fatal(err) } ab, _ := a.Body() bb, _ := b.Body() if string(ab) != string(bb) { t.Errorf("%s: two compositions differ:\n%s\n%s", node, ab, bb) } } unchanged(t, mk, func(in Input) { for _, node := range []string{"anchor", "laptop", "ace"} { if _, _, err := Compose(in, node); err != nil { t.Fatal(err) } } }) } // 1298/AC5: what a send would make is named in the result as foreseen and never invented: an own secret, a port, a core box, a grant's // sealed copy. func TestWhatASendWouldMakeIsNamedAsForeseenAndNeverInvented(t *testing.T) { in := aGrantMesh(t) in.Issued[0].ForProvider = "" in.Catalogue["assistant"] = anAssistant() in.Catalogue["web"] = aWebModule() in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1", OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/nats/broker", Taken: catalogue.TakenAtStart}}} anchor := nodeOf(t, &in, "anchor") anchor.Assigned = append(anchor.Assigned, "assistant", "web", "nats") anchor.SealingKey = keyOf(t, "anchor") anchor.Stated, anchor.Contract = true, VaultBoxContract in.BusMinted = []string{"anchor.nats"} d, problems, err := Compose(in, "anchor") if err != nil || len(problems) != 0 { t.Fatalf("%v %v", err, problems) } if !has(d.Foreseen, "assistant/password") || !has(d.Foreseen, "nats/broker") { t.Errorf("own secrets foreseen: %v", d.Foreseen) } if len(d.ForeseenPorts) != 1 || d.ForeseenPorts[0] != "web:8080" { t.Errorf("foreseen ports: %v", d.ForeseenPorts) } if len(d.CoreBoxesWanted) != 1 || d.CoreBoxesWanted[0].Module != "nats" { t.Errorf("core boxes wanted: %v", d.CoreBoxesWanted) } if !has(d.StoodIn, "the grant of s3-bucket for files on laptop") { t.Errorf("stood in for: %v", d.StoodIn) } body, _ := d.Body() if strings.Contains(string(body), secretValue) { t.Error("a secret value is in the body") } } // 1298/AC6: the body carries the sequence, epoch and generation the Input gives, and the module of each resource and the carried builds // only to a node whose Input says it reads modules (novox/hq issue 1145). func TestTheBodyCarriesTheOrderTheInputGivesAndTheBuildsOnlyToAnEngineThatReadsThem(t *testing.T) { in := aSmallMesh(t) laptop := nodeOf(t, &in, "laptop") laptop.Sequence, laptop.Epoch, laptop.Generation = 42, 57, 9 in.CurrentBuilds = map[string]string{"files": "commit-files", "mesh-wireguard": "commit-wg"} in.BuildIDs = map[string]map[string]string{"files": {"commit-files": "build-7"}} // An assignment of another commit than the one carried does not name the build; the build made from the commit carried does. laptop.Runs = map[string]Run{"mesh-wireguard": {Commit: "commit-wg", Build: "build-9"}, "files": {Commit: "an-older-commit", Build: "build-6"}} d, _, err := Compose(in, "laptop") if err != nil { t.Fatal(err) } body, _ := d.Body() var got map[string]any if err := json.Unmarshal(body, &got); err != nil { t.Fatal(err) } if got["sequence"] != float64(42) || got["epoch"] != float64(57) || got["generation"] != float64(9) { t.Errorf("the order is %v %v %v", got["sequence"], got["epoch"], got["generation"]) } if _, there := got["builds"]; there { t.Errorf("builds are sent to an engine that does not read them: %s", body) } for _, r := range got["resources"].([]any) { if _, there := r.(map[string]any)["module"]; there { t.Errorf("a module is named on a resource of an engine that does not read them: %v", r) } } laptop.ReadsModules = true d, _, err = Compose(in, "laptop") if err != nil { t.Fatal(err) } body, _ = d.Body() got = nil if err := json.Unmarshal(body, &got); err != nil { t.Fatal(err) } builds, _ := got["builds"].(map[string]any) if builds["files"] != "build-7" || builds["mesh-wireguard"] != "build-9" { t.Errorf("builds are %v", builds) } named := false for _, r := range got["resources"].([]any) { if r.(map[string]any)["id"] == "files.config" && r.(map[string]any)["module"] == "files" { named = true } } if !named { t.Errorf("a resource is not named for its module: %s", body) } // Zero is not sent at all. laptop.Sequence, laptop.Epoch, laptop.Generation = 0, 0, 0 d, _, _ = Compose(in, "laptop") body, _ = d.Body() for _, key := range []string{`"sequence"`, `"epoch"`, `"generation"`} { if strings.Contains(string(body), key) { t.Errorf("%s is sent as zero: %s", key, body) } } } // A module left out of a build set keeps the build it was last sent (novox/hq issue 259): the machine keeps that one. func TestAModuleLeftOutKeepsTheBuildItWasLastSent(t *testing.T) { c := newComposer(Input{CurrentBuilds: map[string]string{"a": "new-a", "b": "new-b"}}) plan := catalogue.Resolution{Modules: []catalogue.Manifest{{Module: "a"}, {Module: "b"}, {Module: "c"}}} node := Node{SentBuilds: map[string]string{"b": "old-b", "c": "old-c"}, SentKnown: true} got := c.carriedOn(node, plan, map[string]string{"b": "its settings cannot compose"}) if got["a"] != "new-a" || got["b"] != "old-b" || got["c"] != "" { t.Errorf("carried builds are %v", got) } node.SentKnown = false if got := c.carriedOn(node, plan, map[string]string{"b": "x"}); len(got) != 2 || got["b"] != "" { t.Errorf("when what was sent is not known: %v", got) } } // An adopted node is told which modules were taken and, for every module it runs that is not, the ids of its resources // (novox/hq ADR 0100 and 0103); a converged node is told nothing, and its body is as it was before adoption. func TestAnAdoptedNodeIsToldWhatWasTakenAndWhatIsKept(t *testing.T) { in := aSmallMesh(t) d, _, err := Compose(in, "laptop") if err != nil || d.Adoption != nil { t.Fatalf("a converged node: %v %v", d.Adoption, err) } laptop := nodeOf(t, &in, "laptop") laptop.Adopted, laptop.Taken = true, []string{"mesh-wireguard"} d, problems, err := Compose(in, "laptop") if err != nil || len(problems) != 0 { t.Fatalf("%v %v", err, problems) } if d.Adoption == nil || len(d.Adoption.Taken) != 1 || d.Adoption.Taken[0] != "mesh-wireguard" || len(d.Adoption.Untaken["files"]) == 0 || d.Adoption.Untaken["mesh-wireguard"] != nil { t.Fatalf("adoption is %+v", d.Adoption) } if body, _ := d.Body(); !strings.Contains(string(body), `"adoption"`) { t.Errorf("the adoption is not on the wire: %s", body) } } // Issue 263 (merge_gate_integration_test.go TestIssue263AnIdentityARealMachineNameOverflows...): a change makes the network manager // require the object store's provision; on a machine whose name is six characters its identity is 26 against a bound of 20. The // composition of the provider returns the refusal, naming the module and the provision. func TestIssue263AnIdentityARealMachineNameOverflowsIsARefusal(t *testing.T) { in := aGrantMesh(t) in.Catalogue["resolver"] = storeManifests()[1] in.Catalogue["networkmanager"] = catalogue.Manifest{Module: "networkmanager", Version: "1", Requires: []string{"wildcard-resolution", "s3-bucket"}} laptop, anchor := nodeOf(t, &in, "laptop"), nodeOf(t, &in, "anchor") laptop.Assigned = append(laptop.Assigned, "networkmanager") anchor.Assigned = append(anchor.Assigned, "resolver") in.Holdings = []catalogue.Held{{Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "resolver", Site: "here"}} in.Issued = append(in.Issued, Issued{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "networkmanager", Provider: "anchor", ForProvider: "sealed"}) d, problems, err := Compose(in, "anchor") if err != nil || len(problems) != 0 { t.Fatalf("%v %v", err, problems) } refusals := strings.Join(d.Refusals(), "\n") if !strings.Contains(refusals, "networkmanager") || !strings.Contains(refusals, "s3-bucket") || !strings.Contains(refusals, "laptop") { t.Fatalf("the refusal does not name the module, the provision and the machine: %q", refusals) } // And the consumer that fits is granted. if len(d.Withheld) != 1 || d.Withheld[0].Module != "networkmanager" { t.Errorf("withheld %+v", d.Withheld) } }