324 lines
16 KiB
Go
324 lines
16 KiB
Go
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)
|
|
}
|
|
}
|