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`): 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-". 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:", 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: `..`. 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`. 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`, 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 }