260 lines
10 KiB
Go
260 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// perform makes what a composition named foreseen (novox/hq ADR 0453 clause 3, design 47 R4b, issue 1385).
|
|
//
|
|
// **The composition makes nothing; a send does.** mesh-sdk's composition over an Input names what the next send would make
|
|
// (the ports it would assign, the credentials it would seal, the core boxes it would ask the vault for, the seats it would
|
|
// pass on, the certificate it would issue) and stands in for each. A send composes, performs, reads the stores again and
|
|
// composes again (send in compose_sdk.go), and refuses to send a declaration that still names anything foreseen. A question
|
|
// (status, facts, probes, the merge gate's reading) never reaches this file.
|
|
//
|
|
// What is made is exactly what the controller's own composition made on the way to a declaration it was going to send:
|
|
// - every pair credential no row holds, for every machine that resolves, since planning one node resolved them all;
|
|
// - the seats the mesh passes on, with the hand act (not on a throwaway mesh, whose hand act has nowhere to go);
|
|
// - the machine ports the node's modules were given none for;
|
|
// - the node's own secrets, by the controller's own rules (a bus credential nobody issued was refused by the composition);
|
|
// - the core credentials, asked of the vault every send, so a rotated one reaches the machine;
|
|
// - the certificate of a machine that was never certified.
|
|
//
|
|
// 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 := false
|
|
inv := open.inventory
|
|
|
|
// The seats passed on: recorded in the store with the controller's own hand act (ADR 0449 §4, issue 1317).
|
|
if len(d.Passes.Passes) > 0 {
|
|
var w io.Writer = os.Stderr
|
|
if lw, ok := ctx.Value(leftOutKey{}).(io.Writer); ok {
|
|
w = lw
|
|
}
|
|
for _, line := range d.Passes.Said {
|
|
fmt.Fprintln(w, line)
|
|
}
|
|
if performSeatPasses(ctx, seatPassingOn(inv, w), d.Passes.Passes) {
|
|
did = true
|
|
}
|
|
}
|
|
|
|
// Every pair credential the stores hold none for: made once and kept, and sealed to both ends before it was ever written
|
|
// 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, 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
|
|
}
|
|
|
|
// The ports this node's modules were given none for.
|
|
if len(d.ForeseenPorts) > 0 {
|
|
shelf, err := inv.Catalogue(ctx)
|
|
if err != nil {
|
|
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, fmt.Errorf("the composition named a port the controller cannot read: %q", one)
|
|
}
|
|
fixed := false
|
|
for _, l := range nodeShelf(in, shelf, node)[module].Listens {
|
|
if l.Port == wanted {
|
|
fixed = l.Fixed
|
|
}
|
|
}
|
|
if _, err := inv.PortFor(ctx, node, module, wanted, fixed); err != nil {
|
|
return did, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err)
|
|
}
|
|
did = true
|
|
}
|
|
}
|
|
|
|
// The node's own secrets: a module's superuser password, an administrator, an account. The composition refused what the
|
|
// send refuses (a bus credential nobody issued, a given value under an old key).
|
|
for _, one := range d.StoodIn {
|
|
module, name, ok := strings.Cut(one, "/")
|
|
if !ok || strings.Contains(one, " ") {
|
|
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, err
|
|
}
|
|
did = true
|
|
}
|
|
|
|
// 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, err
|
|
}
|
|
did = true
|
|
break
|
|
}
|
|
}
|
|
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
|
|
// credential sealed to this consumer too. A credential only a person gives is never made (it is not a would-make).
|
|
func makePair(ctx context.Context, inv *inventory.Inventory, p inventory.PairCredential) error {
|
|
var err error
|
|
switch p.Kind {
|
|
case inventory.PairShared:
|
|
_, err = inv.SharedSecretFor(ctx, p.Name, p.Consumer, p.ConsumerModule, p.Provider, p.ProviderModule, p.Local, p.Own)
|
|
case inventory.PairPlain:
|
|
_, err = inv.SecretFor(ctx, p.Name, p.Consumer, p.ConsumerModule, p.Provider, p.Local)
|
|
default:
|
|
return errors.New("a credential only a person gives is not made")
|
|
}
|
|
if inventory.IsNotGiven(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// stillForeseen says what a composed declaration still stands in for: the lists the composition names, and the stand-in itself
|
|
// anywhere in the body (a pair credential's stand-in is not in any list). A declaration that has any is never sent.
|
|
func stillForeseen(d compose.Declared) []string {
|
|
var left []string
|
|
left = append(left, d.Foreseen...)
|
|
left = append(left, d.StoodIn...)
|
|
for _, p := range d.ForeseenPorts {
|
|
left = append(left, "the port "+p)
|
|
}
|
|
for _, c := range d.CoreBoxesWanted {
|
|
left = append(left, "the vault's box for "+c.Module+"/"+c.Name)
|
|
}
|
|
for _, p := range d.Passes.Passes {
|
|
left = append(left, "the pass of "+p.Seat.Name+" to "+p.To.Module+" on "+p.To.Node)
|
|
}
|
|
if body, err := d.Body(); err == nil {
|
|
for _, marker := range []string{catalogue.ForeseenSealed, "asked of the vault by the next send", compose.ForeseenCertificate,
|
|
compose.ForeseenBusUsers} {
|
|
if strings.Contains(string(body), marker) {
|
|
left = append(left, "a stand-in: "+marker)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(left)
|
|
return slicesCompact(left)
|
|
}
|
|
|
|
func slicesCompact(in []string) []string {
|
|
var out []string
|
|
for _, s := range in {
|
|
if len(out) == 0 || out[len(out)-1] != s {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// nodeShelf is the shelf as one node composes its own set: the current one with each module it keeps at the build it runs put
|
|
// in place of the current.
|
|
func nodeShelf(in *meshInput, shelf map[string]catalogue.Manifest, node string) map[string]catalogue.Manifest {
|
|
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 {
|
|
// Held only when sealed to the key the machine has now, or given by a person (which is not the mesh's
|
|
// to make again): a made row under an old key is re-made, as SecretForModule did.
|
|
if r.Node == node && (r.SealedTo == n.SealingKey || r.Origin == inventory.OriginAccepted) {
|
|
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)
|
|
}, true)
|
|
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
|
|
}
|