package delivery import ( "encoding/json" "fmt" "regexp" "strings" ) // The compositions (novox/hq ADR 0539 clauses 1 to 4). A **composition** is one module's resources composed for one node // at one fingerprint: a node composes its own (ADR 0547 §2) and writes each it makes into its node's bucket, // `composed-` (ADR 0593 §6), under `..`, and // the node's own resources (adoption, firewall openings, accounts, the resolver, identities and groups) under // `.node`. A composition's digest and its judgement of the move from the composition the node runs now is // ComposedDigest. 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 // ComposedKeep is how many of a module's newest fingerprints keep their compositions though no want names them // (ADR 0539 clause 4). ComposedKeep = 5 ) // Who writes and who reads a node's compositions (novox/hq ADR 0593 §6): the node's engine alone writes its node's // bucket, unsigned, a record for readers that no engine applies; the grants part creates each bucket, create-only, and // grants the readers read-only on every node's bucket. A reader declares ComposedRead in its manifest's `reads`, and the // engine's manifest announces the state ComposedState so that name resolves. const ( // ComposedState is the local name the node-engine's manifest announces for the compositions of every node. ComposedState = "composed" // ComposedRead is the `reads` entry naming it: the node-engine's module and ComposedState. ComposedRead = "mesh-host." + ComposedState ) // ComposedReaders are the modules that may read every node's compositions (ADR 0593 §6, §8): the provisioner makes its // plans from them, the assigner compares them for mesh-wants.upgrade, and the view shows them. var ComposedReaders = []string{"mesh-delivery-assigner", "mesh-delivery-view", "mesh-provisioner"} // 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, // whose want the operator wrote. 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 }