A composition over the Input makes nothing; a send performs what it named (the pair credentials no row holds, the seats to pass, the ports and own secrets, the core credentials asked of the vault every send, the certificate), reads the stores again and composes again, and refuses a declaration that still stands in for a value anywhere in its body. A question (composeQuestion) never performs. The operation's Input is read once and kept in its context until a send changes what it read, per what the context keeps (recorded builds, the grants step, a settings walk). A core credential the vault did not answer for and no box is kept for refuses the send, where the controller sealed it itself (ADR 0396): the composition has no such fallback (reported). Callers switch in the next commits.
219 lines
8.5 KiB
Go
219 lines
8.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"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) {
|
|
inv := open.inventory
|
|
did := false
|
|
|
|
// 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)
|
|
}
|
|
sp := seatPassingOn(inv, w)
|
|
for _, p := range d.Passes.Passes {
|
|
moved, err := inv.PassSeat(ctx, p.Seat.Name, p.Seat.Scope, p.From, p.To)
|
|
if err != nil {
|
|
// Said, and the record stays as it was: a composition is never refused for it.
|
|
fmt.Fprintf(w, "%s could not be passed from %s on %s to %s on %s: %v\n", p.Seat.Name,
|
|
p.From.Module, p.From.Node, p.To.Module, p.To.Node, err)
|
|
continue
|
|
}
|
|
if !moved {
|
|
continue // another composition passed it first, and wrote the act
|
|
}
|
|
did = true
|
|
if inv.Throwaway() {
|
|
continue
|
|
}
|
|
if err := sp.act(ctx, p); err != nil {
|
|
fmt.Fprintf(w, "%s passed from %s on %s to %s on %s, and the hand act could NOT be recorded: %v\n",
|
|
p.Seat.Name, p.From.Module, p.From.Node, p.To.Module, p.To.Node, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 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, 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, err
|
|
}
|
|
if len(boxes.keep) > 0 {
|
|
did = true
|
|
}
|
|
// A core credential the vault did not answer for and no box was kept for is composed by the controller's own seal
|
|
// (core_box.go, ADR 0396); the composition over an Input has no such fallback, so the send is refused, said.
|
|
if len(boxes.byController) > 0 {
|
|
return did, fmt.Errorf("the vault did not answer for the core credentials of %s (%s) and no box of theirs is kept: "+
|
|
"nothing is sent until it does", node, strings.Join(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, 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)
|
|
}
|