333 lines
11 KiB
Go
333 lines
11 KiB
Go
// 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"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func parse(m map[string]Manifest, reads map[string]ManifestRead) (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 := catalogue.ParseManifest(man.Raw)
|
|
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)
|
|
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)
|
|
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
|
|
}
|