Files
mesh-sdk/go/delivery/composed.go
T
jschoubben 64a502d16a Carry the module's upgrade policy and its source beside a composition's judgement (issue 1943; for issue 1906)
The assigner holds a move of a module of policy record, and only the controller knows a person's own choice of
policy (ADR 0539 clause 14).
2026-10-24 16:40:45 +02:00

233 lines
9.7 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
// The controller's compositions (novox/hq ADR 0539 clauses 1 to 4). A **composition** is one module's resources
// composed for one node at one fingerprint, written by the controller, which keeps the inputs. It lives in a bucket of
// each node, `composed-<node>`, which only that node's node-engine reads (a composition can carry a sealed value), under
// `<node>.<module>.<fingerprint hex>`, and the node's own resources (adoption, firewall openings, accounts, the
// resolver, identities and groups) under `<node>.node`. Beside it, under the same key in `mesh-controller_composed`,
// which the assigner reads, the controller writes the composition's digest and its judgement of the move from the
// composition the node runs now (ComposedDigest). The controller writes both by compare-and-set while it holds the
// lease; the operator's credential is a second writer on the hand path alone (clause 10, mesh-compose).
//
// A composition is derived state (clause 4): its key and its digest are deleted by the controller when its fingerprint
// is no longer among the module's five newest and no want names it, and each bucket keeps five revisions of a key.
const (
// ComposedBucketPrefix starts the name of a node's bucket of compositions, ComposedBucketFor(node).
ComposedBucketPrefix = "composed-"
// ComposedHistory is the revisions a node's composition bucket keeps per key (ADR 0539 clause 4).
ComposedHistory = 5
// ControllerModule is the module of the controller, the writer of the compositions.
ControllerModule = "mesh-controller"
// ControllerComposedBucket is the controller's bucket of each composition's digest and judgement, `<owner
// module>_<name>`, read by the assigner (ADR 0539 clauses 2 and 6).
ControllerComposedBucket = ControllerModule + "_composed"
// ControllerComposedHistory is the revisions that bucket keeps per key.
ControllerComposedHistory = 5
// ComposedKeep is how many of a module's newest fingerprints keep their compositions though no want names them
// (ADR 0539 clause 4).
ComposedKeep = 5
)
// nodeWord is the last part of a node's own key, in the have bucket and in a node's composition bucket alike.
const nodeWord = "node"
var bucketNode = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// ComposedBucketFor is the name of a node's bucket of compositions, "composed-<node>", as declaration.FileBucketFor
// names a node's object bucket (ADR 0488 §4). The node name must be one a bucket and a subject token accept: letters,
// digits, '_' and '-'.
func ComposedBucketFor(node string) (string, error) {
if node == "" {
return "", fmt.Errorf("delivery: the node name is empty; a node's composition bucket is named for its node")
}
if !bucketNode.MatchString(node) {
return "", fmt.Errorf("delivery: the node name %q is not one a bucket accepts: letters, digits, '_' and '-' only", node)
}
return ComposedBucketPrefix + node, nil
}
// ComposedKey is the key, in ComposedBucketFor(node) and in ControllerComposedBucket, of a module's composition for a
// node at a fingerprint: `<node>.<module>.<hex>`, the hex being the fingerprint's key part (FingerprintKeyPart). A
// module named `node` is refused, since `<node>.node` is the node's own key.
func ComposedKey(node, module, fingerprint string) (string, error) {
have, err := HaveKey(node, module)
if err != nil {
return "", err
}
part, err := FingerprintKeyPart(fingerprint)
if err != nil {
return "", err
}
return have + "." + part, nil
}
// ComposedNodeKey is the key of a node's own composition: `<node>.node`.
func ComposedNodeKey(node string) (string, error) {
return HaveNodeKey(node)
}
// ParseComposedKey is the inverse of ComposedKey and ComposedNodeKey: isNode is true for `<node>.node`, with module
// and fingerprint empty. It refuses any other shape, naming the key.
func ParseComposedKey(key string) (node, module, fingerprint string, isNode bool, err error) {
refused := func(why error) (string, string, string, bool, error) {
return "", "", "", false, fmt.Errorf("delivery: %q is not a composition key, <node>.<module>.<fingerprint hex> or <node>.node: %w", key, why)
}
parts := strings.Split(key, ".")
switch len(parts) {
case 2:
if parts[1] != nodeWord {
return refused(fmt.Errorf("a module's composition names its fingerprint"))
}
if err := checkName("node", parts[0]); err != nil {
return refused(err)
}
return parts[0], "", "", true, nil
case 3:
node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1])
if err != nil {
return refused(err)
}
if isNode {
return refused(fmt.Errorf("the node's own key names no fingerprint"))
}
fp, err := ParseFingerprintKeyPart(parts[2])
if err != nil {
return refused(err)
}
return node, module, fp, false, nil
}
return refused(fmt.Errorf("it has %d parts", len(parts)))
}
// Judgement is the controller's judgement of a move from the composition a node runs, as its have names it, to a new
// one (ADR 0539 clauses 2 and 14, by ADR 0377 §3's tests).
type Judgement string
// The judgements of a move.
const (
// JudgedRecreates is a move that recreates a container.
JudgedRecreates Judgement = "recreates"
// JudgedMovesData is a move that gives the module's data another place.
JudgedMovesData Judgement = "moves-data"
// JudgedNeither is a move that does neither, and the judgement of a composition the node never ran.
JudgedNeither Judgement = "neither"
// JudgedCannot is a move that cannot be judged, because the composition the node runs is gone or cannot be read.
JudgedCannot Judgement = "cannot-judge"
)
var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
func checkDigest(digest, of string) error {
if !digestForm.MatchString(digest) {
return fmt.Errorf("delivery: %s %q is not sha256:<64 lowercase hex digits>", of, digest)
}
return nil
}
// ComposedDigest is the value, in ControllerComposedBucket, beside a composition: its Digest (ComposedDigestOf), and
// the Judgement of the move to it from the composition the node runs now, whose digest is Against (empty when the node
// runs no composition of the module). Words say why, and are required for a move that cannot be judged. Rewritten when
// the node's have changes.
//
// Policy is the module's upgrade policy as the controller holds it (ADR 0236, ADR 0539 clause 14), with PolicyFrom
// where it comes from and PolicyWhy why: the assigner holds a move of a module of policy record from it, the operator's
// own choice (`upgrade` with policy) included. Rewritten when the policy changes. Empty on a composition made by hand
// (mesh-compose), whose want the operator writes.
type ComposedDigest struct {
V int `json:"v"`
Digest string `json:"digest"`
Judgement Judgement `json:"judgement"`
Against string `json:"against,omitempty"`
Words string `json:"words,omitempty"`
Policy string `json:"policy,omitempty"`
PolicyFrom string `json:"policy-from,omitempty"`
PolicyWhy string `json:"policy-why,omitempty"`
}
// The upgrade policies and their sources a composition's value names: catalogue.PolicyRoll, PolicyTogether and
// PolicyRecord, and catalogue.FromPerson, FromModule, FromBus, FromData and FromDefault, the same words (the compose
// package's test holds them equal; this package does not import the catalogue).
var (
composedPolicies = []string{"roll", "together", "record"}
composedPolicyFroms = []string{"person", "module", "the bus", "irreplaceable data", "default"}
)
func oneOf(v string, of []string) bool {
for _, o := range of {
if v == o {
return true
}
}
return false
}
func (c ComposedDigest) check() error {
if err := checkDigest(c.Digest, "the composition's digest"); err != nil {
return err
}
if c.Against != "" {
if err := checkDigest(c.Against, "the digest a move is judged against"); err != nil {
return err
}
}
switch c.Judgement {
case JudgedRecreates, JudgedMovesData:
if c.Against == "" {
return fmt.Errorf("delivery: a move judged %s names the composition it is judged against", c.Judgement)
}
case JudgedNeither:
case JudgedCannot:
if c.Words == "" {
return fmt.Errorf("delivery: a move that cannot be judged says why")
}
default:
return fmt.Errorf("delivery: the judgement %q is not %s, %s, %s or %s", c.Judgement, JudgedRecreates,
JudgedMovesData, JudgedNeither, JudgedCannot)
}
if c.Policy != "" || c.PolicyFrom != "" {
if !oneOf(c.Policy, composedPolicies) {
return fmt.Errorf("delivery: the upgrade policy %q is not one of %v", c.Policy, composedPolicies)
}
if !oneOf(c.PolicyFrom, composedPolicyFroms) {
return fmt.Errorf("delivery: the upgrade policy's source %q is not one of %v", c.PolicyFrom, composedPolicyFroms)
}
}
return nil
}
// PolicyRecord is the policy whose moves the assigner holds for a person's yes (ADR 0539 clause 14).
const PolicyRecord = "record"
// EncodeComposedDigest is c as JSON at Version; it refuses an unknown judgement, a digest not of the sha256 form, a
// move judged to recreate or move data against nothing, and a move that cannot be judged without its reason.
func EncodeComposedDigest(c ComposedDigest) ([]byte, error) {
if err := c.check(); err != nil {
return nil, err
}
c.V = Version
return json.Marshal(c)
}
// DecodeComposedDigest reads the value beside a composition; it refuses an unknown version (ErrUnknownVersion) and
// what EncodeComposedDigest refuses.
func DecodeComposedDigest(data []byte) (ComposedDigest, error) {
var c ComposedDigest
if err := decode(data, &c); err != nil {
return ComposedDigest{}, err
}
if err := c.check(); err != nil {
return ComposedDigest{}, err
}
return c, nil
}
// SameComposedDigest says whether two values are the same, so the controller writes only when they differ.
func SameComposedDigest(a, b ComposedDigest) bool { return a == b }