// mesh-compose is mesh-sdk's composer command (novox/hq ADR 0482 clause 4, issue 1621): it reads a composer.Request on // standard input, validates every manifest with this mesh-sdk's reader, composes the machines named in each case and // writes one composer.Verdict on standard output. Built at any commit of mesh-sdk (or at the mesh-sdk a module pins), // it answers what that commit's composition answers, so a program that links one mesh-sdk can have another's judged. // // 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" "os" "path/filepath" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/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 } } // The catalogue's list of predefined keys is the one its manifests' fallbacks are judged against (ADR 0348 ยง7). if req.Predefined == nil { list, err := catalogue.ReadPredefined(*catalogueDir) if err != nil { fmt.Fprintln(stderr, "mesh-compose:", err) return 1 } req.Predefined = list } } req.Machines = append(req.Machines, machines...) if *depth != "" { req.Depth = *depth } if *declarations { req.Declarations = true } v, err := composer.Run(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 } // 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 }