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
256 lines
11 KiB
Go
256 lines
11 KiB
Go
package resolution
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// onNetwork is a set of providers, all of them reachable. Written as a helper because a machine
|
|
// that cannot reach the one answering its requirement is its own case, tested separately.
|
|
// reachable is this machine, on the private network.
|
|
func reachable() mcat.Node {
|
|
n := workstation()
|
|
n.At = "workstation.internal"
|
|
return n
|
|
}
|
|
|
|
func onNetwork(nodes ...string) map[string][]mcat.Provider {
|
|
out := make([]mcat.Provider, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
out = append(out, mcat.Provider{Node: n, At: n + ".internal", Module: "postgres"})
|
|
}
|
|
return map[string][]mcat.Provider{"postgres-database": out}
|
|
}
|
|
|
|
func brokeredShelf() map[string]mcat.Manifest {
|
|
return shelf(mcat.Manifest{Module: "postgres", Version: "1", Provides: mcat.FromAnywhere("postgres-database")}, mcat.Manifest{Module: "meshboard", Version: "1", Requires: []string{"postgres-database"}})
|
|
}
|
|
|
|
func TestADatabaseIsNotInstalledOnEveryMachineThatUsesOne(t *testing.T) {
|
|
// The fault this whole distinction exists for.
|
|
got, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), mcat.World{Offered: onNetwork("anchor")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if have := strings.Join(names(got), " "); strings.Contains(have, "postgres") {
|
|
t.Fatalf("using a database installed one here: %s", have)
|
|
}
|
|
}
|
|
|
|
func TestWhatAMachineTakesFromElsewhereIsRecorded(t *testing.T) {
|
|
// It is the only part of a node's set that stops working when a *different* machine goes
|
|
// away, and it is where a credential will have to be handed back.
|
|
got, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), mcat.World{Offered: onNetwork("anchor")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Needs) != 1 {
|
|
t.Fatalf("got %v", got.Needs)
|
|
}
|
|
if got.Needs[0].Name != "postgres-database" || got.Needs[0].From != "anchor" {
|
|
t.Fatalf("got %v", got.Needs[0])
|
|
}
|
|
if got.Needs[0].For != "meshboard" {
|
|
t.Fatalf("it does not say what wanted it: %v", got.Needs[0])
|
|
}
|
|
}
|
|
|
|
func TestNothingInTheMeshProvidingItIsRefusedWithSomewhereToPutIt(t *testing.T) {
|
|
// Refused rather than installed here. Choosing a machine to put a database on is a decision
|
|
// with consequences, and nothing resolving a web application should make it silently.
|
|
_, err := Resolve(brokeredShelf(), []string{"meshboard"}, workstation(), mcat.World{})
|
|
if err == nil {
|
|
t.Fatal("a database was found in a mesh that has none")
|
|
}
|
|
if !strings.Contains(err.Error(), "assign") || !strings.Contains(err.Error(), "postgres") {
|
|
t.Fatalf("the refusal does not say what to do: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTwoNodesProvidingItIsRefusedRatherThanPicked(t *testing.T) {
|
|
// Same rule as everywhere else. Picking one would be a guess about which database a person
|
|
// meant, and the wrong guess is somebody's data in the wrong place.
|
|
_, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), mcat.World{Offered: onNetwork("anchor", "archive")})
|
|
if err == nil {
|
|
t.Fatal("one of two databases was picked silently")
|
|
}
|
|
for _, want := range []string{"anchor", "archive", "pin"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("the refusal does not name %s: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSayingWhichOneSettlesIt(t *testing.T) {
|
|
got, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), mcat.World{
|
|
Offered: onNetwork("anchor", "archive"),
|
|
Pinned: map[string]mcat.Chosen{"postgres-database": {Node: "archive", Module: "postgres"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Needs) != 1 || got.Needs[0].From != "archive" {
|
|
t.Fatalf("the choice was not taken: %v", got.Needs)
|
|
}
|
|
}
|
|
|
|
func TestBeingPointedAtAMachineThatDoesNotProvideItIsRefused(t *testing.T) {
|
|
// Rather than falling back to one that does. A fallback would quietly move somebody's data to
|
|
// a machine they did not choose, which is the whole reason the question is asked.
|
|
_, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), mcat.World{
|
|
Offered: onNetwork("anchor", "archive"),
|
|
Pinned: map[string]mcat.Chosen{"postgres-database": {Node: "somewhere-else", Module: "postgres"}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("a machine was silently given a different database from the one chosen")
|
|
}
|
|
if !strings.Contains(err.Error(), "somewhere-else") {
|
|
t.Fatalf("the refusal does not say what was chosen: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOneProviderDoesNotOverruleAChoice(t *testing.T) {
|
|
// A single answer is normally taken silently. Not when somebody said they wanted a different
|
|
// one -- that is the mesh overruling a person, which it does nowhere else.
|
|
_, err := Resolve(brokeredShelf(), []string{"meshboard"}, reachable(), mcat.World{
|
|
Offered: onNetwork("anchor"),
|
|
Pinned: map[string]mcat.Chosen{"postgres-database": {Node: "archive", Module: "postgres"}},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("the only database was used although another was chosen")
|
|
}
|
|
if !strings.Contains(err.Error(), "only anchor/postgres provides it") {
|
|
t.Fatalf("the refusal does not say what is available: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestACatalogueThatDisagreesAboutScopeIsRefused(t *testing.T) {
|
|
// If one module says a database is local and another says it is anywhere, the same
|
|
// requirement means two things depending on which one happens to answer it.
|
|
_, err := Resolve(shelf(mcat.Manifest{Module: "postgres", Version: "1", Provides: mcat.FromAnywhere("postgres-database")}, mcat.Manifest{Module: "sqlite", Version: "1", Provides: mcat.Offers("postgres-database")}, mcat.Manifest{Module: "meshboard", Version: "1", Requires: []string{"postgres-database"}}), []string{"meshboard"}, workstation(), mcat.World{})
|
|
if err == nil {
|
|
t.Fatal("a catalogue that disagrees about where a database lives was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "two things") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestALocalRequirementIsStillAnsweredLocally(t *testing.T) {
|
|
// The change must not have made everything brokered. A shell is still installed here.
|
|
got, err := Resolve(shelf(mcat.Manifest{Module: "zsh", Version: "1", Provides: mcat.Offers("shell")}, mcat.Manifest{Module: "tools", Version: "1", Requires: []string{"shell"}}), []string{"tools"}, workstation(), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if have := strings.Join(names(got), " "); !strings.Contains(have, "zsh") {
|
|
t.Fatalf("a shell was not installed on the machine that needs one: %s", have)
|
|
}
|
|
}
|
|
|
|
func boundShelf() map[string]mcat.Manifest {
|
|
return shelf(mcat.Manifest{Module: "postgres", Version: "1", Provides: mcat.FromAnywhere("postgres-database"),
|
|
Serves: map[string]map[string]any{"postgres-database": {"port": 5432, "driver": "postgres"}}}, mcat.Manifest{Module: "meshboard", Version: "1", Requires: []string{"postgres-database"},
|
|
Binds: map[string]string{"postgres-database": "/etc/meshboard/database.json"}},
|
|
)
|
|
}
|
|
|
|
func binding(t *testing.T, out []map[string]any) map[string]any {
|
|
t.Helper()
|
|
for _, r := range out {
|
|
if r["path"] != "/etc/meshboard/database.json" {
|
|
continue
|
|
}
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil {
|
|
t.Fatalf("what the app is told is not readable: %v", err)
|
|
}
|
|
return parsed
|
|
}
|
|
t.Fatalf("the app was told nothing: %v", out)
|
|
return nil
|
|
}
|
|
|
|
func TestAnAppIsToldWhereItsDatabaseIs(t *testing.T) {
|
|
// Knowing it needs the anchor's database is useless to the program that needs it unless the
|
|
// program is told. This is the whole point of the field.
|
|
got, err := Resolve(boundShelf(), []string{"meshboard"}, reachable(), mcat.World{Offered: map[string][]mcat.Provider{"postgres-database": {{Node: "anchor", At: "anchor.internal",
|
|
Serves: map[string]any{"port": 5432, "driver": "postgres"}}}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
told := binding(t, mustDeclare(t, got))
|
|
if told["from"] != "anchor" || told["at"] != "anchor.internal" {
|
|
t.Fatalf("it was not told where: %v", told)
|
|
}
|
|
serves, _ := told["serves"].(map[string]any)
|
|
if serves["port"] != float64(5432) || serves["driver"] != "postgres" {
|
|
t.Fatalf("it was not told how: %v", serves)
|
|
}
|
|
}
|
|
|
|
func TestItSaysWhereTheCredentialIsInstead(t *testing.T) {
|
|
// A missing field looks like a bug; a stated absence looks like a boundary. Somebody wiring
|
|
// this up must not spend an afternoon looking for the password field.
|
|
//
|
|
// It used to say only that the mesh had no way to issue one, which stopped being true when
|
|
// 021 was fixed. A stated absence is only useful while it is accurate — once it is not, it
|
|
// sends the reader somewhere there is nothing to find.
|
|
got, _ := Resolve(boundShelf(), []string{"meshboard"}, reachable(), mcat.World{Offered: map[string][]mcat.Provider{"postgres-database": {{Node: "anchor", At: "anchor.internal"}}}})
|
|
told := binding(t, mustDeclare(t, got))
|
|
note, _ := told["generated"].(string)
|
|
if !strings.Contains(note, "not here") || !strings.Contains(note, "secrets") {
|
|
t.Fatalf("the file does not say where the credential is instead: %v", note)
|
|
}
|
|
for key := range told {
|
|
if strings.Contains(key, "password") || strings.Contains(key, "secret") {
|
|
t.Fatalf("something that looks like a credential appeared: %q", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTwoMachinesThatCannotReachEachOtherAreRefused(t *testing.T) {
|
|
// An app on one machine and a database on another that share no private network is a mesh
|
|
// that reports itself configured and does not work. Said here rather than discovered as a
|
|
// connection timing out.
|
|
_, err := Resolve(boundShelf(), []string{"meshboard"}, workstation(), mcat. // not on the network
|
|
World{Offered: map[string][]mcat.Provider{"postgres-database": {{Node: "anchor", At: "anchor.internal"}}}})
|
|
if err == nil {
|
|
t.Fatal("an app was pointed at a database it has no path to")
|
|
}
|
|
if !strings.Contains(err.Error(), "private network") {
|
|
t.Fatalf("the refusal does not say what is wrong: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), meshNetwork) {
|
|
t.Fatalf("the refusal does not say what to assign: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheProviderBeingOffTheNetworkIsAlsoRefused(t *testing.T) {
|
|
// Both directions, because the failure is identical from either end and the remedy differs.
|
|
_, err := Resolve(boundShelf(), []string{"meshboard"}, reachable(), mcat.World{Offered: map[string][]mcat.Provider{"postgres-database": {{Node: "anchor"}}}})
|
|
if err == nil {
|
|
t.Fatal("an app was pointed at a database that is not on the private network")
|
|
}
|
|
if !strings.Contains(err.Error(), "anchor") {
|
|
t.Fatalf("the refusal does not name the unreachable end: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBindingSomethingAnsweredHereWritesNothing(t *testing.T) {
|
|
// A file saying "it is on this node" is a fact nobody needs and one more thing to keep true.
|
|
got, err := Resolve(shelf(mcat.Manifest{Module: "zsh", Version: "1", Provides: mcat.Offers("shell")}, mcat.Manifest{Module: "tools", Version: "1", Requires: []string{"shell"},
|
|
Binds: map[string]string{"shell": "/etc/tools/shell.json"}},
|
|
), []string{"tools"}, reachable(), mcat.World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range mustDeclare(t, got) {
|
|
if r["path"] == "/etc/tools/shell.json" {
|
|
t.Fatalf("a binding was written for something on this machine: %v", r)
|
|
}
|
|
}
|
|
}
|