Take the catalogue's composition rules into the node-engine
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
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
func mod(name string, provides, requires, capabilities []string, claims ...mcat.Claim) mcat.Manifest {
|
||||
return mcat.Manifest{Module: name, Provides: mcat.Offers(provides...), Requires: requires,
|
||||
Capabilities: capabilities, Claims: claims}
|
||||
}
|
||||
|
||||
func shelf(ms ...mcat.Manifest) map[string]mcat.Manifest {
|
||||
out := map[string]mcat.Manifest{}
|
||||
for _, m := range ms {
|
||||
out[m.Module] = m
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func workstation() mcat.Node {
|
||||
return mcat.Node{Name: "workstation", Site: "house",
|
||||
Capabilities: map[string]bool{"seat": true, "container-runtime": true}}
|
||||
}
|
||||
|
||||
func names(r mcat.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"}, mcat.Claim{Name: "the-seat"}),
|
||||
), []string{"i3"}, workstation(), mcat.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(), mcat.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(), mcat.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(), mcat.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, mcat.Claim{Name: "the-seat"}),
|
||||
mod("wayland", []string{"display-server"}, nil, nil, mcat.Claim{Name: "the-seat"}),
|
||||
), []string{"xorg", "wayland"}, workstation(), mcat.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, mcat.Claim{Name: "the-seat"}),
|
||||
mod("wayland", []string{"display-server"}, nil, nil, mcat.Claim{Name: "the-seat"}),
|
||||
mod("mir", []string{"display-server"}, nil, nil, mcat.Claim{Name: "the-seat"}),
|
||||
)
|
||||
for _, pair := range [][]string{{"xorg", "mir"}, {"wayland", "mir"}} {
|
||||
if _, err := Resolve(catalogue, pair, workstation(), mcat.World{}); err == nil {
|
||||
t.Errorf("%v were both assigned", pair)
|
||||
}
|
||||
}
|
||||
if _, err := Resolve(catalogue, []string{"mir"}, workstation(), mcat.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 := mcat.Node{Name: "server", Capabilities: map[string]bool{"container-runtime": true}}
|
||||
got, err := Resolve(shelf(mod("xorg", nil, nil, []string{"seat"}, mcat.Claim{Name: "the-seat"})), []string{"xorg"}, server, mcat.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 := mcat.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, mcat.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 := mcat.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, mcat.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, mcat.Claim{Name: "the-hub", Scope: mcat.ScopeMesh})),
|
||||
[]string{"hub"}, workstation(), mcat.World{Held: []mcat.Held{{Claim: "the-hub", Scope: mcat.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, mcat.Claim{Name: tc.seat, Scope: mcat.ScopeMesh})),
|
||||
[]string{tc.module}, workstation(), mcat.World{Held: []mcat.Held{{Claim: tc.seat, Scope: mcat.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, mcat.Claim{Name: "dhcp", Scope: mcat.ScopeSite}))
|
||||
|
||||
elsewhere := []mcat.Held{{Claim: "dhcp", Scope: mcat.ScopeSite, Node: "other", Module: "dhcp", Site: "house"}}
|
||||
if _, err := Resolve(catalogue, []string{"dhcp"}, workstation(), mcat.World{Held: elsewhere}); err == nil {
|
||||
t.Error("two DHCP servers at one site were allowed")
|
||||
}
|
||||
|
||||
faraway := []mcat.Held{{Claim: "dhcp", Scope: mcat.ScopeSite, Node: "other", Module: "dhcp", Site: "office"}}
|
||||
if _, err := Resolve(catalogue, []string{"dhcp"}, workstation(), mcat.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(), mcat.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(), mcat.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(), mcat.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, mcat.Claim{Name: "the-seat"}),
|
||||
mod("wayland", nil, nil, nil, mcat.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(), mcat.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(), mcat.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(), mcat.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(), mcat.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(), mcat.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 mcat.Resolution) []map[string]any {
|
||||
t.Helper()
|
||||
out, err := Declaration(r, mcat.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 := mcat.Manifest{
|
||||
Module: "postgres", Version: "1",
|
||||
Provides: mcat.FromAnywhere("postgres-database"),
|
||||
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}},
|
||||
Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"},
|
||||
}
|
||||
consumer := mcat.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"}, mcat.Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{}}, mcat.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"}, mcat.Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{}}, mcat.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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user