The node-engine applies only a composition signed by the controller or the operator, and when the controller cannot compose its own fix the operator composes, signs and writes one at the terminal (ADR 0539 clauses 3 and 10).
210 lines
7.1 KiB
Go
210 lines
7.1 KiB
Go
// mesh-compose composes one module for one node by hand, from the inputs the controller keeps, and signs it with the
|
|
// operator's key, for the hand path of novox/hq ADR 0539 clause 10: a fingerprint the controller never composed (a
|
|
// hand build), while the controller cannot compose its own fix. It composes with mesh-sdk's composition, the one the
|
|
// controller writes with (compose.Compositions), and prints the composition, its key and its digest; only with -write
|
|
// does it write them, with the operator's credential: the composition into the node's bucket composed-<node>, each
|
|
// file over 64 KiB into the node's object bucket, and the digest into mesh-controller_composed. The want is the
|
|
// operator's next act, naming that digest; each act is recorded in the hand-act log.
|
|
//
|
|
// mesh-compose -input <inputs.json> -node <node> -module <module> -fp <src2:...> -key <operator key>
|
|
// [-write -nats <url> -creds <operator credential>]
|
|
//
|
|
// The inputs are a compose.Input as JSON. The key is the operator's ed25519 private key, PKCS#8 in PEM; the
|
|
// node-engine trusts it beside the controller's (ADR 0539 clause 3).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
func main() {
|
|
if err := run(os.Args[1:], os.Stdout); err != nil {
|
|
fmt.Fprintln(os.Stderr, "mesh-compose:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// printed is what mesh-compose prints: where the composition goes, its digest, the composition, and whether it was
|
|
// written.
|
|
type printed struct {
|
|
Bucket string `json:"bucket"`
|
|
Key string `json:"key"`
|
|
Digest string `json:"digest"`
|
|
Composition json.RawMessage `json:"composition"`
|
|
Objects []string `json:"objects,omitempty"`
|
|
Written bool `json:"written"`
|
|
Next string `json:"next"`
|
|
}
|
|
|
|
func run(args []string, stdout io.Writer) error {
|
|
fs := flag.NewFlagSet("mesh-compose", flag.ContinueOnError)
|
|
input := fs.String("input", "", "the controller's kept inputs, a compose.Input as JSON")
|
|
node := fs.String("node", "", "the node to compose for")
|
|
module := fs.String("module", "", "the module to compose")
|
|
fp := fs.String("fp", "", "the module's fingerprint, src2: and 64 hex digits")
|
|
keyFile := fs.String("key", "", "the operator's ed25519 private key, PKCS#8 in PEM")
|
|
write := fs.Bool("write", false, "write the composition, its files and its digest, with the operator's credential")
|
|
url := fs.String("nats", nats.DefaultURL, "with -write: the bus")
|
|
creds := fs.String("creds", "", "with -write: the operator's credential file")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case *input == "" || *node == "" || *module == "" || *fp == "":
|
|
return errors.New("-input, -node, -module and -fp are each required")
|
|
case *keyFile == "":
|
|
return errors.New("-key is required: a composition made by hand is signed by the operator's key, and the node-engine applies no other")
|
|
case *write && *creds == "":
|
|
return errors.New("-write needs -creds: the operator's credential is the only hand writer of a composition")
|
|
}
|
|
key, err := readKey(*keyFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key2, err := delivery.ComposedKey(*node, *module, *fp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bucket, err := delivery.ComposedBucketFor(*node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := os.ReadFile(*input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var in compose.Input
|
|
if err := json.Unmarshal(raw, &in); err != nil {
|
|
return fmt.Errorf("the inputs in %s are not a compose.Input: %w", *input, err)
|
|
}
|
|
all, _, err := compose.Compositions(in, *node, map[string]string{*module: *fp}, compose.Reading)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c, ok := all[key2]
|
|
if !ok {
|
|
return fmt.Errorf("%s does not run %s: its composition holds none of it", *node, *module)
|
|
}
|
|
c, objects, err := c.ByDigest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c, err = delivery.SignComposed(c, key); err != nil {
|
|
return err
|
|
}
|
|
encoded, err := delivery.EncodeComposed(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
digest, err := delivery.ComposedDigestOf(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := printed{Bucket: bucket, Key: key2, Digest: digest, Composition: encoded,
|
|
Next: fmt.Sprintf("write want.%s.%s naming fp %s and composed %s with the operator's credential, and record each act in the hand-act log", *node, *module, *fp, digest)}
|
|
for d := range objects {
|
|
out.Objects = append(out.Objects, d)
|
|
}
|
|
if *write {
|
|
if err := writeIt(*url, *creds, *node, bucket, key2, encoded, digest, objects); err != nil {
|
|
return err
|
|
}
|
|
out.Written = true
|
|
}
|
|
enc := json.NewEncoder(stdout)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(out)
|
|
}
|
|
|
|
func readKey(path string) (ed25519.PrivateKey, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
block, _ := pem.Decode(raw)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("%s holds no PEM block", path)
|
|
}
|
|
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
key, ok := parsed.(ed25519.PrivateKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s is not an ed25519 key", path)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// writeIt writes the files by digest, then the composition, then its digest: a reader that finds the digest finds the
|
|
// composition and its files already there.
|
|
func writeIt(url, creds, node, bucket, key string, encoded []byte, digest string, objects map[string][]byte) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
nc, err := nats.Connect(url, nats.UserCredentials(creds), nats.Name("mesh-compose"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer nc.Close()
|
|
js, err := jetstream.New(nc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(objects) > 0 {
|
|
filesBucket, err := declaration.FileBucketFor(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
store, err := js.ObjectStore(ctx, filesBucket)
|
|
if err != nil {
|
|
return fmt.Errorf("the node's object bucket %s: %w", filesBucket, err)
|
|
}
|
|
for d, body := range objects {
|
|
name, err := declaration.ObjectName(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := store.PutBytes(ctx, name, body); err != nil {
|
|
return fmt.Errorf("the file %s: %w", d, err)
|
|
}
|
|
}
|
|
}
|
|
composed, err := js.KeyValue(ctx, bucket)
|
|
if err != nil {
|
|
return fmt.Errorf("the bucket %s: %w", bucket, err)
|
|
}
|
|
if _, err := composed.Put(ctx, key, encoded); err != nil {
|
|
return fmt.Errorf("%s in %s: %w", key, bucket, err)
|
|
}
|
|
value, err := delivery.EncodeComposedDigest(delivery.ComposedDigest{Digest: digest, Judgement: delivery.JudgedCannot,
|
|
Words: "composed by hand with mesh-compose (ADR 0539 clause 10): the operator writes the want"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
digests, err := js.KeyValue(ctx, delivery.ControllerComposedBucket)
|
|
if err != nil {
|
|
return fmt.Errorf("the bucket %s: %w", delivery.ControllerComposedBucket, err)
|
|
}
|
|
if _, err := digests.Put(ctx, key, value); err != nil {
|
|
return fmt.Errorf("%s in %s: %w", key, delivery.ControllerComposedBucket, err)
|
|
}
|
|
return nil
|
|
}
|