Reconcile with hq 128: hosts template is the region form, node-names is shared

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.
This commit is contained in:
2026-09-27 01:50:11 +02:00
parent 966c5ddad3
commit a6d89e3f73
3 changed files with 54 additions and 15 deletions
+10 -13
View File
@@ -161,21 +161,17 @@ func (g *Generator) Nodes() []Node { return g.nodes }
// Graph is the peer list per node, for showing.
func (g *Generator) Graph() Graph { return g.graph }
// hostsTemplate is the `/etc/hosts` the network module asks the mesh to write — every machine's
// mesh name at its private address, so a service binding the name it was given stays reachable from
// everywhere else. It is a roster template like any module's (catalogue.RosterFile): the mesh owns
// the data, this owns the format.
// hostsTemplate is the mesh's region of `/etc/hosts` — every machine's mesh name at its private
// address, written into a marked region and merged (RosterFile.Shared → `into: block`), so the rest
// of the file (localhost, the machine's own name, other tools' blocks) is kept byte for byte
// (novox/hq issue 128). It is a roster template like any module's: the mesh owns the data, this owns
// the format, and the control plane holds no formatter.
//
// - The loopback floor stays, or things with nothing to do with the mesh break.
// - `127.0.1.1 <node>` only when there is a node, the ordinary Debian self-name line.
// - No floor: no header, no localhost, no `127.0.1.1` — those are the machine's, above the region.
// - A machine's own line is marked, and its mesh name resolves to its mesh address, not loopback.
// - `.Names` is every name the mesh serves (issue 111), so a container reaching a routed name
// finds the machine serving it; machines with no address yet are already left out of the set.
const hostsTemplate = "# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n" +
"# joins or leaves, and an edit would survive until then and vanish.\n\n" +
"127.0.0.1\tlocalhost\n" +
"::1\t\tlocalhost ip6-localhost ip6-loopback\n" +
"{{if .Node}}127.0.1.1\t{{.Node}}\n{{end}}\n" +
const hostsTemplate = "# The mesh's names. This region is replaced whenever a machine joins or leaves.\n" +
"{{range .Names}}{{.Address}}\t{{.FQDN}}\t{{.Name}}{{if eq .Name $.Node}}\t# this machine{{end}}\n{{end}}"
// Manifest is the module the mesh provides for itself.
@@ -194,9 +190,10 @@ func Manifest() map[string]any {
// mesh knows which machines exist and where; writing that into a hosts file is not a thing
// that needs a module to run nowhere. The format is a template like any other roster fact —
// the mesh's own module owns the `/etc/hosts` layout the way dnsmasq owns its zones, and the
// control plane holds no formatter (see catalogue.RosterFile).
// control plane holds no formatter (see catalogue.RosterFile). `shared`: the mesh owns only
// its region of the file and keeps the rest (novox/hq issue 128).
"facts": map[string]any{
"node-names": map[string]any{"path": "/etc/hosts", "template": hostsTemplate},
"node-names": map[string]any{"path": "/etc/hosts", "template": hostsTemplate, "shared": true},
},
"claims": []map[string]any{{"name": TheNetwork, "scope": "node"}},
}