Without them, a machine running the next holder of a seat beside the current one resolves as two holders, is refused, and drops out of the map — and with it the address every other machine composes for what it offers. Found live: the control node vanished from the private network the moment the new bus was assigned beside the old one, and nothing on any machine could be composed.
739 lines
29 KiB
Go
739 lines
29 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"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.
|
|
|
|
// DefaultOverlayCIDR is the range the mesh allocates from when nothing says another.
|
|
const DefaultOverlayCIDR = "10.42.0.0/16"
|
|
|
|
// overlayRange is the range the mesh allocates node addresses from.
|
|
//
|
|
// **The adopted tunnel's range first** (novox/hq ADR 0105): a hub that took over the tunnel it
|
|
// found is at that tunnel's address, its peers are at theirs, and every node's address is
|
|
// composed from the same range — the hub's, and every binding, hosts entry and endpoint derived
|
|
// from it. Those are readers of this; none of them stores the range. Without an adopted tunnel,
|
|
// the range genesis was told, or the default.
|
|
func overlayRange(ctx context.Context, inv *inventory.Inventory) (string, error) {
|
|
tunnel, _, adopted, err := inv.AdoptedTunnel(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if adopted {
|
|
return tunnel.Range, nil
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv(OverlayCIDRVar)); v != "" {
|
|
return v, nil
|
|
}
|
|
return DefaultOverlayCIDR, nil
|
|
}
|
|
|
|
func overlayCommand(ctx context.Context, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("overlay place <node> [flags], overlay name <address> <name>, 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)
|
|
case "name":
|
|
return overlayName(ctx, inv, args[1:])
|
|
|
|
default:
|
|
return fmt.Errorf("overlay has no %q; it has place, name and show", args[0])
|
|
}
|
|
}
|
|
|
|
// overlayName is the operator saying which machine a carried address is (novox/hq issue 112),
|
|
// so the mesh answers for its name until the machine enrols and verifies it.
|
|
func overlayName(ctx context.Context, inv *inventory.Inventory, args []string) error {
|
|
if len(args) != 2 {
|
|
return errors.New("overlay name <carried-address> <node-name>")
|
|
}
|
|
address, name := args[0], args[1]
|
|
if err := inv.NamePeer(ctx, address, name); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("the peer at %s is %s until it enrols — the mesh answers for %s.<suffix> from the "+
|
|
"operator's word, and enrolment under this key must use this name\n", address, name, name)
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// One hub per mesh, refused rather than last-write-wins: with two flagged, which one the
|
|
// graph and the broker address pick is order-dependent — the silent-election fault ADR 0007
|
|
// exists to avoid, one flag over (novox/hq issue 059). Moving the hub is explicit: re-place
|
|
// the old one without --hub first.
|
|
if *hub {
|
|
placed, err := inv.Overlays(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, p := range placed {
|
|
if p.Hub && p.Name != node {
|
|
return fmt.Errorf("%s is already the hub; a mesh has one. Re-place %s without "+
|
|
"--hub first if the hub is moving", p.Name, p.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A hub that took over a tunnel listens on that tunnel's port — it is what the peers dial, and
|
|
// the reason the port is worth having (novox/hq ADR 0105). An endpoint on another port would
|
|
// have the mesh's interface up where no peer is listening for it.
|
|
found, err := inv.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var tunnel inventory.Tunnel
|
|
adoptsTunnel := false
|
|
if *hub && found.Adopted {
|
|
if t, err := inv.TunnelOf(ctx, node); err == nil {
|
|
tunnel = t
|
|
placed, _ := inv.Overlays(ctx)
|
|
for _, o := range placed {
|
|
if o.Name == node && o.Key == t.PublicKey {
|
|
adoptsTunnel = true
|
|
}
|
|
}
|
|
} else if !errors.Is(err, inventory.ErrNoTunnel) {
|
|
return err
|
|
}
|
|
}
|
|
if adoptsTunnel {
|
|
if port := portOfEndpoint(*endpoint); port != strconv.Itoa(tunnel.Port) {
|
|
return fmt.Errorf("%s takes over the tunnel it found on %s, which listens on port %d, and "+
|
|
"its endpoint %q names another port: the peers dial the tunnel's port, so the hub's "+
|
|
"endpoint must be on it", node, tunnel.Interface, tunnel.Port, *endpoint)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
cidr, err := overlayRange(ctx, inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
address, err := inv.AssignAddress(ctx, found.ID, cidr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("%s is at %s on the overlay\n", node, address)
|
|
fmt.Println(" credentials issued for it before this placement keep their old broker address —" +
|
|
" `module issue` them again and push (novox/hq issue 059)")
|
|
switch {
|
|
case adoptsTunnel:
|
|
fmt.Printf(" the hub — it takes over the tunnel it found on %s: range %s, port %d, "+
|
|
"%d peer(s) carried until they enrol\n", tunnel.Interface, tunnel.Range, tunnel.Port,
|
|
len(tunnel.Peers))
|
|
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
|
|
}
|
|
// The tunnels adopted nodes take over, and the peers the hub's carries (novox/hq ADR 0105).
|
|
tunnels, err := inv.Tunnels(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
carried, err := inv.CarriedPeers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodes := make([]overlay.Node, 0, len(places))
|
|
for _, p := range places {
|
|
if !on[p.Name] {
|
|
continue
|
|
}
|
|
n := overlay.Node{
|
|
Name: p.Name, Key: p.Key, Endpoint: p.Endpoint,
|
|
Site: p.Site, Hub: p.Hub, Address: p.Address,
|
|
}
|
|
if t, takes := tunnels[p.Name]; takes && t.NodeAdopted {
|
|
// Only an adopted node is told to take the found unit over: on a converged one there
|
|
// is nothing found to keep, and the host refuses the field. The range and the carried
|
|
// peers do not depend on the mode; the takeover does.
|
|
//
|
|
// **Refused, not composed, when the hub's record disagrees with the tunnel.** A
|
|
// declaration that stopped the found unit and raised the mesh's interface on another
|
|
// port or address would leave every peer dark while reporting the tunnel taken — so a
|
|
// hub placed before it took the tunnel over (or at the wrong port) is named here, and
|
|
// nothing is sent until it is re-placed.
|
|
if wrong := disagrees(p, t.Tunnel); wrong != "" {
|
|
return nil, fmt.Errorf("%s takes over the tunnel on %s and its placement disagrees with it: %s. "+
|
|
"Re-place it — `overlay place %s --hub --endpoint <host>:%d …` — and push again; "+
|
|
"nothing was composed", p.Name, t.Interface, wrong, p.Name, t.Port)
|
|
}
|
|
n.TakesOver = &overlay.TakeOver{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU}
|
|
}
|
|
if p.Hub {
|
|
for _, c := range carried {
|
|
n.Carried = append(n.Carried, overlay.Carried{Key: c.PublicKey, Address: c.Address})
|
|
}
|
|
}
|
|
nodes = append(nodes, n)
|
|
}
|
|
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
|
|
}
|
|
cidr, err := overlayRange(ctx, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g, err := overlay.From(nodes, cidr, "")
|
|
if g != nil {
|
|
// The artifact store, as this network reaches it. Found rather than configured: the
|
|
// provider is whichever module offers it, on whichever machine holds that module — and if
|
|
// nothing does yet (genesis raises the registry before the catalogue knows it), there is
|
|
// no trust to write and nothing is written (novox/hq ADR 0082).
|
|
//
|
|
// Refused rather than composed without it when the question could not be answered: a
|
|
// declaration missing the trust because a lookup failed is a machine that cannot pull,
|
|
// delivered by a push that reported success — and nothing recomposes it until the next
|
|
// push (the shape of novox/hq issues 042/048, reappearing as a race).
|
|
at, port, found, storeErr := artifactStoreOnNetwork(ctx, inv, on)
|
|
if storeErr != nil {
|
|
return nil, fmt.Errorf("finding the artifact store this network reaches: %w", storeErr)
|
|
}
|
|
if found {
|
|
g.TrustRegistry(overlay.InternalName(at) + ":" + port)
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// The tunnel the hub took over, if any, and the peers carried from it (novox/hq ADR 0105):
|
|
// listed apart from the nodes, because they are peers of the tunnel and not nodes of the
|
|
// mesh until they enrol — and once one has, it is listed as the node it became.
|
|
tunnel, hubName, adopted, err := open.inventory.AdoptedTunnel(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
carried, err := open.inventory.CarriedPeers(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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 && adopted:
|
|
fmt.Printf(" hub — over the tunnel it took over on %s (range %s, port %d)",
|
|
tunnel.Interface, tunnel.Range, tunnel.Port)
|
|
case n.Hub && hubName == n.Name && tunnel.Interface != "":
|
|
fmt.Printf(" hub — found a tunnel on %s and did NOT take it over: its key is not the tunnel's; "+
|
|
"`mesh-host overlay take --tunnel %s` on the machine takes it", tunnel.Interface, tunnel.Interface)
|
|
case n.Hub:
|
|
fmt.Print(" hub — found no tunnel; if the machine runs the predecessor's, " +
|
|
"`mesh-host overlay take --tunnel <iface>` there adopts it (novox/hq ADR 0105)")
|
|
case !n.Reachable():
|
|
fmt.Print(" not dialable")
|
|
}
|
|
if n.Site != "" {
|
|
fmt.Printf(" at %s", n.Site)
|
|
}
|
|
if n.TakesOver != nil && !n.Hub {
|
|
fmt.Printf(" takes over %s", n.TakesOver.Interface)
|
|
}
|
|
fmt.Println()
|
|
for _, p := range computed[n.Name] {
|
|
fmt.Printf(" → %-14s %-18s %s\n", p.Name, p.Allowed, p.Why)
|
|
}
|
|
}
|
|
if len(carried) > 0 {
|
|
fmt.Printf("\npeers of the tunnel %s took over — not nodes of the mesh until they enrol:\n", hubName)
|
|
for _, c := range carried {
|
|
state := "not yet enrolled"
|
|
if c.EnrolledAs != "" {
|
|
state = "enrolled as " + c.EnrolledAs + ", which keeps this address"
|
|
}
|
|
fmt.Printf(" %-16s %-14s %s\n", overlay.CarriedName(c.PublicKey), c.Address, state)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// disagrees says how a node's placement differs from the tunnel it takes over — its address not
|
|
// the tunnel's, its endpoint not on the tunnel's port — or nothing when both agree.
|
|
func disagrees(p inventory.Overlay, t inventory.Tunnel) string {
|
|
var wrong []string
|
|
want := t.Address
|
|
if i := strings.Index(want, "/"); i >= 0 {
|
|
want = want[:i]
|
|
}
|
|
if p.Address != want {
|
|
wrong = append(wrong, fmt.Sprintf("its address is %s and the tunnel's is %s", orNothing(p.Address), want))
|
|
}
|
|
if p.Reachable() && portOfEndpoint(p.Endpoint) != strconv.Itoa(t.Port) {
|
|
wrong = append(wrong, fmt.Sprintf("its endpoint %s is not on the tunnel's port %d", p.Endpoint, t.Port))
|
|
}
|
|
return strings.Join(wrong, "; ")
|
|
}
|
|
|
|
func orNothing(s string) string {
|
|
if s == "" {
|
|
return "unset"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// portOfEndpoint is the port in host:port, or empty.
|
|
func portOfEndpoint(endpoint string) string {
|
|
if i := strings.LastIndex(endpoint, ":"); i >= 0 {
|
|
return endpoint[i+1:]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// 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 := onTheNetwork(ctx, inv, shelf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]string{}
|
|
for _, p := range places {
|
|
out[p.Name] = overlay.InternalName(p.Name)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// onTheNetwork is every placed machine that resolves the private network — has an address AND
|
|
// runs what puts it there. "Has an address" alone was true of every placed machine and told you
|
|
// nothing about whether anything could reach it; a name written for such a machine resolves to
|
|
// an address that does not answer, and a connection to it hangs (novox/hq issue 079).
|
|
func onTheNetwork(ctx context.Context, inv *inventory.Inventory,
|
|
shelf map[string]catalogue.Manifest) ([]inventory.Overlay, error) {
|
|
places, err := inv.Overlays(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// **With the seat holders on record**, or a machine running the next holder of a seat beside
|
|
// the current one resolves as two holders, is refused, and drops out of the map — taking the
|
|
// address every other machine composes for what it offers (novox/hq ADR 0131). Found live:
|
|
// the control node vanished from the private network the moment the new bus was assigned
|
|
// beside the old one.
|
|
holdings, err := inv.Holdings(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []inventory.Overlay
|
|
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, Holdings: holdings})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, m := range got.Modules {
|
|
for _, offered := range m.Offers() {
|
|
if offered == overlay.Requirement {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// onThePrivateNetwork is every node's address on the private network, sorted — the same set
|
|
// the names and the resolver mean by it (onTheNetwork), so a rule saying "from the mesh" admits
|
|
// exactly the machines the mesh names.
|
|
//
|
|
// 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,
|
|
shelf map[string]catalogue.Manifest) ([]string, error) {
|
|
places, err := onTheNetwork(ctx, inv, shelf)
|
|
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 — every machine
|
|
// that is on the private network, the same set the resolver means by that.
|
|
//
|
|
// A machine that is not 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. A machine placed on the overlay but not running
|
|
// the module that puts it there is exactly that (novox/hq issue 079).
|
|
func namesInTheMesh(ctx context.Context, inv *inventory.Inventory,
|
|
shelf map[string]catalogue.Manifest) (map[string]string, error) {
|
|
places, err := onTheNetwork(ctx, inv, shelf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]string{}
|
|
for _, p := range places {
|
|
out[overlay.InternalName(p.Name)] = p.Address
|
|
}
|
|
// And the carried peers the operator has named (novox/hq issue 112): machines the
|
|
// predecessor's resolver answers for and the mesh routes to, known by name on the operator's
|
|
// word until they enrol — at which point enrolment verifies the name and the node's own
|
|
// entry takes over above. A name the predecessor answers for must keep resolving until the
|
|
// machine behind it is a node; without these, taking the resolver silences three machines.
|
|
carried, err := inv.CarriedPeers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, p := range carried {
|
|
if p.Named == "" || p.EnrolledAs != "" {
|
|
continue
|
|
}
|
|
if _, taken := out[overlay.InternalName(p.Named)]; taken {
|
|
continue // a node of the mesh owns the name; the stale statement loses
|
|
}
|
|
out[overlay.InternalName(p.Named)] = p.Address
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// artifactStoreOnNetwork is the machine on this network that offers the artifact store, and the
|
|
// port THAT MACHINE put it on — the node's setting when it was given one (novox/hq ADR 0100,
|
|
// 04-ISSUES/102), the mesh's assignment when it made one, and the manifest's own number only when
|
|
// neither says anything. Read exactly as a consumer's binding is, because the trust a machine
|
|
// writes for the store and the address it pulls from are the same fact as what a consumer is told.
|
|
//
|
|
// A lookup failure is an error, never "not found": collapsing the two composed a declaration
|
|
// without the trust whenever the inventory hiccuped, delivered by a push that reported success —
|
|
// and nothing recomposed the machine until the next push. "No store" must mean the mesh has none,
|
|
// not that the question went unanswered.
|
|
func artifactStoreOnNetwork(ctx context.Context, inv *inventory.Inventory,
|
|
on map[string]bool) (node, port string, found bool, err error) {
|
|
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
return "", "", false, fmt.Errorf("reading the catalogue: %w", err)
|
|
}
|
|
providers := map[string]catalogue.Manifest{}
|
|
for name, m := range shelf {
|
|
if _, offers := m.Serves[catalogue.ArtifactStoreProvision]; offers {
|
|
providers[name] = m
|
|
}
|
|
}
|
|
if len(providers) == 0 {
|
|
return "", "", false, nil
|
|
}
|
|
// In a stated order, so two machines offering it would always answer the same one.
|
|
machines := make([]string, 0, len(on))
|
|
for machine := range on {
|
|
machines = append(machines, machine)
|
|
}
|
|
sort.Strings(machines)
|
|
for _, machine := range machines {
|
|
assigned, err := inv.Assigned(ctx, machine)
|
|
if err != nil {
|
|
return "", "", false, fmt.Errorf("reading what %s is assigned: %w", machine, err)
|
|
}
|
|
for _, a := range assigned {
|
|
m, offers := providers[a]
|
|
if !offers {
|
|
continue
|
|
}
|
|
serves, err := servedOnNode(ctx, inv, machine, m, catalogue.ArtifactStoreProvision)
|
|
if err != nil {
|
|
return "", "", false, fmt.Errorf("reading where %s puts the artifact store: %w", machine, err)
|
|
}
|
|
if p, ok := serves["port"]; ok {
|
|
return machine, fmt.Sprintf("%v", p), true, nil
|
|
}
|
|
}
|
|
}
|
|
return "", "", false, nil
|
|
}
|
|
|
|
// artifactStoreAddress is the artifact store as `forNode` reaches it: `<node>.internal:<port>`
|
|
// over the private network, or — when nothing is on the network yet — `127.0.0.1:<port>` for the
|
|
// node that holds the store itself, and "" for any other. The address composed into every
|
|
// reference the mesh built, at the moment it is used and never before (novox/hq 04-ISSUES/102).
|
|
//
|
|
// **Genesis places the network after the store, the broker, the vault and the catalogue.** Each
|
|
// of those is built and pushed to a node that is on no network, and the store is on that same
|
|
// node; an answer of "no store" there would refuse every one of those pushes and hand every one
|
|
// of those builds a base nothing can pull. Loopback is the truth on that machine, and it is the
|
|
// address genesis itself reaches the store by.
|
|
func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory,
|
|
shelf map[string]catalogue.Manifest, forNode string) (string, error) {
|
|
onNetwork, err := whereEveryoneIs(ctx, inv, shelf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
on := map[string]bool{}
|
|
for name := range onNetwork {
|
|
on[name] = true
|
|
}
|
|
node, port, found, err := artifactStoreOnNetwork(ctx, inv, on)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if found {
|
|
return overlay.InternalName(node) + ":" + port, nil
|
|
}
|
|
holder, port, found, err := artifactStoreHolder(ctx, inv)
|
|
if err != nil || !found || holder != forNode {
|
|
return "", err
|
|
}
|
|
return "127.0.0.1:" + port, nil
|
|
}
|
|
|
|
// artifactStoreHolder is whichever node is assigned a module offering the artifact store, on or
|
|
// off the network, and the port that node put it on.
|
|
func artifactStoreHolder(ctx context.Context, inv *inventory.Inventory) (node, port string, found bool, err error) {
|
|
nodes, err := inv.Nodes(ctx)
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
all := map[string]bool{}
|
|
for _, n := range nodes {
|
|
all[n.Name] = true
|
|
}
|
|
return artifactStoreOnNetwork(ctx, inv, all)
|
|
}
|