145 lines
5.0 KiB
Go
145 lines
5.0 KiB
Go
// 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 -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.
|
|
//
|
|
// It opens no network connection, bus, store or file beyond its standard input 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/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")
|
|
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
|
|
}
|
|
raw, err := io.ReadAll(stdin)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, "mesh-compose: standard input 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
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|