From cb1b7a6f5d1ef8ed1279e7ab834d246d1989f1ce Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 06:22:58 +0000 Subject: [PATCH] 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. --- go/compose/input.go | 167 +++++++++++++++++++++++++++++ go/compose/network.go | 238 ++++++++++++++++++++++++++++++++++++++++++ go/compose/resolve.go | 76 ++++++++++++++ go/compose/rest.go | 233 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 714 insertions(+) create mode 100644 go/compose/input.go create mode 100644 go/compose/network.go create mode 100644 go/compose/resolve.go create mode 100644 go/compose/rest.go diff --git a/go/compose/input.go b/go/compose/input.go new file mode 100644 index 0000000..42cf652 --- /dev/null +++ b/go/compose/input.go @@ -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 +} diff --git a/go/compose/network.go b/go/compose/network.go new file mode 100644 index 0000000..698055e --- /dev/null +++ b/go/compose/network.go @@ -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 :%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 +} diff --git a/go/compose/resolve.go b/go/compose/resolve.go new file mode 100644 index 0000000..f994cb9 --- /dev/null +++ b/go/compose/resolve.go @@ -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 +} diff --git a/go/compose/rest.go b/go/compose/rest.go new file mode 100644 index 0000000..f7509e2 --- /dev/null +++ b/go/compose/rest.go @@ -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 +}