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 }