The composer answers the assigner's composition step: the request is read from the file named by MESH_RUN_INPUT and the verdict is the last line of standard output, exit 0; the compose-job path stays. Builds: design 47 G §354, §63, §22; lead answer A-167 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
209 lines
7.9 KiB
Go
209 lines
7.9 KiB
Go
// mesh-compose is the node-engine's composer command (novox/hq ADR 0482 clause 4, ADR 0593 §7, issue 1621): it reads a
|
|
// composer.Request on standard input, reads every manifest, composes the machines named in each case and writes one
|
|
// composer.Verdict on standard output. Built at a commit of the node-engine's repository, it answers what that
|
|
// commit's composition answers, so a group that changes the node-engine's composition is judged by its own next code.
|
|
//
|
|
// mesh-compose < request.json > verdict.json
|
|
// mesh-compose -request request.json > verdict.json
|
|
// mesh-compose -catalogue <dir> [-node <name>]... [-depth resolve|compose] [-declarations] [< request.json]
|
|
//
|
|
// -catalogue reads <dir>/modules/*/module.json and a module.json at <dir>'s root as the first case's manifests (a
|
|
// checkout or `git archive` of the catalogue repository at a ref: the command fetches nothing). -node adds a machine to
|
|
// compose; -declarations answers each machine's declared paths with their owners and modes.
|
|
//
|
|
// Run as a pipeline step it reads its input, a composer.Request at depth compose, from the file named by the environment
|
|
// variable MESH_RUN_INPUT and answers the run's output as its composer.Verdict on the last line of standard output
|
|
// (design 47 G A-167).
|
|
//
|
|
// It opens no network connection, bus, store or file beyond its standard input, the -request file, the file
|
|
// MESH_RUN_INPUT names and the -catalogue directory, and reads no other environment. Exit codes: 0 the verdict is written (a machine that does not compose is a verdict, not an error);
|
|
// 1 the request cannot be read or run, said on standard error; 2 the flags are wrong.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
|
|
|
"github.com/novox/mesh-host/internal/composer"
|
|
)
|
|
|
|
func main() {
|
|
os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
|
|
}
|
|
|
|
type nodes []string
|
|
|
|
func (n *nodes) String() string { return strings.Join(*n, ",") }
|
|
func (n *nodes) Set(v string) error { *n = append(*n, v); return nil }
|
|
|
|
// run is the command: its arguments, standard input, output and error; it answers the exit code.
|
|
func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("mesh-compose", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
catalogueDir := fs.String("catalogue", "", "a catalogue checkout: <dir>/modules/*/module.json are the first case's manifests")
|
|
var machines nodes
|
|
fs.Var(&machines, "node", "a machine to compose (repeatable)")
|
|
depth := fs.String("depth", "", "resolve or compose (default: the request's, else compose)")
|
|
declarations := fs.Bool("declarations", false, "answer each composed machine's declared paths, owners and modes")
|
|
requestFile := fs.String("request", "", "read the request from this file instead of standard input (a run step that hands no standard input)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
if fs.NArg() > 0 {
|
|
fmt.Fprintf(stderr, "mesh-compose: unexpected arguments %q\n", fs.Args())
|
|
return 2
|
|
}
|
|
var raw []byte
|
|
var err error
|
|
// A pipeline step hands its input as a file named by MESH_RUN_INPUT (design 47 G A-167); -request, when given, wins.
|
|
runInput := os.Getenv("MESH_RUN_INPUT")
|
|
if *requestFile == "" && runInput != "" {
|
|
*requestFile = runInput
|
|
}
|
|
if *requestFile != "" {
|
|
raw, err = os.ReadFile(*requestFile)
|
|
} else {
|
|
raw, err = io.ReadAll(stdin)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, "mesh-compose: the request cannot be read:", err)
|
|
return 1
|
|
}
|
|
var req composer.Request
|
|
if strings.TrimSpace(string(raw)) == "" {
|
|
if *catalogueDir == "" {
|
|
fmt.Fprintln(stderr, "mesh-compose: no request on standard input and no -catalogue")
|
|
return 1
|
|
}
|
|
req = composer.Request{V: composer.Version}
|
|
} else if req, err = composer.Decode(raw); err != nil {
|
|
fmt.Fprintln(stderr, "mesh-compose:", err)
|
|
return 1
|
|
}
|
|
if *catalogueDir != "" {
|
|
read, err := readCatalogue(*catalogueDir)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, "mesh-compose:", err)
|
|
return 1
|
|
}
|
|
if len(req.Cases) == 0 {
|
|
req.Cases = []composer.Case{{Name: "catalogue", Manifests: map[string]composer.Manifest{}}}
|
|
}
|
|
if req.Cases[0].Manifests == nil {
|
|
req.Cases[0].Manifests = map[string]composer.Manifest{}
|
|
}
|
|
for name, m := range read {
|
|
if _, given := req.Cases[0].Manifests[name]; !given {
|
|
req.Cases[0].Manifests[name] = m
|
|
}
|
|
}
|
|
// No list of predefined keys is read: the composer judges no manifest (novox/hq ADR 0578 §5).
|
|
}
|
|
req.Machines = append(req.Machines, machines...)
|
|
if *depth != "" {
|
|
req.Depth = *depth
|
|
}
|
|
if *declarations {
|
|
req.Declarations = true
|
|
}
|
|
v, err := runWithSeats(req)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, "mesh-compose:", err)
|
|
return 1
|
|
}
|
|
enc := json.NewEncoder(stdout)
|
|
if err := enc.Encode(v); err != nil {
|
|
fmt.Fprintln(stderr, "mesh-compose: the verdict cannot be written:", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// NoSeatList is what a machine reads, not judged, when the request carries no seat list (novox/hq ADR 0636 §1).
|
|
const NoSeatList = "the composer was handed no seat list, so it composes nothing (a reader of the seat list acts on nothing " +
|
|
"new until it has read the list, novox/hq ADR 0636 §1): the asker gives the seat definitions it judges against in the request's seats"
|
|
|
|
// runWithSeats composes req against the seat list the request carries (Request.Seats: the definitions the asker's seat list
|
|
// reader holds, the members' heads' laid over them in a check, ADR 0636 §4), over the foundation's bootstrapped ones
|
|
// (seatlist.Bootstrapped), as the node-engine composes against the list it read from the bus (ADR 0635 §1). The command
|
|
// compiles no seat list: without one in the request it reads every manifest and judges no machine, each said not judged
|
|
// with NoSeatList (ADR 0593 §9), never a refusal or a pass made without the seats' paths, deliveries and holders.
|
|
func runWithSeats(req composer.Request) (composer.Verdict, error) {
|
|
if len(req.Seats) == 0 {
|
|
machines := req.Machines
|
|
req.Machines = nil
|
|
v, err := composer.Run(req)
|
|
if err != nil || len(machines) == 0 {
|
|
return v, err
|
|
}
|
|
for i := range v.Cases {
|
|
v.Cases[i].Machines = map[string]composer.Machine{}
|
|
for _, node := range machines {
|
|
v.Cases[i].Machines[node] = composer.Machine{NotJudged: NoSeatList}
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
defs := map[string]seatdef.Definition{}
|
|
for _, d := range seatlist.Bootstrapped() {
|
|
defs[d.Seat] = d
|
|
}
|
|
for _, raw := range req.Seats {
|
|
d, err := seatdef.Parse(raw)
|
|
if err != nil {
|
|
return composer.Verdict{}, fmt.Errorf("a seat definition of the request: %w", err)
|
|
}
|
|
defs[d.Seat] = d
|
|
}
|
|
catalogue.UseSeats(catalogue.SeatsFromDefinitions(slices.Collect(maps.Values(defs))))
|
|
return composer.Run(req)
|
|
}
|
|
|
|
// readCatalogue reads every module.json of a catalogue checkout by its module's name: the root's and each of
|
|
// modules/*/. A manifest whose name cannot be read is kept under its directory's name, so the reader's refusal is said.
|
|
func readCatalogue(dir string) (map[string]composer.Manifest, error) {
|
|
paths, err := filepath.Glob(filepath.Join(dir, "modules", "*", "module.json"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "module.json")); err == nil {
|
|
paths = append(paths, filepath.Join(dir, "module.json"))
|
|
}
|
|
if len(paths) == 0 {
|
|
return nil, fmt.Errorf("the catalogue %s holds no module.json", dir)
|
|
}
|
|
sort.Strings(paths)
|
|
out := map[string]composer.Manifest{}
|
|
for _, p := range paths {
|
|
raw, err := os.ReadFile(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var probe struct {
|
|
Module string `json:"module"`
|
|
}
|
|
name := filepath.Base(filepath.Dir(p))
|
|
if json.Unmarshal(raw, &probe) == nil && probe.Module != "" {
|
|
name = probe.Module
|
|
}
|
|
if _, twice := out[name]; twice {
|
|
return nil, errors.New("the catalogue " + dir + " defines " + name + " twice")
|
|
}
|
|
out[name] = composer.Manifest{Raw: raw}
|
|
}
|
|
return out, nil
|
|
}
|