// 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) } // The module the mesh provides itself (the private network) has no repository the indexer reads, so no case carries // it: its manifest comes with mesh-sdk and always wins, as the bus reader has it (frombus readBase). Without it a node // it is assigned to composed nothing (issue 1126, check of mesh-host #200: "nothing provides mesh-wireguard"). for _, name := range delivery.ProvidedModules() { m, _, err := delivery.ProvidedManifest(name) if err != nil { return CaseVerdict{}, fmt.Errorf("composer: %w", err) } shelf[name] = m } 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) }