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, nil, "") 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-, 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-, 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"]) } } // **A shared fact is written into a region of the machine's file, not over it** (novox/hq issue // 128). A hosts file is the machine's — its localhost, the operator's lines, other tools' blocks — // so the mesh owns only a marked region (`into: block`); a resolver's zones file is the mesh's // whole, and carries no `into`. func TestASharedFactIsWrittenIntoARegion(t *testing.T) { roster := map[string]string{"homer.internal": "10.42.0.1"} m := Manifest{Module: "net", Facts: map[string]RosterFile{ "node-names": {Path: "/etc/hosts", Template: "{{range .Names}}{{.FQDN}}\n{{end}}", Shared: true}, "node-zones": {Path: "/etc/zones", Template: "{{range .Machines}}{{.FQDN}}\n{{end}}"}, }} given, err := FactsInto(m, Resolution{Node: "homer"}, roster, roster, nil, "") if err != nil { t.Fatal(err) } by := map[string]map[string]any{} for _, f := range given { by[f["path"].(string)] = f } if by["/etc/hosts"]["into"] != "block" { t.Fatalf("a shared fact is not written into a region, so the mesh writes the file whole: %v", by["/etc/hosts"]) } if _, has := by["/etc/zones"]["into"]; has { t.Fatalf("an unshared fact was written into a region, so the mesh does not own its own file whole: %v", by["/etc/zones"]) } } // **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, nil, "") if err != nil { t.Fatal(err) } s, err := FactsInto(sshish, Resolution{Node: "homer"}, roster, roster, nil, "") 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, 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, 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, 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, nil, "") 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, nil, "") if err != nil { t.Fatal(err) } fromBare, err := FactsInto(Manifest{Module: "a", Facts: map[string]RosterFile{"f": tmpl}}, Resolution{Node: "homer"}, bare, bare, nil, "") 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, nil, "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, nil, "") 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) } } } // A home fact is placed under the operator account's home and chowned to it, and its template sees // each node's account (novox/hq to-be 29) — the ssh-client config is the case. func TestAHomeFactIsPlacedUnderTheAccountsHomeAndOwnedByIt(t *testing.T) { names := map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2"} accounts := map[string]string{"homer": "jo", "marge": "jo"} m := Manifest{Module: "ssh-client", Facts: map[string]RosterFile{ "ssh-config": {Path: ".ssh/config.d/mesh", Home: true, Template: "{{range .Names}}Host {{.Name}}\n HostName {{.FQDN}}\n User {{.Account}}\n{{end}}"}}} given, err := FactsInto(m, Resolution{Node: "homer", Account: "jo"}, names, names, accounts, "") if err != nil { t.Fatal(err) } f := given[0] if f["path"] != "/home/jo/.ssh/config.d/mesh" { t.Fatalf("the home fact was not placed under the account's home: %v", f["path"]) } if f["owner"] != "jo" { t.Fatalf("the home fact is not owned by the account: %v", f["owner"]) } if !strings.Contains(f["content"].(string), "Host marge\n HostName marge.internal\n User jo") { t.Fatalf("the config does not name the peer's account:\n%s", f["content"]) } } // A machine with no operator account gets no home fact — it cannot be placed, so it is left out // rather than written to nowhere. func TestAHomeFactIsSkippedWhereThereIsNoAccount(t *testing.T) { names := map[string]string{"homer.internal": "10.42.0.1"} m := Manifest{Module: "ssh-client", Facts: map[string]RosterFile{ "ssh-config": {Path: ".ssh/config", Home: true, Template: "x"}}} given, err := FactsInto(m, Resolution{Node: "homer"}, names, names, nil, "") if err != nil { t.Fatal(err) } if len(given) != 0 { t.Fatalf("a home fact was placed on a machine with no operator account: %v", given) } }