The tunnel the hub took over routes to machines the predecessor knows by name and the mesh knew only by address — taking the resolver in that state silences three machines at once. Now the operator states which machine a carried address is (overlay name <address> <name>), the statement rides tunnel_peer.named, and namesInTheMesh answers for named not-yet-enrolled peers — one reading, so the hosts fact, a container's hosts and the resolver cannot disagree. Enrolment verifies the word: a machine enrolling under a named peer's key with a different name is refused where the operator can read it, the stated name keeps the carried address, and an enrolled peer's name is the node's — naming it again refuses. The issue's rule holds: a name the predecessor answers for keeps resolving until the machine behind it is a node.
730 lines
28 KiB
Go
730 lines
28 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}
|
|
}
|
|
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
|
|
}
|
|
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})
|
|
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)
|
|
}
|