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) } } }