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 :%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.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 }