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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user