// 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-, 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 -node -module -fp -key // [-write -nats -creds ] // // 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 }