// 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 [-node ]... [-depth resolve|compose] [-declarations] [< request.json] // // -catalogue reads /modules/*/module.json and a module.json at '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. // // It opens no network connection, bus, store or file beyond its standard input, the -request file and the -catalogue // directory, and reads // no 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: /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 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 }