Knowing that a machine needs the anchor's database is useless to the
program that needs it unless the program is told. It knew; nothing was
written anywhere it could read.
Two fields, mirroring contributes/receives in the other direction:
serves: {database: {port: 5432, driver: postgres}} on the provider
binds: {database: /etc/app/database.json} on the consumer
The provider says what a consumer needs to know; the mesh adds the half
only it has — which machine, and what that machine is called on the
private network. The file says, in itself, that it carries no credential
and why. A missing field looks like a bug; a stated absence looks like a
boundary.
Binding something answered on this machine writes nothing. A file saying
"it is on this node" is a fact nobody needs and one more thing to keep
true.
And two machines that share no private network are refused rather than
wired together. An app here and a database there with no path between
them is a mesh that reports itself configured and does not work — the
failure surfaces as a connection timing out, which is the slowest place
to find it. This is checkable now only because the network became
something a machine is given rather than something it has by having an
address.
One fault, found by running it: working out who is on the private network
resolved the mesh, and resolving the mesh asks who is on the private
network. It hung for two minutes. The comment above the function said not
to do that and the function did it anyway; it now resolves each node
locally, which is the right answer to the question regardless — whether a
machine is on the network depends on what it was assigned, not on what it
takes from others.
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{"database": out}
|
|
}
|
|
|
|
func brokeredShelf() map[string]Manifest {
|
|
return shelf(
|
|
Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("database")},
|
|
Manifest{Module: "meshboard", Version: "1", Requires: []string{"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 != "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{"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{"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{"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("database")},
|
|
Manifest{Module: "sqlite", Version: "1", Provides: Offers("database")},
|
|
Manifest{Module: "meshboard", Version: "1", Requires: []string{"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("database"),
|
|
Serves: map[string]map[string]any{"database": {"port": 5432, "driver": "postgres"}}},
|
|
Manifest{Module: "meshboard", Version: "1", Requires: []string{"database"},
|
|
Binds: map[string]string{"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{"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{"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{"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{"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":{"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":{"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)
|
|
}
|
|
}
|