ADR 0593 §1 and §7: compose, its bus reader and runtime reader, takein, validate, the overlay generator, the composer's run and mesh-compose, and the judgement of a move leave mesh-sdk for the node-engine's internal packages, so no other module composes and a change to them rebuilds only the node-engine. The shapes others read (the composer's request and verdict, the overlay's names, compositions and answers) stay in mesh-sdk. The node's grant is now a golden file of what the grants part composes, since the composer of the bus's user list leaves mesh-sdk too. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
237 lines
7.8 KiB
Go
237 lines
7.8 KiB
Go
// Package composer is the node-engine's composition as one function over a plain request (novox/hq ADR 0482 clause 4,
|
|
// ADR 0593 §7): what the command mesh-compose runs, so a group that changes the node-engine's composition is composed by
|
|
// the composer built at that group's head of the node-engine's repository, in a build module's sandbox.
|
|
//
|
|
// **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 the request's input, and the answer is a value.
|
|
// The request's and the verdict's shapes are mesh-sdk's composer package; their logic, which composes, is here.
|
|
package composer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
sdk "git.novox.be/novox/mesh-sdk/go/composer"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/sdkversion"
|
|
|
|
"github.com/novox/mesh-host/internal/compose"
|
|
"github.com/novox/mesh-host/internal/compose/frombus"
|
|
)
|
|
|
|
// The shapes of mesh-sdk's composer package, by the names this package's code was written with.
|
|
type (
|
|
Request = sdk.Request
|
|
Case = sdk.Case
|
|
Manifest = sdk.Manifest
|
|
Verdict = sdk.Verdict
|
|
CaseVerdict = sdk.CaseVerdict
|
|
ManifestRead = sdk.ManifestRead
|
|
Machine = sdk.Machine
|
|
Path = sdk.Path
|
|
)
|
|
|
|
const (
|
|
Version = sdk.Version
|
|
DepthResolve = sdk.DepthResolve
|
|
DepthCompose = sdk.DepthCompose
|
|
)
|
|
|
|
// ErrUnfilled is a manifest named by a digest that nobody replaced by its bytes (sdk.ErrUnfilled).
|
|
var ErrUnfilled = sdk.ErrUnfilled
|
|
|
|
// inputOf is the request's input as this composer's composition input: the request carries it as JSON (mesh-sdk holds no
|
|
// composition), the same bytes either way.
|
|
func inputOf(r Request) (compose.Input, error) {
|
|
var in compose.Input
|
|
raw, err := json.Marshal(r.Input)
|
|
if err != nil {
|
|
return in, fmt.Errorf("composer: the request's input cannot be read: %w", err)
|
|
}
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return in, nil
|
|
}
|
|
if err := json.Unmarshal(raw, &in); err != nil {
|
|
return in, fmt.Errorf("composer: the request's input is not a composition input: %w", err)
|
|
}
|
|
return in, 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
|
|
}
|
|
|
|
// parse decodes every manifest leniently and takes its verdict (novox/hq ADR 0578 §3, §5): the composer judges none. A
|
|
// manifest refused by its verdict is held, composed as its stub without its items (as the bus reader holds one); bytes
|
|
// that do not decode are left out. Both are named in reads.
|
|
func parse(m map[string]Manifest, reads map[string]ManifestRead, verdicts map[string]delivery.ManifestVerdict) (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)
|
|
}
|
|
decoded, err := catalogue.Decode(man.Raw)
|
|
read := ManifestRead{Accepted: err == nil, SetAside: decoded.SetAside()}
|
|
if err != nil {
|
|
read.Refused = fmt.Sprintf("the manifest of %s does not decode: %v", name, err)
|
|
} else if v, ok := verdicts[name]; ok {
|
|
if refused, _ := delivery.TakeManifestVerdict(&v); refused {
|
|
if v.Module == "" {
|
|
v.Module = name
|
|
}
|
|
read = ManifestRead{Refused: v.Why()}
|
|
decoded.Items = nil
|
|
decoded = compose.Stub(decoded)
|
|
}
|
|
}
|
|
if reads != nil {
|
|
reads[name] = read
|
|
}
|
|
if err == nil {
|
|
out[name] = decoded
|
|
}
|
|
}
|
|
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, c.Verdicts)
|
|
if err != nil {
|
|
return CaseVerdict{}, err
|
|
}
|
|
base, err := inputOf(r)
|
|
if err != nil {
|
|
return CaseVerdict{}, err
|
|
}
|
|
in, err := frombus.Placed(base, c.Placements)
|
|
if err != nil {
|
|
return CaseVerdict{}, fmt.Errorf("composer: the case %q: %w", c.Name, err)
|
|
}
|
|
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, 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
|
|
}
|
|
|
|
// Package is the import path of the command, which a build module builds at the node-engine's repository's head of a
|
|
// group that changes the node-engine's composition (novox/hq ADR 0593 §7): mesh-sdk's composer.Package names the same.
|
|
const Package = "github.com/novox/mesh-host/cmd/mesh-compose"
|
|
|
|
// Decode reads a request (sdk.Decode); it refuses one that is not JSON or of another version.
|
|
func Decode(data []byte) (Request, error) { return sdk.Decode(data) }
|