Files
mesh-sdk/go/compose/frombus/frombus.go
T
jschoubben 0de3006415
mesh/delivery merged: source.main moved to novox/mesh-sdk@98f2f2406921
mesh/delivery-group group fix/1126-apply-culprit merged: every member is merged
mesh/check pass 0de30064156a on 7aedd28f01dc | unit 2667 run, 590 kept, 36 skipped | integration 2676 run, 633 kept, 29 skipped | over bound: unit (sl…
A module refused alone is held, composed as what runs of it, on the node that applies as in its answer: one rule for both paths (issue 1126)
The applying path (frombus.Compose) composed with no culprit, so one module's
refusal came back as the whole node's: novox's apply would have refused all its
modules for de-spiegel's unset route names. composeEach is now the one rule
Compose and Answer both follow:
- a module whose stub changes the refusal is held (compose.Node.Held); in the
  applying path a stand-in for a module placed here holds that module; only a
  refusal of no single module refuses the node;
- a held module is composed from what runs of it (compose.Carried of its own
  composition in Running) and carried in the node's own wherever that runs it,
  folded or leaving the fold, so the node's own apply never removes it (issue
  1945 by a new path) and what the others read of it is what runs;
- none of a held module's own composition is in what is applied;
- a module whose composition differs when the held ones are composed as stubs
  is held too; the node's own differing refuses the node;
- since a held module is composed from its new manifest, what reads that
  manifest is held with it: the providers of what it contributes to (the
  proxy its routes) and, when it listens, the filter's holder.

Quick fix weighed: a culprit loop in Compose alone. Thorough, chosen: one
shared function, since the kind met twice (seed-8 shadow, now the apply), and
review rounds 1 and 2 found its leaks.
2026-10-28 17:22:35 +01:00

757 lines
32 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; 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"
"fmt"
"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)
}
// 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 (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 indexer's placement of the nodes it names: a node's candidate placement
// (delivery.PlacementCandidate), so a node composes in shadow with a catalogue pull request's placement and compares
// the result with what it runs (issue 1126); 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
// 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.
func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, error) {
return input(ctx, r, mesh, s, nil)
}
// input is Input; with refuse set, a module placed on the composing node whose build cannot be read or taken in is
// handed to refuse and composed as a stub (compose.Stub, kept for that node alone) rather than ending the read, so a
// node's answer names every module it cannot compose (issue 1126).
func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module string, err error)) (compose.Input, error) {
placements, err := r.Placements(ctx)
if err != nil {
return compose.Input{}, 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 compose.Input{}, 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 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
}
}
}
}
parse := func(raw []byte) (catalogue.Manifest, error) {
if mesh.Predefined != nil {
return catalogue.ParseManifestWith(raw, mesh.Predefined)
}
return catalogue.ParseManifestForInputs(raw)
}
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)
}
return parse(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)
}
} 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 compose.Input{}, 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 {
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 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)
}
if err := fromBuilds(ctx, r, &in, s, placements[s.Node], modules, read, parse, refuse); 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)
}
}
// 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{}
in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() })
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)
return out, skipped, held, err
}
// 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, 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)) 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)
}
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)
}
return resolved, nil
}
for _, pm := range p.Modules {
m, ok := modules[pm.Module]
if !ok || m.FP2 == "" || s.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 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 := r.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 {
continue // no done build at the trunk's: 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(s.KeptAt) {
fp := s.KeptAt[module]
b, err := r.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 ""
}