diff --git a/cmd/mesh-controller/compose_perform.go b/cmd/mesh-controller/compose_perform.go index ba85b8c2..3305106c 100644 --- a/cmd/mesh-controller/compose_perform.go +++ b/cmd/mesh-controller/compose_perform.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "slices" "sort" "strconv" "strings" @@ -33,14 +34,7 @@ import ( // // perform returns whether it made or recorded anything: the reason the caller reads the stores again. func perform(ctx context.Context, open *stores, in *meshInput, node string, d compose.Declared) (bool, error) { - did, _, err := performSend(ctx, open, in, node, d) - return did, err -} - -// performSend is perform, and the core modules whose credentials the vault did not answer for with no box kept: the -// controller seals those itself until the vault holds them (core_box.go, ADR 0396), and the send carries that seal. -func performSend(ctx context.Context, open *stores, in *meshInput, node string, d compose.Declared) (bool, []string, error) { - did, sealedByController := false, []string(nil) + did := false inv := open.inventory // The seats passed on: recorded in the store with the controller's own hand act (ADR 0449 ยง4, issue 1317). @@ -79,7 +73,7 @@ func performSend(ctx context.Context, open *stores, in *meshInput, node string, // down. The error is the one planFor met: the machine that needs it, and why none could be made. for _, p := range in.wouldMake { if err := makePair(ctx, inv, p); err != nil { - return did, nil, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %w", + return did, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %w", p.ConsumerModule, p.Consumer, p.Name, p.Provider, err) } did = true @@ -89,13 +83,13 @@ func performSend(ctx context.Context, open *stores, in *meshInput, node string, if len(d.ForeseenPorts) > 0 { shelf, err := inv.Catalogue(ctx) if err != nil { - return did, nil, err + return did, err } for _, one := range d.ForeseenPorts { module, port, ok := strings.Cut(one, ":") wanted, perr := strconv.Atoi(port) if !ok || perr != nil { - return did, nil, fmt.Errorf("the composition named a port the controller cannot read: %q", one) + return did, fmt.Errorf("the composition named a port the controller cannot read: %q", one) } fixed := false for _, l := range nodeShelf(in, shelf, node)[module].Listens { @@ -104,7 +98,7 @@ func performSend(ctx context.Context, open *stores, in *meshInput, node string, } } if _, err := inv.PortFor(ctx, node, module, wanted, fixed); err != nil { - return did, nil, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err) + return did, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err) } did = true } @@ -118,45 +112,22 @@ func performSend(ctx context.Context, open *stores, in *meshInput, node string, continue // a grant's sealed copy, the certificate or the bus's list: made below or by nobody } if _, err := inv.SecretForModule(ctx, node, module, name); err != nil { - return did, nil, err + return did, err } did = true } - // The core's own credentials, asked of the vault on every send so a rotated one reaches the machine (core_box.go). - n, ok := nodeOf(in.Input, node) - if ok && n.Stated && n.Contract >= compose.VaultBoxContract { - resolved, err := compose.Resolve(in.Input, node) - if err != nil { - return did, nil, err - } - hands := n.Stated && n.Contract >= compose.HandedContract - handing := catalogue.Rendering{Hands: hands, Provisioner: provisionerAccount(resolved.Plan)} - boxes, err := coreBoxesFor(ctx, inv, node, resolved.Plan.Modules, func(m catalogue.Manifest) bool { - return handing.Handing(m) - }, Allocating) - if err != nil { - return did, nil, err - } - if len(boxes.keep) > 0 { - did = true - } - // A core credential the vault did not answer for and no box was kept for is sealed by the controller itself until the - // vault holds it (core_box.go, ADR 0396): the composition stands in a box for it, and the send carries the seal. - sealedByController = boxes.byController - } - // The certificate of a machine that was never certified: issued and kept, with the authority when there is none yet. for _, one := range d.StoodIn { if strings.HasPrefix(one, "the certificate and the authority of ") { if _, _, err := certificateFor(ctx, open, node); err != nil { - return did, nil, err + return did, err } did = true break } } - return did, sealedByController, nil + return did, nil } // makePair makes one pair credential by the controller's own read of it: the credential for the pair, or the provider's one @@ -220,3 +191,85 @@ func nodeShelf(in *meshInput, shelf map[string]catalogue.Manifest, node string) n, _ := nodeOf(in.Input, node) return shelfWith(shelf, n.Kept) } + +// prepareSend is what a send does before it composes, every send and whatever the composition would have named: the +// controller's own composition did it on the way, so a module's file that names an own secret composes (mesh-sdk's +// composition can stand in for none: a file naming a secret not made yet is refused in every mode), and the core's credentials +// are asked of the vault on every send, so a rotated one reaches the machine and a silent vault is said (core_box.go). +// +// - Each own secret of a module the node runs (not one handed to the provisioner, not one issued outside the mesh, not one a +// person gives) that no row holds is made, unless the composition would refuse it (a bus credential nobody issued): that +// refusal is the composition's to say, in its words. +// - The core's credentials are asked of the vault, the boxes newly answered are kept, and the modules the vault did not answer +// for with no box kept are returned: the controller seals those itself until the vault holds them (ADR 0396). +func prepareSend(ctx context.Context, open *stores, node string) ([]string, error) { + in, err := inputFor(ctx, open) + if err != nil { + return nil, err + } + if in.refused[node] != nil { + return nil, nil // empty-on-error: refused alone for what it keeps; the composition that follows says so, in its words + } + resolved, err := compose.Resolve(in.Input, node) + if err != nil { + return nil, nil // empty-on-error: it does not compose for its own reasons; the composition that follows says them + } + n, _ := nodeOf(in.Input, node) + hands := n.Stated && n.Contract >= compose.HandedContract + handing := catalogue.Rendering{Hands: hands, Provisioner: provisionerAccount(resolved.Plan)} + inv := open.inventory + did := false + if n.SealingKey != "" { + held := map[string]bool{} + for _, r := range in.OwnSecrets { + if r.Node == node { + held[r.Module+"/"+r.Name] = true + } + } + for _, m := range resolved.Plan.Modules { + if handing.Handing(m) { + continue + } + plain := m.OwnSecrets.Plain() + names := make([]string, 0, len(plain)) + for name := range plain { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if plain[name].IssuedBy == catalogue.IssuedOutside || held[m.Module+"/"+name] { + continue + } + switch name { + case "broker": + if !slices.Contains(in.BusMinted, node+"."+m.Module) { + continue + } + case compose.ProvisionerSecret: + if !slices.Contains(in.BusMinted, "provisioner."+node) { + continue + } + } + if _, err := inv.SecretForModule(ctx, node, m.Module, name); err == nil { + did = true + } + } + } + } + var byController []string + if n.Stated && n.Contract >= compose.VaultBoxContract { + boxes, err := coreBoxesFor(ctx, inv, node, resolved.Plan.Modules, func(m catalogue.Manifest) bool { + return handing.Handing(m) + }, Allocating) + if err != nil { + return nil, err + } + did = did || len(boxes.keep) > 0 + byController = boxes.byController + } + if did { + // What was made is in the stores now: the composition reads them afresh. + forgetInput(ctx) + } + return byController, nil +} diff --git a/cmd/mesh-controller/compose_sdk.go b/cmd/mesh-controller/compose_sdk.go index 28896e58..280fdcdf 100644 --- a/cmd/mesh-controller/compose_sdk.go +++ b/cmd/mesh-controller/compose_sdk.go @@ -228,7 +228,10 @@ func composeSend(ctx context.Context, open *stores, node string) (sendable, erro func composeSendValidated(ctx context.Context, open *stores, node string) (sendable, []string, error) { // The core modules whose credentials the controller sealed itself because the vault was silent and no box was kept // (ADR 0396): the composition stands in a vault box for each, and a send carries the controller's own seal instead. - var byController []string + byController, err := prepareSend(ctx, open, node) + if err != nil { + return sendable{}, nil, err + } for attempt := 0; ; attempt++ { in, err := inputFor(ctx, open) if err != nil { @@ -276,11 +279,9 @@ func composeSendValidated(ctx context.Context, open *stores, node string) (senda return sendable{}, nil, fmt.Errorf("%s was composed twice and still names what only a send makes (%s): nothing is sent "+ "that stands in for a value", node, strings.Join(left, "; ")) } - _, sealed, err := performSend(ctx, open, in, node, d) - if err != nil { + if _, err := perform(ctx, open, in, node, d); err != nil { return sendable{}, nil, err } - byController = sealed // What was made is in the stores now: the next read is a fresh one. forgetInput(ctx) }