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
252 lines
8.6 KiB
Go
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
|
|
}
|