mesh-sdk takes the catalogue, the validator and a store-free composition, the fingerprint's new form and the delivery parts' key layouts (design 47 R4a, issues 1353-1364, 1298) #48
@@ -0,0 +1,325 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user