Compose a node's own modules from their builds and read what providers elsewhere serve, so a self-composing node hands moved modules and settles served settings as the controller does (issue 1126)

frombus composed every module from the indexer's raw manifest: no moved module
was handed, so the node stood in for own secrets the vault leaves to the
provisioner (the seed's refusals on all four nodes), and a provider elsewhere
had no config values, so mailu's served domain refused shanks.

- takein: the controller's take-in of a done key (artifacts, launchers, the
  templates archive's texts, Resolve) as one SDK rule; catalogue.Launchers holds
  the launcher rule buildstep now delegates to.
- frombus: each module placed on the composing node, and each kept at another
  build (Scope.KeptAt, a fingerprint), composes from its built manifest; another
  node's modules give the keys their served values fill (Reader.Served).
- buscompose: Declared.Served and Principal.ServedReads grant every node the read
  of exactly those keys of mesh-config_resolved.
- compose.ArtifactStoreAddress: the store as the node's composition routes to it.
This commit is contained in:
2026-10-27 21:11:20 +01:00
parent 9f92233a28
commit 04de3e5da2
16 changed files with 890 additions and 45 deletions
+138 -20
View File
@@ -14,10 +14,13 @@
// - 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, or the digest the
// composing node's want names where it runs another build (Scope.Kept);
// - 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).
// 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
@@ -40,6 +43,7 @@ import (
"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
@@ -64,6 +68,18 @@ type Reader interface {
// 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
@@ -79,8 +95,16 @@ type Mesh struct {
}
// Scope says who composes. Node is the node composing; only its config values are read unless Every is set, as the
// composition check sets it to compose every node. Kept is, by module, the manifest digest the composing node's wants
// name where it differs from the indexer's trunk: that node composes the module as it runs it.
// 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
@@ -91,7 +115,7 @@ type Mesh struct {
type Scope struct {
Node string
Every bool
Kept map[string]string
KeptAt map[string]string
Placements map[string]*delivery.Placement
InOwn []string
}
@@ -178,15 +202,18 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
}
}
}
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)
}
if mesh.Predefined != nil {
return catalogue.ParseManifestWith(raw, mesh.Predefined)
}
return catalogue.ParseManifestForInputs(raw)
return parse(raw)
}
for _, name := range sortedKeys(modules) {
m := modules[name]
@@ -230,6 +257,13 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
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 {
@@ -245,16 +279,6 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
}
n.InOwn[module] = true
}
for _, module := range sortedKeys(s.Kept) {
man, err := read(module, s.Kept[module])
if err != nil {
return compose.Input{}, err
}
if n.Kept == nil {
n.Kept = map[string]catalogue.Manifest{}
}
n.Kept[module] = man
}
}
in.Nodes = append(in.Nodes, n)
in.Holdings = append(in.Holdings, p.Held()...)
@@ -284,6 +308,9 @@ func Input(ctx context.Context, r Reader, mesh Mesh, s Scope) (compose.Input, er
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); 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.
@@ -325,6 +352,97 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s
return compose.CompositionsApplied(in, s.Node, fps)
}
// 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)) error {
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.Node)
}
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] != "" {
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 {
return fmt.Errorf("frombus: the build of %s at %s: %w", pm.Module, m.FP2, err)
}
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 {
return err
}
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)
if err != nil {
return fmt.Errorf("frombus: the build of %s at %s: %w", module, fp, err)
}
if b == nil || b.State != delivery.BuildDone {
return fmt.Errorf("frombus: %s is wanted at %s, of which the builder holds no done build", module, fp)
}
resolved, err := built(module, fp, b)
if err != nil {
return err
}
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 {