326 lines
14 KiB
Go
326 lines
14 KiB
Go
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)
|
|
}
|
|
}
|