256 lines
10 KiB
Go
256 lines
10 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 each node knows of itself (its place on the private network, capabilities, accounts, keys, networks,
|
|
// tunnel, and on the hub the adopted tunnel and its carried peers): the node-engine's state `records`;
|
|
// - 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"
|
|
"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 (delivery.NodeRecordsBucket).
|
|
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()...)
|
|
if rec.Tunnel != nil {
|
|
if in.Tunnels == nil {
|
|
in.Tunnels = map[string]compose.FoundTunnel{}
|
|
}
|
|
in.Tunnels[node] = compose.FoundTunnel{Tunnel: tunnelOf(*rec.Tunnel), NodeAdopted: rec.Adopted}
|
|
if rec.Place.Hub {
|
|
in.Adopted = &compose.AdoptedTunnel{Tunnel: tunnelOf(*rec.Tunnel), Hub: node, Adopted: rec.HubAdopted}
|
|
}
|
|
}
|
|
if rec.Place.Hub {
|
|
for _, c := range rec.Carried {
|
|
in.CarriedPeers = append(in.CarriedPeers, compose.CarriedPeer{PublicKey: c.PublicKey, Address: c.Address,
|
|
EnrolledAs: c.EnrolledAs, Named: c.Named})
|
|
}
|
|
}
|
|
}
|
|
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 and its placement.
|
|
func nodeOf(rec delivery.NodeRecord, p delivery.Placement) compose.Node {
|
|
n := compose.Node{
|
|
Name: rec.Node,
|
|
Place: compose.Place{Key: rec.Place.Key, Endpoint: rec.Place.Endpoint, Site: rec.Place.Site, Hub: rec.Place.Hub, Address: rec.Place.Address},
|
|
Assigned: p.Assigned(), PublicDomain: rec.PublicDomain,
|
|
Account: rec.Account, AccountHome: rec.AccountHome, AgentAccount: rec.AgentAccount, AgentAccountHome: rec.AgentHome,
|
|
Adopted: rec.Adopted, Pins: p.ChosenPins(), Bound: p.Bound(), Ports: p.HostPorts(), SealingKey: rec.SealingKey,
|
|
Taken: rec.Taken, 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
|
|
}
|
|
|
|
// 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
|
|
}
|