Files
jschoubben 79a9e17df0 The broker credential resolves the mesh-broker seat, not the hub (issue 059)
An adversarial review of the 055 fix found it encoded the wrong invariants, latent while
every mesh keeps its broker on the hub. Now: the address is the overlay name of the node
ASSIGNED a module claiming the mesh-broker seat (the hub stands in only while nothing holds
the seat — genesis); "on the overlay" is what whereEveryoneIs answers (resolved the
networking module), not "has an address"; a portless genesis address defaults to 5671
instead of silently disabling the path; a second `overlay place --hub` is refused rather
than last-write-wins; and `overlay place` says that earlier credentials keep their old
address. A test now binds the controller's own module.json to its seat, so deleting the
claim fails the suite.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-18 01:02:26 +02:00

510 lines
20 KiB
Go

package catalogue
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func mod(name string, provides, requires, capabilities []string, claims ...Claim) Manifest {
return Manifest{Module: name, Provides: Offers(provides...), Requires: requires,
Capabilities: capabilities, Claims: claims}
}
func shelf(ms ...Manifest) map[string]Manifest {
out := map[string]Manifest{}
for _, m := range ms {
out[m.Module] = m
}
return out
}
func workstation() Node {
return Node{Name: "workstation", Site: "house",
Capabilities: map[string]bool{"seat": true, "container-runtime": true}}
}
func names(r Resolution) []string {
var out []string
for _, m := range r.Modules {
out = append(out, m.Module)
}
return out
}
func TestARequirementWithOneAnswerIsTakenSilently(t *testing.T) {
// `install i3` should bring in xorg without asking anybody anything, because there was no
// choice to make. This is what keeps the refusing rule from being tiresome.
got, err := Resolve(shelf(
mod("i3", nil, []string{"xorg"}, []string{"seat"}),
mod("xorg", []string{"display-server"}, nil, []string{"seat"}, Claim{Name: "the-seat"}),
), []string{"i3"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
if len(got.Modules) != 2 {
t.Fatalf("resolved %v; i3 should have brought xorg with it", names(got))
}
if got.Because["xorg"] == "assigned" {
t.Error("xorg is recorded as assigned; it was required")
}
}
func TestARequirementWithSeveralAnswersIsRefusedAndNamed(t *testing.T) {
// The mesh does not pick. A default would be a choice made for somebody who finds out later,
// and naming the candidates is the whole remedy.
_, err := Resolve(shelf(
mod("editor", nil, []string{"shell"}, nil),
mod("bash", []string{"shell"}, nil, nil),
mod("zsh", []string{"shell"}, nil, nil),
mod("fish", []string{"shell"}, nil, nil),
), []string{"editor"}, workstation(), World{})
if err == nil {
t.Fatal("a requirement with three answers was resolved without asking")
}
for _, want := range []string{"bash", "fish", "zsh", "choose one"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not mention %q: %v", want, err)
}
}
}
func TestARequirementWithNoAnswerIsRefused(t *testing.T) {
_, err := Resolve(shelf(mod("i3", nil, []string{"xorg"}, nil)), []string{"i3"}, workstation(), World{})
if err == nil || !strings.Contains(err.Error(), "nothing provides") {
t.Fatalf("a requirement nothing satisfies gave %v", err)
}
}
func TestSeveralModulesMayProvideTheSameThingAndCoexist(t *testing.T) {
// Shells. Nothing is claimed, so any number may be assigned — which is the case that made
// "flavor" look necessary and turns out to need nothing at all.
got, err := Resolve(shelf(
mod("bash", []string{"shell"}, nil, nil),
mod("zsh", []string{"shell"}, nil, nil),
mod("fish", []string{"shell"}, nil, nil),
), []string{"bash", "zsh", "fish"}, workstation(), World{})
if err != nil {
t.Fatalf("three shells could not coexist: %v", err)
}
if len(got.Modules) != 3 {
t.Errorf("resolved %v", names(got))
}
}
func TestTwoModulesClaimingOneThingAreRefused(t *testing.T) {
// xorg and wayland. Neither knows the other exists — the refusal comes from both claiming
// the seat, which is what lets a third display server be added without editing either.
_, err := Resolve(shelf(
mod("xorg", []string{"display-server"}, nil, nil, Claim{Name: "the-seat"}),
mod("wayland", []string{"display-server"}, nil, nil, Claim{Name: "the-seat"}),
), []string{"xorg", "wayland"}, workstation(), World{})
if err == nil {
t.Fatal("two modules claiming the seat were both assigned")
}
if !strings.Contains(err.Error(), "the-seat") || !strings.Contains(err.Error(), "per node") {
t.Errorf("the refusal does not say what was claimed or how widely: %v", err)
}
}
func TestAThirdModuleNeedsNoChangeToTheOthers(t *testing.T) {
// The property claims exist for. A third display server says what it claims and nothing else
// in the catalogue is touched — where pairwise exclusion would need xorg and wayland edited
// to know about it, and the edits would grow as the square of the count.
catalogue := shelf(
mod("xorg", []string{"display-server"}, nil, nil, Claim{Name: "the-seat"}),
mod("wayland", []string{"display-server"}, nil, nil, Claim{Name: "the-seat"}),
mod("mir", []string{"display-server"}, nil, nil, Claim{Name: "the-seat"}),
)
for _, pair := range [][]string{{"xorg", "mir"}, {"wayland", "mir"}} {
if _, err := Resolve(catalogue, pair, workstation(), World{}); err == nil {
t.Errorf("%v were both assigned", pair)
}
}
if _, err := Resolve(catalogue, []string{"mir"}, workstation(), World{}); err != nil {
t.Errorf("the newcomer alone was refused: %v", err)
}
}
func TestADirectlyAssignedModuleTheMachineCannotHostIsReportedNotRefused(t *testing.T) {
// A person put a display server on a seatless machine. The remedy differs from a missing module
// and the message has to say which — nothing can be installed to give a server a seat. But it is
// one module on the wrong machine, not a reason the whole node fails to resolve: it is kept out
// of what the node runs and reported as un-applied, so the healthy modules beside it still
// converge.
server := Node{Name: "server", Capabilities: map[string]bool{"container-runtime": true}}
got, err := Resolve(shelf(mod("xorg", nil, nil, []string{"seat"}, Claim{Name: "the-seat"})), []string{"xorg"}, server, World{})
if err != nil {
t.Fatalf("one un-hostable assignment refused the whole node: %v", err)
}
if len(got.Modules) != 0 {
t.Errorf("xorg was declared on a machine that cannot run it: %v", names(got))
}
if len(got.Unhostable) != 1 || got.Unhostable[0].Module != "xorg" {
t.Fatalf("xorg was not reported as un-applied: %+v", got.Unhostable)
}
if len(got.Unhostable[0].Missing) != 1 || got.Unhostable[0].Missing[0] != "seat" {
t.Errorf("the missing capability was not named: %+v", got.Unhostable[0])
}
if !strings.Contains(WrongMachine("xorg", "seat", "server"), "wrong machine") {
t.Errorf("the report reads like a missing module")
}
}
func TestAnUnhostableModuleSomethingRequiresRefusesTheNode(t *testing.T) {
// The other side of the distinction. A module the machine cannot host that is *required* by
// something running here makes the set incoherent: the requiring module cannot have its
// requirement met on this machine, so it is refused rather than quietly declared without it.
server := Node{Name: "server", Capabilities: map[string]bool{"container-runtime": true}}
_, err := Resolve(shelf(
mod("desktop", nil, []string{"display-server"}, nil),
mod("xorg", []string{"display-server"}, nil, []string{"seat"}),
), []string{"desktop"}, server, World{})
if err == nil {
t.Fatal("a node requiring a module the machine cannot host was resolved")
}
if !strings.Contains(err.Error(), "wrong machine") {
t.Errorf("the refusal reads like a missing module: %v", err)
}
}
func TestOneUnhostableAssignmentDoesNotTakeDownTheHealthyModules(t *testing.T) {
// The bug the whole-mesh dry-run found: fail2ban declares a capability the host lacks, and its
// one un-hostable assignment refused the entire node's resolution — so a push refused to send
// the healthy modules beside it too. The healthy modules must still resolve and converge; the
// un-hostable one is reported as un-applied, neither silently dropped nor fatal to the rest.
server := Node{Name: "server", Capabilities: map[string]bool{"container-runtime": true}}
got, err := Resolve(shelf(
mod("web", nil, nil, nil),
mod("cache", nil, nil, nil),
mod("cron", nil, nil, nil),
// The one on the wrong machine: it needs a firewall the machine's profile does not report.
mod("fail2ban", nil, nil, []string{"firewall"}),
), []string{"web", "cache", "cron", "fail2ban"}, server, World{})
if err != nil {
t.Fatalf("one un-hostable assignment refused the whole node: %v", err)
}
// The healthy three resolved.
if got := names(got); len(got) != 3 {
t.Fatalf("the healthy modules did not all resolve: %v", got)
}
for _, m := range got.Modules {
if m.Module == "fail2ban" {
t.Error("fail2ban was declared on a machine that cannot run it")
}
}
// The un-hostable one is reported, not silently dropped.
if len(got.Unhostable) != 1 || got.Unhostable[0].Module != "fail2ban" {
t.Fatalf("fail2ban was not reported as un-applied: %+v", got.Unhostable)
}
if len(got.Unhostable[0].Missing) != 1 || got.Unhostable[0].Missing[0] != "firewall" {
t.Errorf("the missing capability was not named: %+v", got.Unhostable[0])
}
}
func TestAMeshWideClaimIsHeldByOneNode(t *testing.T) {
// The hub, said as a claim rather than hard-coded. Another node already holds it, so this one
// cannot.
_, err := Resolve(shelf(mod("hub", nil, nil, nil, Claim{Name: "the-hub", Scope: ScopeMesh})),
[]string{"hub"}, workstation(),
World{Held: []Held{{Claim: "the-hub", Scope: ScopeMesh, Node: "anchor", Module: "hub"}}})
if err == nil {
t.Fatal("two nodes both hold a mesh-wide claim")
}
if !strings.Contains(err.Error(), "anchor") || !strings.Contains(err.Error(), "one per mesh") {
t.Errorf("the refusal does not say who holds it: %v", err)
}
}
func TestAFoundationModuleCannotBeRaisedOnASecondNode(t *testing.T) {
// novox/hq 04-ISSUES/056. The store and broker are adopted in place on the control-node; each
// foundation module claims a mesh-scoped seat named after its server — the same mechanism that
// keeps one controller — so a second `assign` to another node is refused rather than silently
// raising a second postgres or broker that holds none of the first's data.
for _, tc := range []struct{ module, seat string }{
{"postgres", "mesh-store"},
{"lavinmq", "mesh-broker"},
{"mesh-controller", "mesh-controller"},
} {
_, err := Resolve(
shelf(mod(tc.module, nil, nil, nil, Claim{Name: tc.seat, Scope: ScopeMesh})),
[]string{tc.module}, workstation(),
World{Held: []Held{{Claim: tc.seat, Scope: ScopeMesh, Node: "anchor", Module: tc.module}}})
if err == nil {
t.Fatalf("%s was raised on a second node though %s is a mesh-wide seat", tc.module, tc.seat)
}
if !strings.Contains(err.Error(), "one per mesh") {
t.Errorf("%s: the refusal does not say it is one per mesh: %v", tc.module, err)
}
}
}
func TestASiteClaimOnlyCollidesWithinThatSite(t *testing.T) {
// A DHCP server per segment. Two of them is a fault at one site and perfectly ordinary
// across two, and treating site as mesh would forbid the ordinary case.
catalogue := shelf(mod("dhcp", nil, nil, nil, Claim{Name: "dhcp", Scope: ScopeSite}))
elsewhere := []Held{{Claim: "dhcp", Scope: ScopeSite, Node: "other", Module: "dhcp", Site: "house"}}
if _, err := Resolve(catalogue, []string{"dhcp"}, workstation(), World{Held: elsewhere}); err == nil {
t.Error("two DHCP servers at one site were allowed")
}
faraway := []Held{{Claim: "dhcp", Scope: ScopeSite, Node: "other", Module: "dhcp", Site: "office"}}
if _, err := Resolve(catalogue, []string{"dhcp"}, workstation(), World{Held: faraway}); err != nil {
t.Errorf("a DHCP server at another site was treated as a collision: %v", err)
}
}
func TestTwoModulesWritingOneFileAreRefusedWithoutAnyClaim(t *testing.T) {
// This conflict costs no manifest field: the mesh already holds every resource of every
// module, so two declaring one path are visible without either knowing the other exists.
a := mod("a", nil, nil, nil)
a.Resources = []map[string]any{{"id": "conf", "type": "file", "path": "/etc/thing.conf"}}
b := mod("b", nil, nil, nil)
b.Resources = []map[string]any{{"id": "conf", "type": "file", "path": "/etc/thing.conf"}}
_, err := Resolve(shelf(a, b), []string{"a", "b"}, workstation(), World{})
if err == nil {
t.Fatal("two modules writing the same file were both assigned")
}
if !strings.Contains(err.Error(), "/etc/thing.conf") {
t.Errorf("the refusal does not name the file: %v", err)
}
}
func TestResourceIdentitiesCarryTheirModule(t *testing.T) {
// Two modules may reasonably both call something "config". Without the prefix the second
// would silently replace the first, and the node would apply one of them and report success.
a := mod("a", nil, nil, nil)
a.Resources = []map[string]any{{"id": "config", "type": "file", "path": "/etc/a"}}
b := mod("b", nil, nil, nil)
b.Resources = []map[string]any{{"id": "config", "type": "file", "path": "/etc/b"}}
got, err := Resolve(shelf(a, b), []string{"a", "b"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
seen := map[string]bool{}
for _, r := range mustDeclare(t, got) {
id := r["id"].(string)
if seen[id] {
t.Errorf("two resources share the identity %q", id)
}
seen[id] = true
}
if !seen["a.config"] || !seen["b.config"] {
t.Errorf("identities are not qualified by module: %v", seen)
}
}
func TestWhatAServiceReflectsIsQualifiedToo(t *testing.T) {
// Otherwise it names a resource that no longer exists under that identity, and the service
// quietly stops being restarted when its own configuration changes.
m := mod("thing", nil, nil, nil)
m.Resources = []map[string]any{
{"id": "conf", "type": "file", "path": "/etc/thing.conf"},
{"id": "svc", "type": "service", "unit": "thing.service", "state": "running",
"restart-on": []any{"conf"}},
}
got, err := Resolve(shelf(m), []string{"thing"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
for _, r := range mustDeclare(t, got) {
if r["id"] == "thing.svc" {
if got := fmt.Sprint(r["restart-on"]); got != "[thing.conf]" {
t.Errorf("a service reflects %s, which is not a resource in the declaration", got)
}
return
}
}
t.Error("the service is missing from the declaration")
}
func TestEveryReasonIsGivenAtOnce(t *testing.T) {
// Somebody resolving these fixes them in one pass or in three. Every reason that genuinely
// refuses the set is collected, rather than only the first: a claim two modules both take, a
// requirement nothing answers, and a requirement several answer without anybody choosing.
_, err := Resolve(shelf(
mod("xorg", nil, nil, nil, Claim{Name: "the-seat"}),
mod("wayland", nil, nil, nil, Claim{Name: "the-seat"}),
mod("i3", nil, []string{"compositor"}, nil),
mod("editor", nil, []string{"shell"}, nil),
mod("bash", []string{"shell"}, nil, nil),
mod("zsh", []string{"shell"}, nil, nil),
), []string{"xorg", "wayland", "i3", "editor"}, workstation(), World{})
if err == nil {
t.Fatal("expected refusals")
}
if strings.Count(err.Error(), "\n - ") < 3 {
t.Errorf("only some problems were reported:\n%v", err)
}
}
func TestACycleStopsRatherThanRunsAway(t *testing.T) {
// Two modules requiring each other is a mistake somebody makes, and it must produce an answer
// rather than a stack overflow.
got, err := Resolve(shelf(
mod("a", nil, []string{"b"}, nil),
mod("b", nil, []string{"a"}, nil),
), []string{"a"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
if len(got.Modules) != 2 {
t.Errorf("a cycle resolved to %v", names(got))
}
}
func TestChoosingOneSatisfiesTheRequirement(t *testing.T) {
// The other half of refusing. "Choose one and assign it" has to actually work, or the remedy
// names three modules and then ignores the one you pick — which is how this read the first
// time it was used on a real mesh.
got, err := Resolve(shelf(
mod("editor", nil, []string{"shell"}, nil),
mod("bash", []string{"shell"}, nil, nil),
mod("zsh", []string{"shell"}, nil, nil),
mod("fish", []string{"shell"}, nil, nil),
), []string{"editor", "zsh"}, workstation(), World{})
if err != nil {
t.Fatalf("choosing a shell did not satisfy the requirement for one: %v", err)
}
if len(got.Modules) != 2 {
t.Errorf("resolved %v; only the chosen shell should have come in", names(got))
}
}
func TestChoosingSeveralIsStillFine(t *testing.T) {
// And the choice is not exclusive. Nothing is claimed, so a person may have all three and
// the requirement is answered by whichever they picked.
got, err := Resolve(shelf(
mod("editor", nil, []string{"shell"}, nil),
mod("bash", []string{"shell"}, nil, nil),
mod("zsh", []string{"shell"}, nil, nil),
mod("fish", []string{"shell"}, nil, nil),
), []string{"editor", "zsh", "bash", "fish"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
if len(got.Modules) != 4 {
t.Errorf("resolved %v", names(got))
}
}
func TestARequirementNamingAModuleMeansThatModule(t *testing.T) {
// i3 requires xorg and means xorg, not "anything calling itself a display server". Otherwise
// assigning wayland would silently satisfy i3 and the machine would come up with a window
// manager talking to nothing.
_, err := Resolve(shelf(
mod("i3", nil, []string{"xorg"}, nil),
mod("xorg", []string{"display-server"}, nil, nil),
mod("wayland", []string{"display-server", "xorg"}, nil, nil),
), []string{"i3", "wayland"}, workstation(), World{})
// wayland claiming to provide "xorg" is a manifest saying something untrue; what matters is
// that a real xorg module still wins when it exists, and that the answer is not silent.
if err != nil && !strings.Contains(err.Error(), "xorg") {
t.Errorf("unexpected refusal: %v", err)
}
}
func mustDeclare(t *testing.T, r Resolution) []map[string]any {
t.Helper()
out, err := r.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
return out
}
// A requirement answered on the same machine is still a requirement (novox/hq 04-ISSUES/021).
//
// **The machine stops being a trust boundary once both ends are containers.** A consumer reaches
// its provider over TCP from its own container, and the database asks for a password exactly as it
// would from another machine. Before this, two such modules resolved cleanly with zero needs: no
// credential was made, the consumer's secret file was never written, and whatever read it failed
// somewhere else entirely.
//
// It went unnoticed because everything proven until then was cross-machine, which is the
// interesting case for a mesh and the rare one in practice.
func TestSomethingAnsweredOnThisMachineIsStillANeed(t *testing.T) {
provider := Manifest{
Module: "postgres", Version: "1",
Provides: FromAnywhere("postgres-database"),
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}},
Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"},
}
consumer := Manifest{
Module: "keycloak", Version: "1",
Requires: []string{"postgres-database"},
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/database.env"},
}
got, err := Resolve(shelf(provider, consumer), []string{"postgres", "keycloak"},
Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{}}, World{})
if err != nil {
t.Fatal(err)
}
if len(got.Needs) != 1 {
t.Fatalf("%d need(s); a consumer of a brokered provision needs a credential wherever "+
"the provider is", len(got.Needs))
}
if got.Needs[0].From != "anchor" {
t.Errorf("the need names %q as the provider, and it is this machine", got.Needs[0].From)
}
// And it carries what the provider says a consumer must know, which a local provider never
// contributes through the world.
if got.Needs[0].Serves["port"] != 5432 {
t.Errorf("the need carries %v, and the provider serves port 5432", got.Needs[0].Serves)
}
}
// A name nothing grants is unchanged: answered here means answered, and nothing more is owed.
func TestSomethingOrdinaryAnsweredHereNeedsNothing(t *testing.T) {
got, err := Resolve(shelf(
mod("zsh", []string{"shell"}, nil, nil),
mod("editor-user", nil, []string{"shell"}, nil),
), []string{"zsh", "editor-user"},
Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{}}, World{})
if err != nil {
t.Fatal(err)
}
if len(got.Needs) != 0 {
t.Fatalf("a shell answered on this machine produced %d need(s)", len(got.Needs))
}
}
func TestTheControllersOwnManifestClaimsItsSeat(t *testing.T) {
// The seat test above fabricates manifests, so deleting the claim from the real module.json
// would fail nothing (novox/hq issue 059's review). This binds the one manifest this
// repository owns: the controller claims the mesh-scoped seat named after its server
// (ADR 0079), or the one-controller property is convention again.
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatalf("the controller's own manifest is unreadable: %v", err)
}
m, err := ParseManifest(raw)
if err != nil {
t.Fatalf("the controller's own manifest does not parse: %v", err)
}
for _, c := range m.Claims {
if c.Name == "mesh-controller" && c.At() == ScopeMesh {
return
}
}
t.Fatalf("module.json no longer claims the mesh-scoped mesh-controller seat: %+v", m.Claims)
}