The merge commit took only the staged index; these reconciliation edits sat unstaged in the working tree. Integrate the template mechanism with #79's region write (hq 128): RosterFile gains Shared, FactsInto sets into:block for a shared fact, /etc/hosts becomes the region form (no floor) and node-names is marked shared. Without this the merge would have regressed /etc/hosts back to a whole-file write, replacing the operator's own lines.
222 lines
10 KiB
Go
222 lines
10 KiB
Go
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"])
|
|
}
|
|
}
|
|
|
|
// **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, "")
|
|
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, "")
|
|
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)
|
|
}
|
|
}
|
|
}
|