Files
mesh-controller/internal/catalogue/machine_into_files.go
T
jschoubben dd920ff854 Give the resolver the mesh's suffix as a local domain and a module its machine's address
hal dnsmasq-app conversion, hq 08-connectivity. Converting the resolver from the module it
replaces made it forward what it cannot answer, which is what the predecessor's does, and
that found two things the controller did not say.

A resolver that forwards must not send a mesh name it does not know upstream: the
`node-zones` fact now carries `local=/<suffix>/` beside the wildcards, written here rather
than in the daemon's configuration because the suffix is the mesh's choice and this file is
the one place the mesh writes what it chose. The default lives in one helper now instead of
being spelled in two functions.

The predecessor points the container runtime's `dns` at the machine's own tunnel address —
a container cannot reach the machine's loopback. A module writing that key needs the
address, and `${machine:at}` is the machine's name; a runtime's resolver list cannot be a
name it would need that resolver to look up. So a module may say `${machine:address}`: what
`at` resolves to, read from the same names the hosts file and the wildcards are written
from, absent — and refused — off the network like `at` is.

The `mesh-resolver` and `resolver-data` constants go: nothing provided or consumed either,
the fact and `mesh-addressing` are the mechanism, and a requirement nothing provides is
refused at resolution.

Tests: the catalogue's dnsmasq, resolv-conf and resolved-split-dns manifests are parsed
and composed as a machine would receive them — fixed upstreams, no-resolv, 127.0.0.1, the
machines file, the runtime's key, the pair that decides what a machine asks refused on one
node; and on a real mesh the resolver's machines file is composed with a wildcard per
machine on the network and composed again without one that left, mirroring the hosts fact.
2026-09-23 23:55:34 +02:00

106 lines
4.2 KiB
Go

package catalogue
import (
"fmt"
"regexp"
"sort"
"strings"
)
// What a module may say about the machine it is running on.
//
// **A module cannot know where it will be assigned, and sometimes it must say so anyway.** Every
// other name in a declaration is either the module's own — which it wrote — or something it
// requires, which arrives as a binding. The machine underneath is neither: it is chosen when the
// module is assigned, long after the manifest was written, and until now nothing carried it into a
// file.
//
// The case that found this is a certificate authority inside the mesh (novox/hq ADR 0066). A proxy
// reaches it at the address the mesh handed over, `<machine>.internal` — so the authority's own
// certificate has to be issued for that name, or the first thing that happens is the proxy refusing
// to talk to it. The authority is the one thing that cannot be told its name by a binding: it
// provides, it does not require. Written as a literal it would be a manifest carrying one
// deployment's machine name, which is the shape [ADR 0066] exists to remove.
//
// Three facts, all the mesh's own vocabulary — the same `node` and `at` a contribution already
// carries, and the address behind `at`, for software that takes an address and not a name. The
// case that found the third is a resolver pointing the container runtime at itself: the runtime's
// list of resolvers is addresses, because a name there would have to be resolved by the resolver
// it names. Nothing about what a machine is *for*: that would be the mesh learning what a module
// means, which it does not do.
// ofMachine is where a module says a fact about the machine underneath it belongs:
// ${machine:<key>}.
var ofMachine = regexp.MustCompile(`\$\{machine:([a-z0-9][a-z0-9_-]*)\}`)
// machineUsed are the keys a file's content asks for, first appearance first.
func machineUsed(content string) []string {
var used []string
seen := map[string]bool{}
for _, m := range ofMachine.FindAllStringSubmatch(content, -1) {
if key := m[1]; !seen[key] {
seen[key] = true
used = append(used, key)
}
}
return used
}
// machineFacts is what a module may name about the machine it was assigned to.
//
// `at` is absent rather than empty when the machine is not on the private network. A module asking
// to be reached at an address that does not exist is a misconfiguration, and it is said here —
// where the module and the machine are both named — rather than discovered later as a certificate
// nobody can verify.
//
// `address` is what `at` resolves to, read from the names the control plane composed — the same map
// the hosts file and the resolver's wildcards are written from, so a file naming the machine's
// address and the file every other machine reaches it by cannot disagree. Absent, like `at`, when
// the machine is off the network or the mesh has not placed it.
func machineFacts(r Resolution, names map[string]string) map[string]string {
out := map[string]string{"name": r.Node}
if r.At != "" {
out["at"] = r.At
if address := names[r.At]; address != "" {
out["address"] = address
}
}
return out
}
// machineInto replaces a file's ${machine:…} placeholders with what the mesh knows about the
// machine the module was assigned to.
//
// A key the mesh does not hold is refused, for the same reason a binding's is: left alone, the
// literal would be written into a configuration file and read as a value.
func machineInto(resource map[string]any, facts map[string]string, module string) error {
if fmt.Sprint(resource["type"]) != "file" {
return nil
}
content, ok := resource["content"].(string)
if !ok {
return nil
}
for _, key := range machineUsed(content) {
value, has := facts[key]
if !has {
return fmt.Errorf(
"%s has a file that says ${machine:%s}, and this machine says %s",
module, key, orNothing(namesOfFacts(facts)))
}
resource["content"] = strings.ReplaceAll(
content, fmt.Sprintf("${machine:%s}", key), value)
content = resource["content"].(string)
}
return nil
}
func namesOfFacts(facts map[string]string) []string {
out := make([]string, 0, len(facts))
for k := range facts {
out = append(out, k)
}
sort.Strings(out)
return out
}