Services are named under the machine they run on — postgres.novox.internal, plex.ace.internal. The first label is the service and the rest is the node, so what has to resolve is anything under a node's name. What routes it once it arrives is a proxy's concern and stays separate. A hosts file cannot do that. It answers exact names, and a wildcard there would mean writing down every service in advance — which is the enumeration the arrangement exists to avoid. novox/hq 08-connectivity named this exact case as the trigger for needing a resolver rather than a file, and it is the first thing to meet it. The mesh writes the data and runs no daemon. A resolver is third-party software, and third-party software runs on the mesh rather than being of it (ADR 0001): the mesh has no business shipping one, choosing which one, or knowing its configuration language. What only the mesh can know is which machines exist and where they are. A module that runs a resolver requires what this provides and reads one file, so swapping the daemon changes that module and nothing here. Separate from names rather than part of them: a machine with no container runtime can still have a hosts file, and folding them together would take exact names away from a machine that cannot run a daemon in order to give it a wildcard it cannot use either. A machine with no address is left out. A wildcard pointing at nothing is worse than no wildcard — every name under it resolves and then hangs, where an unresolvable name fails at once and says which name it was.
100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
package overlay
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Services are named under the machine they run on, so what must resolve is anything under a
|
|
// node's name — not the node's name alone.
|
|
//
|
|
// This is what a hosts file cannot do: it answers exact names, and a wildcard there would mean
|
|
// writing down every service in advance, which is the enumeration the arrangement exists to
|
|
// avoid.
|
|
func TestEverythingUnderANodesNameGoesToThatNode(t *testing.T) {
|
|
written := Wildcards([]Node{
|
|
{Name: "novox", Address: "10.42.0.1"},
|
|
{Name: "ace", Address: "10.42.0.2"},
|
|
})
|
|
for _, want := range []string{
|
|
"address=/novox.internal/10.42.0.1",
|
|
"address=/ace.internal/10.42.0.2",
|
|
} {
|
|
if !strings.Contains(written, want) {
|
|
t.Fatalf("missing %q:\n%s", want, written)
|
|
}
|
|
}
|
|
|
|
// Sorted, because this file is compared against its last version on every apply and a set
|
|
// that reorders itself would rewrite it — and restart what reads it — for no change.
|
|
if strings.Index(written, "/ace.") > strings.Index(written, "/novox.") {
|
|
t.Fatalf("the machines are not in a stable order:\n%s", written)
|
|
}
|
|
}
|
|
|
|
// A machine with no address is left out.
|
|
//
|
|
// A wildcard pointing at nothing is worse than no wildcard: every name under it resolves and then
|
|
// hangs, where an unresolvable name fails at once and says which name it was.
|
|
func TestAMachineWithNoAddressGetsNoWildcard(t *testing.T) {
|
|
written := Wildcards([]Node{
|
|
{Name: "novox", Address: "10.42.0.1"},
|
|
{Name: "unplaced"},
|
|
})
|
|
if strings.Contains(written, "unplaced") {
|
|
t.Fatalf("a machine with no address was given a wildcard:\n%s", written)
|
|
}
|
|
if !strings.Contains(written, "novox.internal") {
|
|
t.Fatalf("the machine that does have one lost it:\n%s", written)
|
|
}
|
|
}
|
|
|
|
// A mesh where nobody is on the private network says so rather than producing an empty file that
|
|
// reads as "nothing was generated".
|
|
func TestAMeshWithNoAddressesSaysSo(t *testing.T) {
|
|
written := Wildcards(nil)
|
|
if !strings.Contains(written, "No machine in this mesh has an address") {
|
|
t.Fatalf("an empty answer is indistinguishable from a failure to answer:\n%s", written)
|
|
}
|
|
}
|
|
|
|
// The suffix a mesh chose is used, not a hardcoded one.
|
|
func TestTheMeshsOwnSuffixIsUsed(t *testing.T) {
|
|
t.Setenv(SuffixVar, "mesh.example")
|
|
written := Wildcards([]Node{{Name: "novox", Address: "10.42.0.1"}})
|
|
if !strings.Contains(written, "address=/novox.mesh.example/10.42.0.1") {
|
|
t.Fatalf("the mesh's own suffix was not used:\n%s", written)
|
|
}
|
|
}
|
|
|
|
// A machine not on the network is given no resolver data, which is an answer rather than an
|
|
// error: a node assigned the module before it is placed is in exactly that state.
|
|
func TestAMachineNotOnTheNetworkGetsNoResolverData(t *testing.T) {
|
|
_, part, err := ResolverFor([]Node{{Name: "novox", Address: "10.42.0.1"}}).Resources("stranger")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if part {
|
|
t.Fatal("a machine not on the network was given the mesh's resolver data")
|
|
}
|
|
}
|
|
|
|
// And a machine on it gets the whole set, including itself: a service on this machine reached by
|
|
// its own mesh name must arrive the same way it would from anywhere else.
|
|
func TestAMachineGetsTheWholeSetIncludingItself(t *testing.T) {
|
|
got, part, err := ResolverFor([]Node{
|
|
{Name: "novox", Address: "10.42.0.1"},
|
|
{Name: "ace", Address: "10.42.0.2"},
|
|
}).Resources("novox")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !part || len(got) != 1 {
|
|
t.Fatalf("expected one file for a machine on the network, got %d (part=%v)", len(got), part)
|
|
}
|
|
content, _ := got[0]["content"].(string)
|
|
if !strings.Contains(content, "novox.internal") || !strings.Contains(content, "ace.internal") {
|
|
t.Fatalf("the machine was not given the whole mesh:\n%s", content)
|
|
}
|
|
}
|