The map the control plane hands a resolution holds both: the machines, and every name the mesh was told to route to whichever machine serves it. A container's hosts wants all of it, so a routed name resolves to the proxy. 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 none of it, so a routed name with the suffix appended — drive.example.test.internal — is a name nobody will ever ask for, standing beside the machines and looking as real. Found composing the resolver's first assignment on a live machine, before pushing it. hq issue 111.
185 lines
7.7 KiB
Go
185 lines
7.7 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What only the mesh knows, written where a module asks for it.
|
|
//
|
|
// **The graph is the control plane's; using it is the module's.** The mesh knows which machines
|
|
// exist, what they are called, and where they are. Turning that into a name that resolves is
|
|
// somebody's software, and which software is a choice the mesh should not be making.
|
|
//
|
|
// This replaced three modules — names, a resolver's data, and the private network's own
|
|
// configuration — that existed only because computed output needed somewhere to live. They ran no
|
|
// software and could not be swapped for anything, which is the test of whether something is a
|
|
// module at all (novox/hq ADR 0040).
|
|
|
|
const (
|
|
// FactNodeNames is every machine's name and address, as a hosts file.
|
|
//
|
|
// Exact names only: `homer` and `homer.internal` resolve to homer. Anything *under* a machine
|
|
// is a wildcard, which a hosts file cannot express — that is FactNodeZones.
|
|
FactNodeNames = "node-names"
|
|
|
|
// FactNodeZones is every machine as a wildcard: `*.homer.internal` is homer.
|
|
//
|
|
// Written in the form a resolver reads. A machine's own name and everything under it are one
|
|
// fact — if homer is at an address, so is anything homer serves.
|
|
FactNodeZones = "node-zones"
|
|
)
|
|
|
|
// facts is every fact the mesh computes, and what writes it.
|
|
//
|
|
// **A closed list.** A module asking for a fact the mesh does not have is asking for a file nobody
|
|
// will write, and finding that out on a machine — as a daemon that starts, reads nothing, and
|
|
// answers no queries — is worse than being told where the manifest is.
|
|
// A fact is written from the names it is about. `every` is every name the mesh serves — machines
|
|
// and the names it was told to route; `machines` is only the machines. A fact takes the set it is
|
|
// true of, and the two must not be confused (novox/hq 04-ISSUES/111).
|
|
var facts = map[string]func(r Resolution, every, machines map[string]string, suffix string) string{
|
|
FactNodeNames: func(r Resolution, every, _ map[string]string, suffix string) string {
|
|
return nodeNames(r, every, suffix)
|
|
},
|
|
FactNodeZones: func(r Resolution, _, machines map[string]string, suffix string) string {
|
|
return nodeZones(r, machines, suffix)
|
|
},
|
|
}
|
|
|
|
// FactsInto renders the facts a module asked for, as files it will be given.
|
|
//
|
|
// The module owns everything after the file exists: loading it, restarting on it, what a resolver
|
|
// does with it. This only puts it there.
|
|
func FactsInto(m Manifest, r Resolution, addresses, machines map[string]string, suffix string) ([]map[string]any, error) {
|
|
if len(m.Facts) == 0 {
|
|
return nil, nil
|
|
}
|
|
names := make([]string, 0, len(m.Facts))
|
|
for name := range m.Facts {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
out := make([]map[string]any, 0, len(names))
|
|
for _, name := range names {
|
|
write, known := facts[name]
|
|
if !known {
|
|
return nil, fmt.Errorf(
|
|
"%s asks the mesh for %q, which it does not compute. It has %s",
|
|
m.Module, name, spokenFacts())
|
|
}
|
|
path := m.Facts[name]
|
|
if !strings.HasPrefix(path, "/") {
|
|
return nil, fmt.Errorf(
|
|
"%s asks for %q at %q, which is not an absolute path", m.Module, name, path)
|
|
}
|
|
out = append(out, map[string]any{
|
|
"id": "fact-" + name, "type": "file", "path": path, "mode": "0644",
|
|
"content": write(r, addresses, machines, suffix),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// spokenFacts lists them, so a refusal says what would have worked.
|
|
func spokenFacts() string {
|
|
names := make([]string, 0, len(facts))
|
|
for name := range facts {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
// nodeNames is every machine's name and address, as a hosts file.
|
|
//
|
|
// **A machine with no address is left out.** The mesh has a record for it — somebody added it —
|
|
// and does not yet know where it is, which is the ordinary state between adding a machine and it
|
|
// joining. Writing the name anyway would give a name that resolves to nothing, and a connection to
|
|
// that hangs; leaving it out fails at once and says the name is unknown.
|
|
func nodeNames(r Resolution, addresses map[string]string, suffix string) string {
|
|
var b strings.Builder
|
|
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n")
|
|
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n\n")
|
|
// The floor every Linux expects, and which removing would break things that have nothing to do
|
|
// with the mesh.
|
|
b.WriteString("127.0.0.1\tlocalhost\n")
|
|
b.WriteString("::1\t\tlocalhost ip6-localhost ip6-loopback\n")
|
|
if r.Node != "" {
|
|
fmt.Fprintf(&b, "127.0.1.1\t%s\n", r.Node)
|
|
}
|
|
b.WriteString("\n")
|
|
for _, name := range sortedNames(addresses) {
|
|
at := addresses[name]
|
|
internal, bare := meshName(name, suffix)
|
|
// Its mesh name resolves to its address on the private network rather than to loopback,
|
|
// so a service binding the name it was given stays reachable from everywhere else.
|
|
fmt.Fprintf(&b, "%s\t%s\t%s", at, internal, bare)
|
|
if bare == r.Node {
|
|
b.WriteString("\t# this machine")
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// nodeZones is every machine as a wildcard, in the form a resolver reads.
|
|
//
|
|
// `*.homer.internal` is homer, which is the whole rule: if homer is at an address, so is anything
|
|
// homer serves. A module wanting this runs the resolver; the mesh only says what is true.
|
|
//
|
|
// **And the suffix itself, as a local domain.** A resolver that forwards what it cannot answer
|
|
// would otherwise send a mesh name it does not know — a machine that left, a typo — to a public
|
|
// resolver, which is a leak of the mesh's names for no answer. `local=` keeps everything under the
|
|
// suffix here: answered from the lines below or refused. Written in this file rather than in the
|
|
// resolver's own configuration because the suffix is the mesh's choice (the operator may have
|
|
// picked another) and this file is the one place the mesh writes what it chose.
|
|
func nodeZones(_ Resolution, addresses map[string]string, suffix string) string {
|
|
var b strings.Builder
|
|
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n")
|
|
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n\n")
|
|
fmt.Fprintf(&b, "local=/%s/\n", strings.TrimPrefix(suffixOr(suffix), "."))
|
|
for _, name := range sortedNames(addresses) {
|
|
internal, _ := meshName(name, suffix)
|
|
fmt.Fprintf(&b, "address=/%s/%s\n", internal, addresses[name])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// meshName is a machine's internal name and its bare one, from either. The control plane keys
|
|
// the names it hands a resolution by the internal name (`homer.internal`), the same map a
|
|
// container gets as its hosts; a caller that keys by the bare name gets the same answer. The
|
|
// suffix is the one the control plane composed those names with, handed down rather than written
|
|
// here a second time — the alternative was `homer.internal.internal` on every machine.
|
|
func meshName(name, suffix string) (internal, bare string) {
|
|
dotted := "." + strings.TrimPrefix(suffixOr(suffix), ".")
|
|
if strings.HasSuffix(name, dotted) {
|
|
return name, strings.TrimSuffix(name, dotted)
|
|
}
|
|
return name + dotted, name
|
|
}
|
|
|
|
// suffixOr is the suffix given, or the one the mesh composes names with when none was handed down.
|
|
// The one place the default is written in this file, so a fact and a name cannot disagree about it.
|
|
func suffixOr(suffix string) string {
|
|
if suffix == "" {
|
|
return "internal"
|
|
}
|
|
return suffix
|
|
}
|
|
|
|
func sortedNames(addresses map[string]string) []string {
|
|
out := make([]string, 0, len(addresses))
|
|
for name, at := range addresses {
|
|
// See nodeNames: a machine the mesh cannot place is left out rather than named at nothing.
|
|
if at == "" {
|
|
continue
|
|
}
|
|
out = append(out, name)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|