Compose the private network and the rest of the mesh over a plain Input: Generators and TheRestOfTheMesh (issues 1360, 1361). Ported from mesh-control 6c2a8d62 network.go, plan.go, seat_pass.go. Store-backed tests (aMesh) are rebuilt as Inputs from the same values; the writes they exercised (a placement through overlay place, a seat passed through the store) are Input values or a returned Pass, and the controller-only tests of a failed read or write have no counterpart here. Unit level 1.04 s of 2 s.

This commit is contained in:
2026-10-20 06:22:58 +00:00
parent 3cd2bd8104
commit cb1b7a6f5d
4 changed files with 714 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
// Package compose is the controller's composition of a machine, as a function over a plain input (novox/hq ADR 0453
// clause 3, ADR 0446 clause 4): what the rest of the mesh offers and holds, the private network's generator, one
// node's resolution and, in the issues after, its rendering and its declaration.
//
// **It never allocates, makes, writes, logs or raises.** The controller composes with a store behind it; this
// package is its Reading and Foreseeing modes over data. Everything the controller reads from its store is a field of
// Input, and everything a send would make or record (a port, a credential, a seat passed on, a node left out) is
// returned for the caller to act on, never done. Input is never changed by a call. No field holds a secret value
// the composition needs: sealed values are optional inputs, and without them the composition uses stand-ins named
// foreseen. The logic is the controller's, ported from mesh-controller cmd/mesh-controller at commit
// 6c2a8d62100cb7d491f8eb9a623e1eeea5ec0de1 (plan.go, network.go, holdings.go, seat_pass.go), in the same order, with the
// same refusals in the same words, so that a comparison node for node is a fair one.
//
// It imports the standard library and mesh-sdk's own packages only.
package compose
import (
"errors"
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Input is everything the composition reads. Each field is what one read of the controller's inventory answers, named
// beside it; the controller's adapter fills it from its stores and the assigner from the mesh's facts and buckets
// (ADR 0446 clause 2).
type Input struct {
// Catalogue is the shelf: every module's manifest by name (inv.Catalogue), as it is to be composed. A recorded
// module kept at the build a machine runs (keepRecorded) is already in it, put there by the caller.
Catalogue map[string]catalogue.Manifest
// Nodes is every node the mesh has (inv.Nodes and inv.Overlays together, one row per node).
Nodes []Node
// Holdings is every seat whose holder is on record (inv.Holdings).
Holdings []catalogue.Held
// Licences is every licence the mesh holds, with the machines that use it (open.Licences); none for a mesh with no
// licence.
Licences []Licence
// OverlayCIDR is the range the mesh allocates from when no tunnel was adopted, as genesis was told it (the
// controller's OverlayCIDRVar); empty means DefaultOverlayCIDR. The package reads no environment.
OverlayCIDR string
// Tunnels is, by node name, the found tunnel whose key is the node's overlay key (inv.Tunnels).
Tunnels map[string]FoundTunnel
// Adopted is the hub's found tunnel and the hub's name, whether or not the hub took it (inv.AdoptedTunnel); nil
// when no hub has one.
Adopted *AdoptedTunnel
// CarriedPeers is every peer of the adopted tunnel (inv.CarriedPeers).
CarriedPeers []CarriedPeer
// Joining is the nodes that hold a live enrolment token (inv.NodesWithALiveToken).
Joining []string
}
// Node is one node of the mesh and what the composition reads about it.
type Node struct {
Name string
// Place is where it is on the private network (inv.Overlays).
Place Place
// Assigned is the modules assigned to it, in the order the controller reads them (inv.Assigned).
Assigned []string
// Capabilities are the ones present on it (inv.ProfileOf).
Capabilities map[string]bool
// PublicDomain is the domain it composes its routed names under (inv.PublicDomainOf).
PublicDomain string
// Account, AccountHome, AgentAccount and AgentAccountHome are its operator and agent logins (inv.NodeByName).
Account, AccountHome, AgentAccount, AgentAccountHome string
// Adopted is whether it is adopted rather than converged.
Adopted bool
// Pins is the provider each provision was pinned to for it (inv.PinsFor).
Pins map[string]catalogue.Chosen
// Bound is where each of its consumers of a provision that keeps data was last sent (inv.BindingsFor), by consumer
// module and provision.
Bound map[string]map[string]catalogue.Chosen
// Settings is its settings layers by module (inv.SettingsFor); a module with none is absent.
Settings map[string][]catalogue.Layer
// Ports is the machine port each module's software port was given on it (inv.PortsFor), by module then port.
Ports map[string]map[int]int
}
// Place is where a node is, as its placement says (inv.Overlay).
type Place struct {
Key string
Endpoint string
Site string
Hub bool
Address string
}
// Reachable says whether the node can be dialled.
func (p Place) Reachable() bool { return strings.TrimSpace(p.Endpoint) != "" }
// Tunnel is a tunnel found on a node (inv.Tunnel).
type Tunnel struct {
Interface string
Unit string
Config string
Port int
MTU int
// Address is the interface's own address with its prefix length, Range the network that prefix names.
Address string
Range string
PublicKey string
Peers []TunnelPeer
}
// TunnelPeer is one peer of a found tunnel.
type TunnelPeer struct {
PublicKey string
Address string
}
// FoundTunnel is a node's tunnel and whether the node is adopted (inv.FoundTunnel).
type FoundTunnel struct {
Tunnel
NodeAdopted bool
}
// AdoptedTunnel is the hub's tunnel, the hub's name, and whether the hub's overlay key is the tunnel's, which is
// whether the private network took the tunnel over.
type AdoptedTunnel struct {
Tunnel Tunnel
Hub string
Adopted bool
}
// CarriedPeer is a peer of the adopted tunnel (inv.CarriedPeer).
type CarriedPeer struct {
PublicKey string
Address string
EnrolledAs string
Named string
}
// Licence is one licence the mesh holds: its name, what a consumer may know of it, and the machines and modules
// that use it.
type Licence struct {
Name string
Serves map[string]any
Holders []Holder
}
// Holder is a module on a node that uses a licence.
type Holder struct {
Node string
Module string
}
// ErrNoSuchNode is what the controller's inventory answers for a node it does not have.
var ErrNoSuchNode = errors.New("no node of that name")
// node is the node of that name, or the refusal the controller's inventory gives for one it does not have.
func (in Input) node(name string) (Node, error) {
for _, n := range in.Nodes {
if n.Name == name {
return n, nil
}
}
return Node{}, fmt.Errorf("%w: %s", ErrNoSuchNode, name)
}
// sorted is the nodes by name, the order the controller reads them in.
func (in Input) sorted() []Node {
out := append([]Node(nil), in.Nodes...)
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
+238
View File
@@ -0,0 +1,238 @@
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
}
+76
View File
@@ -0,0 +1,76 @@
package compose
import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// LicenceProvision is what a module requires in order to be given a licence (the controller's licences.Provision).
const LicenceProvision = "model-access"
// resolution works out what one node should run, from what was assigned to it: the controller's planFor up to and
// including the resolution (plan.go planFor), and no further. A failure to compose the node's own modules is wrapped as
// notResolvable; every other failure is returned as it is. What it leaves to the issue after is the credentials of
// what the node takes from elsewhere and its settings.
func (c *composer) resolution(nodeName string) (catalogue.Resolution, error) {
node, err := c.in.node(nodeName)
if err != nil {
return catalogue.Resolution{}, err
}
shelf := c.in.Catalogue
world, _, _, err := c.theRestOfTheMesh(nodeName)
if err != nil {
return catalogue.Resolution{}, err
}
world.Pinned = node.Pins
// Where each of its consumers of a provision that keeps data was last sent (novox/hq ADR 0232).
world.Bound = node.Bound
onNetwork, err := c.whereEveryoneIs()
if err != nil {
return catalogue.Resolution{}, err
}
// What this mesh can answer with a record rather than a machine, and which record each of this node's modules was
// put on.
world.Licences, world.Using = c.licencesFor(nodeName)
resolved, err := catalogue.Resolve(shelf, node.Assigned,
catalogue.Node{Name: nodeName, Site: node.Place.Site, Capabilities: node.Capabilities,
At: onNetwork[nodeName], PublicDomain: node.PublicDomain,
Account: node.Account, AccountHome: node.AccountHome,
AgentAccount: node.AgentAccount, AgentAccountHome: node.AgentAccountHome}, world)
if err != nil {
// The node's own set does not compose: the only failure a mesh-wide gatherer may pass over.
return catalogue.Resolution{}, notResolvable{err}
}
return resolved, nil
}
// licencesFor is what this node can be answered with by record, and what it was put on. A mesh with no licences at all
// is the ordinary case and not an error.
func (c *composer) licencesFor(node string) (map[string][]catalogue.Record, map[string]map[string]catalogue.Record) {
all := c.in.Licences
if len(all) == 0 {
return nil, nil
}
offered := map[string][]catalogue.Record{}
byName := map[string]catalogue.Record{}
for _, one := range all {
record := catalogue.Record{Name: one.Name, Serves: one.Serves}
offered[LicenceProvision] = append(offered[LicenceProvision], record)
byName[one.Name] = record
}
using := map[string]map[string]catalogue.Record{}
for _, one := range all {
for _, h := range one.Holders {
if h.Node != node {
continue
}
if using[h.Module] == nil {
using[h.Module] = map[string]catalogue.Record{}
}
using[h.Module][LicenceProvision] = byName[one.Name]
}
}
return offered, using
}
+233
View File
@@ -0,0 +1,233 @@
package compose
import (
"errors"
"fmt"
"sort"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// notResolvable marks the one failure in a node's resolution that is a statement about the node: its assigned modules
// do not compose. Every other failure means the mesh could not be asked. Whoever gathers across the mesh skips a node
// that cannot resolve and never a node that could not be read (novox/hq 04-ISSUES/152).
type notResolvable struct{ err error }
func (n notResolvable) Error() string { return n.err.Error() }
func (n notResolvable) Unwrap() error { return n.err }
// unresolvable reports whether err is a node's own set failing to compose.
func unresolvable(err error) bool {
var n notResolvable
return errors.As(err, &n)
}
// Passes is what a composition found that the mesh must record, and did not: each seat held once whose holder on record
// is assigned nowhere that exactly one claimant takes over (novox/hq ADR 0449 clause 4), and the lines that say what was
// found. The caller records a pass and writes its hand act; this package does neither.
type Passes struct {
// Passes are the seats to pass, in the order they were found.
Passes []catalogue.SeatPass
// Said are the lines the finding says (a seat two claimants could take, one nothing can).
Said []string
}
// LeftOut is a node that does not resolve in the view of the rest of the mesh, with why: it offers nothing and holds
// nothing as far as every other machine's plan can tell (novox/hq issue 188).
type LeftOut struct {
Node string
Err error
}
// String is the line the controller says of it.
func (l LeftOut) String() string {
return fmt.Sprintf("%s is left out of the rest of the mesh: it does not resolve: %v", l.Node, l.Err)
}
// TheRestOfTheMesh is what every other node holds and offers, as the controller works it out (plan.go theRestOfTheMesh):
// two passes over the others, because what a node offers needs it resolved and resolving it may need what the mesh
// offers. The first takes brokered requirements on trust and answers only what each node offers; the second answers
// everything with that in hand. Nothing is declared from the first.
//
// It writes nothing. A seat the mesh passes on is returned in Passes for the caller to record; the World carries the
// holdings as they stand after the passes, and Input's own are unchanged. A node left out is returned with its reason, in
// the order the controller said them (the first pass's, then the second's when no node is excluded).
func TheRestOfTheMesh(in Input, exclude string) (catalogue.World, Passes, []LeftOut, error) {
c := newComposer(in)
return c.theRestOfTheMesh(exclude)
}
func (c *composer) theRestOfTheMesh(exclude string) (catalogue.World, Passes, []LeftOut, error) {
shelf := c.in.Catalogue
holdings := append([]catalogue.Held(nil), c.in.Holdings...)
nodes := c.in.sorted()
onNetwork, err := c.whereEveryoneIs()
if err != nil {
return catalogue.World{}, Passes{}, nil, err
}
type candidate struct {
node catalogue.Node
assigned []string
pins map[string]catalogue.Chosen
}
var others []candidate
// Every machine's assignments, before the holdings are read by any resolution (novox/hq ADR 0449 clause 4): a seat
// held once whose holder on record is assigned on no machine passes to its only claimant here.
everyAssigned := map[string][]string{}
for _, n := range nodes {
if len(n.Assigned) > 0 {
everyAssigned[n.Name] = n.Assigned
}
}
var passes Passes
holdings, passes = passSeatsOn(holdings, assignmentsOf(shelf, everyAssigned))
for _, n := range nodes {
if n.Name == exclude {
continue
}
theirs := everyAssigned[n.Name]
if len(theirs) == 0 {
continue
}
others = append(others, candidate{
node: catalogue.Node{Name: n.Name, Site: n.Place.Site, Capabilities: n.Capabilities,
At: onNetwork[n.Name], PublicDomain: n.PublicDomain},
assigned: theirs, pins: n.Pins})
}
var left []LeftOut
offered := map[string][]catalogue.Provider{}
var firstHeld []catalogue.Held
for _, o := range others {
got, err := catalogue.Resolve(shelf, o.assigned, o.node, catalogue.World{Unchecked: true, Holdings: holdings})
if err != nil {
// Said, not skipped: a machine dropped here offers nothing and holds nothing.
left = append(left, LeftOut{Node: o.node.Name, Err: err})
continue
}
firstHeld = append(firstHeld, got.Claims...)
for _, m := range got.Modules {
for _, name := range m.OffersAt(catalogue.ScopeMesh) {
// What that module says a consumer needs to know, with that node's settings on it.
serves, layers, err := c.servedOnNode(o.node.Name, m, name)
if err != nil {
return catalogue.World{}, Passes{}, nil, err
}
// And its own composed names, as that node composes them (novox/hq ADR 0346).
serves, leftOut, err := got.ServedOwnNames(m, name, serves, catalogue.WithDefaults(m, layers))
if err != nil {
return catalogue.World{}, Passes{}, nil, err
}
offered[name] = append(offered[name], catalogue.Provider{
Node: o.node.Name, At: o.node.At, Serves: serves, Module: m.Module, LeftOut: leftOut})
}
}
}
for k := range offered {
sort.Slice(offered[k], func(i, j int) bool {
return offered[k][i].Node < offered[k][j].Node
})
}
// The second pass is given the first pass's holdings (novox/hq ADR 0110).
world := catalogue.World{Offered: offered, Held: firstHeld, Holdings: holdings}
var held []catalogue.Held
for _, o := range others {
// Each machine is resolved with its own pins, as its plan is.
theirs := world
theirs.Pinned = o.pins
got, err := catalogue.Resolve(shelf, o.assigned, o.node, theirs)
if err != nil {
// Said only for the whole-mesh view: with one machine excluded, the view is partial by design.
if exclude == "" {
left = append(left, LeftOut{Node: o.node.Name, Err: err})
}
continue
}
held = append(held, got.Claims...)
}
world.Held = held
return world, passes, left, nil
}
// passSeatsOn finds, in the composition that finds it, the new holder of each seat held once whose holder on record is
// assigned nowhere and which has exactly one claimant able to hold it (novox/hq ADR 0449 clause 4), and answers the
// holdings as they are after it, with the passes. Nothing is recorded.
func passSeatsOn(holdings []catalogue.Held, assignments []catalogue.Assignment) ([]catalogue.Held, Passes) {
passes, said := catalogue.SeatsToPassOn(holdings, assignments)
out := Passes{Said: said}
for _, p := range passes {
out.Passes = append(out.Passes, p)
var next []catalogue.Held
for _, h := range holdings {
if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == p.Seat.Name {
continue
}
next = append(next, h)
}
p.To.Site = p.From.Site
holdings = append(next, p.To)
}
return holdings, out
}
// assignmentsOf are the assignments of every machine, as the passing of a seat reads them. A module assigned and
// missing from the shelf is still assigned: an assignment with no claims, so its holder is not taken for gone.
func assignmentsOf(shelf map[string]catalogue.Manifest, assigned map[string][]string) []catalogue.Assignment {
var out []catalogue.Assignment
names := make([]string, 0, len(assigned))
for node := range assigned {
names = append(names, node)
}
sort.Strings(names)
for _, node := range names {
for _, module := range assigned[node] {
m, ok := shelf[module]
if !ok {
m = catalogue.Manifest{Module: module}
}
out = append(out, catalogue.Assignment{Node: node, Manifest: m})
}
}
return out
}
// servedOnNode is what a module on a node tells a consumer of one of its provisions, with THAT node's ports on it: the
// port the node was given over the one the mesh assigned over the manifest's own, settled with the node's settings
// layers. The one derivation for every reader of a provider's address.
func (c *composer) servedOnNode(node string, m catalogue.Manifest, provision string) (map[string]any, []catalogue.Layer, error) {
ports, layers, err := c.portsGivenOn(node, m)
if err != nil {
return nil, nil, err
}
serves := catalogue.ServedOn(m, provision, ports)
if len(serves) > 0 {
serves, err = catalogue.Settle(serves, catalogue.WithDefaults(m, layers))
if err != nil {
return nil, nil, err
}
}
return serves, layers, nil
}
// portsGivenOn is where a node puts a module's ports — assigned, then given over them — and the node's settings layers
// for the module.
func (c *composer) portsGivenOn(node string, m catalogue.Manifest) (map[int]int, []catalogue.Layer, error) {
n, err := c.in.node(node)
if err != nil {
return nil, nil, err
}
ports := map[int]int{}
for wanted, at := range n.Ports[m.Module] {
ports[wanted] = at
}
layers := n.Settings[m.Module]
if given, err := catalogue.GivenPorts(m, layers); err == nil {
for wanted, at := range given {
ports[wanted] = at
}
}
return ports, layers, nil
}