ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
161 lines
7.0 KiB
Go
161 lines
7.0 KiB
Go
package resolution
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// resolverMachines is the anchor and the home server holding the resolver, and a laptop holding nothing.
|
|
var resolverMachines = map[string]string{
|
|
"anchor.internal": "10.42.0.1", "laptop.internal": "10.42.0.2", "home.internal": "10.42.0.3"}
|
|
|
|
// bothResolvers is the two holders on record, as `seat mesh-dns-resolver --add` leaves them.
|
|
var bothResolvers = []mcat.Held{
|
|
{Claim: "mesh-dns-resolver", Scope: mcat.ScopeMesh, Node: "anchor", Module: "dnsmasq"},
|
|
{Claim: "mesh-dns-resolver", Scope: mcat.ScopeMesh, Node: "home", Module: "dnsmasq"},
|
|
}
|
|
|
|
// uplinks is every module in the catalogue holding node-uplink, and so writing the machine's resolver
|
|
// file (novox/hq ADR 0223 part 2): the program that would otherwise rewrite it is the one that writes it.
|
|
// dhcpcd's left the catalogue with ADR 0226, held by no machine.
|
|
var uplinks = []string{"networkmanager", "systemd-networkd"}
|
|
|
|
// managing is a machine as each uplink module needs it: able to install, run a service and run the
|
|
// manager that module is for.
|
|
func managing(node string) mcat.Node {
|
|
caps := map[string]bool{"package-manager": true, "service-manager": true}
|
|
for _, u := range uplinks {
|
|
caps["uplink-"+u] = true
|
|
}
|
|
return mcat.Node{Name: node, At: node + ".internal", Capabilities: caps}
|
|
}
|
|
|
|
// twoResolverShelf is the resolver, the uplink modules that write what a machine asks, and a stand-in
|
|
// answering `mesh-addressing`.
|
|
func twoResolverShelf(t *testing.T) map[string]mcat.Manifest {
|
|
t.Helper()
|
|
out := map[string]mcat.Manifest{
|
|
"net": {Module: "net", Version: "1", Provides: []mcat.Offer{{Name: "mesh-addressing"}}},
|
|
"dnsmasq": catalogueManifest(t, "dnsmasq"),
|
|
}
|
|
for _, u := range uplinks {
|
|
out[u] = catalogueManifest(t, u)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// worldWithout is the rest of the mesh as a plan for one machine sees it: every other holder's claim
|
|
// and offer, and both holders on record.
|
|
func worldWithout(node string) mcat.World {
|
|
w := mcat.World{Holdings: bothResolvers, Offered: map[string][]mcat.Provider{}}
|
|
for _, h := range bothResolvers {
|
|
if h.Node == node {
|
|
continue
|
|
}
|
|
w.Held = append(w.Held, h)
|
|
w.Offered["wildcard-resolution"] = append(w.Offered["wildcard-resolution"], mcat.Provider{Node: h.Node, At: h.Node + ".internal", Module: h.Module})
|
|
}
|
|
return w
|
|
}
|
|
|
|
// The requirement stays with what writes the file (ADR 0223 part 1): a machine is refused when nothing
|
|
// in the mesh resolves, rather than given a file listing nothing.
|
|
func TestEveryUplinkRequiresTheMeshsResolver(t *testing.T) {
|
|
for _, uplink := range uplinks {
|
|
found := false
|
|
for _, r := range catalogueManifest(t, uplink).Requires {
|
|
found = found || r == "wildcard-resolution"
|
|
}
|
|
if !found {
|
|
t.Errorf("%s writes the resolver file and does not require wildcard-resolution", uplink)
|
|
}
|
|
}
|
|
_, err := Resolve(twoResolverShelf(t), []string{"networkmanager"}, managing("laptop"), mcat.World{})
|
|
if err == nil || !strings.Contains(err.Error(), "wildcard-resolution") {
|
|
t.Errorf("an uplink was composed on a mesh with no resolver: %v", err)
|
|
}
|
|
}
|
|
|
|
// A holder answers its own requirement, even though the other holder sorts first (issue 258 kept).
|
|
func TestAHolderAnswersItsOwnRequirement(t *testing.T) {
|
|
got, err := Resolve(twoResolverShelf(t), []string{"dnsmasq", "networkmanager"},
|
|
managing("home"), worldWithout("home"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, n := range got.Needs {
|
|
if n.Name == "wildcard-resolution" && n.From != "home" {
|
|
t.Errorf("the home server's uplink is bound to %s; it holds the seat itself", n.From)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A seat held once is still held once: a second claimant on another machine is refused while
|
|
// nothing is on record, and a store recording two holders of it is refused, naming the seat.
|
|
func TestASingleHolderMeshSeatStillRefusesASecondHolder(t *testing.T) {
|
|
store := shelf(mod("postgres", nil, nil, nil, mcat.Claim{Name: "mesh-store", Scope: mcat.ScopeMesh}))
|
|
other := mcat.Held{Claim: "mesh-store", Scope: mcat.ScopeMesh, Node: "anchor", Module: "postgres"}
|
|
if _, err := Resolve(store, []string{"postgres"}, workstation(), mcat.World{Held: []mcat.Held{other}}); err == nil ||
|
|
!strings.Contains(err.Error(), "one per mesh") {
|
|
t.Errorf("a second claimant of a seat held once was not refused: %v", err)
|
|
}
|
|
here := mcat.Held{Claim: "mesh-store", Scope: mcat.ScopeMesh, Node: workstation().Name, Module: "postgres"}
|
|
_, err := Resolve(store, []string{"postgres"}, workstation(), mcat.World{Held: []mcat.Held{other}, Holdings: []mcat.Held{other, here}})
|
|
if err == nil || !strings.Contains(err.Error(), "on record as held by 2") {
|
|
t.Errorf("two holders on record for a seat held once were not refused: %v", err)
|
|
}
|
|
}
|
|
|
|
// Replicated is not "whoever is assigned": two claimants with nothing on record are refused, as for
|
|
// any mesh seat, and each holder is added by an act.
|
|
func TestTwoUnrecordedClaimantsOfTheReplicatedSeatAreRefused(t *testing.T) {
|
|
w := worldWithout("home")
|
|
w.Holdings = nil
|
|
_, err := Resolve(twoResolverShelf(t), []string{"dnsmasq"}, mcat.Node{Name: "home", At: "home.internal"}, w)
|
|
if err == nil || !strings.Contains(err.Error(), "seat mesh-dns-resolver --to") {
|
|
t.Errorf("a second resolver with nothing on record was not refused, naming the handover: %v", err)
|
|
}
|
|
}
|
|
|
|
// Every consumer is bound to the same holder whatever order the mesh resolved its machines in.
|
|
func TestTheFirstHolderIsTheFirstProvider(t *testing.T) {
|
|
providers := []mcat.Provider{{Node: "anchor", Module: "dnsmasq"}, {Node: "home", Module: "dnsmasq"}}
|
|
for _, held := range [][]mcat.Held{bothResolvers, {bothResolvers[1], bothResolvers[0]}} {
|
|
if p, ok := HolderAmong("wildcard-resolution", providers, held); !ok || p.Node != "anchor" {
|
|
t.Errorf("held in order %v answered %v", held, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// One host record per machine, beside its wildcard (novox/hq issue 262): a name with a host record
|
|
// says it exists and has no IPv6 address, where the wildcard alone said there is no such name, and
|
|
// musl reads that as final.
|
|
func TestTheResolverHasOneHostRecordPerMachine(t *testing.T) {
|
|
got, err := Resolve(twoResolverShelf(t), []string{"dnsmasq"}, mcat.Node{Name: "anchor", At: "anchor.internal"}, mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := Declaration(got, mcat.Rendering{Names: resolverMachines, Machines: resolverMachines, Suffix: "internal",
|
|
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, _ := byID(out)["dnsmasq.fact-node-zones"]["content"].(string)
|
|
records := map[string]int{}
|
|
for _, line := range strings.Split(content, "\n") {
|
|
if strings.HasPrefix(line, "host-record=") {
|
|
records[strings.TrimPrefix(line, "host-record=")]++
|
|
}
|
|
}
|
|
for name, at := range resolverMachines {
|
|
if records[name+","+at] != 1 {
|
|
t.Errorf("%s has %d host records at %s, and has one:\n%s", name, records[name+","+at], at, content)
|
|
}
|
|
}
|
|
if len(records) != len(resolverMachines) {
|
|
t.Errorf("%d host records for %d machines:\n%s", len(records), len(resolverMachines), content)
|
|
}
|
|
}
|