The map the control plane hands a resolution holds both: the machines, and every name the mesh was told to route to whichever machine serves it. A container's hosts wants all of it, so a routed name resolves to the proxy. A resolver's zones want only the machines: told the mesh's suffix is its own it answers authoritatively for everything under it and forwards none of it, so a routed name with the suffix appended — drive.example.test.internal — is a name nobody will ever ask for, standing beside the machines and looking as real. Found composing the resolver's first assignment on a live machine, before pushing it. hq issue 111.
189 lines
8.4 KiB
Go
189 lines
8.4 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Keyed by the internal name, as the control plane hands them (issue 079).
|
|
var threeMachines = map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2", "bart.internal": ""}
|
|
|
|
// **`*.homer.internal` is homer. That is the whole rule.** And the suffix itself is local: a
|
|
// resolver that forwards what it cannot answer must not send a mesh name it does not know — a
|
|
// machine that left, a typo — to a public resolver (hal dnsmasq-app conversion, novox/hq
|
|
// 08-connectivity).
|
|
func TestEveryMachineIsAWildcardUnderItsOwnName(t *testing.T) {
|
|
out := nodeZones(Resolution{Node: "homer"}, threeMachines, "")
|
|
for _, want := range []string{
|
|
"local=/internal/",
|
|
"address=/homer.internal/10.42.0.1",
|
|
"address=/marge.internal/10.42.0.2",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A machine the mesh has a record for and cannot place is left out of both.
|
|
//
|
|
// **Not an oversight — the alternative is worse.** A name written with no address resolves to
|
|
// nothing, and a connection to that hangs. Leaving it out fails at once and says the name is
|
|
// unknown, which is a thing somebody can act on.
|
|
func TestAMachineWithNoAddressIsNotNamed(t *testing.T) {
|
|
for _, out := range []string{
|
|
nodeNames(Resolution{Node: "homer"}, threeMachines, ""),
|
|
nodeZones(Resolution{Node: "homer"}, threeMachines, ""),
|
|
} {
|
|
if strings.Contains(out, "bart") {
|
|
t.Fatalf("a machine with no address was named, so its name resolves to nothing:\n%s", out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A machine's own mesh name points at its address on the private network, not at loopback — or a
|
|
// service binding the name it was given is unreachable from everywhere else.
|
|
func TestAMachinesOwnNameIsItsMeshAddress(t *testing.T) {
|
|
out := nodeNames(Resolution{Node: "homer"}, threeMachines, "")
|
|
var line string
|
|
for _, l := range strings.Split(out, "\n") {
|
|
if strings.Contains(l, "homer.internal") {
|
|
line = l
|
|
}
|
|
}
|
|
if !strings.HasPrefix(line, "10.42.0.1") {
|
|
t.Fatalf("a machine's own mesh name is not its mesh address: %q", line)
|
|
}
|
|
// And the loopback floor is still there, or things with nothing to do with the mesh break.
|
|
if !strings.Contains(out, "127.0.0.1\tlocalhost") {
|
|
t.Fatalf("the loopback floor was removed:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// A module says where it wants a fact, and is given a file.
|
|
func TestAModuleIsGivenTheFactsItAskedFor(t *testing.T) {
|
|
m := Manifest{Module: "dnsmasq", Facts: map[string]string{FactNodeZones: "/etc/mesh/zones.conf"}}
|
|
given, err := FactsInto(m, Resolution{Node: "homer"}, threeMachines, threeMachines, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(given) != 1 {
|
|
t.Fatalf("expected one file, got %d", len(given))
|
|
}
|
|
if given[0]["path"] != "/etc/mesh/zones.conf" || given[0]["type"] != "file" {
|
|
t.Fatalf("not written where it was asked for: %v", given[0])
|
|
}
|
|
if !strings.Contains(given[0]["content"].(string), "homer.internal") {
|
|
t.Fatalf("the file does not hold the fact: %v", given[0]["content"])
|
|
}
|
|
}
|
|
|
|
// **Asking for a fact the mesh does not have is refused here, not on a machine.** A daemon that
|
|
// starts, reads a file nobody wrote, and answers no queries is a much worse way to find out.
|
|
func TestAskingForAFactTheMeshDoesNotHaveIsRefused(t *testing.T) {
|
|
m := Manifest{Module: "dnsmasq", Facts: map[string]string{"the-weather": "/etc/weather"}}
|
|
_, err := FactsInto(m, Resolution{}, nil, nil, "")
|
|
if err == nil {
|
|
t.Fatal("a module asked for something nobody computes and was given nothing, silently")
|
|
}
|
|
for _, known := range []string{FactNodeNames, FactNodeZones} {
|
|
if !strings.Contains(err.Error(), known) {
|
|
t.Fatalf("the refusal does not say what would have worked: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And a relative path is refused, or a module decides where the mesh writes on a machine.
|
|
func TestAFactMustBeAskedForAtAnAbsolutePath(t *testing.T) {
|
|
m := Manifest{Module: "dnsmasq", Facts: map[string]string{FactNodeNames: "etc/hosts"}}
|
|
if _, err := FactsInto(m, Resolution{}, nil, nil, ""); err == nil {
|
|
t.Fatal("a relative path was accepted")
|
|
}
|
|
}
|
|
|
|
// **The names the control plane hands a resolution are already internal names** — `homer.internal`,
|
|
// the same map every container gets as its hosts. Appending the suffix again wrote
|
|
// `homer.internal.internal` into every hosts file and every resolver's zones, and the large mesh
|
|
// bed's name test was the first to read it back. Either key gives the same files.
|
|
func TestNamesKeyedByInternalNameAreNotSuffixedTwice(t *testing.T) {
|
|
internal := map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2"}
|
|
bare := map[string]string{"homer": "10.42.0.1", "marge": "10.42.0.2"}
|
|
if a, b := nodeZones(Resolution{Node: "homer"}, internal, ""), nodeZones(Resolution{Node: "homer"}, bare, ""); a != b {
|
|
t.Fatalf("the zones differ by how the names were keyed:\n%s\n---\n%s", a, b)
|
|
}
|
|
if a, b := nodeNames(Resolution{Node: "homer"}, internal, ""), nodeNames(Resolution{Node: "homer"}, bare, ""); a != b {
|
|
t.Fatalf("the hosts differ by how the names were keyed:\n%s\n---\n%s", a, b)
|
|
}
|
|
zones := nodeZones(Resolution{Node: "homer"}, internal, "")
|
|
if strings.Contains(zones, "internal.internal") || !strings.Contains(zones, "address=/homer.internal/10.42.0.1") {
|
|
t.Fatalf("the zones carry a doubled suffix or miss the name:\n%s", zones)
|
|
}
|
|
hosts := nodeNames(Resolution{Node: "homer"}, internal, "")
|
|
if !strings.Contains(hosts, "10.42.0.1\thomer.internal\thomer\t# this machine") {
|
|
t.Fatalf("the hosts line for the machine itself is not name, bare name and the mark:\n%s", hosts)
|
|
}
|
|
}
|
|
|
|
// The suffix the control plane composed the names with is the one the facts write — an operator
|
|
// who chose another does not get `.internal` appended to it.
|
|
func TestTheFactsWriteTheSuffixTheNamesWereComposedWith(t *testing.T) {
|
|
names := map[string]string{"homer.lan": "10.42.0.1"}
|
|
zones := nodeZones(Resolution{Node: "homer"}, names, "lan")
|
|
if !strings.Contains(zones, "address=/homer.lan/10.42.0.1") || strings.Contains(zones, "internal") {
|
|
t.Fatalf("the zones do not carry the operator's suffix as given:\n%s", zones)
|
|
}
|
|
if !strings.Contains(zones, "local=/lan/") {
|
|
t.Fatalf("the local domain is not the operator's suffix, so its names would leak upstream:\n%s", zones)
|
|
}
|
|
hosts := nodeNames(Resolution{Node: "homer"}, names, "lan")
|
|
if !strings.Contains(hosts, "10.42.0.1\thomer.lan\thomer\t# this machine") {
|
|
t.Fatalf("the hosts line does not carry the operator's suffix as given:\n%s", hosts)
|
|
}
|
|
}
|
|
|
|
// novox/hq 04-ISSUES/111: the map the control plane hands a resolution holds every name the mesh
|
|
// serves — the machines, and the names it was told to route to whichever machine serves them. A
|
|
// container's hosts wants all of it. A resolver's zones want only the machines: told the mesh's
|
|
// suffix is its own, it answers authoritatively for everything under it and forwards nothing, so a
|
|
// routed name written there with the suffix appended is a name nobody will ever ask for, standing
|
|
// beside the machines and looking as real.
|
|
func TestTheResolverIsToldTheMachinesAndNotTheNamesTheMeshMerelyServes(t *testing.T) {
|
|
machines := map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2"}
|
|
every := map[string]string{
|
|
"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2",
|
|
"drive.example.test": "10.42.0.1", "git.example.test": "10.42.0.2",
|
|
}
|
|
m := Manifest{Module: "resolver", Facts: map[string]string{
|
|
FactNodeZones: "/etc/zones.conf", FactNodeNames: "/etc/hosts",
|
|
}}
|
|
given, err := FactsInto(m, Resolution{Node: "homer"}, every, machines, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
by := map[string]string{}
|
|
for _, f := range given {
|
|
by[f["path"].(string)] = f["content"].(string)
|
|
}
|
|
|
|
zones := by["/etc/zones.conf"]
|
|
for _, machine := range []string{"address=/homer.internal/10.42.0.1", "address=/marge.internal/10.42.0.2"} {
|
|
if !strings.Contains(zones, machine) {
|
|
t.Fatalf("the resolver was not told %q:\n%s", machine, zones)
|
|
}
|
|
}
|
|
for _, served := range []string{"drive.example.test", "git.example.test"} {
|
|
if strings.Contains(zones, served) {
|
|
t.Fatalf("the resolver was told %q, a name the mesh serves rather than a machine:\n%s", served, zones)
|
|
}
|
|
}
|
|
|
|
// And the hosts file is the other way about: every name, so a container reaching a routed name
|
|
// finds the machine serving it.
|
|
hosts := by["/etc/hosts"]
|
|
for _, name := range []string{"homer.internal", "drive.example.test", "git.example.test"} {
|
|
if !strings.Contains(hosts, name) {
|
|
t.Fatalf("a container would not resolve %q from its hosts:\n%s", name, hosts)
|
|
}
|
|
}
|
|
}
|