The mesh computes every name under a machine, for a resolver to answer
Services are named under the machine they run on — postgres.novox.internal, plex.ace.internal. The first label is the service and the rest is the node, so what has to resolve is anything under a node's name. What routes it once it arrives is a proxy's concern and stays separate. A hosts file cannot do that. It answers exact names, and a wildcard there would mean writing down every service in advance — which is the enumeration the arrangement exists to avoid. novox/hq 08-connectivity named this exact case as the trigger for needing a resolver rather than a file, and it is the first thing to meet it. The mesh writes the data and runs no daemon. A resolver is third-party software, and third-party software runs on the mesh rather than being of it (ADR 0001): the mesh has no business shipping one, choosing which one, or knowing its configuration language. What only the mesh can know is which machines exist and where they are. A module that runs a resolver requires what this provides and reads one file, so swapping the daemon changes that module and nothing here. Separate from names rather than part of them: a machine with no container runtime can still have a hosts file, and folding them together would take exact names away from a machine that cannot run a daemon in order to give it a wildcard it cannot use either. A machine with no address is left out. A wildcard pointing at nothing is worse than no wildcard — every name under it resolves and then hangs, where an unresolvable name fails at once and says which name it was.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A resolver answers every name under a node, not just the node.
|
||||
//
|
||||
// **Services are named under the machine they run on** — `postgres.novox.internal`,
|
||||
// `plex.ace.internal`. The first label is the service and the rest is the node, so what has to
|
||||
// resolve is *anything* under a node's name, going to that node's address. A reverse proxy there
|
||||
// routes by the name it was asked for, which is a separate concern and stays separate.
|
||||
//
|
||||
// **This is what a hosts file cannot do.** It answers exact names; a wildcard would mean writing
|
||||
// down every service name in advance, which is the enumeration the arrangement exists to avoid.
|
||||
// novox/hq 08-connectivity named exactly this as the trigger for needing a resolver rather than a
|
||||
// file, and it is the first thing to meet it.
|
||||
//
|
||||
// What is generated is the data, not the daemon's configuration language. One line per node,
|
||||
// in the form dnsmasq reads because that is what the module runs — and if a mesh runs something
|
||||
// else, this is the shape it translates from rather than a second thing to compute.
|
||||
|
||||
// ResolverPath is where the mesh writes what a node must answer.
|
||||
const ResolverPath = "/etc/mesh-resolver/nodes.conf"
|
||||
|
||||
// Wildcards is one line per node: everything under its name, and the name itself.
|
||||
//
|
||||
// A machine with no address is left out. A wildcard pointing at nothing is worse than no wildcard:
|
||||
// every name under it would resolve and then hang, where an unresolvable name fails at once and
|
||||
// says which name it was.
|
||||
func Wildcards(nodes []Node) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# Generated by the mesh. Do not edit — it is replaced whenever a machine\n")
|
||||
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n")
|
||||
b.WriteString("#\n")
|
||||
b.WriteString("# Each line answers the node's own name AND everything under it, so a service\n")
|
||||
b.WriteString("# is reached at <service>.<node>." + Suffix() + " without the mesh being told\n")
|
||||
b.WriteString("# the service exists. What routes it there once it arrives is the proxy's.\n\n")
|
||||
|
||||
named := make([]Node, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
if strings.TrimSpace(n.Address) == "" {
|
||||
continue
|
||||
}
|
||||
named = append(named, n)
|
||||
}
|
||||
sort.Slice(named, func(i, j int) bool { return named[i].Name < named[j].Name })
|
||||
|
||||
for _, n := range named {
|
||||
fmt.Fprintf(&b, "address=/%s/%s\n", InternalName(n.Name), n.Address)
|
||||
}
|
||||
if len(named) == 0 {
|
||||
b.WriteString("# No machine in this mesh has an address on the private network.\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ResolverGenerator answers what one node's resolver must know.
|
||||
type ResolverGenerator struct{ nodes []Node }
|
||||
|
||||
// ResolverFor builds it over the machines on the private network.
|
||||
func ResolverFor(nodes []Node) *ResolverGenerator { return &ResolverGenerator{nodes: nodes} }
|
||||
|
||||
// Resources is the one file. The daemon that reads it is the module's, not the mesh's.
|
||||
func (g *ResolverGenerator) Resources(node string) ([]map[string]any, bool, error) {
|
||||
var here bool
|
||||
for _, n := range g.nodes {
|
||||
if n.Name == node {
|
||||
here = true
|
||||
}
|
||||
}
|
||||
if !here {
|
||||
// Assigned and not yet on the network. Ordinary and brief.
|
||||
return nil, false, nil
|
||||
}
|
||||
return []map[string]any{{
|
||||
"id": "nodes", "type": "file", "path": ResolverPath,
|
||||
"content": Wildcards(g.nodes), "mode": "0644",
|
||||
}}, true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user