The mesh computes every name under a machine, for a resolver to answer
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.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user