239 lines
7.9 KiB
Go
239 lines
7.9 KiB
Go
package compose
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"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)
|
|
}
|
|
|
|
// 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() {
|
|
plan, err := c.resolution(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 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] = 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
|
|
// (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.in.Catalogue, 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
|
|
}
|