A provider is a (node, module) pair (design 23), and the pin — the one way a consumer names its provider — named only the node. Two modules on one node can both answer a provision (public-acme and step-ca both offer acme-ca on novox), and then the resolver, given a pin naming that node, took the last provider listed: a coin flip. The same ambiguity beside the consumer was settled by a map walk — random per plan — which is how novox's own route-proxy got its issuer (novox/hq #258). - `pin <node> <provision> <from-node> <module>`: both halves, always. The console gains `pin` and `unpin`. The provider may be on the consumer's own node, since two modules beside it can both answer. - The resolver refuses ambiguity instead of picking, across machines and beside the consumer alike, naming every candidate as node/module and the form of the pin that settles it. A plain capability that grants nothing and serves nothing (three shells beside an editor) is not a choice to put to anybody and stays as it was. - provision_pin gains a nullable module (0050); records made before are completed where the node they name answers once, and left for a person where it answers twice (0051). - The provider of something already satisfied is looked for among what was assigned, not only what the walk has reached — a consumer reached before the provider beside it no longer loses its binding. - The start-time check that every declared verb is runnable samples each verb's required arguments from its schema instead of three guessed keys. Live consequence: a node that has two providers of one bound provision assigned (novox: acme-ca) resolves only once pinned — `pin novox acme-ca novox public-acme`.
333 lines
13 KiB
Go
333 lines
13 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", Module: "postgres"})
|
|
}
|
|
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]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(),
|
|
World{
|
|
Offered: onNetwork("anchor", "archive"),
|
|
Pinned: map[string]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(),
|
|
World{
|
|
Offered: onNetwork("anchor"),
|
|
Pinned: map[string]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(
|
|
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 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(),
|
|
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, "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(), // 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)
|
|
}
|
|
}
|