Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
635 lines
26 KiB
Go
635 lines
26 KiB
Go
// Package delivery is the one home of the keys and values the parts of delivery read and write on the bus
|
|
// (novox/hq ADR 0429 §2: the key layouts another repository reads live in mesh-sdk; research 063 section 01). The
|
|
// node-engine writes the have keys, the watchman of the repository mesh-delivery writes presence and pulse, the
|
|
// view reads all of them, and the controller grants them; every one imports this package, and nothing else defines
|
|
// these names.
|
|
//
|
|
// **Buckets** are named `<owner module>_<name>` as the broker names a module's state (provider.BucketOf): the
|
|
// node-engine's `have`, and the watchman's `presence` and `pulse`.
|
|
//
|
|
// **Keys.** A have key is `<node>.<module>`, and a node's own facts are `<node>.node`, so a module may not be named
|
|
// `node`. A presence key is `<node>` for a node and `part.<part>.<instance>` for a part's instance, so a node may
|
|
// not be named `part`. A pulse key is `<bucket>.<key>`: the full name of the bucket watched, then that bucket's key,
|
|
// which may hold dots, so it splits at the first dot. A name is refused when it is empty or holds a dot, a wildcard,
|
|
// whitespace or a character a bus key refuses, and the error names it; but a module's name may hold dots between its
|
|
// parts (`novox.be`), and keys with each dot escaped as EscapeName writes it (`novox=2ebe`, issue 1150).
|
|
//
|
|
// **Values** are JSON of under about 500 bytes, each carrying its version in `v`. A reader refuses a value whose
|
|
// version is missing or one it does not know, with an error that is ErrUnknownVersion (research 063 section 05
|
|
// question 9 item 7): a writer writes a new version only once every reader's have shows a version that reads it. A
|
|
// writer writes only when the new value differs from the one before (the Same functions).
|
|
//
|
|
// **Source, candidates, modules and the forge** (issue 1200; ADR 0437 §5, release 3): the gate's bucket `source` with
|
|
// its one key `main` (every repository's merged head and the group that moved it), the indexer's `candidates.<group>`
|
|
// and `modules.<module>` (a module's source fingerprint, or none and why), and the forge bridge's messages named
|
|
// `forge.<owner>.<name>.<kind>` (source.go).
|
|
//
|
|
// **Merges, asks, owed and answers** (issue 1201; ADR 0437 §6, §8): the gate's `merges.<group>`, its asks
|
|
// `merge.<group>.<heads hash>` (the router's ask whole), its owed keys `<repository>.<number>.<head>.<act>` that the
|
|
// forge bridge performs, and the teller's answer under the ask's own key (gate.go).
|
|
//
|
|
// **The source fingerprint** (issue 1203; ADR 0437 §5): one function of named inputs that the controller's builder and
|
|
// the indexer both call, so they agree byte for byte (fingerprint.go).
|
|
//
|
|
// **The release 4 layouts** (issues 1356 to 1359; ADR 0453 clauses 4, 6 and 16): the new fingerprint form (fingerprint2.go),
|
|
// the indexer's manifests by digest and the new fields of its modules and candidates (manifests.go, source.go), the
|
|
// builder's build keys (builder.go) and the checker's verdicts and kept verdicts (checker.go). A manifests value is the
|
|
// one exception to the size above: a manifest is up to about 21 KB, whole, byte for byte. The bus module's have on the
|
|
// node holding mesh-broker is the other: its user list's handing carries a sealed box of every loaded user's hash, some
|
|
// kilobytes (handed.go).
|
|
//
|
|
// Standard library only, and this module's own asks, declaration and seal packages (a composition holds declaration
|
|
// resources; the adoption handing is a sealed box, handed.go).
|
|
package delivery
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The buckets of delivery's parts. Each is written by one owner: every node's node-engine writes the have keys of
|
|
// its own node only, and the watchman writes presence and pulse.
|
|
const (
|
|
HaveBucket = "node-engine_have"
|
|
PresenceBucket = WatchmanModule + "_presence"
|
|
PulseBucket = WatchmanModule + "_pulse"
|
|
)
|
|
|
|
// HaveHistory and PresenceHistory are the revisions the have and presence buckets keep per key (research 063
|
|
// section 01).
|
|
const (
|
|
HaveHistory = 20
|
|
PresenceHistory = 20
|
|
)
|
|
|
|
// PulseMoveBound is how long a move may take: a pulse key lives for this time minus the move's age, and the bucket
|
|
// expires it on its own (the server writes a delete marker), so a move that overruns it is seen to stall.
|
|
const PulseMoveBound = 120 * time.Second
|
|
|
|
// Version is the version of every value this package writes and the only one it reads.
|
|
const Version = 1
|
|
|
|
// ErrUnknownVersion is what a decode returns for a value whose version is missing or not Version. Test it with
|
|
// errors.Is; the message names the version seen and the one read.
|
|
var ErrUnknownVersion = errors.New("delivery: unknown value version")
|
|
|
|
// Phase is how far a module's change has come on a node.
|
|
type Phase string
|
|
|
|
// The phases of a module's change on a node.
|
|
const (
|
|
PhaseBackingUp Phase = "backing-up"
|
|
PhaseApplying Phase = "applying"
|
|
PhaseChecking Phase = "checking"
|
|
PhaseRunning Phase = "running"
|
|
PhaseFailed Phase = "failed"
|
|
// PhaseRemoved is written by the node-engine, once, for a module its applied declaration no longer holds and
|
|
// for which it had written a have key; the key is never deleted (issue 1369, ADR 0408 §4), so a reader can tell
|
|
// that the module stopped.
|
|
PhaseRemoved Phase = "removed"
|
|
// PhaseHeld is a module whose backup before an apply failed: it keeps running what it has, nothing of the new
|
|
// composition is applied, and the node waits for the answer in its want (novox/hq ADR 0539 clause 16). Detail
|
|
// carries the backup's own words.
|
|
PhaseHeld Phase = "held"
|
|
// PhaseWaitingYes is a module of policy record whose composed move the node agent judged to recreate a container or
|
|
// move its data (novox/hq ADR 0547 §3): nothing of it is applied, Judgement says the judgement and Waits the digest
|
|
// of the composition waiting, and the assigner asks a person; the node applies it once its want's Yes names Waits.
|
|
PhaseWaitingYes Phase = "waiting-yes"
|
|
)
|
|
|
|
// Health is what the module's own check says.
|
|
type Health string
|
|
|
|
// The healths a have carries.
|
|
const (
|
|
HealthOK Health = "ok"
|
|
HealthFailed Health = "failed"
|
|
HealthWaits Health = "waits"
|
|
HealthUnknown Health = "unknown"
|
|
)
|
|
|
|
var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
|
|
|
|
// checkName refuses a name that is empty or holds a dot, a wildcard, whitespace or a character a bus key refuses.
|
|
func checkName(kind, name string) error {
|
|
if name == "" {
|
|
return fmt.Errorf("delivery: the %s name is empty", kind)
|
|
}
|
|
if !nameOK.MatchString(name) {
|
|
return fmt.Errorf("delivery: the %s name %q holds a dot, a wildcard, whitespace or a character a key refuses", kind, name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EscapeName is name as a key part: a character outside letters, digits, `-`, `/` and `_` is written `=` and its
|
|
// two lowercase hex digits per byte, and `=` itself is escaped, so any non-empty name keys and a name of allowed
|
|
// characters only keys as it is. UnescapeName is its inverse.
|
|
func EscapeName(name string) string {
|
|
var b strings.Builder
|
|
for i := 0; i < len(name); i++ {
|
|
c := name[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '-', c == '/', c == '_':
|
|
b.WriteByte(c)
|
|
default:
|
|
fmt.Fprintf(&b, "=%02x", c)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// UnescapeName is the inverse of EscapeName; it refuses a part with a bare `=` or any character EscapeName leaves
|
|
// escaped.
|
|
func UnescapeName(part string) (string, error) {
|
|
var out []byte
|
|
for i := 0; i < len(part); i++ {
|
|
c := part[i]
|
|
if c != '=' {
|
|
if !nameOK.MatchString(string(c)) {
|
|
return "", fmt.Errorf("delivery: %q holds %q, which a key part escapes", part, c)
|
|
}
|
|
out = append(out, c)
|
|
continue
|
|
}
|
|
if i+3 > len(part) {
|
|
return "", fmt.Errorf("delivery: %q ends in an incomplete escape", part)
|
|
}
|
|
v, err := hex.DecodeString(part[i+1 : i+3])
|
|
if err != nil || part[i+1:i+3] != strings.ToLower(part[i+1:i+3]) {
|
|
return "", fmt.Errorf("delivery: %q holds a malformed escape %q", part, part[i:i+3])
|
|
}
|
|
out = append(out, v[0])
|
|
i += 2
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// HaveKey is the key, in HaveBucket, of a module's have on a node: `<node>.<module>`. A module named `node` is
|
|
// refused, since `<node>.node` is the node's own facts.
|
|
func HaveKey(node, module string) (string, error) {
|
|
if err := checkName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
part, err := moduleKeyPart(module)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if module == "node" {
|
|
return "", fmt.Errorf(`delivery: the module name "node" is refused: %s.node is the node's own key`, node)
|
|
}
|
|
return node + "." + part, nil
|
|
}
|
|
|
|
var moduleNameOK = regexp.MustCompile(`^[-/_a-zA-Z0-9]+(\.[-/_a-zA-Z0-9]+)*$`)
|
|
|
|
// moduleKeyPart is a module's name as a key part (issue 1150): a name may hold a dot (`novox.be`), which a key part
|
|
// may not, so a dot is escaped as EscapeName writes it (`novox=2ebe`); a name of letters, digits, `-`, `/` and `_` keys
|
|
// as it is. A name that is empty, or holds a wildcard, whitespace, `=`, or an empty part between dots, is refused.
|
|
func moduleKeyPart(module string) (string, error) {
|
|
if module == "" {
|
|
return "", fmt.Errorf("delivery: the module name is empty")
|
|
}
|
|
if !moduleNameOK.MatchString(module) {
|
|
return "", fmt.Errorf("delivery: the module name %q holds a wildcard, whitespace, '=', an empty part between dots or a character a key refuses", module)
|
|
}
|
|
return EscapeName(module), nil
|
|
}
|
|
|
|
// parseModuleKeyPart is the inverse of moduleKeyPart; it refuses a part that is not the one moduleKeyPart writes for
|
|
// its name (an escape of a character that keys as it is, an upper-case escape, an escape of a refused character).
|
|
func parseModuleKeyPart(part string) (string, error) {
|
|
if err := checkName("module", part); err != nil {
|
|
return "", err
|
|
}
|
|
module, err := UnescapeName(part)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if back, err := moduleKeyPart(module); err != nil || back != part {
|
|
return "", fmt.Errorf("delivery: the module part %q is not a module name as a key writes it", part)
|
|
}
|
|
return module, nil
|
|
}
|
|
|
|
// HaveNodeWord is the last token of a node's own facts' key in HaveBucket.
|
|
const HaveNodeWord = nodeWord
|
|
|
|
// HaveNodeKey is the key, in HaveBucket, of a node's own facts: `<node>.node`.
|
|
func HaveNodeKey(node string) (string, error) {
|
|
if err := checkName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
return node + "." + HaveNodeWord, nil
|
|
}
|
|
|
|
// ParseHaveKey is the inverse of HaveKey and HaveNodeKey: isNode is true for `<node>.node`, with module empty.
|
|
func ParseHaveKey(key string) (node, module string, isNode bool, err error) {
|
|
node, module, ok := strings.Cut(key, ".")
|
|
if !ok {
|
|
return "", "", false, fmt.Errorf("delivery: %q is not a have key, <node>.<module>", key)
|
|
}
|
|
if err := checkName("node", node); err != nil {
|
|
return "", "", false, fmt.Errorf("delivery: %q is not a have key: %w", key, err)
|
|
}
|
|
if module == "node" {
|
|
return node, "", true, nil
|
|
}
|
|
module, err = parseModuleKeyPart(module)
|
|
if err != nil {
|
|
return "", "", false, fmt.Errorf("delivery: %q is not a have key: %w", key, err)
|
|
}
|
|
return node, module, false, nil
|
|
}
|
|
|
|
// PresenceNodeKey is the key, in PresenceBucket, of a node: `<node>`. A node named `part` is refused.
|
|
func PresenceNodeKey(node string) (string, error) {
|
|
if err := checkName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
if node == "part" {
|
|
return "", errors.New(`delivery: the node name "part" is refused: part.<part>.<instance> is a part's key`)
|
|
}
|
|
return node, nil
|
|
}
|
|
|
|
// PresencePartKey is the key, in PresenceBucket, of a part's instance: `part.<part>.<instance>`.
|
|
func PresencePartKey(part, instance string) (string, error) {
|
|
if err := checkName("part", part); err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkName("instance", instance); err != nil {
|
|
return "", err
|
|
}
|
|
return "part." + part + "." + instance, nil
|
|
}
|
|
|
|
// ParsePresenceKey is the inverse of PresenceNodeKey and PresencePartKey: for a node's key part and instance are
|
|
// empty, for a part's key node is.
|
|
func ParsePresenceKey(key string) (node, part, instance string, err error) {
|
|
if !strings.Contains(key, ".") {
|
|
node, err = PresenceNodeKey(key)
|
|
if err != nil {
|
|
return "", "", "", fmt.Errorf("delivery: %q is not a presence key: %w", key, err)
|
|
}
|
|
return node, "", "", nil
|
|
}
|
|
fields := strings.Split(key, ".")
|
|
if len(fields) != 3 || fields[0] != "part" {
|
|
return "", "", "", fmt.Errorf("delivery: %q is not a presence key, <node> or part.<part>.<instance>", key)
|
|
}
|
|
if _, err := PresencePartKey(fields[1], fields[2]); err != nil {
|
|
return "", "", "", fmt.Errorf("delivery: %q is not a presence key: %w", key, err)
|
|
}
|
|
return "", fields[1], fields[2], nil
|
|
}
|
|
|
|
// PulseKey is the key, in PulseBucket, of the move watched at key of bucket: `<bucket>.<key>`. The bucket is the
|
|
// full bucket name and holds no dot; the key may.
|
|
func PulseKey(bucket, key string) (string, error) {
|
|
if err := checkName("bucket", bucket); err != nil {
|
|
return "", err
|
|
}
|
|
if key == "" || strings.HasPrefix(key, ".") || strings.HasSuffix(key, ".") || strings.Contains(key, "..") {
|
|
return "", fmt.Errorf("delivery: the pulse key %q is empty or has an empty part", key)
|
|
}
|
|
for _, part := range strings.Split(key, ".") {
|
|
if err := checkName("key", part); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return bucket + "." + key, nil
|
|
}
|
|
|
|
// ParsePulseKey is the inverse of PulseKey, splitting at the first dot.
|
|
func ParsePulseKey(pulseKey string) (bucket, key string, err error) {
|
|
bucket, key, ok := strings.Cut(pulseKey, ".")
|
|
if !ok {
|
|
return "", "", fmt.Errorf("delivery: %q is not a pulse key, <bucket>.<key>", pulseKey)
|
|
}
|
|
if _, err := PulseKey(bucket, key); err != nil {
|
|
return "", "", fmt.Errorf("delivery: %q is not a pulse key: %w", pulseKey, err)
|
|
}
|
|
return bucket, key, nil
|
|
}
|
|
|
|
// PulseTTL is the time to live of a pulse key for a move of the given age: PulseMoveBound minus the age. At or past
|
|
// the bound the move is stalled, with no time left and nothing to write: the watchman raises it at once.
|
|
func PulseTTL(moveAge time.Duration) (ttl time.Duration, stalled bool) {
|
|
if moveAge >= PulseMoveBound {
|
|
return 0, true
|
|
}
|
|
return PulseMoveBound - moveAge, false
|
|
}
|
|
|
|
// Have is a module's state on a node, written by that node's node-engine under HaveKey. FP is the module's
|
|
// fingerprint once the indexer exists (design 47 release 3); until then Build is the build the node was sent, and
|
|
// the view compares by build.
|
|
//
|
|
// Composed is the digest of the composition the node runs for the module (novox/hq ADR 0539 clause 9): the
|
|
// controller judges a move against it (clause 14), and the assigner reads it to know a want was applied.
|
|
type Have struct {
|
|
V int `json:"v"`
|
|
FP string `json:"fp,omitempty"`
|
|
Composed string `json:"composed,omitempty"`
|
|
Build string `json:"build,omitempty"`
|
|
Phase Phase `json:"phase"`
|
|
Health Health `json:"health"`
|
|
Detail string `json:"detail,omitempty"`
|
|
// UserList is what the node-engine of the node holding mesh-broker wrote of the bus's user list, in the have of the
|
|
// bus module (novox/hq ADR 0547 §7, §8). Nil on every other have.
|
|
UserList *UserListWritten `json:"user-list,omitempty"`
|
|
// Judgement and Waits are, in phase waiting-yes, the node agent's judgement of the move and the digest of the
|
|
// composition that waits for a person's yes (ADR 0547 §3); empty otherwise.
|
|
Judgement Judgement `json:"judgement,omitempty"`
|
|
Waits string `json:"waits,omitempty"`
|
|
// Sealed is, for a module the node composes itself, each value its running composition carries sealed to the node's
|
|
// key (a secret, a credential, a vault box), as applied (ADR 0547 §3, issue 1126): what the node's composer reuses
|
|
// after a restart while it opens to the vault's value, so a value sealed again moves no composition. Openable by the
|
|
// node alone, as the vault hands them over the bus. Empty on every other have.
|
|
Sealed []string `json:"sealed,omitempty"`
|
|
// SealedAt is Sealed by slot (Seals: a field of a resource, issue 1126, C1): the composer reuses a box only at the
|
|
// slot that holds it, so a provider and a consumer of one value on one node each keep their own. Written in Sealed's
|
|
// place by a node-engine that knows it; Sealed is read only from a have written before.
|
|
SealedAt Seals `json:"sealed-at,omitempty"`
|
|
}
|
|
|
|
// UserListWritten is the bus's user list as the node-engine of the bus node wrote it to the bus module's file, applied
|
|
// alone and with no want (novox/hq ADR 0547 §8): where it was taken from, what that source holds, and what the file
|
|
// holds now. The bus reloads the file itself when its digest changes (the module's entrypoint), so Digest is the list
|
|
// the bus loads. Wanted differs from Digest only while a write failed, and Error says why.
|
|
type UserListWritten struct {
|
|
// Source is the key the list was taken from: the controller's composition of the bus module for this node
|
|
// (`<node>.<module>.<fp>`), or the grants part's `user-list` once it took over.
|
|
Source string `json:"source"`
|
|
Revision uint64 `json:"revision"`
|
|
// Wanted is the digest of the list the source holds, `sha256:<hex>`.
|
|
Wanted string `json:"wanted"`
|
|
// Digest is the digest of the file on the machine after the write, `sha256:<hex>`; empty when there is none.
|
|
Digest string `json:"digest,omitempty"`
|
|
Users int `json:"users"`
|
|
At time.Time `json:"at"`
|
|
Error string `json:"error,omitempty"`
|
|
// Follows is which list the node follows now (FollowController or FollowGrants, novox/hq ADR 0547 clause 8), empty
|
|
// from a node-engine before the switch.
|
|
Follows string `json:"follows,omitempty"`
|
|
// Key is the bus node's sealing key, its public half as SPKI PEM: the key the grants part seals its list to
|
|
// (clause 7). Public, as the vault holds it.
|
|
Key string `json:"key,omitempty"`
|
|
// Unwatched says why the bus node does not watch the grants part's key now (its watch refused or ended, and tried
|
|
// again), empty while it does or where it need not (ADR 0547 clause 8): it then keeps the last list it followed.
|
|
Unwatched string `json:"unwatched,omitempty"`
|
|
// Handed is the hashes of the users of the list on the machine, sealed to the vault, for its adoption of the
|
|
// accounts it holds no value for (ADR 0547 clause 9, handed.go); nil while the vault's key is not known.
|
|
Handed *AccountsHanded `json:"handed,omitempty"`
|
|
}
|
|
|
|
// HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole:
|
|
// this package names the facts and does not type them.
|
|
//
|
|
// Composed is the digest of the node's own composition it runs (ADR 0539 clause 9).
|
|
type HaveNode struct {
|
|
V int `json:"v"`
|
|
Composed string `json:"composed,omitempty"`
|
|
Windows json.RawMessage `json:"windows,omitempty"`
|
|
Busy json.RawMessage `json:"busy,omitempty"`
|
|
Strays json.RawMessage `json:"strays,omitempty"`
|
|
Filters json.RawMessage `json:"filters,omitempty"`
|
|
Profile json.RawMessage `json:"profile,omitempty"`
|
|
// Record is what the machine observes of itself, typed, for every node and composing part to read (issue 1126,
|
|
// ADR 0555 §3): the node's record lives in its own have key, so it needs no bucket, writer or grant of its own.
|
|
Record *NodeRecord `json:"record,omitempty"`
|
|
// Sealed is, on a node composing its own, each value its own composition carries sealed to the node's key, as
|
|
// applied (Have.Sealed for the node's own, issue 1126): a value the vault seals again after a restart moves no
|
|
// composition, so a restart applies nothing again.
|
|
Sealed []string `json:"sealed,omitempty"`
|
|
// SealedAt is Sealed by slot (Have.SealedAt).
|
|
SealedAt Seals `json:"sealed-at,omitempty"`
|
|
// Candidates is the node's answer to each placement candidate naming it, by group (PlacementCandidate): written by
|
|
// its shadow composer whenever an answer changes, removed when the candidate key goes. The node applies nothing of a
|
|
// candidate.
|
|
Candidates map[string]CandidateAnswer `json:"candidates,omitempty"`
|
|
// Composes is whose compositions the node applies: its own (ComposesOwn, the catalogue places it) or the
|
|
// controller's (ComposesController); ComposesWhy says why in words. Written by the node agent at each start, so a
|
|
// node returning to the controller's compositions when its placement key is deleted says so (issue 1126). Empty
|
|
// from an engine older than the field.
|
|
Composes string `json:"composes,omitempty"`
|
|
ComposesWhy string `json:"composes-why,omitempty"`
|
|
}
|
|
|
|
// Whose compositions a node applies (HaveNode.Composes).
|
|
const (
|
|
ComposesOwn = "own"
|
|
ComposesController = "controller"
|
|
)
|
|
|
|
// Presence is whether a node or a part's instance is online; the entry's own time says since when. The watchman
|
|
// writes it.
|
|
type Presence struct {
|
|
V int `json:"v"`
|
|
Online bool `json:"online"`
|
|
}
|
|
|
|
// Pulse is the value of a pulse key: the watched key mirrored, its revision, and who owes the next move (a node or
|
|
// a part instance). The watchman writes it with the time to live PulseTTL gives.
|
|
type Pulse struct {
|
|
V int `json:"v"`
|
|
Bucket string `json:"bucket"`
|
|
Key string `json:"key"`
|
|
Revision uint64 `json:"revision"`
|
|
Owes string `json:"owes,omitempty"`
|
|
}
|
|
|
|
func checkVersion(data []byte) error {
|
|
var probe struct {
|
|
V *int `json:"v"`
|
|
}
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
return fmt.Errorf("delivery: value is not JSON: %w", err)
|
|
}
|
|
if probe.V == nil {
|
|
return fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownVersion, Version)
|
|
}
|
|
if *probe.V != Version {
|
|
return fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownVersion, *probe.V, Version)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decode(data []byte, into any) error {
|
|
if err := checkVersion(data); err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(data, into); err != nil {
|
|
return fmt.Errorf("delivery: value does not fit its shape: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h Have) check() error {
|
|
switch h.Phase {
|
|
case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld:
|
|
if h.Judgement != "" || h.Waits != "" {
|
|
return fmt.Errorf("delivery: only a have waiting for a yes names a judgement and the composition waiting")
|
|
}
|
|
case PhaseWaitingYes:
|
|
if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot {
|
|
return fmt.Errorf("delivery: a have waiting for a yes names the judgement %q, not recreates, moves-data or cannot-judge", h.Judgement)
|
|
}
|
|
if err := checkDigest(h.Waits, "the composition a have waits a yes for"); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("delivery: phase %q is not backing-up, applying, checking, running, failed, removed, held or waiting-yes", h.Phase)
|
|
}
|
|
if h.Composed != "" {
|
|
if err := checkDigest(h.Composed, "the have's composition digest"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
switch h.Health {
|
|
case HealthOK, HealthFailed, HealthWaits, HealthUnknown:
|
|
default:
|
|
return fmt.Errorf("delivery: health %q is not ok, failed, waits or unknown", h.Health)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeHave is h as JSON at Version; it refuses a phase or health outside the lists.
|
|
func EncodeHave(h Have) ([]byte, error) {
|
|
if err := h.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
h.V = Version
|
|
return json.Marshal(h)
|
|
}
|
|
|
|
// DecodeHave reads a have; it refuses an unknown version (ErrUnknownVersion) and a phase or health outside the lists.
|
|
func DecodeHave(data []byte) (Have, error) {
|
|
var h Have
|
|
if err := decode(data, &h); err != nil {
|
|
return Have{}, err
|
|
}
|
|
if err := h.check(); err != nil {
|
|
return Have{}, err
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
func (n HaveNode) check() error {
|
|
switch n.Composes {
|
|
case "", ComposesOwn, ComposesController:
|
|
default:
|
|
return fmt.Errorf("delivery: a node composes %q, neither %s nor %s", n.Composes, ComposesOwn, ComposesController)
|
|
}
|
|
for group, a := range n.Candidates {
|
|
if group == "" {
|
|
return fmt.Errorf("delivery: a node's candidate answer names no group")
|
|
}
|
|
if err := a.check(); err != nil {
|
|
return fmt.Errorf("delivery: the answer to %s's candidate: %w", group, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeHaveNode is n as JSON at Version; it refuses a candidate answer CandidateAnswer's rules refuse.
|
|
func EncodeHaveNode(n HaveNode) ([]byte, error) {
|
|
if err := n.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
n.V = Version
|
|
return json.Marshal(n)
|
|
}
|
|
|
|
// DecodeHaveNode reads a node's facts; it refuses an unknown version (ErrUnknownVersion) and a candidate answer that
|
|
// is not one.
|
|
func DecodeHaveNode(data []byte) (HaveNode, error) {
|
|
var n HaveNode
|
|
if err := decode(data, &n); err != nil {
|
|
return n, err
|
|
}
|
|
if err := n.check(); err != nil {
|
|
return HaveNode{}, err
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// EncodePresence is p as JSON at Version.
|
|
func EncodePresence(p Presence) ([]byte, error) {
|
|
p.V = Version
|
|
return json.Marshal(p)
|
|
}
|
|
|
|
// DecodePresence reads a presence; it refuses an unknown version (ErrUnknownVersion).
|
|
func DecodePresence(data []byte) (Presence, error) {
|
|
var p Presence
|
|
err := decode(data, &p)
|
|
return p, err
|
|
}
|
|
|
|
// EncodePulse is p as JSON at Version.
|
|
func EncodePulse(p Pulse) ([]byte, error) {
|
|
p.V = Version
|
|
return json.Marshal(p)
|
|
}
|
|
|
|
// DecodePulse reads a pulse; it refuses an unknown version (ErrUnknownVersion).
|
|
func DecodePulse(data []byte) (Pulse, error) {
|
|
var p Pulse
|
|
err := decode(data, &p)
|
|
return p, err
|
|
}
|
|
|
|
// SameHave says whether two haves are the same value, so a writer writes only when they differ.
|
|
func SameHave(a, b Have) bool {
|
|
ua, ub := a.UserList, b.UserList
|
|
a.UserList, b.UserList = nil, nil
|
|
if !slices.Equal(a.Sealed, b.Sealed) || !maps.Equal(a.SealedAt, b.SealedAt) {
|
|
return false
|
|
}
|
|
a.Sealed, b.Sealed = nil, nil
|
|
a.SealedAt, b.SealedAt = nil, nil
|
|
if !reflect.DeepEqual(a, b) || (ua == nil) != (ub == nil) {
|
|
return false
|
|
}
|
|
if ua == nil {
|
|
return true
|
|
}
|
|
x, y := *ua, *ub
|
|
x.At, y.At = time.Time{}, time.Time{}
|
|
x.Handed, y.Handed = nil, nil
|
|
return x == y && ua.At.Equal(ub.At) && sameHanded(ua.Handed, ub.Handed)
|
|
}
|
|
|
|
// SameHaveNode says whether two node facts are the same value, comparing each carried JSON without its spacing.
|
|
func SameHaveNode(a, b HaveNode) bool {
|
|
return a.V == b.V && a.Composed == b.Composed && sameJSON(a.Windows, b.Windows) && sameJSON(a.Busy, b.Busy) &&
|
|
sameJSON(a.Strays, b.Strays) && sameJSON(a.Filters, b.Filters) && sameJSON(a.Profile, b.Profile) &&
|
|
(a.Record == nil) == (b.Record == nil) && (a.Record == nil || SameNodeRecord(*a.Record, *b.Record)) &&
|
|
jsonEqual(a.Sealed, b.Sealed) && maps.Equal(a.SealedAt, b.SealedAt) && jsonEqual(a.Candidates, b.Candidates) && a.Composes == b.Composes &&
|
|
a.ComposesWhy == b.ComposesWhy
|
|
}
|
|
|
|
// SamePresence says whether two presences are the same value.
|
|
func SamePresence(a, b Presence) bool { return a == b }
|
|
|
|
// SamePulse says whether two pulses are the same value.
|
|
func SamePulse(a, b Pulse) bool { return a == b }
|
|
|
|
func sameJSON(a, b json.RawMessage) bool {
|
|
var ca, cb bytes.Buffer
|
|
if json.Compact(&ca, a) != nil || json.Compact(&cb, b) != nil {
|
|
return bytes.Equal(a, b)
|
|
}
|
|
return bytes.Equal(ca.Bytes(), cb.Bytes())
|
|
}
|