// Package composer is mesh-sdk's composition and validator as one function over a plain request (novox/hq ADR 0482 // clause 4, issue 1621): what the command mesh-compose runs, so a program that links any one mesh-sdk can have the // composition of another commit judged by building that commit's command and reading its verdict. // // **It reads its request and nothing else.** No bus, store, network, file or environment: the manifests come inside the // request (raw, as their repository holds them), the rest of the mesh is a compose.Input, and the answer is a value. // A manifest named only by its digest in the delivery indexer's bucket is filled before the run by Fill, by whoever // holds the bus (the Go build module's compose job); Run refuses a digest left unfilled. package composer import ( "encoding/json" "errors" "fmt" "slices" "sort" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/sdkversion" ) // Version is the version of a Request and of a Verdict; a reader refuses another. const Version = 1 // Package is the import path of the command, which a compose job builds at a commit (in mesh-sdk's own module, or in // a module that requires mesh-sdk, at the version it pins). const Package = "git.novox.be/novox/mesh-sdk/go/cmd/mesh-compose" // The depths of a composition. const ( // DepthResolve resolves each machine's set over the rest of the mesh (compose.Resolve): what the assigner judges // while the facts snapshot is not on the bus (issue 1461). DepthResolve = "resolve" // DepthCompose composes each machine's whole declaration and runs the node-engine's validator over it // (compose.Compose). DepthCompose = "compose" ) // Request is everything one run composes: the rest of the mesh, one or more cases of manifests composed over it, and // the machines composed in each case. type Request struct { V int `json:"v"` // Input is the composition's input without its catalogue (and without each node's kept manifests): the cases give // those. A Catalogue or Kept it holds is ignored. Input compose.Input `json:"input"` // Cases are composed one after the other over Input, each with its own manifests; their names are unique. Cases []Case `json:"cases"` // Machines are the nodes composed in every case; none means the manifests are only read. Machines []string `json:"machines,omitempty"` // Depth is DepthResolve or DepthCompose; empty is DepthCompose. Depth string `json:"depth,omitempty"` // HoldingsFromClaims adds to Input's holdings every seat an assigned module claims, held on its node (the // assigner's input, which has no holdings of record). HoldingsFromClaims bool `json:"holdings_from_claims,omitempty"` // Declarations asks each composed machine's declared paths (DepthCompose only). Declarations bool `json:"declarations,omitempty"` // Predefined is the list of predefined keys at the catalogue's root (predefined.json, novox/hq ADR 0348 ยง7) that // every config value falling back to a key is judged against, when the asker has it (mesh-compose -catalogue // reads it). Without it a fallback is read as catalogue.ParseManifestForInputs reads it: against the list the mesh // published when this process holds one, else each key as its value declares it, for the merge gate judges the // fallback in the catalogue and a manifest that only carries one is not refused here. Predefined catalogue.PredefinedList `json:"predefined,omitempty"` } // Case is one catalogue composed: every module's manifest by name, and per node the manifests kept at the build it runs. type Case struct { Name string `json:"name"` Manifests map[string]Manifest `json:"manifests"` Kept map[string]map[string]Manifest `json:"kept,omitempty"` } // Manifest is a module's manifest as its repository holds it (Raw, the bytes), or the digest the delivery indexer keeps // it under (Digest), which Fill replaces by the bytes. On the wire it is the manifest itself, or {"digest": "..."}. type Manifest struct { Raw json.RawMessage Digest string } // MarshalJSON writes the raw manifest, or {"digest": ...} when only the digest is known. func (m Manifest) MarshalJSON() ([]byte, error) { if len(m.Raw) > 0 { return m.Raw, nil } return json.Marshal(map[string]string{"digest": m.Digest}) } // UnmarshalJSON reads {"digest": "..."} (an object with that one key) as a digest, and any other value as the raw // manifest. func (m *Manifest) UnmarshalJSON(raw []byte) error { var probe map[string]json.RawMessage if json.Unmarshal(raw, &probe) == nil && len(probe) == 1 { if d, ok := probe["digest"]; ok { var digest string if err := json.Unmarshal(d, &digest); err != nil { return fmt.Errorf("a manifest's digest is not a string: %w", err) } *m = Manifest{Digest: digest} return nil } } *m = Manifest{Raw: slices.Clone(raw)} return nil } // Verdict is what one run answers: the mesh-sdk the command was built with, and each case's verdict in the order asked. type Verdict struct { V int `json:"v"` SDK string `json:"sdk"` Cases []CaseVerdict `json:"cases"` } // CaseVerdict is one case's: every manifest read or refused, every machine composed or not. type CaseVerdict struct { Name string `json:"name"` Manifests map[string]ManifestRead `json:"manifests"` Machines map[string]Machine `json:"machines,omitempty"` } // ManifestRead is one manifest as this mesh-sdk reads it: accepted, or refused with the reader's words. type ManifestRead struct { Accepted bool `json:"accepted"` Refused string `json:"refused,omitempty"` } // Machine is one machine composed: whether it composes (resolves, at DepthResolve), why not, the node-engine // validator's problems and the composition's refusals (DepthCompose), and its declared paths when asked. type Machine struct { Composes bool `json:"composes"` Reason string `json:"reason,omitempty"` Problems []string `json:"problems,omitempty"` Refusals []string `json:"refusals,omitempty"` Declared []Path `json:"declared,omitempty"` } // Path is one path a declaration puts on the machine, with its kind (the resource's type), owner, group and mode as // declared, and the module it is of. type Path struct { Path string `json:"path"` Kind string `json:"kind"` Owner string `json:"owner,omitempty"` Group string `json:"group,omitempty"` Mode string `json:"mode,omitempty"` Module string `json:"module,omitempty"` } // ErrUnfilled is a manifest named by a digest that nobody replaced by its bytes. var ErrUnfilled = errors.New("composer: a manifest is named by its digest and was not filled") // Decode reads a request; it refuses one that is not JSON or of another version. func Decode(data []byte) (Request, error) { var r Request if err := json.Unmarshal(data, &r); err != nil { return Request{}, fmt.Errorf("composer: the request is not JSON of its shape: %w", err) } if r.V != Version { return Request{}, fmt.Errorf("composer: a request of version %d, this composer reads %d", r.V, Version) } return r, nil } // Fill replaces every manifest named by its digest by the bytes get answers for that digest; a digest get cannot answer // stops it, named. func Fill(r *Request, get func(digest string) ([]byte, error)) error { fill := func(m map[string]Manifest) error { for name, man := range m { if len(man.Raw) > 0 { continue } raw, err := get(man.Digest) if err != nil { return fmt.Errorf("composer: the manifest of %s (digest %s): %w", name, man.Digest, err) } m[name] = Manifest{Raw: raw} } return nil } for _, c := range r.Cases { if err := fill(c.Manifests); err != nil { return err } for _, kept := range c.Kept { if err := fill(kept); err != nil { return err } } } return nil } // Run composes every case of r and answers the verdict, its SDK this binary's mesh-sdk. An error is a request it // cannot run (an unknown depth, a case named twice, a manifest left as a digest); a machine that does not compose is a // verdict. func Run(r Request) (Verdict, error) { depth := r.Depth if depth == "" { depth = DepthCompose } if depth != DepthResolve && depth != DepthCompose { return Verdict{}, fmt.Errorf("composer: the depth %q is not resolve or compose", r.Depth) } v := Verdict{V: Version, SDK: sdkversion.Version()} seen := map[string]bool{} for _, c := range r.Cases { if seen[c.Name] { return Verdict{}, fmt.Errorf("composer: the case %q is named twice", c.Name) } seen[c.Name] = true cv, err := runCase(r, c, depth) if err != nil { return Verdict{}, err } v.Cases = append(v.Cases, cv) } return v, nil } // readManifest reads one manifest as this run reads every one: against the request's list of predefined keys when it // has one, else as the indexer and the builder read it (Request.Predefined). func readManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.Manifest, error) { if predefined != nil { return catalogue.ParseManifestWith(raw, predefined) } return catalogue.ParseManifestForInputs(raw) } func parse(m map[string]Manifest, reads map[string]ManifestRead, predefined catalogue.PredefinedList) (map[string]catalogue.Manifest, error) { out := map[string]catalogue.Manifest{} for name, man := range m { if len(man.Raw) == 0 { return nil, fmt.Errorf("%w: %s (digest %s)", ErrUnfilled, name, man.Digest) } parsed, err := readManifest(man.Raw, predefined) if reads != nil { if err != nil { reads[name] = ManifestRead{Refused: err.Error()} } else { reads[name] = ManifestRead{Accepted: true} } } if err == nil { out[name] = parsed } } return out, nil } func runCase(r Request, c Case, depth string) (CaseVerdict, error) { cv := CaseVerdict{Name: c.Name, Manifests: map[string]ManifestRead{}} shelf, err := parse(c.Manifests, cv.Manifests, r.Predefined) if err != nil { return CaseVerdict{}, err } in := r.Input in.Catalogue = shelf in.Nodes = slices.Clone(in.Nodes) in.Holdings = slices.Clone(in.Holdings) for i := range in.Nodes { in.Nodes[i].Kept = nil if kept := c.Kept[in.Nodes[i].Name]; len(kept) > 0 { k, err := parse(kept, nil, r.Predefined) if err != nil { return CaseVerdict{}, err } in.Nodes[i].Kept = k } } if r.HoldingsFromClaims { in.Holdings = append(in.Holdings, HoldingsOf(shelf, in.Nodes)...) } if len(r.Machines) == 0 { return cv, nil } cv.Machines = map[string]Machine{} for _, node := range r.Machines { cv.Machines[node] = machine(in, node, depth, r.Declarations) } return cv, nil } // HoldingsOf is every seat an assigned module claims, held on the node it is assigned to, in node order: the holdings // of an input that has none of record (the assigner's, mesh-delivery-assigner internal/assign input). func HoldingsOf(shelf map[string]catalogue.Manifest, nodes []compose.Node) []catalogue.Held { var out []catalogue.Held sorted := slices.Clone(nodes) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name }) for _, n := range sorted { for _, mod := range n.Assigned { for _, cl := range shelf[mod].Claims { scope := cl.Scope if seat, ok := catalogue.SeatNamed(cl.Name); ok && scope == "" { scope = seat.Scope } out = append(out, catalogue.Held{Claim: cl.Name, Scope: scope, Node: n.Name, Module: mod}) } } } return out } func machine(in compose.Input, node, depth string, declarations bool) Machine { if depth == DepthResolve { if _, err := compose.Resolve(in, node); err != nil { return Machine{Reason: err.Error()} } return Machine{Composes: true} } d, problems, err := compose.Compose(in, node) if err != nil { return Machine{Reason: err.Error()} } m := Machine{Composes: len(problems) == 0, Problems: problems, Refusals: d.Refusals()} if len(problems) > 0 { m.Reason = "the node-engine's validator refuses the declaration" } if declarations { m.Declared = pathsOf(d) } return m } // pathsOf is every resource of a declaration that names a path, with its owner, group and mode as declared, sorted by // path. func pathsOf(d compose.Declared) []Path { var out []Path str := func(r map[string]any, key string) string { if v, ok := r[key]; ok && v != nil { return fmt.Sprint(v) } return "" } for _, r := range d.Resources { p := str(r, "path") if p == "" { continue } out = append(out, Path{Path: p, Kind: str(r, "type"), Owner: str(r, "owner"), Group: str(r, "group"), Mode: str(r, "mode"), Module: d.ModuleOf[str(r, "id")]}) } sort.Slice(out, func(i, j int) bool { if out[i].Path != out[j].Path { return out[i].Path < out[j].Path } return out[i].Kind < out[j].Kind }) return out }