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-`, which only that node's node-engine reads (a composition can carry a sealed value), under // `..`, and the node's own resources (adoption, firewall openings, accounts, the // resolver, identities and groups) under `.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-hand-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, `_`, 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-", 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: `..`, the hex being the fingerprint's key part (FingerprintKeyPart). A // module named `node` is refused, since `.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`. func ComposedNodeKey(node string) (string, error) { return HaveNodeKey(node) } // ParseComposedKey is the inverse of ComposedKey and ComposedNodeKey: isNode is true for `.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, .. or .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" // JudgedUnchanged is a composition equal to the module's slice of the declaration the node last applied, for a node // whose have names no composition yet (the first want at the switch, novox/hq ADR 0540 clause 9): applying it // changes nothing on the node, so it needs no person's yes. JudgedUnchanged Judgement = "unchanged" ) 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-hand-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"` // UserListOnly says the composition differs from the one the node's have names only in the bus's user list (the // bus module's file `.bus-users`), whatever the judgement says of its mount: the assigner writes such a // revision into the bus node's want at once, outside the record hold, at the fingerprint the node runs (ADR 0539 // clause 7). UserListOnly bool `json:"user-list-only,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, JudgedUnchanged: 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, %s or %s", c.Judgement, JudgedRecreates, JudgedMovesData, JudgedNeither, JudgedCannot, JudgedUnchanged) } 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 }