package compose import ( "errors" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/overlay" ) // aWebModule publishes one port in the short form, which the mesh is free to move (MachineSide). func aWebModule() catalogue.Manifest { return catalogue.Manifest{Module: "web", Version: "1", Listens: []catalogue.Listening{{Name: "http", Port: 8080, From: catalogue.FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "web", "ports": []any{"8080"}, "image": "registry.example/web@sha256:" + strings.Repeat("a", 64)}}} } // aStoreModule is mesh-controller's sendable_integration_test.go TestConsumersAreToldTheGivenPort store (cmd/mesh-controller // at 6c2a8d62): publishing 5432:5432, offering a database to the mesh. func aStoreModule() catalogue.Manifest { return catalogue.Manifest{Module: "store", Version: "1", Provides: []catalogue.Offer{{Name: "database", Scope: catalogue.ScopeMesh}}, Listens: []catalogue.Listening{{Port: 5432, From: catalogue.FromMesh}}, Guards: []int{5432}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store", "ports": []any{"5432:5432"}, "image": "registry.example/pg@sha256:" + strings.Repeat("b", 64)}}} } func placed(t *testing.T, in Input, node string) Placed { t.Helper() got, err := Places(in, node) if err != nil { t.Fatalf("%v", err) } return got } // 1363/AC1 and AC2: a port the Input holds as allocated on the node is the one composed; with none, no number is invented: the // manifest's own is composed and the port is named foreseen, as the Foreseeing mode does. func TestAnAllocatedPortIsComposedAndAnUnallocatedOneIsNamedForeseenNotInvented(t *testing.T) { mk := func() Input { in := aMesh(t) in.Catalogue["web"] = aWebModule() nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "web") return in } in := mk() got := placed(t, in, "laptop") if p := got.Rendering.Ports["web"][8080]; p != 8080 { t.Errorf("a port nobody allocated is composed as %d, not as the manifest's own", p) } if len(got.ForeseenPorts) != 1 || got.ForeseenPorts[0] != "web:8080" { t.Errorf("foreseen ports are %v", got.ForeseenPorts) } nodeOf(t, &in, "laptop").Ports = map[string]map[int]int{"web": {8080: 20123}} got = placed(t, in, "laptop") if p := got.Rendering.Ports["web"][8080]; p != 20123 || len(got.ForeseenPorts) != 0 { t.Errorf("the allocated port is composed as %d (foreseen %v)", p, got.ForeseenPorts) } // A port the module does not publish is not the mesh's to move, so it is never foreseen. m := aWebModule() m.Resources = nil in.Catalogue["web"] = m nodeOf(t, &in, "laptop").Ports = nil got = placed(t, in, "laptop") if got.Rendering.Ports["web"][8080] != 8080 || len(got.ForeseenPorts) != 0 { t.Errorf("an unpublished port: %v foreseen %v", got.Rendering.Ports, got.ForeseenPorts) } unchanged(t, mk, func(in Input) { placed(t, in, "laptop") }) } // 1363/AC3: a port the operator gave is used, over the allocated one, and a consumer elsewhere is told it // (mesh-controller sendable_integration_test.go TestConsumersAreToldTheGivenPort). func TestAPortTheOperatorGaveIsUsedAndNotReallocated(t *testing.T) { in := aMesh(t) in.Catalogue["store"] = aStoreModule() in.Catalogue["app"] = catalogue.Manifest{Module: "app", Version: "1", Requires: []string{"database"}} anchor := nodeOf(t, &in, "anchor") anchor.Assigned = append(anchor.Assigned, "store") nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "app") anchor.Ports = map[string]map[int]int{"store": {5432: 20999}} anchor.Settings = map[string][]catalogue.Layer{"store": {{From: "anchor", Placement: map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5433}}}}} got := placed(t, in, "anchor") if p := got.Rendering.Ports["store"][5432]; p != 5433 { t.Errorf("the store is on %d, not the port it was given", p) } if g := got.Rendering.Given["store"][5432]; g != 5433 { t.Errorf("the given port is %d", g) } if len(got.ForeseenPorts) != 0 { t.Errorf("a given port is foreseen: %v", got.ForeseenPorts) } resolved, err := Resolve(in, "laptop") if err != nil { t.Fatal(err) } if told := need(t, resolved.Plan, "app", "database").Serves["port"]; told != 5433 { t.Errorf("the consumer is told the store is on %v", told) } // One holder per machine port across the node's modules: two given the same refused, in the controller's words. in.Catalogue["store2"] = func() catalogue.Manifest { m := aStoreModule() m.Module, m.Provides = "store2", nil m.Resources = []map[string]any{{"id": "server", "type": "container", "name": "other", "ports": []any{"5432:5432"}, "image": "registry.example/pg@sha256:" + strings.Repeat("c", 64)}} return m }() anchor.Assigned = append(anchor.Assigned, "store2") anchor.Settings["store2"] = []catalogue.Layer{{From: "anchor", Placement: map[string]any{catalogue.PortsSetting: map[string]any{"5432": 5433}}}} _, err = Places(in, "anchor") if err == nil || !errors.Is(err, ErrPortTaken) || !strings.Contains(err.Error(), "store and store2 are both given machine port 5433 on anchor") && !strings.Contains(err.Error(), "store2 and store are both given machine port 5433 on anchor") { t.Fatalf("two modules given one machine port: %v", err) } } // 1363/AC4: the seats held and taken reach the Rendering: where each mesh-scoped seat's holder put its ports, from the node's // given and assigned ports, never the manifest's own; and on an adopted node a holder assigned but not taken counts only by its // given ports (novox/hq ADR 0100). func TestSeatsHeldAndTakenReachTheRendering(t *testing.T) { in := aMesh(t) holder := aStoreModule() holder.Claims = []catalogue.Claim{{Name: "mesh-store", Scope: catalogue.ScopeMesh}} in.Catalogue["store"] = holder anchor := nodeOf(t, &in, "anchor") anchor.Assigned = append(anchor.Assigned, "store") // The shelf is read whole, in this node's set or not: laptop, running none, is told where the anchor's store is only by the // settings and ports of its own machine, which it has none of. anchor.Ports = map[string]map[int]int{"store": {5432: 20999}} got := placed(t, in, "anchor") if at := got.Rendering.Seats["mesh-store"][5432]; at != 20999 { t.Errorf("the seat's holder is told to be on %d", at) } if len(placed(t, in, "laptop").Rendering.Seats) != 0 { t.Errorf("laptop is told of a holder it has no port for: %v", placed(t, in, "laptop").Rendering.Seats) } // Adopted, and the holder assigned but not taken: the found container's ports, not the mesh's assignment. anchor.Adopted = true if len(placed(t, in, "anchor").Rendering.Seats) != 0 { t.Errorf("an untaken holder on an adopted node counts by the mesh's assignment: %v", placed(t, in, "anchor").Rendering.Seats) } anchor.Taken = []string{"store"} got = placed(t, in, "anchor") if at := got.Rendering.Seats["mesh-store"][5432]; at != 20999 || !got.Rendering.Adopted || !got.Rendering.Taken["store"] { t.Errorf("a taken holder: seats %v adopted %v taken %v", got.Rendering.Seats, got.Rendering.Adopted, got.Rendering.Taken) } } // 1363/AC4: local networks and outward links (mesh-controller local_network_test.go TestNodeShowsWhichAttachedNetworksCountAndWhy, // the values): a private network on a link facing outside counts, a public subnet and the private network's own link never do; a // machine that said none judges to nothing. func TestLocalNetworksAndOutwardLinksReachTheRendering(t *testing.T) { in := aMesh(t) laptop := nodeOf(t, &in, "laptop") laptop.OutwardLinks = []string{"eth0", "eth1"} laptop.Networks = []catalogue.AttachedNetwork{ {Link: "eth0", Network: "192.168.1.10/24"}, {Link: "eth1", Network: "203.0.113.7/24"}, {Link: overlay.Interface, Network: "10.10.0.4/24"}} laptop.NetworksSaid = true got := placed(t, in, "laptop").Rendering if len(got.LocalNetworks) != 1 || got.LocalNetworks[0] != "192.168.1.0/24" { t.Errorf("the local networks are %v", got.LocalNetworks) } if len(got.OutwardLinks) != 2 || got.OutwardLinks[0] != "eth0" { t.Errorf("the outward links are %v", got.OutwardLinks) } laptop.NetworksSaid = false if got := placed(t, in, "laptop").Rendering; len(got.LocalNetworks) != 0 { t.Errorf("a machine that never reported has local networks %v", got.LocalNetworks) } } // 1363/AC4: accounts, the names of the mesh, the mesh range and the operator's export reach the Rendering. func TestAccountsNamesAndTheOperatorsExportReachTheRendering(t *testing.T) { in := aMesh(t) nodeOf(t, &in, "anchor").Account = "jochens" in.OperatorExport = &catalogue.KeptExport{} in.OverlayCIDR = "10.99.0.0/16" got := placed(t, in, "anchor").Rendering if got.Accounts["anchor"] != "jochens" || len(got.Accounts) != 1 { t.Errorf("accounts are %v", got.Accounts) } if got.Names["anchor.internal"] != "10.77.0.1" || got.Names["laptop.internal"] != "10.77.0.2" || len(got.Names) != 2 { t.Errorf("names are %v", got.Names) } if len(got.Machines) != 2 || got.Machines["laptop.internal"] != "10.77.0.2" { t.Errorf("machines are %v", got.Machines) } if len(got.Mesh) != 2 || got.Mesh[0] != "10.77.0.1" || got.Mesh[1] != "10.77.0.2" { t.Errorf("the mesh's addresses are %v", got.Mesh) } if got.MeshRange != "10.99.0.0/16" || got.Suffix != "internal" || got.TunnelInterface != overlay.Interface { t.Errorf("range %q suffix %q tunnel %q", got.MeshRange, got.Suffix, got.TunnelInterface) } if got.Kept != nil { t.Error("the operator's export reaches a node where nothing keeps it") } keeper := catalogue.Manifest{Module: "keeper", Version: "1", Keeps: "/var/lib/keeper/export.json"} in.Catalogue["keeper"] = keeper nodeOf(t, &in, "anchor").Assigned = append(nodeOf(t, &in, "anchor").Assigned, "keeper") if got := placed(t, in, "anchor").Rendering; got.Kept != in.OperatorExport { t.Errorf("a module that keeps the export is not given it: %v", got.Kept) } // And the names of a machine that left the network leave with it. nodeOf(t, &in, "laptop").Assigned = nil if got := placed(t, in, "anchor").Rendering; len(got.Names) != 1 || got.Names["laptop.internal"] != "" { t.Errorf("a machine off the network is named: %v", got.Names) } } // The names the operator gave to peers of an adopted tunnel answer until they enrol (novox/hq issue 112); a node of the mesh owns // its name. func TestACarriedPeerIsNamedUntilItEnrols(t *testing.T) { in := aMesh(t) in.CarriedPeers = []CarriedPeer{{PublicKey: "K1", Address: "192.0.2.5", Named: "printer"}, {PublicKey: "K2", Address: "192.0.2.6", Named: "laptop"}, {PublicKey: "K3", Address: "192.0.2.7", Named: "late", EnrolledAs: "late"}} got := placed(t, in, "anchor").Rendering if got.Names["printer.internal"] != "192.0.2.5" || got.Names["laptop.internal"] != "10.77.0.2" || got.Names["late.internal"] != "" { t.Errorf("names are %v", got.Names) } } // A node-engine older than a contract is not sent the field it would refuse (novox/hq ADR 0240 and after). func TestTheNodeEnginesContractGatesWhatItIsSent(t *testing.T) { in := aMesh(t) laptop := nodeOf(t, &in, "laptop") if r := placed(t, in, "laptop").Rendering; r.ReadsHealth || r.JudgesRoot || r.ReadsBusy || r.Hands { t.Errorf("an engine that never stated anything reads %+v", r) } laptop.Stated, laptop.Contract = true, ReadinessContract if r := placed(t, in, "laptop").Rendering; !r.ReadsHealth || r.JudgesRoot || r.ReadsBusy || r.Hands { t.Errorf("an engine at the readiness contract reads %+v", r) } laptop.Contract = HandedContract if r := placed(t, in, "laptop").Rendering; !r.ReadsHealth || !r.JudgesRoot || !r.ReadsBusy || !r.Hands { t.Errorf("an engine at the handed contract reads %+v", r) } } // The artifact store as a node reaches it: over the private network by its holder's name and the port it put it on, or on loopback // for the holder alone while nothing is on the network (novox/hq 04-ISSUES/102). func TestTheArtifactStoreIsReachedOverTheNetworkOrOnLoopbackForItsHolder(t *testing.T) { in := aMesh(t) in.Catalogue["artifacts"] = catalogue.Manifest{Module: "artifacts", Version: "1", Serves: map[string]map[string]any{catalogue.ArtifactStoreProvision: {"port": 5100}}} anchor := nodeOf(t, &in, "anchor") anchor.Assigned = append(anchor.Assigned, "artifacts") if got := placed(t, in, "laptop").Rendering.ArtifactStore; got != "anchor.internal:5100" { t.Errorf("laptop reaches the store at %q", got) } nodeOf(t, &in, "laptop").Assigned = nil anchor.Assigned = []string{"artifacts"} // off the network: the holder alone reaches it if got := placed(t, in, "anchor").Rendering.ArtifactStore; got != "127.0.0.1:5100" { t.Errorf("the holder reaches the store at %q", got) } if got := placed(t, in, "laptop").Rendering.ArtifactStore; got != "" { t.Errorf("a machine off the network reaches the store at %q", got) } in.Built = []string{"mesh-sdk"} if got := placed(t, in, "anchor").Rendering.Built; !got["mesh-sdk"] { t.Errorf("built is %v", got) } } // A zone a module declares is placed where the mesh put it: settled from that node's settings, at the node's private address, on // the port the answering listen is published on there (novox/hq ADR 0199); the same zone declared twice is refused, by name. func TestAZoneIsPlacedWhereTheMeshPutItAndTheSameZoneTwiceIsRefused(t *testing.T) { in := aMesh(t) lab := catalogue.Manifest{Module: "lab", Version: "1", Zone: &catalogue.Zone{Name: "${setting:zone}", Listen: "dns"}, Listens: []catalogue.Listening{{Name: "dns", Port: 53, Protocol: "udp", From: catalogue.FromMesh}}} in.Catalogue["lab"] = lab anchor := nodeOf(t, &in, "anchor") anchor.Assigned = append(anchor.Assigned, "lab") anchor.Settings = map[string][]catalogue.Layer{"lab": {{From: "anchor", Values: map[string]any{"zone": "Lab.Example."}}}} anchor.Ports = map[string]map[int]int{"lab": {53: 5353}} zones := placed(t, in, "laptop").Rendering.Zones if len(zones) != 1 || zones[0] != (catalogue.ZoneAt{Zone: "lab.example", Node: "anchor", Module: "lab", Address: "10.77.0.1", Port: 5353}) { t.Fatalf("zones are %+v", zones) } laptop := nodeOf(t, &in, "laptop") laptop.Assigned = append(laptop.Assigned, "lab") laptop.Settings = map[string][]catalogue.Layer{"lab": {{From: "laptop", Values: map[string]any{"zone": "lab.example"}}}} if _, err := Places(in, "laptop"); err == nil || !strings.Contains(err.Error(), "the mesh's zones cannot be forwarded") { t.Fatalf("one zone declared twice: %v", err) } } // Every holder of a replicated seat is listed by internal name at its private address (novox/hq ADR 0223): the holders on record, // and only the sole claimant when there are none; a holder off the private network is left out. func TestEveryHolderOfAReplicatedSeatIsListedOnTheNetwork(t *testing.T) { in := aMesh(t) resolver := catalogue.Manifest{Module: "resolver", Version: "1", Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}}, Claims: []catalogue.Claim{{Name: "mesh-dns-resolver", Scope: catalogue.ScopeMesh}}} in.Catalogue["resolver"] = resolver anchor, laptop := nodeOf(t, &in, "anchor"), nodeOf(t, &in, "laptop") anchor.Assigned = append(anchor.Assigned, "resolver") // One claimant and nothing on record: the sole claimant is listed. if got := placed(t, in, "laptop").Rendering.Holders["mesh-dns-resolver"]; len(got) != 1 || got["anchor.internal"] != "10.77.0.1" { t.Errorf("the sole claimant: %v", got) } // Both on record: both are listed (two claimants with nothing on record would be refused at resolution). laptop.Assigned = append(laptop.Assigned, "resolver") in.Holdings = []catalogue.Held{ {Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "resolver"}, {Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "laptop", Module: "resolver"}} got := placed(t, in, "laptop").Rendering.Holders["mesh-dns-resolver"] if len(got) != 2 || got["anchor.internal"] != "10.77.0.1" || got["laptop.internal"] != "10.77.0.2" { t.Errorf("both holders on record: %v", got) } // A holder off the private network is left out: a resolver named at an address nothing answers waits out its timeout. in.Nodes = append(in.Nodes, Node{Name: "ace", Place: Place{Address: "10.77.0.3"}, Assigned: []string{"resolver"}}) in.Holdings = append(in.Holdings, catalogue.Held{Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "ace", Module: "resolver"}) if got := placed(t, in, "laptop").Rendering.Holders["mesh-dns-resolver"]; len(got) != 2 || got["ace.internal"] != "" { t.Errorf("a holder off the network is listed: %v", got) } }