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.
83 lines
3.3 KiB
Go
83 lines
3.3 KiB
Go
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
|
|
}
|