Files
mesh-sdk/go/compose/network.go
T

263 lines
8.8 KiB
Go

package compose
import (
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/overlay"
)
// The private network: who is on it, where, and what they are called. Ported from mesh-controller
// cmd/mesh-controller/network.go at 6c2a8d62.
// DefaultOverlayCIDR is the range the mesh allocates from when nothing says another.
const DefaultOverlayCIDR = "10.42.0.0/16"
// composer is one composition over an Input: it reads Input, changes nothing in it, and keeps what the controller
// said on its standard error as lines to return.
type composer struct {
in Input
said []string
seen map[string]bool
}
func newComposer(in Input) *composer { return &composer{in: in, seen: map[string]bool{}} }
// say keeps a line the controller would have written to its log, once.
func (c *composer) say(format string, args ...any) {
line := fmt.Sprintf(format, args...)
if c.seen[line] {
return
}
c.seen[line] = true
c.said = append(c.said, line)
}
// suffix is what the mesh's internal names end in: the Input's, and overlay.DefaultSuffix when it gives none. The package never calls
// overlay.Suffix or overlay.InternalName, which read the environment, so the assigner and the controller compose the same names
// from the same Input.
func (c *composer) suffix() string {
if v := strings.TrimPrefix(strings.TrimSpace(c.in.InternalSuffix), "."); v != "" {
return v
}
return overlay.DefaultSuffix
}
// internalName is a node's name inside the mesh.
func (c *composer) internalName(node string) string { return node + "." + c.suffix() }
// saidBeside is the lines already said and then every line the composer kept, once each, in the order they were found.
func (c *composer) saidBeside(lines []string) []string {
for _, l := range c.said {
if !slices.Contains(lines, l) {
lines = append(lines, l)
}
}
return lines
}
// overlayRange is the range the mesh allocates node addresses from: the adopted tunnel's first (novox/hq ADR 0105),
// then the range genesis was told, then the default.
func (c *composer) overlayRange() string {
if a := c.in.Adopted; a != nil && a.Adopted {
return a.Tunnel.Range
}
if v := strings.TrimSpace(c.in.OverlayCIDR); v != "" {
return v
}
return DefaultOverlayCIDR
}
// network builds the private network over the machines that resolved the module for it. A machine is on it because it
// was given the module, or because it is joining with a key and an address (novox/hq ADR 0169).
func (c *composer) network(on map[string]bool, refused map[string]string) (*overlay.Generator, error) {
tunnels := c.in.Tunnels
carried := c.in.CarriedPeers
isJoining := map[string]bool{}
for _, name := range c.in.Joining {
isJoining[name] = true
}
var nodes []overlay.Node
for _, node := range c.in.sorted() {
p := node.Place
if !on[node.Name] && !(isJoining[node.Name] && p.Key != "" && p.Address != "") {
continue
}
n := overlay.Node{
Name: node.Name, Key: p.Key, Endpoint: p.Endpoint,
Site: p.Site, Hub: p.Hub, Address: p.Address,
}
if t, takes := tunnels[node.Name]; takes && t.NodeAdopted {
// Only an adopted node is told to take the found unit over. A hub whose placement disagrees with the tunnel is
// refused, not composed.
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", node.Name, t.Interface, wrong, node.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 _, cp := range carried {
n.Carried = append(n.Carried, overlay.Carried{Key: cp.PublicKey, Address: cp.Address})
}
}
nodes = append(nodes, n)
}
if len(nodes) == 0 {
// Nobody was given it: an empty network is a legitimate mesh, not a broken one.
return overlay.Empty(), nil
}
g, err := overlay.From(nodes, c.overlayRange(), "")
if err != nil && len(refused) > 0 {
// The network is missing something, and some machines could not be resolved at all: almost always the same fact.
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: the nodes on it and each one's peers.
func Graph(in Input) ([]overlay.Node, overlay.Graph, error) {
c := newComposer(in)
on, refused, err := c.whoResolves(overlay.Requirement)
if err != nil {
return nil, nil, err
}
g, err := c.network(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, resolved rather than read from the assignments.
func (c *composer) whoResolves(requirement string) (map[string]bool, map[string]string, error) {
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.
refused := map[string]string{}
for _, n := range c.in.sorted() {
resolved, err := c.planFor(n.Name)
switch {
case unresolvable(err):
refused[n.Name] = err.Error()
continue
case err != nil:
// Not a node that does not resolve: a question that went unanswered.
return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err)
}
for _, m := range resolved.Plan.Modules {
for _, offered := range m.Offers() {
if offered == requirement {
on[n.Name] = true
}
}
}
}
return on, refused, nil
}
// Generators is what every node's declaration is composed with, over the whole mesh: the private network's generator
// (one generator now; the names and a resolver's zone file are facts a module asks for in its manifest).
func Generators(in Input) (map[string]catalogue.Generator, error) {
c := newComposer(in)
return c.generators()
}
func (c *composer) generators() (map[string]catalogue.Generator, error) {
on, refused, err := c.whoResolves(overlay.Addressing)
if err != nil {
return nil, err
}
net, err := c.network(on, refused)
if err != nil {
return nil, err
}
return map[string]catalogue.Generator{overlay.Name: net}, 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 Place, t 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 ""
}
// whereEveryoneIs is each machine's name on the private network, for the ones on it. Resolved without consulting the
// rest of the mesh (it is called from that path), and refused without the catalogue.
func (c *composer) whereEveryoneIs() (map[string]string, error) {
if c.in.Catalogue == nil {
return nil, errors.New("asked where everyone is without the catalogue, which cannot be answered")
}
out := map[string]string{}
for _, p := range c.onTheNetwork() {
out[p.Name] = c.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
// (novox/hq issue 079). A machine that does not resolve is not counted, and said.
func (c *composer) onTheNetwork() []Node {
var out []Node
for _, n := range c.in.sorted() {
if n.Place.Address == "" {
continue
}
if len(n.Assigned) == 0 {
continue
}
got, err := catalogue.Resolve(c.shelfFor(n), n.Assigned,
catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities},
catalogue.World{Unchecked: true, Holdings: c.in.Holdings})
if err != nil {
c.say("%s is not counted as on the network: it does not resolve: %v", n.Name, err)
continue
}
for _, m := range got.Modules {
for _, offered := range m.Offers() {
if offered == overlay.Requirement {
out = append(out, n)
}
}
}
}
return out
}