Facts carry the format as a template, so the control plane holds none (ADR 0120)
A roster fact used to be a name from a closed list, each formatted in Go here — node-names as a hosts file, node-zones as a resolver's zones. Every new consumer (ssh's known_hosts, an authorized_keys) meant another formatter in the control plane, in the consumer's own configuration language. Now a fact is a path and a Go template over the roster view (this node, the suffix, and every served name vs the machines). The mesh owns the data; the module owns the format. /etc/hosts is a template on the network module; dnsmasq's zones move to dnsmasq. The controller renders and reads neither. WireGuard stays a computed generator: the overlay is the substrate delivery rides on, and its config is topology, not a roster projection. Output is byte-for-byte unchanged, pinned by the hosts golden tests and the resolver tests that compose the real dnsmasq manifest.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Keyed by the internal name, as the control plane hands them (issue 079). bart has no address —
|
||||
// the ordinary state between adding a machine and it joining.
|
||||
var threeMachines = map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2", "bart.internal": ""}
|
||||
|
||||
// A module says where it wants a roster file and in what format, and is given the rendered file.
|
||||
func TestAModuleIsGivenTheFileItAskedFor(t *testing.T) {
|
||||
m := Manifest{Module: "resolver", Facts: map[string]RosterFile{
|
||||
"zones": {Path: "/etc/mesh/zones.conf", Template: "{{range .Machines}}address=/{{.FQDN}}/{{.Address}}\n{{end}}"},
|
||||
}}
|
||||
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])
|
||||
}
|
||||
// The id is fact-<name>, which is what a restart-on names when the roster changes.
|
||||
if given[0]["id"] != "fact-zones" {
|
||||
t.Fatalf("the file's id is not fact-<name>, so a restart-on cannot find it: %v", given[0]["id"])
|
||||
}
|
||||
if !strings.Contains(given[0]["content"].(string), "address=/homer.internal/10.42.0.1") {
|
||||
t.Fatalf("the file does not hold the fact: %v", given[0]["content"])
|
||||
}
|
||||
}
|
||||
|
||||
// **The format is the module's — the mesh renders whatever template it gives.** The same roster
|
||||
// through two templates is two entirely different files, and the control plane reads neither.
|
||||
func TestTheFormatIsTheModulesOwn(t *testing.T) {
|
||||
roster := map[string]string{"homer.internal": "10.42.0.1"}
|
||||
hostsish := Manifest{Module: "a", Facts: map[string]RosterFile{
|
||||
"f": {Path: "/f", Template: "{{range .Names}}{{.Address}}\t{{.Name}}\n{{end}}"}}}
|
||||
sshish := Manifest{Module: "b", Facts: map[string]RosterFile{
|
||||
"f": {Path: "/f", Template: "{{range .Names}}Host {{.Name}}\n HostName {{.FQDN}}\n{{end}}"}}}
|
||||
|
||||
h, err := FactsInto(hostsish, Resolution{Node: "homer"}, roster, roster, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := FactsInto(sshish, Resolution{Node: "homer"}, roster, roster, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h[0]["content"] != "10.42.0.1\thomer\n" {
|
||||
t.Fatalf("the hosts-shaped template did not render its format: %q", h[0]["content"])
|
||||
}
|
||||
if s[0]["content"] != "Host homer\n HostName homer.internal\n" {
|
||||
t.Fatalf("the ssh-shaped template did not render its format: %q", s[0]["content"])
|
||||
}
|
||||
}
|
||||
|
||||
// **A template that will not parse is refused here, not on a machine.** A daemon that starts, reads
|
||||
// a file the mesh could not render, and answers nothing is a much worse way to find out.
|
||||
func TestABrokenTemplateIsRefusedHere(t *testing.T) {
|
||||
m := Manifest{Module: "resolver", Facts: map[string]RosterFile{
|
||||
"zones": {Path: "/etc/zones", Template: "{{range .Machines}}oops"}}}
|
||||
_, err := FactsInto(m, Resolution{}, nil, nil, "")
|
||||
if err == nil {
|
||||
t.Fatal("a template that does not parse was accepted, so the machine gets an empty file")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "resolver") || !strings.Contains(err.Error(), "zones") {
|
||||
t.Fatalf("the refusal does not say whose template, or which: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A template reading something the mesh does not compute is refused, not rendered empty. The roster
|
||||
// is a closed shape; asking it for the weather fails where the manifest is.
|
||||
func TestATemplateReadingWhatTheMeshDoesNotHaveIsRefused(t *testing.T) {
|
||||
m := Manifest{Module: "resolver", Facts: map[string]RosterFile{
|
||||
"zones": {Path: "/etc/zones", Template: "{{.Weather}}"}}}
|
||||
if _, err := FactsInto(m, Resolution{}, nil, nil, ""); err == nil {
|
||||
t.Fatal("a template read a field nobody computes and rendered anyway, silently")
|
||||
}
|
||||
}
|
||||
|
||||
// A relative path is refused, or a module decides where the mesh writes on a machine.
|
||||
func TestAFactMustBeAskedForAtAnAbsolutePath(t *testing.T) {
|
||||
m := Manifest{Module: "resolver", Facts: map[string]RosterFile{
|
||||
"hosts": {Path: "etc/hosts", Template: "x"}}}
|
||||
if _, err := FactsInto(m, Resolution{}, nil, nil, ""); err == nil {
|
||||
t.Fatal("a relative path was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// A machine the mesh has a record for and cannot place is left out of the roster.
|
||||
//
|
||||
// **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 TestAMachineWithNoAddressIsNotInTheRoster(t *testing.T) {
|
||||
m := Manifest{Module: "a", Facts: map[string]RosterFile{
|
||||
"f": {Path: "/f", Template: "{{range .Machines}}{{.Name}}\n{{end}}"}}}
|
||||
given, err := FactsInto(m, Resolution{Node: "homer"}, threeMachines, threeMachines, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(given[0]["content"].(string), "bart") {
|
||||
t.Fatalf("a machine with no address was in the roster, so its name resolves to nothing:\n%s", given[0]["content"])
|
||||
}
|
||||
}
|
||||
|
||||
// **The names the control plane hands a resolution are already internal names** — `homer.internal`,
|
||||
// the same map every container gets as its hosts. A roster entry's FQDN is that name, not it with
|
||||
// the suffix appended a second time; either key gives the same entries.
|
||||
func TestNamesAreNotSuffixedTwice(t *testing.T) {
|
||||
internal := map[string]string{"homer.internal": "10.42.0.1"}
|
||||
bare := map[string]string{"homer": "10.42.0.1"}
|
||||
tmpl := RosterFile{Path: "/f", Template: "{{range .Machines}}{{.FQDN}} {{.Name}}\n{{end}}"}
|
||||
|
||||
fromInternal, err := FactsInto(Manifest{Module: "a", Facts: map[string]RosterFile{"f": tmpl}}, Resolution{Node: "homer"}, internal, internal, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fromBare, err := FactsInto(Manifest{Module: "a", Facts: map[string]RosterFile{"f": tmpl}}, Resolution{Node: "homer"}, bare, bare, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fromInternal[0]["content"] != fromBare[0]["content"] {
|
||||
t.Fatalf("the roster differs by how the names were keyed:\n%q\n%q", fromInternal[0]["content"], fromBare[0]["content"])
|
||||
}
|
||||
got := fromInternal[0]["content"].(string)
|
||||
if strings.Contains(got, "internal.internal") || !strings.Contains(got, "homer.internal homer") {
|
||||
t.Fatalf("the entry carries a doubled suffix or the wrong bare name:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The suffix the control plane composed the names with is the one a template sees — an operator who
|
||||
// chose another does not get `.internal`. `.Suffix` is the bare form, and FQDNs carry it.
|
||||
func TestTheSuffixIsCarriedAsComposed(t *testing.T) {
|
||||
names := map[string]string{"homer.lan": "10.42.0.1"}
|
||||
m := Manifest{Module: "a", Facts: map[string]RosterFile{
|
||||
"f": {Path: "/f", Template: "local=/{{.Suffix}}/\n{{range .Machines}}{{.FQDN}}\n{{end}}"}}}
|
||||
given, err := FactsInto(m, Resolution{Node: "homer"}, names, names, "lan")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := given[0]["content"].(string)
|
||||
if !strings.Contains(got, "local=/lan/") || strings.Contains(got, "internal") {
|
||||
t.Fatalf("the operator's suffix was not carried, so its names would be wrong:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "homer.lan") {
|
||||
t.Fatalf("the FQDN does not carry the operator's suffix:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq 04-ISSUES/111: a template is given both sets and chooses. `.Names` is every name the mesh
|
||||
// serves — the machines and the names it was told to route; `.Machines` is only the machines. A
|
||||
// container's hosts wants every name so a routed name resolves to the machine serving it; a resolver
|
||||
// told the suffix is its own wants only the machines, or a routed name written there with the suffix
|
||||
// is a name nobody will ever ask for, standing beside the machines and looking as real.
|
||||
func TestATemplateChoosesMachinesOrEveryName(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]RosterFile{
|
||||
"zones": {Path: "/etc/zones", Template: "{{range .Machines}}{{.FQDN}}\n{{end}}"},
|
||||
"hosts": {Path: "/etc/hosts", Template: "{{range .Names}}{{.FQDN}}\n{{end}}"},
|
||||
}}
|
||||
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"]
|
||||
for _, served := range []string{"drive.example.test", "git.example.test"} {
|
||||
if strings.Contains(zones, served) {
|
||||
t.Fatalf("a template over .Machines saw %q, a name the mesh serves rather than a machine:\n%s", served, zones)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(zones, "homer.internal") {
|
||||
t.Fatalf("a template over .Machines did not see the machines:\n%s", zones)
|
||||
}
|
||||
|
||||
hosts := by["/etc/hosts"]
|
||||
for _, name := range []string{"homer.internal", "drive.example.test", "git.example.test"} {
|
||||
if !strings.Contains(hosts, name) {
|
||||
t.Fatalf("a template over .Names did not see %q, so a container would not resolve it:\n%s", name, hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user