main's mesh-compose is the composer command (ADR 0482 clause 4) and keeps its name, which composer.Package and the build steps name; feat/r6's hand path tool, which composes one module for one node and signs it with the operator's key (ADR 0539 clause 10), moves to cmd/mesh-hand-compose.
242 lines
10 KiB
Go
242 lines
10 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-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, `<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"
|
|
// 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 `<module>.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 }
|