Files
mesh-sdk/go/compose/frombus/frombus.go
T
2026-10-26 20:08:32 +01:00

359 lines
16 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).
//
// **Own secrets and pair credentials** come from the vault sealed to the composing node's key (OwnSecrets, Credentials),
// for that node alone: as a consumer and as a provider, exactly the keys compose.CredentialsNeeded names. **Not yet
// read** (each a later subtask of issue 1126): licences (none is placed today) and core boxes. Without one
// the composition stands in for it, and Compose refuses a composition that stands in for anything (compose.ErrStoodIn):
// a node never applies a stand-in. 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)
// OwnSecrets is the own secrets of the composing node's modules, each sealed to that node's key, as the vault answers
// the node's engine (the vault's verb `own`, ADR 0550 §5); asked only for the node composing itself.
OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecretRow, error)
// Credentials is, for the composing node, each pair credential it reads as a consumer, held and sealed to its key,
// and each it grants as a provider, with the copy sealed to it (the vault's `own` with the keys, ADR 0550 §5). A key
// the vault holds no value for is left out; the composition then stands in for it, and Compose refuses it.
Credentials(ctx context.Context, node string, asConsumer, asProvider []compose.CredentialKey) ([]compose.Credential, []compose.Issued, 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{}}
if in.Authority == "" {
// The mesh's own certificate, public, as the composing node holds it; for the check of every node, as the first
// node holding one does.
if rec, ok := records[s.Node]; ok && rec.Authority != "" {
in.Authority = rec.Authority
} else {
for _, node := range sortedKeys(records) {
if a := records[node].Authority; a != "" {
in.Authority = a
break
}
}
}
}
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)
}
// Its own modules' secrets, sealed to its own key by the vault: a composition carries them sealed (ADR 0550 §5).
// Asked only when a module placed on it declares one: a node none of whose modules has a secret composes
// whether or not the vault answers.
if declaresOwnSecrets(in, s.Node) {
if in.OwnSecrets, err = r.OwnSecrets(ctx, s.Node); err != nil {
return compose.Input{}, fmt.Errorf("frombus: the own secrets of %s's modules: %w", s.Node, err)
}
}
// The core's credentials travel as the vault's box sealed to the machine (ADR 0349 §16): the same values, which
// the composition reads for a core module as its kept boxes.
for _, o := range in.OwnSecrets {
if json.Valid([]byte(o.Sealed)) {
in.KeptCoreBoxes = append(in.KeptCoreBoxes, compose.CoreBox{Node: o.Node, Module: o.Module, Name: o.Name,
SealedTo: o.SealedTo, Box: json.RawMessage(o.Sealed)})
}
}
}
return in, nil
}
// Compose is the node's compositions over the bus (ADR 0547 §2): Input for s, then compose.CompositionsApplied at the
// fingerprints fps gives each module: composed in the Reading mode as the controller composes them, and refused when it
// stands in for anything the input does not carry (compose.ErrStoodIn), since the node applies what it composed.
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
}
// The pair credentials its composition reads, asked of the vault sealed to it (ADR 0550 §5).
asConsumer, asProvider, err := compose.CredentialsNeeded(in, s.Node)
if err != nil {
return nil, nil, err
}
if len(asConsumer)+len(asProvider) > 0 {
if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil {
return nil, nil, fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err)
}
}
return compose.CompositionsApplied(in, s.Node, fps)
}
// declaresOwnSecrets says a module assigned to node declares an own secret, in the manifest the node composes it from.
func declaresOwnSecrets(in compose.Input, node string) bool {
for _, n := range in.Nodes {
if n.Name != node {
continue
}
for _, module := range n.Assigned {
m, ok := n.Kept[module]
if !ok {
m = in.Catalogue[module]
}
if len(m.OwnSecrets) > 0 {
return true
}
}
}
return false
}
// 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
}