290 lines
12 KiB
Go
290 lines
12 KiB
Go
// Package frombus fills mesh-sdk's composition input from what is on the bus, so a node agent, the composition check
|
|
// and the grants part compose with one composer over the same inputs (novox/hq ADR 0547 §2, ADR 0549 §3; issue 1126).
|
|
//
|
|
// **One composer, three callers.** compose.Compositions is the composer; Input is how each caller fills its
|
|
// compose.Input from the bus: the node agent for its own node (ADR 0547 §2), the assigner's composition check for every
|
|
// node (ADR 0539 §6's check, ADR 0547 §4), and the grants part (ADR 0547 §5). Each reads the bus with its own credential
|
|
// through a Reader, so this package holds no bus client, no store and no timer: what it is given is all it reads.
|
|
//
|
|
// **What it reads, and from whom.**
|
|
// - which modules run on each node, with their pins, holdings, host ports, bindings and settings placement: the
|
|
// indexer's state `placement`, one key per node (ADR 0549 §2);
|
|
// - what a person decided of each node (its endpoint, site, hub, address on the private network, public domain and
|
|
// accounts): the same placement key, Placement.Decided;
|
|
// - what each node observes of itself (its tunnel key, capabilities, keys, certificate, links and networks): the
|
|
// node's own have key, `<node>.node` (HaveNode.Record); the tunnel found at a node's adoption and the names of the hub's peers are decided,
|
|
// in its placement, and whether the hub took the tunnel over and who enrolled under a peer's key are read from both;
|
|
// - every module's manifest: the indexer's `modules.<module>` and its manifests by digest, or the digest the
|
|
// composing node's want names where it runs another build (Scope.Kept);
|
|
// - each module's config values on a node: mesh-config's resolved state, the composing node's slice alone unless the
|
|
// caller composes every node (ADR 0547 §2, the prepare step's L4).
|
|
//
|
|
// **Not yet read** (each a later subtask of issue 1126): licences, own secrets, issued credentials and core boxes, which
|
|
// the vault answers sealed to the node (ADR 0550 §3, §5). Without them the composition stands in for each and names it
|
|
// foreseen, as compose.Reading does for a value the Input does not hold. No node is joining before ADR 0547's third
|
|
// milestone (ADR 0550 §6), so Joining is always empty.
|
|
package frombus
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/netip"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
)
|
|
|
|
// Reader is the bus as a caller reads it with its own credential. Each method reads one part's state; an error is a
|
|
// read that failed, never an absent key (an absent key is an empty answer).
|
|
type Reader interface {
|
|
// Placements is every node's placement, by node (delivery.PlacementBucket).
|
|
Placements(ctx context.Context) (map[string]delivery.Placement, error)
|
|
// Records is every node's record, by node: HaveNode.Record of each node's own have key, `<node>.node` of
|
|
// delivery.HaveBucket (ADR 0555 §3).
|
|
Records(ctx context.Context) (map[string]delivery.NodeRecord, error)
|
|
// Modules is every module the indexer holds, by module (delivery.ModulesBucket).
|
|
Modules(ctx context.Context) (map[string]delivery.Module, error)
|
|
// Manifest is the raw manifest of a digest (delivery.ManifestsBucket).
|
|
Manifest(ctx context.Context, digest string) ([]byte, error)
|
|
// Values is one module's config values on one node as mesh-config resolved them (mesh-config's state `resolved`,
|
|
// keys `<node>.<module>.<key>`), decoded; nil when it has none.
|
|
Values(ctx context.Context, node, module string) (map[string]any, error)
|
|
}
|
|
|
|
// Mesh is what is true of the whole mesh and is no part's state on the bus yet: the private network's range and the
|
|
// internal names' suffix as genesis was told them, and the mesh's authority certificate (public). Empty fields take
|
|
// compose's defaults.
|
|
type Mesh struct {
|
|
OverlayCIDR string
|
|
InternalSuffix string
|
|
Authority string
|
|
// Predefined is the list of predefined keys mesh-config published, which a manifest's values falling back to a
|
|
// predefined key are judged against; nil reads each manifest as the indexer does (catalogue.ParseManifestForInputs).
|
|
Predefined catalogue.PredefinedList
|
|
}
|
|
|
|
// Scope says who composes. Node is the node composing; only its config values are read unless Every is set, as the
|
|
// composition check sets it to compose every node. Kept is, by module, the manifest digest the composing node's wants
|
|
// name where it differs from the indexer's trunk: that node composes the module as it runs it.
|
|
type Scope struct {
|
|
Node string
|
|
Every bool
|
|
Kept map[string]string
|
|
}
|
|
|
|
// Input is the composition input for s, read through r.
|
|
func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, error) {
|
|
placements, err := r.Placements(ctx)
|
|
if err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: placement: %w", err)
|
|
}
|
|
records, err := r.Records(ctx)
|
|
if err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: node records: %w", err)
|
|
}
|
|
modules, err := r.Modules(ctx)
|
|
if err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: the indexer's modules: %w", err)
|
|
}
|
|
if _, ok := placements[s.Node]; !ok && !s.Every {
|
|
return compose.Input{}, fmt.Errorf("frombus: %s has no placement in the catalogue", s.Node)
|
|
}
|
|
|
|
in := compose.Input{OverlayCIDR: mesh.OverlayCIDR, InternalSuffix: mesh.InternalSuffix, Authority: mesh.Authority,
|
|
Catalogue: map[string]catalogue.Manifest{}}
|
|
read := func(module, digest string) (catalogue.Manifest, error) {
|
|
raw, err := r.Manifest(ctx, digest)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, fmt.Errorf("frombus: the manifest of %s (digest %s): %w", module, digest, err)
|
|
}
|
|
if mesh.Predefined != nil {
|
|
return catalogue.ParseManifestWith(raw, mesh.Predefined)
|
|
}
|
|
return catalogue.ParseManifestForInputs(raw)
|
|
}
|
|
for _, name := range sortedKeys(modules) {
|
|
m := modules[name]
|
|
if m.Manifest == "" {
|
|
continue // a registered module the indexer has not read: nothing to compose it from
|
|
}
|
|
man, err := read(name, m.Manifest)
|
|
if err != nil {
|
|
return compose.Input{}, err
|
|
}
|
|
in.Catalogue[name] = man
|
|
}
|
|
|
|
// The module the mesh provides itself (the private network) has no repository the indexer reads: its manifest comes
|
|
// with mesh-sdk and always wins, as the controller's providedModules and the grants part have it.
|
|
provided, err := json.Marshal(overlay.Manifest())
|
|
if err != nil {
|
|
return compose.Input{}, err
|
|
}
|
|
var private catalogue.Manifest
|
|
if err := json.Unmarshal(provided, &private); err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: the private network's manifest mesh-sdk provides: %w", err)
|
|
}
|
|
in.Catalogue[overlay.Name] = private
|
|
|
|
var missing []string
|
|
for _, node := range nodesOf(placements, records) {
|
|
rec, hasRecord := records[node]
|
|
p := placements[node]
|
|
if !hasRecord {
|
|
if len(p.Modules) > 0 {
|
|
missing = append(missing, node)
|
|
}
|
|
continue
|
|
}
|
|
n := nodeOf(rec, p)
|
|
withValues := s.Every || node == s.Node
|
|
for _, m := range p.Modules {
|
|
var values map[string]any
|
|
if withValues {
|
|
if values, err = r.Values(ctx, node, m.Module); err != nil {
|
|
return compose.Input{}, fmt.Errorf("frombus: %s's config values on %s: %w", m.Module, node, err)
|
|
}
|
|
}
|
|
if layers := m.Layers(node, values); layers != nil {
|
|
if n.Settings == nil {
|
|
n.Settings = map[string][]catalogue.Layer{}
|
|
}
|
|
n.Settings[m.Module] = layers
|
|
}
|
|
}
|
|
if node == s.Node {
|
|
for _, module := range sortedKeys(s.Kept) {
|
|
man, err := read(module, s.Kept[module])
|
|
if err != nil {
|
|
return compose.Input{}, err
|
|
}
|
|
if n.Kept == nil {
|
|
n.Kept = map[string]catalogue.Manifest{}
|
|
}
|
|
n.Kept[module] = man
|
|
}
|
|
}
|
|
in.Nodes = append(in.Nodes, n)
|
|
in.Holdings = append(in.Holdings, p.Held()...)
|
|
// A tunnel found at adoption is the node's private network's when the node's tunnel key is the tunnel's (the
|
|
// controller's inv.Tunnels); the hub's found tunnel is the mesh's adopted one whether or not the hub took it over
|
|
// (inv.AdoptedTunnel), and its peers are carried once it did (inv.CarriedPeers).
|
|
if t := p.Decided.Tunnel; t != nil {
|
|
if rec.TunnelKey == t.PublicKey {
|
|
if in.Tunnels == nil {
|
|
in.Tunnels = map[string]compose.FoundTunnel{}
|
|
}
|
|
in.Tunnels[node] = compose.FoundTunnel{Tunnel: tunnelOf(*t), NodeAdopted: p.Decided.Adopted}
|
|
}
|
|
if p.Decided.Hub {
|
|
in.Adopted = &compose.AdoptedTunnel{Tunnel: tunnelOf(*t), Hub: node, Adopted: rec.TunnelKey == t.PublicKey}
|
|
}
|
|
}
|
|
}
|
|
if in.Adopted != nil && in.Adopted.Adopted {
|
|
in.CarriedPeers = carriedPeers(in.Adopted.Tunnel, placements[in.Adopted.Hub].Decided.PeerNames, records)
|
|
}
|
|
if len(missing) > 0 {
|
|
return compose.Input{}, fmt.Errorf("frombus: modules are placed on %s, whose agent has written no record yet",
|
|
strings.Join(missing, ", "))
|
|
}
|
|
if !s.Every {
|
|
if _, ok := records[s.Node]; !ok {
|
|
return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node)
|
|
}
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.Compositions in the Reading
|
|
// mode at the fingerprints fps gives each module, as the controller composes them.
|
|
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (map[string]delivery.Composed, []string, error) {
|
|
in, err := Input(ctx, r, mesh, s)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return compose.Compositions(in, s.Node, fps, compose.Reading)
|
|
}
|
|
|
|
// nodeOf is the composition's node from its record (what the machine observes) and its placement (what a person
|
|
// decided of it, and where its modules run).
|
|
func nodeOf(rec delivery.NodeRecord, p delivery.Placement) compose.Node {
|
|
d := p.Decided
|
|
n := compose.Node{
|
|
Name: rec.Node,
|
|
Place: compose.Place{Key: rec.TunnelKey, Endpoint: d.Endpoint, Site: d.Site, Hub: d.Hub, Address: d.Address},
|
|
Assigned: p.Assigned(), PublicDomain: d.PublicDomain,
|
|
Account: d.Account, AccountHome: d.AccountHome, AgentAccount: d.AgentAccount, AgentAccountHome: d.AgentHome,
|
|
Adopted: d.Adopted, Taken: d.Taken, Pins: p.ChosenPins(), Bound: p.Bound(), Ports: p.HostPorts(), SealingKey: rec.SealingKey,
|
|
OutwardLinks: rec.OutwardLinks, Networks: rec.Networks, NetworksSaid: rec.NetworksSaid,
|
|
ServingKey: rec.ServingKey, Certificate: rec.Certificate, Contract: rec.Contract, Stated: rec.Contract > 0,
|
|
}
|
|
if len(rec.Capabilities) > 0 {
|
|
n.Capabilities = make(map[string]bool, len(rec.Capabilities))
|
|
for _, c := range rec.Capabilities {
|
|
n.Capabilities[c] = true
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func tunnelOf(t delivery.RecordTunnel) compose.Tunnel {
|
|
out := compose.Tunnel{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU,
|
|
Address: t.Address, Range: t.Range, PublicKey: t.PublicKey}
|
|
for _, p := range t.Peers {
|
|
out.Peers = append(out.Peers, compose.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// carriedPeers is every peer of the hub's adopted tunnel, by address, each with the node that enrolled under its key
|
|
// where one has (a node whose tunnel key is the peer's) and the name a person gave it (inv.CarriedPeers).
|
|
func carriedPeers(t compose.Tunnel, names map[string]string, records map[string]delivery.NodeRecord) []compose.CarriedPeer {
|
|
byKey := map[string]string{}
|
|
for node, rec := range records {
|
|
if rec.TunnelKey != "" {
|
|
byKey[rec.TunnelKey] = node
|
|
}
|
|
}
|
|
var out []compose.CarriedPeer
|
|
for _, p := range t.Peers {
|
|
address, _, _ := strings.Cut(p.Address, "/")
|
|
out = append(out, compose.CarriedPeer{PublicKey: p.PublicKey, Address: address, EnrolledAs: byKey[p.PublicKey], Named: names[p.PublicKey]})
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
a, errA := netip.ParseAddr(out[i].Address)
|
|
b, errB := netip.ParseAddr(out[j].Address)
|
|
if errA != nil || errB != nil {
|
|
return out[i].Address < out[j].Address
|
|
}
|
|
return a.Less(b)
|
|
})
|
|
return out
|
|
}
|
|
|
|
// nodesOf is every node named by a placement or a record, by name: the order the controller reads them in.
|
|
func nodesOf(placements map[string]delivery.Placement, records map[string]delivery.NodeRecord) []string {
|
|
seen := map[string]bool{}
|
|
for n := range placements {
|
|
seen[n] = true
|
|
}
|
|
for n := range records {
|
|
seen[n] = true
|
|
}
|
|
return sortedKeys(seen)
|
|
}
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|