Files
mesh-host/internal/compositions/composed.go
T
jschoubben 5906e18028 Node-engine: a composition, its codec, digest and keys come from the node-engine client; the candidate answer and its limits from the seat's contract; the declared packages are the engine's own (H-2, H-3, ADR 0634 §1; issue 1966)
The engine's Composed keeps the catalogue's Plan and the declaration types and goes through nodeengineclient's codec; the fingerprint key part is meshcatalogclient's. The candidate answer is nodeengine.CandidateAnswer; the have still holds it through an interim JSON bridge (link.interimAnswers) until the Have group moves.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:10:45 +01:00

252 lines
8.6 KiB
Go

package compositions
import (
"encoding/json"
"fmt"
"regexp"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/keys"
)
var bucketNode = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// NodeOwn is the node's own parts in a composition (the node-engine seat's state composed, key `<node>.node`): its layout
// is the seat's contract (nodeengine.NodeOwn); the engine holds them as the declaration package's types.
type NodeOwn struct {
Adoption *declaration.Adoption `json:"adoption,omitempty"`
Identities []declaration.Identity `json:"identities,omitempty"`
Groups []declaration.Group `json:"groups,omitempty"`
Libraries []declaration.Library `json:"libraries,omitempty"`
Provisioner string `json:"provisioner_account,omitempty"`
}
// Composed is a composition (the node-engine seat's state composed): one module's resources composed for one node at one
// fingerprint, or, with Module and FP empty, the node's own. Its codec, digest and keys are the node-engine client's
// (nodeengineclient); the engine holds the plan as the catalogue's Plan, which the client carries as JSON.
type Composed struct {
V int
Node string
Module string
FP string
Data bool
Resources []json.RawMessage
Filled []string
Own NodeOwn
Plan *catalogue.Plan
Refused string
}
// Bucket constants of a node's compositions (the state composed is one bucket per node).
const (
// BucketPrefix starts the name of a node's bucket of compositions, BucketFor(node).
BucketPrefix = "composed-"
// History is the revisions a node's composition bucket keeps per key.
History = 5
// Keep is how many of a module's newest fingerprints keep their compositions though no want names them.
Keep = 5
)
// BucketFor is the name of a node's bucket of compositions, "composed-<node>".
func BucketFor(node string) (string, error) {
if node == "" {
return "", fmt.Errorf("compositions: the node name is empty; a node's composition bucket is named for its node")
}
if !bucketNode.MatchString(node) {
return "", fmt.Errorf("compositions: the node name %q is not one a bucket accepts: letters, digits, '_' and '-' only", node)
}
return BucketPrefix + node, nil
}
func toClient(c Composed) (nodeengineclient.Composition, error) {
out := nodeengineclient.Composition{V: c.V, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data,
Resources: c.Resources, Filled: c.Filled, Refused: c.Refused}
raw, err := json.Marshal(c.Own)
if err != nil {
return out, err
}
if err := json.Unmarshal(raw, &out.Own); err != nil {
return out, fmt.Errorf("compositions: the node's own parts do not fit the seat's layout: %w", err)
}
if c.Plan != nil {
if out.Plan, err = json.Marshal(c.Plan); err != nil {
return out, err
}
}
return out, nil
}
func fromClient(c nodeengineclient.Composition) (Composed, error) {
out := Composed{V: c.V, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, Resources: c.Resources,
Filled: c.Filled, Refused: c.Refused}
raw, err := json.Marshal(c.Own)
if err != nil {
return out, err
}
if err := json.Unmarshal(raw, &out.Own); err != nil {
return out, fmt.Errorf("compositions: the node's own parts do not fit the engine's: %w", err)
}
if len(c.Plan) > 0 {
out.Plan = &catalogue.Plan{}
if err := json.Unmarshal(c.Plan, out.Plan); err != nil {
return out, fmt.Errorf("compositions: the plan does not fit: %w", err)
}
}
return out, nil
}
// Encode is c as the client writes it.
func Encode(c Composed) ([]byte, error) {
cc, err := toClient(c)
if err != nil {
return nil, err
}
return nodeengineclient.EncodeComposition(cc)
}
// Decode reads a composition as the client reads it.
func Decode(data []byte) (Composed, error) {
cc, err := nodeengineclient.DecodeComposition(data)
if err != nil {
return Composed{}, err
}
return fromClient(cc)
}
// DigestOf is a composition's digest, "sha256:<hex>", of its encoding without its plan; a refused one has none.
func DigestOf(c Composed) (string, error) {
cc, err := toClient(c)
if err != nil {
return "", err
}
return nodeengineclient.ComposedDigest(cc)
}
// Same says whether two compositions are the same value, their plans included.
func Same(a, b Composed) bool {
ca, errA := toClient(a)
cb, errB := toClient(b)
return errA == nil && errB == nil && nodeengineclient.SameComposition(ca, cb)
}
// Key is the key of a module's composition for a node at a fingerprint: `<node>.<module>.<hex>`.
func Key(node, module, fingerprint string) (string, error) {
part, err := meshcatalogclient.KeyPart(fingerprint)
if err != nil {
return "", err
}
return nodeengineclient.ComposedKey(node, module, part)
}
// NodeKey is the key of a node's own composition: `<node>.node`.
func NodeKey(node string) (string, error) {
if err := keys.CheckName("node", node); err != nil {
return "", err
}
return node + "." + nodeengineclient.NodeWord, nil
}
// ParseKey is the inverse of Key and NodeKey: isNode is true for `<node>.node`, with module and fingerprint empty.
func ParseKey(key string) (node, module, fingerprint string, isNode bool, err error) {
node, module, part, isNode, err := nodeengineclient.ParseComposedKey(key)
if err != nil || isNode {
return node, module, "", isNode, err
}
return node, module, meshcatalogclient.SourcePrefix2 + part, false, nil
}
// ByDigest is c with every file over 64 KiB given by its digest and size, and the content of each by digest, for the
// writer to put in the node's object bucket (declaration.FileBucketFor) under declaration.ObjectName before it writes
// the composition. A file that is sealed, filled with secrets or written into another stays as it is.
func (c Composed) ByDigest() (Composed, map[string][]byte, error) {
out := c
out.Resources = make([]json.RawMessage, len(c.Resources))
objects := map[string][]byte{}
for i, raw := range c.Resources {
out.Resources[i] = raw
r, err := declaration.ParseResource(raw)
if err != nil {
return Composed{}, nil, err
}
f, ok := r.(*declaration.File)
if !ok || !f.TravelsByDigest() {
continue
}
ref, body, err := f.AsReference()
if err != nil {
return Composed{}, nil, err
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return Composed{}, nil, err
}
delete(fields, "content")
delete(fields, "bytes")
fields["digest"], _ = json.Marshal(ref.Digest)
fields["size"], _ = json.Marshal(ref.Size)
rewritten, err := json.Marshal(fields)
if err != nil {
return Composed{}, nil, err
}
out.Resources[i] = rewritten
objects[ref.Digest] = body
}
return out, objects, nil
}
// Split splits a node's composed resources into its compositions, by key (Key, NodeKey): each resource into the
// composition of the module it says, under that module's fingerprint in fps, and a resource that says no module into the
// node's own, with own. Each filled path goes with the module whose file declares it. The order of the resources is kept
// within each composition. A resource of a module fps names no fingerprint for is refused, naming it.
func Split(node string, resources []json.RawMessage, fps map[string]string, filled []string, own NodeOwn) (map[string]Composed, error) {
nodeKey, err := NodeKey(node)
if err != nil {
return nil, err
}
out := map[string]Composed{nodeKey: {Node: node, Own: own, Resources: []json.RawMessage{}}}
pathOf := map[string]string{}
for _, raw := range resources {
var head struct {
ID string `json:"id"`
Type string `json:"type"`
Module string `json:"module"`
Path string `json:"path"`
}
if err := json.Unmarshal(raw, &head); err != nil {
return nil, fmt.Errorf("compositions: a resource of %s is not JSON: %w", node, err)
}
key := nodeKey
if head.Module != "" {
fp, ok := fps[head.Module]
if !ok {
return nil, fmt.Errorf("compositions: the resource %q is of %q, whose fingerprint on %s is not given", head.ID, head.Module, node)
}
if key, err = Key(node, head.Module, fp); err != nil {
return nil, err
}
if _, ok := out[key]; !ok {
out[key] = Composed{Node: node, Module: head.Module, FP: fp}
}
}
if head.Type == string(declaration.TypeFile) && head.Module != "" {
pathOf[head.Path] = key
}
c := out[key]
c.Resources = append(c.Resources, raw)
out[key] = c
}
for _, path := range filled {
key, ok := pathOf[path]
if !ok {
return nil, fmt.Errorf("compositions: the provisioner fills %q on %s, and no module's file declares it", path, node)
}
c := out[key]
c.Filled = append(c.Filled, path)
out[key] = c
}
return out, nil
}