mesh/delivery merged: source.main moved to novox/mesh-sdk@307b6234e6bb
mesh/delivery-group group fix/1960-no-hold-spread merged: every member is merged
mesh/check pass dbcaa29a1a38 on 394fec8a63c1 | unit 3318 run, 1951 kept, 38 skipped | integration 3994 run, 2125 kept, 38 skipped | over bound: unit, …
On novox on 2026-10-29 de-spiegel, held for an unset config value, held mailu, route-proxy and nftables ("it reads
what de-spiegel contributes or listens on"), and mailu held postgres. mesh-sdk #128 (issue 1126) stopped the hold
spreading to consumers and collectors but kept dependents(): the provider of what a held module contributes to, and
the filter where it listens, were held behind it, transitively.
- Quick fix: drop dependents() alone. The filter and the proxy would then read a held module's new manifest, which
may not be what runs (review of the apply, round 2).
- Thorough fix, chosen: no module is held behind another, in the apply and in the answer (dependents, heldBehind and
the held-behind path of heldAsToday removed). A held module offers what it last offered: its contributions and
listens from its manifest at the build its own composition runs at (ranOf), else the manifest it is composed from.
Only the part of its offer that does not compose is left out, by provision (culprit): de-spiegel's routes leave
route-proxy without them, while mailu keeps its smtp account; the hold names it.
Tests: rows changed in the existing answer test (de-spiegel held, mailu and proxy composed; site held, the filter
composed with what runs; leave-out by provision). No new test function.
812 lines
35 KiB
Go
812 lines
35 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"
|
|
"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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 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.
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
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.
|
|
for _, name := range delivery.ProvidedModules() {
|
|
raw, err := json.Marshal(overlay.Manifest())
|
|
if err != nil {
|
|
return compose.Input{}, err
|
|
}
|
|
var private catalogue.Manifest
|
|
if err := json.Unmarshal(raw, &private); err != nil {
|
|
return compose.Input{}, 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 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, ranOf(ctx, r, mesh, s.Running, fps))
|
|
return out, skipped, held, err
|
|
}
|
|
|
|
// parser reads a manifest as the indexer does, against the mesh's predefined keys when given.
|
|
func parser(mesh Mesh) func([]byte) (catalogue.Manifest, error) {
|
|
return func(raw []byte) (catalogue.Manifest, error) {
|
|
if mesh.Predefined != nil {
|
|
return catalogue.ParseManifestWith(raw, mesh.Predefined)
|
|
}
|
|
return catalogue.ParseManifestForInputs(raw)
|
|
}
|
|
}
|
|
|
|
// 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, 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 ""
|
|
}
|