Files
mesh-controller/internal/catalogue/resolve_test.go
T
jschoubben df62bb57e5 A requirement answered on this machine is still a requirement
novox/hq 04-ISSUES/021. Two modules where one provided what the other
required, on one node, resolved cleanly with zero needs: no credential
was made, the consumer's secret file was never written, and whatever
read it would fail somewhere else entirely. Nothing was refused and
nothing was reported.

The world a node resolves against is every OTHER node, so a provider on
the same machine never became a Needed, and the credential loop walks
Needs. Every step reasonable, the sum a silent gap.

It survived because everything proven until now was cross-machine —
the interesting case for a mesh and the rare one in practice. The first
module to want a database on its own machine was the first real one.

The assumption underneath was that a local consumer needs no credential,
which holds for a process reaching a unix socket where the system can
vouch for the caller. It does not hold for containers, which is how
nearly everything here runs: the consumer reaches the provider over TCP
from its own container and the database asks for a password exactly as
it would from another machine. **The machine stops being a trust
boundary once both ends are containers.**

A brokered provision answered here is now a need naming this node, and
carries what the provider serves — which a local provider never
contributes through the world. A name nothing grants is unchanged: a
shell answered here is answered, and nothing more is owed. Both
directions tested, both injections bite.
2026-09-01 02:15:26 +02:00

394 lines
15 KiB
Go

package catalogue
import (
"fmt"
"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 TestAMissingCapabilityIsSaidToBeTheWrongMachine(t *testing.T) {
// The remedy differs from a missing module and the message has to say which. Nothing can be
// installed to give a server a seat.
server := Node{Name: "server", Capabilities: map[string]bool{"container-runtime": true}}
_, err := Resolve(shelf(mod("xorg", nil, nil, []string{"seat"}, Claim{Name: "the-seat"})), []string{"xorg"}, server, World{})
if err == nil {
t.Fatal("a display server was assigned to a machine with no seat")
}
if !strings.Contains(err.Error(), "wrong machine") {
t.Errorf("the refusal reads like a missing module: %v", err)
}
}
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 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 four.
server := Node{Name: "server", Capabilities: map[string]bool{}}
_, err := Resolve(shelf(
mod("xorg", nil, nil, []string{"seat"}, Claim{Name: "the-seat"}),
mod("wayland", nil, nil, []string{"seat"}, Claim{Name: "the-seat"}),
), []string{"xorg", "wayland"}, server, 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))
}
}