From 233b6be5ddd15ed274252db429287f47716f1125 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 22:31:02 +0100 Subject: [PATCH] Compose each node's provisioning plans from the bus alone, in a Planning mode that reads no secret, so mesh-provisioner composes the plans the controller did (issue 1954 L6-P1, ADR 0570 clause 2) --- go/compose/credentials.go | 8 ++++++ go/compose/frombus/frombus.go | 42 ++++++++++++++++++++++++++++++ go/compose/frombus/frombus_test.go | 18 +++++++++++++ go/compose/render.go | 2 +- 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/go/compose/credentials.go b/go/compose/credentials.go index 9766887..8d39bbd 100644 --- a/go/compose/credentials.go +++ b/go/compose/credentials.go @@ -23,6 +23,10 @@ const ( // Foreseeing composes an own secret the next send would make with a stand-in and names it foreseen, while one the send would // refuse (a bus credential nobody issued, a given secret under an old key) is refused here, in the same words. Foreseeing + // Planning composes for a provisioning plan alone (novox/hq ADR 0570 clause 2): every own secret the Input holds none + // for stands in, named foreseen, and nothing is refused for a value, since a plan names secrets and never carries one. + // The seat mesh-provisioner's holder composes so, reading no secret; a rendering in this mode is never applied. + Planning ) // ForeseenCertificate and ForeseenBusUsers stand for what the Input does not carry, in a composition asked ahead of the send. @@ -344,6 +348,10 @@ func (c *composer) ownSecretsFor(node Node, plan catalogue.Resolution, handing c continue } sealed, held := c.moduleSecretIfIssued(node, m.Module, name) + if !held && mode == Planning { + mark(m.Module, name) + continue + } if !held && mode == Foreseeing { // Asked ahead of the send: what the send would do about it, by the send's own rules. Made by it -- a stand-in, // named; refused by it -- refused here, the same words (novox/hq issue 275). diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index d8dd9f7..8478333 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -426,6 +426,48 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s 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) +} + +// 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) { + resolved, err := compose.Resolve(in, node) + if err != nil { + return nil, fmt.Errorf("frombus: the plans of %s: %w", node, err) + } + rendered, err := compose.Render(in, node, mode) + if err != nil { + return nil, fmt.Errorf("frombus: the plans of %s: %w", node, err) + } + gens, err := compose.Generators(in) + 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 +} + // 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) { diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index cd3d74d..69d6333 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -694,6 +694,24 @@ func TestInput(t *testing.T) { wj, _ := json.MarshalIndent(fromStoreMoved, "", " ") t.Errorf("composed from the build differs from the controller's take-in:\n got %s\nwant %s", gj, wj) } + // ADR 0570 clause 2 (issue 1954 L6-P1): the seat mesh-provisioner's plans, composed from the bus alone and with no + // secret read (the Planning mode), are the controller's handedPlans over its own input, which holds its secrets + // (compose_sdk.go: Resolve, Render in Foreseeing, Generators, Plan.Compose), and restic, handed, has one. + noSecrets := movedMesh(true) + noSecrets.own = nil + seat, err := Plans(ctx, noSecrets, Mesh{}, Scope{Node: "n1"}) + if err != nil { + t.Fatalf("the plans from the bus: %v", err) + } + controllers, err := plansIn(storeIn, "n1", compose.Foreseeing) + if err != nil { + t.Fatal(err) + } + if _, handed := seat["restic"]; !handed || !reflect.DeepEqual(seat, controllers) { + gj, _ := json.MarshalIndent(seat, "", " ") + wj, _ := json.MarshalIndent(controllers, "", " ") + t.Errorf("the seat's plans differ from the controller's:\n got %s\nwant %s", gj, wj) + } // And what a provider on another node serves from its settings (mailu's smtp domain, issue 1126: shanks refused // '${setting:domain} ... nothing sets "domain"'): read for that provider's served keys alone, through Served. diff --git a/go/compose/render.go b/go/compose/render.go index e8dca30..66d9814 100644 --- a/go/compose/render.go +++ b/go/compose/render.go @@ -71,7 +71,7 @@ func (c *composer) render(nodeName string, mode Mode) (Rendered, error) { return Rendered{}, err } r.Needed = needed - if mode == Foreseeing || len(foreseenOwn) > 0 { + if mode == Foreseeing || mode == Planning || len(foreseenOwn) > 0 { r.Foreseen = foreseenOwn } var names []string