Provisions were named after roles: provides "database", requires "database". Nothing distinguished engines, so a module written against PostgreSQL could be matched to a provider of SQL Server, resolve as satisfied, deploy, and fail on its first query — with nothing connecting that error back to a match made elsewhere by something that believed it had done its job. The failure is in the direction that hides. Refusing on ambiguity exists precisely so this does not happen, and the generic name walked around it: with one provider of each name nothing is ambiguous, so nothing is asked. How it got in: every resolver test had exactly one provider per name, so no mismatch was expressible and none was caught. The fixtures agreed with the design — the same fault as the imagined test output in 04-ISSUES/005, at the level of a name. Refused rather than documented, because the old naming *was* the documented convention. Providing database/db/sql/sql-database is now a parse error naming what to write instead. The rule is about coupling, not specificity everywhere: route and resolver stay role-named, because a consumer genuinely cannot tell which proxy answered. novox/hq ADR 0027.
329 lines
12 KiB
Go
329 lines
12 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Where the answer to a requirement is allowed to live.
|
|
//
|
|
// A shell, a display server and a private network have to be on the machine that needs them. A
|
|
// database does not — it runs somewhere and is reached over the network. Both were written
|
|
// `requires`, so both were answered the same way, and the second answer was to install PostgreSQL
|
|
// on every machine that runs a web application.
|
|
|
|
// 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() Node {
|
|
n := workstation()
|
|
n.At = "workstation.internal"
|
|
return n
|
|
}
|
|
|
|
func onNetwork(nodes ...string) map[string][]Provider {
|
|
out := make([]Provider, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
out = append(out, Provider{Node: n, At: n + ".internal"})
|
|
}
|
|
return map[string][]Provider{"postgres-database": out}
|
|
}
|
|
|
|
func brokeredShelf() map[string]Manifest {
|
|
return shelf(
|
|
Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("postgres-database")},
|
|
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(),
|
|
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(),
|
|
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(), 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(),
|
|
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(),
|
|
World{
|
|
Offered: onNetwork("anchor", "archive"),
|
|
Pinned: map[string]string{"postgres-database": "archive"},
|
|
})
|
|
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(),
|
|
World{
|
|
Offered: onNetwork("anchor", "archive"),
|
|
Pinned: map[string]string{"postgres-database": "somewhere-else"},
|
|
})
|
|
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(),
|
|
World{
|
|
Offered: onNetwork("anchor"),
|
|
Pinned: map[string]string{"postgres-database": "archive"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("the only database was used although another was chosen")
|
|
}
|
|
if !strings.Contains(err.Error(), "only anchor 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(
|
|
Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("postgres-database")},
|
|
Manifest{Module: "sqlite", Version: "1", Provides: Offers("postgres-database")},
|
|
Manifest{Module: "meshboard", Version: "1", Requires: []string{"postgres-database"}},
|
|
), []string{"meshboard"}, workstation(), 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(
|
|
Manifest{Module: "zsh", Version: "1", Provides: Offers("shell")},
|
|
Manifest{Module: "tools", Version: "1", Requires: []string{"shell"}},
|
|
), []string{"tools"}, workstation(), 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 TestTheShortFormStillMeansHere(t *testing.T) {
|
|
// `"provides": ["shell"]` must keep meaning what it meant, or every existing manifest
|
|
// silently changes meaning.
|
|
m, err := ParseManifest([]byte(`{"module":"zsh","version":"1","provides":["shell"]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(m.Provides) != 1 || m.Provides[0].At() != ScopeNode {
|
|
t.Fatalf("a plain name is no longer node-scoped: %v", m.Provides)
|
|
}
|
|
}
|
|
|
|
func TestASiteScopedProvisionIsRefused(t *testing.T) {
|
|
// Meaningful for a claim — one DHCP server per segment — and not yet meaningful for a
|
|
// provision, because nothing knows how to reach "the one at my site".
|
|
_, err := ParseManifest([]byte(
|
|
`{"module":"dns","version":"1","provides":[{"name":"resolver","scope":"site"}]}`))
|
|
if err == nil {
|
|
t.Fatal("a site-scoped provision was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "site") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
// Being told, which is the difference between knowing and being able to.
|
|
|
|
func boundShelf() map[string]Manifest {
|
|
return shelf(
|
|
Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("postgres-database"),
|
|
Serves: map[string]map[string]any{"postgres-database": {"port": 5432, "driver": "postgres"}}},
|
|
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(),
|
|
World{Offered: map[string][]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 TestItSaysItCarriesNoCredential(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.
|
|
got, _ := Resolve(boundShelf(), []string{"meshboard"}, reachable(),
|
|
World{Offered: map[string][]Provider{"postgres-database": {{Node: "anchor", At: "anchor.internal"}}}})
|
|
told := binding(t, mustDeclare(t, got))
|
|
note, _ := told["generated"].(string)
|
|
if !strings.Contains(note, "no credential") {
|
|
t.Fatalf("the file does not say what it does not carry: %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(), // not on the network
|
|
World{Offered: map[string][]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(),
|
|
World{Offered: map[string][]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(
|
|
Manifest{Module: "zsh", Version: "1", Provides: Offers("shell")},
|
|
Manifest{Module: "tools", Version: "1", Requires: []string{"shell"},
|
|
Binds: map[string]string{"shell": "/etc/tools/shell.json"}},
|
|
), []string{"tools"}, reachable(), 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBindingWhatYouDoNotRequireIsRefused(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module":"app","version":"1",
|
|
"binds":{"postgres-database":"/etc/app/db.json"}}`))
|
|
if err == nil {
|
|
t.Fatal("a module was told about something it never asked for")
|
|
}
|
|
if !strings.Contains(err.Error(), "does not require") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestServingWhatYouDoNotProvideIsRefused(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module":"app","version":"1",
|
|
"serves":{"postgres-database":{"port":5432}}}`))
|
|
if err == nil {
|
|
t.Fatal("a module served something it does not provide")
|
|
}
|
|
if !strings.Contains(err.Error(), "does not provide") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|