/etc/hosts is the machine's: the distribution's localhost lines, the operator's own entries, and marked blocks other tools maintain there. Writing node-names whole replaced all of it the moment the private network was taken, and every later write by those tools was lost at the next machine joining. The node-names fact is now emitted with into: "block", so the host owns only its marked region and keeps the rest byte for byte. The region holds only the mesh's names: no header claiming the file, no localhost, no 127.0.1.1 line — the floor was never the mesh's to write. How a fact is written is a property of the fact in the closed table; node-zones stays a whole file the mesh owns. Sequencing: a host older than the block mode refuses the whole declaration on an unknown into, so every host must be upgraded before this controller is rolled out.
240 lines
11 KiB
Go
240 lines
11 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)
|
|
}
|
|
}
|
|
|
|
// novox/hq issue 128: the names are the mesh's region of the machine's hosts file, and only that.
|
|
//
|
|
// **The loopback floor is the machine's now, not the mesh's.** It was written here while the mesh
|
|
// wrote the whole file. Written into a region, a `localhost` or a `127.0.1.1 homer` of the mesh's
|
|
// would stand beside the distribution's own — a second answer nobody could say the owner of, and
|
|
// one that goes when the mesh leaves. A header claiming the file would be a lie about the rest of
|
|
// it. And no line may look like the host's own markers, or the host would refuse the region.
|
|
func TestTheNamesAreOnlyTheMeshsRegionOfTheFile(t *testing.T) {
|
|
out := nodeNames(Resolution{Node: "homer"}, threeMachines, "")
|
|
want := "# The mesh's names. This region is replaced whenever a machine joins or leaves.\n" +
|
|
"10.42.0.1\thomer.internal\thomer\t# this machine\n" +
|
|
"10.42.0.2\tmarge.internal\tmarge\n"
|
|
if out != want {
|
|
t.Fatalf("the region is not exactly the mesh's names:\n%s\n--- want ---\n%s", out, want)
|
|
}
|
|
for _, floor := range []string{"localhost", "127.0.", "::1", "Generated by the mesh", "# BEGIN mesh ", "# END mesh "} {
|
|
if strings.Contains(out, floor) {
|
|
t.Fatalf("the region carries %q, which is not the mesh's to write:\n%s", floor, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The hosts file is written into; the resolver's zones are written whole. A property of each fact,
|
|
// so a module asking for node-names at another path still does not get a file of the mesh's.
|
|
func TestOnlyTheNamesAreWrittenIntoASharedFile(t *testing.T) {
|
|
m := Manifest{Module: "resolver", Facts: map[string]string{
|
|
FactNodeZones: "/etc/mesh-resolver/nodes.conf", FactNodeNames: "/etc/hosts",
|
|
}}
|
|
given, err := FactsInto(m, Resolution{Node: "homer"}, threeMachines, threeMachines, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(given) != 2 {
|
|
t.Fatalf("expected two files, got %v", given)
|
|
}
|
|
for _, f := range given {
|
|
switch f["path"] {
|
|
case "/etc/hosts":
|
|
if f["into"] != "block" {
|
|
t.Errorf("the hosts file is written over rather than into: %v", f)
|
|
}
|
|
case "/etc/mesh-resolver/nodes.conf":
|
|
// The mesh owns the resolver's zones; nothing else writes there.
|
|
if into, set := f["into"]; set {
|
|
t.Errorf("the zones file is written into (%v), and it is the mesh's whole", into)
|
|
}
|
|
if !strings.HasPrefix(f["content"].(string), "# Generated by the mesh.") {
|
|
t.Errorf("the zones file lost its header: %v", f["content"])
|
|
}
|
|
default:
|
|
t.Errorf("a file nobody asked for: %v", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|