// 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` (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.` 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` 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 `..`), 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 }