Files
mesh-sdk/go/compose/frombus/frombus.go
T
jschoubben 5d26c658f5 Compose a node's plans once per read, resolving each machine once (issue 1954)
A node's plans took 2.6-13.6 s of CPU on the provisioner: one composition
resolved every machine of the mesh some two hundred times (who is on the
private network, and the rest of the mesh, worked out again on every
look), three compositions did so again each, and the holder composed
twice. A composer now works each out once over its unchanging Input;
Composing gives Resolve, Render and Generators one composer;
PlansHeldTwice composes the wanted and the newest builds over one read of
the bus, says where the time went by phase, and the states reader keeps a
build's take-in by its content. WatchedStates takes a process's own
writes into the copy at once, so a holder reads its own states from it.

Measured on a captured novox mesh: 11.7 s to 0.49 s per composition of an
unchanged node, plans and provisions identical on all four nodes.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 16:18:44 +01:00

1168 lines
52 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 seat mesh-placement's holder judging a
// proposal for every node (ADR 0567 §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
// seat mesh-placement's state `placement`, one key per node (ADR 0567 §1);
// - 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; for each module placed on
// the composing node, its built manifest instead: the builder's done key at the trunk's FP2, or at the fingerprint
// the node's want names where it runs another build (Scope.KeptAt), resolved by the controller's own take-in rule
// (takein.Manifest), a moved module's templates archive read in the artifact store (issue 1126);
// - 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); of another node, only the keys its modules'
// served values fill (catalogue.ServedSettings), which every consumer is told (issue 1126).
//
// **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 holds the module it stands in for (or refuses the node, for a stand-in
// of no module placed there; 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"
"errors"
"fmt"
"maps"
"net/netip"
"slices"
"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/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
"git.novox.be/novox/mesh-sdk/go/takein"
)
// 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)
// Build is a module's build at a fingerprint, the builder's key (delivery.BuilderBuildsBucket, BuilderBuildKey); nil
// when the builder holds none. The composing node composes each of its modules from its build (issue 1126).
Build(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error)
// Archive is the archive at url, a kept reference routed to the artifact store as the composing node reaches it
// (catalogue.Routed), checked against digest: a moved module's templates archive, read back for its texts as the
// controller's take-in reads it (takein.Built).
Archive(ctx context.Context, url, digest string) ([]byte, error)
// Served is, of one module's config values on another node than the composing one (mesh-config's state `resolved`,
// keys `<node>.<module>.<key>`), the keys named, decoded: the keys its served values and zone name fill
// (catalogue.ServedSettings), which every consumer is told and so every node may read (issue 1126). Nil when none
// is set.
Served(ctx context.Context, node, module string, keys []string) (map[string]any, error)
}
// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (delivery.SignersBucket,
// key delivery.SignersAuthorityKey; ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the
// controller's and the vault's; one that has not, or that answers none, composes the controller's alone, as before.
type SignersReader interface {
// VaultAuthority is the vault's authority certificate as PEM, checked (delivery.DecodeSignerAuthority); empty when the
// vault has published none. A read that fails is said by the reader and answered empty: a node that may not read the
// state yet composes as before rather than not at all.
VaultAuthority(ctx context.Context) string
}
// HandedReader is a Reader that also says, of the composing node's modules, which the vault judged handed to the
// provisioner when it answered OwnSecrets (the vault's `own`, its `handed`: catalogue.AssignmentHands of the plan in
// each module's assignment). The node's composition then hands exactly those (compose.Node.Handed, issue 774), so the
// vault and the node never judge one module two ways. Nil, or a Reader without it (a vault older than the field),
// composes by the manifest and the machine, as before.
type HandedReader interface {
Handed(ctx context.Context, node string) map[string]bool
}
// 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. No longer read: a reader decodes a manifest and
// takes the indexer's verdict on it, and judges nothing (novox/hq ADR 0578 §3); kept so a caller that sets it builds.
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 (of another node's modules, only the values their served values fill
// are read, Reader.Served). KeptAt is, by module, the fingerprint the composing node's want names where it differs from
// the indexer's trunk: that node composes the module from its build at that fingerprint, as it runs it.
//
// **Composed from its build** (issue 1126): each module placed on the composing node, and each in KeptAt, composes from
// its built manifest (takein.Manifest over the builder's done key at the trunk's FP2 or the kept fingerprint), as the
// controller composes from the manifest its take-in resolved: so a moved module is handed to the provisioner, a resource
// naming an artifact names what was built, and a templated file has its text. A placed module the builder holds no done
// build of at the trunk's FP2 composes from the indexer's manifest, as before. Every leaves the catalogue as the indexer
// read it: the check of every node judges placement, and reads no archive.
//
// Placements, when set, stands in for the placement of the nodes it names: a proposal's candidates
// (delivery.PlacementCandidate), so a node composes in shadow with a proposal laid over and compares the result with
// what it runs (ADR 0567 §3); a nil value removes the node's placement. The rest is read as always.
//
// InOwn is the modules the composing node runs inside its own composition (InOwn of what it runs), composed there again
// (compose.Node.InOwn) rather than cut out as modules no build is given for (issue 1126).
//
// Running is what the composing node runs; of it, its own composition (`<node>.node`) is given to the composition
// (compose.Node.RunsOwn), so a module folded there that names a build of the old form is left to the node, carried
// as it runs (issue 1126). Nil composes every folded module from its manifest.
type Scope struct {
Node string
Every bool
KeptAt map[string]string
Placements map[string]*delivery.Placement
InOwn []string
Running []delivery.Composed
// HeldToday is, in an answer, the modules the node keeps today as they run ("node" for its own composition), each with
// how ("held", "failed", "runs a build of the old form"): a candidate that cannot compose one, or holds one only
// behind one of them, keeps it as today, which is said under CandidateAnswer.Held and is no refusal (issue 1126).
HeldToday map[string]string
// SameValue, in an answer, says whether two sealed values open to the same value (the node opens them with its own
// key), so a differing resource shows a value sealed again as such (delivery.CompareComposedWith). Nil shows them
// as differing.
SameValue func(a, b string) (same, known bool)
}
// InOwn is, of the modules p places on its node, those whose resources the node's own composition it runs
// (`<node>.node` in running) carries, by the module their ids name (declaration.ModuleGuess), sorted: the modules the
// controller's composed writer folded there because no fingerprint of the new form names them (mesh-wireguard, a module
// run at a build of the old form; issue 1126). A node composing itself passes it as Scope.InOwn, so its own
// composition keeps them as it runs them until a want names a build of the new form for one.
func InOwn(running []delivery.Composed, p delivery.Placement) []string {
placed := map[string]bool{}
for _, m := range p.Modules {
placed[m.Module] = true
}
in := map[string]bool{}
for _, c := range running {
if c.Module != "" {
continue
}
for _, raw := range c.Resources {
var head struct {
ID string `json:"id"`
Module string `json:"module"`
}
if json.Unmarshal(raw, &head) != nil || head.Module != "" {
continue
}
if m, ok := declaration.ModuleGuess(head.ID); ok && placed[m] {
in[m] = true
}
}
}
return sortedKeys(in)
}
// Input is the composition input for s, read through r.
//
// **A refused manifest never ends the read** (novox/hq ADR 0578 §3): each manifest is decoded leniently
// (catalogue.Decode) and the indexer's verdict on it taken from its modules value (delivery.TakeManifestVerdict, a missing
// one accepted); a module whose manifest the indexer's judge refused, or whose bytes do not decode, stands in the
// catalogue as its stub (unusable), so what it provides and requires still shapes the rest, and is named with why in
// the input's Refused; a field set aside is named in its SetAside. Every other module of the read goes on.
func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, error) {
return input(ctx, r, mesh, s, nil, nil)
}
// taken is a module's manifest as a reader takes it (novox/hq ADR 0578 §3): decoded leniently, with the indexer's verdict
// from its modules value. A refused one, or bytes that do not decode, stands as its stub (unusable) and why says so; a
// manifest not read past a field it set aside holds its name alone and is held the same.
func taken(name string, raw []byte, m delivery.Module) (man catalogue.Manifest, why string) {
if v := m.VerdictOf(name); v != nil {
if refused, _ := delivery.TakeManifestVerdict(v); refused {
return unusable(name, raw), v.Why()
}
}
man, err := catalogue.Decode(raw)
switch {
case err != nil:
return unusable(name, raw), fmt.Sprintf("the manifest of %s does not decode: %v", name, err)
case man.ReadNoFurther():
return unusable(name, raw), fmt.Sprintf("the manifest of %s could not be read past a field this reader does not "+
"know: %s", name, man.SetAside())
}
return man, ""
}
// input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in, or whose
// manifest the indexer refused, is also handed to refuse and composed as a stub (compose.Stub, kept for that node alone),
// so a node's answer names every module it cannot compose (issue 1126, issue 774); a module placed elsewhere whose
// manifest is refused is read for its offer alone (unusable). A manifest the bus does not answer for still ends the
// read: that is the bus's, not the module's.
func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module string, err error), wanted map[string]string) (compose.Input, error) {
b, err := readBase(ctx, r, mesh, s)
b.refuseOwn(refuse)
if err != nil {
return compose.Input{}, err
}
return b.finish(ctx, r, r.Build, mesh, s, b.in, refuse, wanted)
}
// base is the part of a node's input that the fingerprints it composes its modules at leave alone (issue 1954): read once
// for its plans at the wanted builds and at the newest, whose builds alone differ (PlansHeldTwice).
type base struct {
in compose.Input
placements map[string]delivery.Placement
records map[string]delivery.NodeRecord
modules map[string]delivery.Module
// ownRefused is each module placed on the composing node whose manifest the indexer refused, with why, in the order
// read: every scope holds it.
ownRefused [][2]string
}
// refuseOwn hands refuse each module placed on the composing node whose manifest is refused.
func (b base) refuseOwn(refuse func(module string, err error)) {
if refuse == nil {
return
}
for _, r := range b.ownRefused {
refuse(r[0], errors.New(r[1]))
}
}
// cloned is the input with its own catalogue, nodes and kept manifests, what finishing it sets: two scopes finished over
// one base never see each other's builds.
func (b base) cloned() compose.Input {
in := b.in
in.Catalogue = maps.Clone(b.in.Catalogue)
in.Nodes = slices.Clone(b.in.Nodes)
for i := range in.Nodes {
in.Nodes[i].Kept = maps.Clone(in.Nodes[i].Kept)
}
in.KeptCoreBoxes = slices.Clone(b.in.KeptCoreBoxes)
in.OwnSecrets = slices.Clone(b.in.OwnSecrets)
return in
}
// readBase is input up to the builds: placement, records, the indexer's modules and manifests, each node's values.
func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
var b base
placements, err := r.Placements(ctx)
if err != nil {
return b, fmt.Errorf("frombus: placement: %w", err)
}
if len(s.Placements) > 0 {
given := make(map[string]delivery.Placement, len(placements)+len(s.Placements))
for node, p := range placements {
given[node] = p
}
for node, p := range s.Placements {
if p == nil {
delete(given, node)
continue
}
if p.Node != node {
return b, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
}
given[node] = *p
}
placements = given
}
records, err := r.Records(ctx)
if err != nil {
return b, fmt.Errorf("frombus: node records: %w", err)
}
modules, err := r.Modules(ctx)
if err != nil {
return b, fmt.Errorf("frombus: the indexer's modules: %w", err)
}
if _, ok := placements[s.Node]; !ok && !s.Every {
return b, 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
}
}
}
}
// And the vault's authority beside it, once the vault publishes one (ADR 0550 §2): a certificate either signed checks
// against the bundle. The controller's stays while a certificate it issued is in a node's record.
if sr, ok := r.(SignersReader); ok {
if vault := sr.VaultAuthority(ctx); vault != "" {
in.Authority = delivery.AuthorityBundle(in.Authority, vault)
}
}
named := func(field *map[string]string, module, why string) {
if *field == nil {
*field = map[string]string{}
}
(*field)[module] = why
}
ownPlaced := map[string]bool{}
for _, pm := range placements[s.Node].Modules {
ownPlaced[pm.Module] = true
}
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
}
raw, err := r.Manifest(ctx, m.Manifest)
if err != nil {
// Not read is no refusal of the module: a bus that does not answer is waited out by the caller.
return b, fmt.Errorf("frombus: the manifest of %s (digest %s): %w", name, m.Manifest, err)
}
// One module's manifest the indexer refused refuses that module alone, by the rule a build that cannot be read
// follows (issue 774: power's bar item refused every node's composition, 2026-10-30; ADR 0578 §3: one refused
// manifest silenced disk-load for 45 hours). It stands in as its stub, so what it provides and requires still
// shapes the rest; it is held on the composing node where it is placed there, and elsewhere only its offer is read.
man, why := taken(name, raw, m)
if why != "" {
named(&in.Refused, name, why)
if ownPlaced[name] {
b.ownRefused = append(b.ownRefused, [2]string{name, why})
}
} else if aside := man.SetAside(); aside != "" {
named(&in.SetAside, name, aside)
}
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.
for _, name := range delivery.ProvidedModules() {
raw, err := json.Marshal(overlay.Manifest())
if err != nil {
return b, err
}
var private catalogue.Manifest
if err := json.Unmarshal(raw, &private); err != nil {
return b, fmt.Errorf("frombus: the manifest mesh-sdk provides for %s: %w", name, err)
}
in.Catalogue[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 b, fmt.Errorf("frombus: %s's config values on %s: %w", m.Module, node, err)
}
} else if keys := catalogue.ServedSettings(in.Catalogue[m.Module]); len(keys) > 0 {
// What a provider elsewhere serves from its settings is told to every consumer: the composing node reads
// those keys of it, and no other (issue 1126: mailu's smtp domain, a consumer's on shanks).
if values, err = r.Served(ctx, node, m.Module, keys); err != nil {
return b, fmt.Errorf("frombus: what %s serves on %s (%s): %w", m.Module, node,
strings.Join(keys, ", "), 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 {
// What its runtime serves as it runs: what a module composed as what runs of it contributes there (issue 1126).
n.RunsTools = compose.ToolsRunning(s.Running)
for _, module := range s.InOwn {
if n.InOwn == nil {
n.InOwn = map[string]bool{}
}
n.InOwn[module] = true
}
for i := range s.Running {
// The operator export as the vault runs it: the controller composes it from its store, which the node
// does not read; until the export is on the bus, the node carries the file it runs (issue 1126, C6:
// mesh-vault.kept on novox, "in what runs, and not composed").
if m := s.Running[i].Module; m != "" {
if kept := keptOf(s.Running[i]); kept != nil {
if in.KeptRuns == nil {
in.KeptRuns = map[string]map[string]map[string]any{}
}
if in.KeptRuns[s.Node] == nil {
in.KeptRuns[s.Node] = map[string]map[string]any{}
}
in.KeptRuns[s.Node][m] = kept
}
continue
}
if s.Running[i].Module == "" {
own := s.Running[i]
n.RunsOwn = &own
// Its bus membership as it runs it: the controller composes it from its store, which the node does
// not read; until the grants part hands memberships sealed to the node, the node's own composition
// carries the one it runs (issue 1126, seed #531 on novox: "bus-membership in what runs, not
// composed", which applied would have taken the node's bus credential away).
if sealed := membershipOf(own); sealed != "" {
if in.BusMemberships == nil {
in.BusMemberships = map[string]string{}
}
in.BusMemberships[s.Node] = sealed
}
}
}
}
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 b, fmt.Errorf("frombus: modules are placed on %s, whose agent has written no record yet",
strings.Join(missing, ", "))
}
b.in, b.placements, b.records, b.modules = in, placements, records, modules
return b, nil
}
// finish is input from its base on: the composing node's modules from their builds (build reads one), its own secrets.
// in is the base's input, or a copy of it (base.cloned) where one base is finished more than once.
func (b base) finish(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error),
mesh Mesh, s Scope, in compose.Input, refuse func(module string, err error), wanted map[string]string) (compose.Input, error) {
var err error
parse := parser(mesh)
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)
}
man, err := parse(raw)
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("frombus: the manifest of %s (digest %s) does not decode: %w", module, digest, err)
}
return man, nil
}
if !s.Every {
if _, ok := b.records[s.Node]; !ok {
return compose.Input{}, fmt.Errorf("frombus: %s has written no record of itself yet", s.Node)
}
if err := fromBuilds(ctx, r, build, &in, s, b.placements[s.Node], b.modules, read, parse, refuse, wanted); err != nil {
return compose.Input{}, err
}
// 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)
}
// And which of its modules the vault left out of that answer as handed: the node hands exactly those.
if hr, ok := r.(HandedReader); ok {
if handed := hr.Handed(ctx, s.Node); handed != nil {
for i := range in.Nodes {
if in.Nodes[i].Name == s.Node {
in.Nodes[i].Handed = handed
}
}
}
}
}
// 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 composed at the fingerprints fps
// gives each module in the Reading mode, as the controller composes them, by the one rule its answer to a candidate
// follows too (composeEach, issue 1126): a module refused alone (its build unread or not taken in, its composition
// refused, a stand-in for a value of its own the input does not carry, compose.ErrStoodIn) is held, named in held with
// why, and none of its own composition is in out, so the node keeps what it last applied of it and applies the rest; a
// node never applies a stand-in. Only a refusal that is no single module's (the node's own resources, a value served
// elsewhere, a stand-in for the node's own) refuses the node, err.
func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]delivery.Composed, skipped []string, held map[string]string, err error) {
held = map[string]string{}
// A node refused whole says what it held on the way, since the refusal can be of a held module's stub (issue 774,
// 2026-10-30 19:30: node-tools held, then "its build produced 0 bundle(s)" with its own refusal unsaid).
defer func() {
if err != nil && len(held) > 0 {
var said []string
for _, m := range sortedKeys(held) {
said = append(said, m+": "+held[m])
}
err = fmt.Errorf("%w (held before: %s)", err, strings.Join(said, "; "))
}
}()
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, fps)
if err != nil {
return nil, nil, held, 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, held, 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, held, fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err)
}
}
out, skipped, _, err = composeEach(in, s.Node, fps, true, s.Running, held, ranOf(ctx, r, mesh, s.Running, fps))
return out, skipped, held, err
}
// Plans is the provisioning plan of each module handed to the provisioner on the scope's node, composed from the bus
// alone (novox/hq ADR 0570 clause 2): the controller's handedPlans over the same input (mesh-control
// cmd/mesh-controller/compose_sdk.go), its rendering in the Foreseeing mode with the mesh's generators. A plan names
// templates, files, own secrets by name, items, collections and provisions, never a value, so no value is read for it:
// the seat mesh-provisioner's holder composes every node's plans with a Reader that reads no secret.
func Plans(ctx context.Context, r Reader, mesh Mesh, s Scope) (map[string]catalogue.Plan, error) {
in, err := Input(ctx, r, mesh, s)
if err != nil {
return nil, err
}
return PlansOf(in, s.Node)
}
// PlansHeld is Plans composed module by module (issue 774: one bad module must not drop its node): a module placed on
// the scope's node whose build cannot be read or taken in, or whose plan refuses the node's, is held, by the one rule
// Compose follows (composeEach, culprit): composed as its stub so what it provides and requires still shapes the rest,
// named in held with why, and none of its plan in plans. Only a refusal that is no single module's refuses the node,
// err. The seat mesh-provisioner's holder composes so: a node with one bad manifest still has every other module's plan.
func PlansHeld(ctx context.Context, r Reader, mesh Mesh, s Scope) (plans map[string]catalogue.Plan, held map[string]string, err error) {
held = map[string]string{}
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, nil)
if err != nil {
return nil, held, err
}
plans, err = plansHeldOf(in, s.Node, held)
return plans, held, err
}
// plansHeldOf is the plans of node over in, every module in held held, and each module a refusal is of held too until
// the plans compose (culprit's rule over the Planning mode: its stub first, then its bare stub).
func plansHeldOf(in compose.Input, node string, held map[string]string) (map[string]catalogue.Plan, error) {
return heldOf(in, node, held, func(in compose.Input) (map[string]catalogue.Plan, error) { return plansIn(in, node, compose.Planning) })
}
// heldOf composes compose1 over in with every module in held held, and each module a refusal is of held too until it
// composes (culprit's rule: its stub first, then its bare stub); what compose1 answers of a held module is left out.
// Plans and data items are composed so (PlansHeld, DataItemsHeld): one module never ends the read (novox/hq ADR 0578 §3).
func heldOf[V any](in compose.Input, node string, held map[string]string, compose1 func(compose.Input) (map[string]V, error)) (map[string]V, error) {
for _, m := range sortedKeys(held) {
in = stubbed(in, node, m, "")
}
// Each try holds one module more or leaves out all of one held module's offer: bounded by twice the modules placed.
for tries := 0; tries <= 2*len(assignedOn(in, node))+1; tries++ {
plans, err := compose1(in)
if err == nil {
for m := range held {
delete(plans, m)
}
return plans, nil
}
var order []string
for _, m := range assignedOn(in, node) {
if strings.Contains(err.Error(), m) && len(order) < maxCulprits {
order = append(order, m)
}
}
module, leave := "", ""
find:
for _, l := range []string{"", leaveAll} {
for _, m := range order {
if _, done := held[m]; done && l == "" {
continue
}
if _, again := compose1(stubbed(in, node, m, l)); again == nil || again.Error() != err.Error() {
module, leave = m, l
break find
}
}
}
if module == "" {
return nil, err
}
if _, done := held[module]; !done {
held[module] = err.Error()
}
in = stubbed(in, node, module, leave)
}
return nil, fmt.Errorf("frombus: what %s composes does not compose with %d module(s) held: %s", node, len(held),
strings.Join(sortedKeys(held), ", "))
}
// PlansOf is Plans over an input already filled, rendered in the Planning mode (compose.Planning): every own secret the
// input holds none for stands in, so no value and no record of one is needed.
func PlansOf(in compose.Input, node string) (map[string]catalogue.Plan, error) {
return plansIn(in, node, compose.Planning)
}
// plansIn is the controller's handedPlans in a mode: Foreseeing is the controller's own.
func plansIn(in compose.Input, node string, mode compose.Mode) (map[string]catalogue.Plan, error) {
// One composition for the three, so each node of the mesh is resolved once (issue 1954).
resolved, rendered, gens, err := compose.Composing(in, node, mode)
if err != nil {
return nil, fmt.Errorf("frombus: the plans of %s: %w", node, err)
}
with := rendered.Rendering
with.Generators = gens
composed, err := resolved.Plan.Compose(with)
if err != nil {
return nil, fmt.Errorf("frombus: the plans of %s: %w", node, err)
}
return composed.Plans, nil
}
// unusable is what stands in for a module's manifest the mesh refuses: its stub (compose.Stub)
// read from its JSON unjudged, so what it provides, requires and serves still shapes the rest, and without its items,
// since nothing judged what they would put into another module's collection. Bytes that are no manifest of the module
// stand in as a stub that offers nothing.
func unusable(module string, raw []byte) catalogue.Manifest {
var m catalogue.Manifest
if json.Unmarshal(raw, &m) != nil || m.Module != module {
m = catalogue.Manifest{Module: module}
}
m.Items = nil
return compose.Stub(m)
}
// parser reads a manifest as every reader does (novox/hq ADR 0578 §3): decoded leniently, judged by nothing here.
func parser(Mesh) func([]byte) (catalogue.Manifest, error) {
return catalogue.Decode
}
// ranOf is what a module last offered the others, read when it is held (composeEach, issue 1960): its manifest at the
// build its own composition runs at (running), where that is another build than the one it is composed at (fps) and
// the builder holds it done. False where nothing is known of it (it runs no composition of its own, it runs the build
// it is composed at, or its build is unread): the manifest it is composed from is its offer then. Only what it says to
// others is taken of it, which needs no take-in of its artifacts.
func ranOf(ctx context.Context, r Reader, mesh Mesh, running []delivery.Composed, fps map[string]string) func(string) (catalogue.Manifest, bool) {
parse := parser(mesh)
return func(module string) (catalogue.Manifest, bool) {
for _, c := range running {
if c.Module != module || c.FP == "" || c.FP == fps[module] {
continue
}
b, err := r.Build(ctx, module, c.FP)
if err != nil || b == nil || b.State != delivery.BuildDone || b.Manifest == "" {
return catalogue.Manifest{}, false
}
raw, err := r.Manifest(ctx, b.Manifest)
if err != nil {
return catalogue.Manifest{}, false
}
man, err := parse(raw)
return man, err == nil
}
return catalogue.Manifest{}, false
}
}
// fromBuilds puts in the built manifest of each module placed on the composing node, at the trunk's FP2, and of each
// module it keeps at another build (Scope.KeptAt, into its node's Kept), read from the builder's done key and resolved
// by the one take-in rule (takein.Manifest), a templates archive read in the artifact store as the node reaches it
// (issue 1126).
func fromBuilds(ctx context.Context, r Reader, build func(ctx context.Context, module, fp string) (*delivery.BuilderBuild, error),
in *compose.Input, s Scope, p delivery.Placement, modules map[string]delivery.Module,
read func(module, digest string) (catalogue.Manifest, error), parse func([]byte) (catalogue.Manifest, error),
refuse func(module string, err error), wanted map[string]string) error {
// stub keeps a refused module in the composition as a stub for this node alone (compose.Stub): what it provides
// and requires still shapes the rest.
stub := func(module string, err error) {
refuse(module, err)
for i := range in.Nodes {
if in.Nodes[i].Name != s.Node {
continue
}
man, ok := in.Nodes[i].Kept[module]
if !ok {
man, ok = in.Catalogue[module]
}
if !ok {
return
}
if in.Nodes[i].Kept == nil {
in.Nodes[i].Kept = map[string]catalogue.Manifest{}
}
in.Nodes[i].Kept[module] = compose.Stub(man)
}
}
address, addressErr := "", error(nil)
addressed := false
archive := func(ctx context.Context, reference, digest string) ([]byte, error) {
if !addressed {
address, addressErr = compose.ArtifactStoreAddress(*in, s.Node)
addressed = true
}
switch {
case addressErr != nil:
return nil, addressErr
case address == "":
return nil, fmt.Errorf("%s reaches no artifact store (%s)", s.Node, storeSought(*in))
}
return r.Archive(ctx, catalogue.Routed(reference, address), digest)
}
built := func(module, fp string, b *delivery.BuilderBuild) (catalogue.Manifest, error) {
if b.Manifest == "" {
return catalogue.Manifest{}, fmt.Errorf("frombus: the build of %s at %s names no manifest", module, fp)
}
// A take-in kept by the reader is read back as below, never shared: each composition holds its own manifest.
keeper, keeps := r.(takenInKeeper)
if keeps && !addressed {
address, addressErr = compose.ArtifactStoreAddress(*in, s.Node)
addressed = true
}
// Kept by the store's address too: a node that reaches no store takes in as before, and fails where it did.
key, keyed := takenInKey(module, fp, address, *b)
keyed = keyed && addressErr == nil && address != ""
if keeps && keyed {
if raw, ok := keeper.takenIn(key); ok {
resolved, err := parse(raw)
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("frombus: the built manifest of %s at %s: %w", module, fp, err)
}
return resolved, nil
}
}
src, err := read(module, b.Manifest)
if err != nil {
return catalogue.Manifest{}, err
}
resolved, err := takein.Manifest(ctx, src, *b, archive)
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("frombus: %s at %s: %w", module, fp, err)
}
// Read back as the mesh holds a built manifest (the controller keeps the resolved manifest's bytes and reads them
// as any manifest): a template's text is then in the manifest's own words, its ${config:…} in the settings form.
raw, err := json.Marshal(resolved)
if err != nil {
return catalogue.Manifest{}, err
}
if resolved, err = parse(raw); err != nil {
return catalogue.Manifest{}, fmt.Errorf("frombus: the built manifest of %s at %s: %w", module, fp, err)
}
if keeps && keyed {
keeper.keepTakenIn(key, raw)
}
return resolved, nil
}
// A module wanted at another build than the trunk's is composed from the build its want names (issue 774), as one
// the scope keeps at another build is.
keptAt := maps.Clone(s.KeptAt)
for _, pm := range p.Modules {
if m, ok := modules[pm.Module]; ok && m.FP2 != "" && wanted[pm.Module] != "" && wanted[pm.Module] != m.FP2 &&
keptAt[pm.Module] == "" && !slices.Contains(s.InOwn, pm.Module) {
if keptAt == nil {
keptAt = map[string]string{}
}
keptAt[pm.Module] = wanted[pm.Module]
}
}
runs := map[string]bool{}
for _, c := range s.Running {
if c.Module != "" {
runs[c.Module] = true
}
}
for _, pm := range p.Modules {
m, ok := modules[pm.Module]
if !ok || m.FP2 == "" || keptAt[pm.Module] != "" || slices.Contains(s.InOwn, pm.Module) {
// A module it runs inside its own composition is composed there as before: no build of the new form is what
// it runs (Scope.InOwn).
continue
}
if _, refused := in.Refused[pm.Module]; refused {
continue // held as its stub: no build of a refused manifest is taken in (ADR 0578 §3)
}
if man, ok := in.Catalogue[pm.Module]; !ok || (man.Build == nil && !man.Templated()) {
continue // nothing is built of it: its manifest is what it composes from
}
b, err := build(ctx, pm.Module, m.FP2)
if err != nil {
err = fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.FP2, err)
if refuse == nil {
return err
}
stub(pm.Module, err)
continue
}
if b == nil || b.State != delivery.BuildDone {
if refuse != nil && runs[pm.Module] {
// A module that runs is never composed from the indexer's manifest, which names no build's artifacts (no
// runtime's bundle): it is held, keeping what runs, until its build lands (issue 774, 2026-10-30 19:30:
// node-tools on ace, g14 and shanks, "its build produced 0 bundle(s)", and every module refused).
stub(pm.Module, fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build yet; it "+
"keeps running what it runs", pm.Module, m.FP2))
}
continue // nothing of it runs: composed from the indexer's manifest, as before
}
resolved, err := built(pm.Module, m.FP2, b)
if err != nil {
if refuse == nil {
return err
}
stub(pm.Module, err)
continue
}
in.Catalogue[pm.Module] = resolved
}
for i := range in.Nodes {
if in.Nodes[i].Name != s.Node {
continue
}
for _, module := range sortedKeys(keptAt) {
fp := keptAt[module]
b, err := build(ctx, module, fp)
switch {
case err != nil:
err = fmt.Errorf("frombus: the build of %s at %s: %w", module, fp, err)
case b == nil || b.State != delivery.BuildDone:
err = fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build", module, fp)
}
var resolved catalogue.Manifest
if err == nil {
resolved, err = built(module, fp, b)
}
if err != nil {
if refuse == nil {
return err
}
stub(module, err)
continue
}
if in.Nodes[i].Kept == nil {
in.Nodes[i].Kept = map[string]catalogue.Manifest{}
}
in.Nodes[i].Kept[module] = resolved
}
}
return nil
}
// 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 {
n := compose.Node{
Name: rec.Node,
Place: compose.Place{Key: rec.TunnelKey},
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,
}
placeNode(&n, p)
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
}
// placeNode sets what a person decided of n and where its modules run, from its placement; what the machine observes
// (its keys, links, networks, certificate) and its settings layers are left as they are.
func placeNode(n *compose.Node, p delivery.Placement) {
d := p.Decided
n.Place = compose.Place{Key: n.Place.Key, Endpoint: d.Endpoint, Site: d.Site, Hub: d.Hub, Address: d.Address}
n.Assigned, n.PublicDomain = p.Assigned(), d.PublicDomain
n.Account, n.AccountHome, n.AgentAccount, n.AgentAccountHome = d.Account, d.AccountHome, d.AgentAccount, d.AgentHome
n.Adopted, n.Taken = d.Adopted, d.Taken
n.Pins, n.Bound, n.Ports = p.ChosenPins(), p.Bound(), p.HostPorts()
}
// Placed is in with the placements given standing in for those of the nodes they name (issue 1126): each such node's
// decided parts, modules, pins, bindings, host ports, settings placement and holdings are the given placement's, its
// config values those in already carries for it (the layer of the node's own name), and what the machine observes is
// kept. A nil placement leaves the node in the mesh with nothing placed on it. A node in does not hold is refused: it has
// no record to compose it from. The found tunnel and the hub's peers are read from in as they are, not from the given
// placement. It is how a check composes a change's own placement over an input already filled (composer.Case's
// Placements).
func Placed(in compose.Input, placements map[string]*delivery.Placement) (compose.Input, error) {
if len(placements) == 0 {
return in, nil
}
out := in
out.Nodes = append([]compose.Node(nil), in.Nodes...)
index := map[string]int{}
for i, n := range out.Nodes {
index[n.Name] = i
}
for _, node := range sortedKeys(placements) {
p := placements[node]
i, ok := index[node]
if !ok {
return compose.Input{}, fmt.Errorf("frombus: a placement is given for %s, which the input holds no node for", node)
}
if p == nil {
p = &delivery.Placement{Node: node}
}
if p.Node != node {
return compose.Input{}, fmt.Errorf("frombus: the placement given for %s names the node %q", node, p.Node)
}
n := out.Nodes[i]
placeNode(&n, *p)
settings := map[string][]catalogue.Layer{}
for _, m := range p.Modules {
var values map[string]any
for _, l := range n.Settings[m.Module] {
if l.From == node {
values = l.Values
}
}
if layers := m.Layers(node, values); layers != nil {
settings[m.Module] = layers
}
}
n.Settings = nil
if len(settings) > 0 {
n.Settings = settings
}
out.Nodes[i] = n
}
out.Holdings = nil
for _, h := range in.Holdings {
if _, given := placements[h.Node]; !given {
out.Holdings = append(out.Holdings, h)
}
}
for _, node := range sortedKeys(placements) {
if p := placements[node]; p != nil {
out.Holdings = append(out.Holdings, p.Held()...)
}
}
return out, nil
}
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
}
// storeSought says, for a node that reaches no artifact store, what the composition holds of one: the nodes it holds and
// where each module serving the artifact store is placed, so the refusal names why (issue 1126).
func storeSought(in compose.Input) string {
var nodes, where []string
for _, n := range in.Nodes {
nodes = append(nodes, n.Name)
for _, a := range n.Assigned {
if m, ok := in.Catalogue[a]; ok {
if _, serves := m.Serves[catalogue.ArtifactStoreProvision]; serves {
where = append(where, a+" on "+n.Name)
}
}
}
}
sort.Strings(nodes)
sort.Strings(where)
said := "the composition holds " + strings.Join(nodes, ", ")
if len(where) == 0 {
return said + "; no module placed there serves " + catalogue.ArtifactStoreProvision
}
return said + "; " + catalogue.ArtifactStoreProvision + " is served by " + strings.Join(where, ", ") +
", none of it on the private network with a port"
}
// keptOf is the operator export's file (catalogue.KeptID) of a module's composition as it runs, or nil.
func keptOf(c delivery.Composed) map[string]any {
for _, raw := range c.Resources {
var r map[string]any
if json.Unmarshal(raw, &r) == nil && r["id"] == c.Module+"."+catalogue.KeptID() {
return r
}
}
return nil
}
// membershipOf is the sealed value of the bus membership resource (catalogue.BusMembershipID) of a node's own
// composition, or "".
func membershipOf(own delivery.Composed) string {
for _, raw := range own.Resources {
var r struct {
ID string `json:"id"`
Sealed string `json:"sealed"`
}
if json.Unmarshal(raw, &r) == nil && r.ID == catalogue.BusMembershipID() {
return r.Sealed
}
}
return ""
}
// DataItems is every data item the modules placed on scope's node declare, placed there, composed from the bus as Plans
// composes (novox/hq ADR 0571 clause 3): disk-load measures these. A module whose manifest the indexer refused is held
// and its items left out; DataItemsHeld names it, and a reader that must say what it leaves out reads that.
func DataItems(ctx context.Context, r Reader, mesh Mesh, s Scope) (map[string][]catalogue.PlacedDataItem, []string, error) {
items, unplaced, _, err := DataItemsHeld(ctx, r, mesh, s)
return items, unplaced, err
}
// DataItemsHeld is DataItems and, by module, why each module placed on scope's node is held: its manifest refused by
// the indexer's judge or not decoded (compose.Input.Refused). One refused manifest never ends the read (novox/hq ADR
// 0578 §3: disk-load measured nothing for 45 hours when one did).
func DataItemsHeld(ctx context.Context, r Reader, mesh Mesh, s Scope) (items map[string][]catalogue.PlacedDataItem, unplaced []string, held map[string]string, err error) {
held = map[string]string{}
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }, nil)
if err != nil {
return nil, nil, held, err
}
// Held module by module, as PlansHeld holds them: a refused stub or one module's composition refusal (an item's
// template) holds that module and never ends the read.
items, err = heldOf(in, s.Node, held, func(in compose.Input) (map[string][]catalogue.PlacedDataItem, error) {
var err error
items, unplaced, err = compose.DataItems(withoutHeldData(in, s.Node), s.Node)
return items, err
})
return items, unplaced, held, err
}
// withoutHeldData is in with no data item of a module held on node: none of a held module's items is measured, so its
// data never refuses the rest (its stub keeps its data for the backups the plans compose).
func withoutHeldData(in compose.Input, node string) compose.Input {
out := in
out.Nodes = append([]compose.Node(nil), in.Nodes...)
for i := range out.Nodes {
n := &out.Nodes[i]
if n.Name != node || len(n.Held) == 0 {
continue
}
kept := maps.Clone(n.Kept)
for m := range n.Held {
if man, ok := kept[m]; ok {
man.Data = nil
kept[m] = man
}
}
n.Kept = kept
}
return out
}