Rename mesh-control -> mesh-controller, substrate -> foundation
One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/overlay"
|
||||
)
|
||||
|
||||
// the private network: who is on it, where, and what they are called.
|
||||
//
|
||||
// Split out of main.go, which had reached 2,769 lines because appending was always the
|
||||
// cheapest next step. That is how novox/hq ADR 0001 records `hal/sdk` reaching 34,636:
|
||||
// nothing in it was wrong, and no one edit was the one that should have been a new file.
|
||||
|
||||
func overlayCIDR() string {
|
||||
if v := strings.TrimSpace(os.Getenv(OverlayCIDRVar)); v != "" {
|
||||
return v
|
||||
}
|
||||
return "10.42.0.0/16"
|
||||
}
|
||||
|
||||
func overlayCommand(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("overlay place <node> [flags], or overlay show")
|
||||
}
|
||||
// Answered before anything is opened. A message about which command to use should not need a
|
||||
// database to say so, and needing one turns a redirect into a connection error.
|
||||
if args[0] == "push" {
|
||||
return errors.New("`overlay push` is now `push`, which sends a node its network AND " +
|
||||
"what its assignments resolve to — the two are computed from one picture of the " +
|
||||
"mesh, and sending them separately would let them disagree")
|
||||
}
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer open.Close()
|
||||
inv := open.inventory
|
||||
|
||||
switch args[0] {
|
||||
case "place":
|
||||
return overlayPlace(ctx, inv, args[1:])
|
||||
case "show":
|
||||
return overlayShow(ctx, open)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("overlay has no %q; it has place and show", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func overlayPlace(ctx context.Context, inv *inventory.Inventory, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New(
|
||||
"overlay place <node> [--endpoint host:port] [--site name] [--hub], or --nothing")
|
||||
}
|
||||
node := args[0]
|
||||
|
||||
set := flag.NewFlagSet("overlay place", flag.ContinueOnError)
|
||||
endpoint := set.String("endpoint", "", "where this node can be dialled, or empty for nowhere")
|
||||
site := set.String("site", "", "where this machine physically is, or empty if it roams")
|
||||
hub := set.Bool("hub", false, "this node is the hub every other routes through")
|
||||
nothing := set.Bool("nothing", false,
|
||||
"place it with nothing set: not dialable, no site, not the hub")
|
||||
if err := set.Parse(args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// **All three are declared together, so saying nothing took all three away.** The sibling of
|
||||
// `node public-domain`: `overlay place anchor` reads like it places the node it names, and it
|
||||
// silently unset the endpoint every other machine dials, the site it is in, and the hub if it
|
||||
// was the hub — every path through it going with them, at the moment somebody was trying to
|
||||
// look at it.
|
||||
//
|
||||
// A placement with nothing set is a real thing to want — a machine that roams and opens every
|
||||
// path itself is exactly that — so it keeps a way to say so, by name.
|
||||
if set.NFlag() == 0 {
|
||||
return fmt.Errorf("overlay place %s was given nothing to place it with, and all three are "+
|
||||
"declared together — it would take away the endpoint other machines dial %s at, its "+
|
||||
"site, and the hub if it is the hub. Say --endpoint/--site/--hub, or --nothing if that "+
|
||||
"is what you meant", node, node)
|
||||
}
|
||||
if *nothing && (*endpoint != "" || *site != "" || *hub) {
|
||||
return fmt.Errorf("give %s a placement or --nothing, not both: they say opposite things "+
|
||||
"and the mesh will not choose between them", node)
|
||||
}
|
||||
|
||||
// Declared, all three. The address is evidence of reachability and is not the fact, and hub
|
||||
// election by address prefix fails silently (novox/hq ADR 0007).
|
||||
if err := inv.SetPlace(ctx, node, *endpoint, *site, *hub, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
found, err := inv.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
address, err := inv.AssignAddress(ctx, found.ID, overlayCIDR())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s is at %s on the overlay\n", node, address)
|
||||
switch {
|
||||
case *hub:
|
||||
fmt.Println(" the hub — every node not sharing a site routes through it")
|
||||
case *endpoint == "":
|
||||
fmt.Println(" not dialable — it opens every path itself")
|
||||
}
|
||||
if *site != "" {
|
||||
fmt.Printf(" at %s, so it peers directly with anything else there\n", *site)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// network builds the private network over the machines that resolved the module for it.
|
||||
//
|
||||
// Not over every node the mesh knows. **A machine is on the private network because it was given
|
||||
// the module**, and one that was not is absent from every peer list and from the names — which is
|
||||
// the only thing "not on the network" can mean. Until this, having an address was enough, and
|
||||
// there was no way to keep a machine off.
|
||||
//
|
||||
// Every node at once, which is the whole reason this is the control plane's work: a peer list is
|
||||
// derived from all the others, so no node could compute its own.
|
||||
func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool,
|
||||
refused map[string]string) (*overlay.Generator, error) {
|
||||
places, err := inv.Overlays(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes := make([]overlay.Node, 0, len(places))
|
||||
for _, p := range places {
|
||||
if !on[p.Name] {
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, overlay.Node{
|
||||
Name: p.Name, Key: p.Key, Endpoint: p.Endpoint,
|
||||
Site: p.Site, Hub: p.Hub, Address: p.Address,
|
||||
})
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
// Nobody was given it. An empty network is a legitimate mesh, not a broken one, so this
|
||||
// answers rather than refusing -- Compute would refuse for want of a hub, and reporting
|
||||
// "no hub" to somebody who never asked for a network would be a lie about the cause.
|
||||
return overlay.Empty(), nil
|
||||
}
|
||||
g, err := overlay.From(nodes, overlayCIDR(), "")
|
||||
if err != nil && len(refused) > 0 {
|
||||
// The network is missing something, and some machines could not be resolved at all. Those
|
||||
// are almost always the same fact: a node that does not resolve contributes nothing, so
|
||||
// reporting "no hub" would name a consequence and hide the cause.
|
||||
var who []string
|
||||
for name, why := range refused {
|
||||
who = append(who, fmt.Sprintf(" %s: %s", name, why))
|
||||
}
|
||||
sort.Strings(who)
|
||||
return nil, fmt.Errorf("%w\n\nand %d node(s) could not be resolved at all, which is "+
|
||||
"probably why:\n%s", err, len(refused), strings.Join(who, "\n"))
|
||||
}
|
||||
return g, err
|
||||
}
|
||||
|
||||
// graph is the whole mesh's network, for showing it.
|
||||
func graph(ctx context.Context, open *stores) ([]overlay.Node, overlay.Graph, error) {
|
||||
inv := open.inventory
|
||||
on, refused, err := whoResolves(ctx, open, overlay.Requirement)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
g, err := network(ctx, inv, on, refused)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return g.Nodes(), g.Graph(), nil
|
||||
}
|
||||
|
||||
// whoResolves is the machines whose resolution answers a requirement, and why the others did not.
|
||||
//
|
||||
// By what a module **provides**, not by its name. WireGuard is one way to have a private network
|
||||
// and there could be others, so a machine is on the network because something it runs provides
|
||||
// one — asking for a particular module by name would be the mistake this whole mechanism exists
|
||||
// to avoid.
|
||||
//
|
||||
// Resolved rather than read from the assignment table, because a module can arrive by being
|
||||
// required by something else, and a machine that needs the private network to do its job is on it
|
||||
// for the same reason as one that was handed it directly.
|
||||
func whoResolves(ctx context.Context, open *stores, requirement string) (
|
||||
map[string]bool, map[string]string, error) {
|
||||
inv := open.inventory
|
||||
nodes, err := inv.Nodes(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
on := map[string]bool{}
|
||||
// Why a node could not be resolved, kept rather than raised: one broken node must not stop
|
||||
// the rest being described, and whoever is rendering that node will raise it themselves.
|
||||
refused := map[string]string{}
|
||||
for _, n := range nodes {
|
||||
plan, _, err := planFor(ctx, open, n.Name)
|
||||
if err != nil {
|
||||
refused[n.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
for _, m := range plan.Modules {
|
||||
for _, offered := range m.Offers() {
|
||||
if offered == requirement {
|
||||
on[n.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return on, refused, nil
|
||||
}
|
||||
|
||||
// rendering is everything a declaration needs, computed over the whole mesh.
|
||||
func generators(ctx context.Context, open *stores) (
|
||||
map[string]catalogue.Generator, error) {
|
||||
inv := open.inventory
|
||||
on, refused, err := whoResolves(ctx, open, overlay.Addressing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
net, err := network(ctx, inv, on, refused)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// **One generator now.** Two more used to sit beside it — the names and a resolver's zone
|
||||
// file — as modules that ran nothing. Both are facts a module asks for in its manifest
|
||||
// (`facts:` — catalogue.FactsInto), computed from the same machines this sees: the ones on the
|
||||
// private network, because a name for a machine not on it would resolve to an address nothing
|
||||
// can reach.
|
||||
return map[string]catalogue.Generator{
|
||||
overlay.Name: net,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func overlayShow(ctx context.Context, open *stores) error {
|
||||
nodes, computed, err := graph(ctx, open)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
// Not "this mesh has no nodes", which it said until the network became a module and was
|
||||
// then a lie about the cause: a mesh can have every node it will ever have and nobody on
|
||||
// the private network, because nobody asked for one.
|
||||
fmt.Printf("nobody is on the private network — assign %s to put a machine on it\n",
|
||||
overlay.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, n := range nodes {
|
||||
place := n.Address
|
||||
if place == "" {
|
||||
// Said, not skipped. A node with no place is a node with no network, and it should
|
||||
// be visible here rather than quietly absent from a list of who is on it.
|
||||
place = "no address — run `overlay place`"
|
||||
}
|
||||
fmt.Printf("%-16s %-14s", n.Name, place)
|
||||
switch {
|
||||
case n.Hub:
|
||||
fmt.Print(" hub")
|
||||
case !n.Reachable():
|
||||
fmt.Print(" not dialable")
|
||||
}
|
||||
if n.Site != "" {
|
||||
fmt.Printf(" at %s", n.Site)
|
||||
}
|
||||
fmt.Println()
|
||||
for _, p := range computed[n.Name] {
|
||||
fmt.Printf(" → %-14s %-18s %s\n", p.Name, p.Allowed, p.Why)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SilentFor is how long a node may be quiet before the mesh says so.
|
||||
//
|
||||
// A node speaks every minute, so three of them missed is a gap rather than a slow one. The number
|
||||
// is not the point — being able to say "out of touch" at all is, and nothing could before.
|
||||
const SilentFor = 3 * time.Minute
|
||||
|
||||
// whereEveryoneIs is each machine's name on the private network, for the ones on it.
|
||||
//
|
||||
// **Resolved without consulting the rest of the mesh**, and that is not an optimisation. Every
|
||||
// other path here answers a question about one node by resolving the others; this one is called
|
||||
// *from* that path, so doing the same would not terminate — which it did not, for two minutes,
|
||||
// until it was run.
|
||||
//
|
||||
// An unchecked resolution is exactly right for the question anyway. Whether a machine is on the
|
||||
// private network depends on what it was assigned and what that requires, both of which are local
|
||||
// facts. What it takes *from* other machines does not change the answer.
|
||||
//
|
||||
// The distinction that matters is kept: a machine absent from the network module's own view is
|
||||
// absent here, so "has an address" is not mistaken for "is reachable" — which it was, before the
|
||||
// network became something a machine is given.
|
||||
func whereEveryoneIs(ctx context.Context, inv *inventory.Inventory,
|
||||
shelf map[string]catalogue.Manifest) (map[string]string, error) {
|
||||
|
||||
if shelf == nil {
|
||||
// Refused rather than answered. Being on the private network is a conclusion about what a
|
||||
// node resolves to, so with no catalogue nothing resolves and the honest answer is
|
||||
// "nobody" — which is wrong, indistinguishable from a mesh with no overlay, and refused
|
||||
// every certificate the mesh was asked for while saying the machine was on no network.
|
||||
return nil, errors.New(
|
||||
"asked where everyone is without the catalogue, which cannot be answered")
|
||||
}
|
||||
places, err := inv.Overlays(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, p := range places {
|
||||
if p.Address == "" {
|
||||
continue
|
||||
}
|
||||
assigned, err := inv.Assigned(ctx, p.Name)
|
||||
if err != nil || len(assigned) == 0 {
|
||||
continue
|
||||
}
|
||||
caps, _ := inv.ProfileOf(ctx, p.Name)
|
||||
got, err := catalogue.Resolve(shelf, assigned,
|
||||
catalogue.Node{Name: p.Name, Site: p.Site, Capabilities: caps},
|
||||
catalogue.World{Unchecked: true})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, m := range got.Modules {
|
||||
for _, offered := range m.Offers() {
|
||||
if offered == overlay.Requirement {
|
||||
out[p.Name] = overlay.InternalName(p.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// onThePrivateNetwork is every node's address on the overlay, sorted.
|
||||
//
|
||||
// A node with no address is left out rather than rendered as an empty source: an empty entry in a
|
||||
// source set is a syntax error in the rule file, and a rule file that does not load leaves the
|
||||
// node filtering whatever it was filtering before -- the one outcome worse than a wrong rule,
|
||||
// because nothing reports it.
|
||||
func onThePrivateNetwork(ctx context.Context, inv *inventory.Inventory) ([]string, error) {
|
||||
places, err := inv.Overlays(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
for _, p := range places {
|
||||
if strings.TrimSpace(p.Address) != "" {
|
||||
out = append(out, p.Address)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// namesInTheMesh is every machine's internal name and the address behind it.
|
||||
//
|
||||
// A machine with no address has no name: writing one that resolves to nothing is worse than not
|
||||
// writing it, because a connection to an address that does not answer hangs where a name that
|
||||
// does not resolve fails at once and says so. That is the rule the hosts file already follows,
|
||||
// and this is the same set read the same way.
|
||||
func namesInTheMesh(ctx context.Context, inv *inventory.Inventory) (map[string]string, error) {
|
||||
places, err := inv.Overlays(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for _, p := range places {
|
||||
if strings.TrimSpace(p.Address) == "" {
|
||||
continue
|
||||
}
|
||||
out[overlay.InternalName(p.Name)] = p.Address
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user