The asks state gains the kind revert, keyed revert.<node>.<module>.<hex> after the reverted build's fingerprint; like the backup ask it needs no proof of who answered, so the view may answer it, and an asking that ends unanswered while the hold stands is written again. Builds: design 47 G §710, §300 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
294 lines
12 KiB
Go
294 lines
12 KiB
Go
// Package asks holds the layout of the assigner's state `asks`: the keys of the asks the assigner puts to a person and the
|
|
// value it keeps at each, which the assigner writes and the view and the teller read (novox/hq ADR 0634 §1, ADR 0635 §5). It
|
|
// is the assigner's own published package. The gate's merge ask (`merge.<group>.<heads hash>`) needs proof of who answered
|
|
// (ADR 0421 §1), so it is answered on the phone alone until issue 953 closes. The assigner's asks need no such proof, and the
|
|
// view may answer them: the hold of a fingerprint whose health check failed (`hold.<module>.<fingerprint hex>`: release it or
|
|
// keep it held), the restore of a data module set back (`restore.<node>.<module>.<fingerprint hex>`: restore and lose newer
|
|
// writes, or leave it stopped, ADR 0429 §6), a backup that failed before an apply, a move a person must allow for a module of
|
|
// policy record, and the authorising ask of a proposal of assignments.
|
|
package asks
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/catalogkey"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/kept"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
const (
|
|
// AskKindHold is the kind of the assigner's ask about a held fingerprint.
|
|
AskKindHold = "hold"
|
|
// AskKindRestore is the kind of the assigner's ask about a data module set back.
|
|
AskKindRestore = "restore"
|
|
// AskKindBackup is the kind of the assigner's ask about a backup before an apply that failed: back up again, or go on
|
|
// without one (novox/hq ADR 0539 clause 16).
|
|
AskKindBackup = "backup"
|
|
// AskKindRecord is the kind of the assigner's ask about a move a person must allow, for a module of policy record: a
|
|
// composition judged to recreate a container or move data, or one that cannot be judged (ADR 0539 clause 14).
|
|
AskKindRecord = "record"
|
|
// AskKindAssignments is the kind of the authorising ask about a proposal of assignments (novox/hq ADR 0567 §5):
|
|
// `assignments.<proposal id>`. The kind needs no proof, as the bus's put does not: its yes is an Approve, which only a
|
|
// channel proving who answered takes and the view never does (ViewAnswerFits).
|
|
AskKindAssignments = "assignments"
|
|
// AskKindOutside is the kind of the assigner's ask about a moved module whose failing health check has waited past its bound
|
|
// on an outside cause (design 47 G §706): go on, or count it as failed. `outside.<node>.<module>.<fingerprint hex>`.
|
|
AskKindOutside = "outside"
|
|
// AskKindRevert is the kind of the assigner's ask about a held revert, one with no restore point (design 47 G §710): keep it
|
|
// held, or start the reverted build on the live data. `revert.<node>.<module>.<fingerprint hex>`, the reverted build's.
|
|
AskKindRevert = "revert"
|
|
)
|
|
|
|
// AskKey is an ask's key read whole: its Kind, and the parts its kind has: a merge's Group and heads Hash, a hold's Module
|
|
// and fingerprint FP, a restore's, a backup's, an outside's and a revert's Node, Module and FP, a record's Node, Module and the digest Composed of the
|
|
// composition it would allow, the assignments' Proposal.
|
|
type AskKey struct {
|
|
Proposal string
|
|
Kind string
|
|
Group string
|
|
Hash string
|
|
Node string
|
|
Module string
|
|
FP string
|
|
Composed string
|
|
}
|
|
|
|
// AskNeedsProof says whether an ask of this kind must prove who answered it, so the view may not answer it: the merge's
|
|
// Approve does (ADR 0421 §1, issue 953), the assigner's hold and restore do not. An unknown kind is refused.
|
|
func AskNeedsProof(kind string) (bool, error) {
|
|
switch kind {
|
|
case meshdeliveryclient.AskKindMerge:
|
|
return true, nil
|
|
case AskKindHold, AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert:
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold,
|
|
AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments, AskKindOutside, AskKindRevert)
|
|
}
|
|
|
|
// HoldAskKey is the key of the ask about a module's held fingerprint: `hold.<module>.<hex>`.
|
|
func HoldAskKey(module, fingerprint string) (string, error) {
|
|
key, err := builds.BuilderBuildKey(module, fingerprint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindHold + "." + key, nil
|
|
}
|
|
|
|
// RestoreAskKey is the key of the ask about a data module set back on a node from a fingerprint:
|
|
// `restore.<node>.<module>.<hex>`.
|
|
func RestoreAskKey(node, module, fingerprint string) (string, error) {
|
|
if err := keys.CheckName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
key, err := builds.BuilderBuildKey(module, fingerprint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindRestore + "." + node + "." + key, nil
|
|
}
|
|
|
|
// OutsideAskKey is the key of the ask about a module's failing check on a node that waited past its bound on an outside cause:
|
|
// `outside.<node>.<module>.<hex>`.
|
|
func OutsideAskKey(node, module, fingerprint string) (string, error) {
|
|
key, err := RestoreAskKey(node, module, fingerprint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindOutside + strings.TrimPrefix(key, AskKindRestore), nil
|
|
}
|
|
|
|
// OutsideAbout is what the outside ask is about: `<node>.<module>.<hex>` (design 47 G §706).
|
|
func OutsideAbout(key string) string { return strings.TrimPrefix(key, AskKindOutside+".") }
|
|
|
|
// BackupAskKey is the key of the ask about a module's failed backup on a node before it applies a fingerprint:
|
|
// `backup.<node>.<module>.<hex>`.
|
|
func BackupAskKey(node, module, fingerprint string) (string, error) {
|
|
part, err := catalogkey.KeyPart(fingerprint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
key, err := nodeengineclient.ComposedKey(node, module, part)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindBackup + "." + key, nil
|
|
}
|
|
|
|
// RevertAskKey is the key of the ask about a held revert of a module on a node to the reverted build's fingerprint:
|
|
// `revert.<node>.<module>.<hex>` (design 47 G §710).
|
|
func RevertAskKey(node, module, fingerprint string) (string, error) {
|
|
key, err := BackupAskKey(node, module, fingerprint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindRevert + strings.TrimPrefix(key, AskKindBackup), nil
|
|
}
|
|
|
|
// AssignmentsAskKey is the key of the authorising ask about a proposal of assignments: `assignments.<proposal id>` (ADR 0567 §5).
|
|
func AssignmentsAskKey(proposal string) (string, error) {
|
|
if err := meshmoduleassignerclient.CheckProposalID(proposal); err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindAssignments + "." + proposal, nil
|
|
}
|
|
|
|
// RecordAskKey is the key of the ask about a move of a module on a node to the composition whose digest it names:
|
|
// `record.<node>.<module>.<digest hex>`, so a new revision at the same fingerprint is asked again (ADR 0539 clause 14).
|
|
func RecordAskKey(node, module, composed string) (string, error) {
|
|
have, err := nodeengineclient.HaveKey(node, module)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := kept.CheckDigest(composed, "a record ask's composition digest"); err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindRecord + "." + have + "." + strings.TrimPrefix(composed, "sha256:"), nil
|
|
}
|
|
|
|
// ParseAnyAskKey reads an ask key of any kind (a merge, hold, restore, backup, record, assignments, outside or revert ask); it refuses an unknown
|
|
// kind and a key whose parts do not fit its kind, naming the key.
|
|
func ParseAnyAskKey(key string) (AskKey, error) {
|
|
kind, rest, _ := strings.Cut(key, ".")
|
|
refused := func(err error) (AskKey, error) {
|
|
return AskKey{}, fmt.Errorf("asks: %q is not an ask key: %w", key, err)
|
|
}
|
|
switch kind {
|
|
case meshdeliveryclient.AskKindMerge:
|
|
_, group, hash, err := meshdeliveryclient.ParseAskKey(key)
|
|
if err != nil {
|
|
return AskKey{}, err
|
|
}
|
|
return AskKey{Kind: kind, Group: group, Hash: hash}, nil
|
|
case AskKindHold:
|
|
module, fp, err := builds.ParseBuilderBuildKey(rest)
|
|
if err != nil {
|
|
return refused(err)
|
|
}
|
|
return AskKey{Kind: kind, Module: module, FP: fp}, nil
|
|
case AskKindRestore:
|
|
node, build, ok := strings.Cut(rest, ".")
|
|
if !ok {
|
|
return refused(fmt.Errorf("a restore ask is restore.<node>.<module>.<fingerprint hex>"))
|
|
}
|
|
if err := keys.CheckName("node", node); err != nil {
|
|
return refused(err)
|
|
}
|
|
module, fp, err := builds.ParseBuilderBuildKey(build)
|
|
if err != nil {
|
|
return refused(err)
|
|
}
|
|
return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil
|
|
case AskKindOutside:
|
|
k, err := ParseAnyAskKey(AskKindRestore + "." + rest)
|
|
if err != nil {
|
|
return refused(fmt.Errorf("an outside ask is outside.<node>.<module>.<fingerprint hex>"))
|
|
}
|
|
k.Kind = kind
|
|
return k, nil
|
|
case AskKindBackup, AskKindRevert:
|
|
node, module, part, isNode, err := nodeengineclient.ParseComposedKey(rest)
|
|
if err != nil || isNode {
|
|
return refused(fmt.Errorf("a %s ask is %s.<node>.<module>.<fingerprint hex>", kind, kind))
|
|
}
|
|
fp, err := catalogkey.ParseKeyPart(part)
|
|
if err != nil {
|
|
return refused(err)
|
|
}
|
|
return AskKey{Kind: kind, Node: node, Module: module, FP: fp}, nil
|
|
case AskKindAssignments:
|
|
if err := meshmoduleassignerclient.CheckProposalID(rest); err != nil {
|
|
return refused(err)
|
|
}
|
|
return AskKey{Kind: kind, Proposal: rest}, nil
|
|
case AskKindRecord:
|
|
parts := strings.Split(rest, ".")
|
|
if len(parts) != 3 {
|
|
return refused(fmt.Errorf("a record ask is record.<node>.<module>.<digest hex>"))
|
|
}
|
|
node, module, isNode, err := nodeengineclient.ParseHaveKey(parts[0] + "." + parts[1])
|
|
if err != nil || isNode {
|
|
return refused(fmt.Errorf("a record ask is record.<node>.<module>.<digest hex>"))
|
|
}
|
|
composed := "sha256:" + parts[2]
|
|
if err := kept.CheckDigest(composed, "a record ask's composition digest"); err != nil {
|
|
return refused(err)
|
|
}
|
|
return AskKey{Kind: kind, Node: node, Module: module, Composed: composed}, nil
|
|
}
|
|
_, err := AskNeedsProof(kind)
|
|
return refused(err)
|
|
}
|
|
|
|
// Ask is the value of a key in the assigner's state asks: the router's ask whole, as the assigner composes it, so the teller
|
|
// publishes it unchanged. Asking counts the times the assigner wrote the ask at this key, as the gate's ask does: 0 or 1 for
|
|
// the first, n for the n-th, and the ask's id is meshdeliveryclient.AskingID(key, Asking), so each asking has its own id and
|
|
// an answer to an earlier asking is never taken for a later one. The ask is the operator-channel seat's as its JSON: its id,
|
|
// its options and its digest are checked by the assigner and the teller through operator-channel's client (novox/hq ADR
|
|
// 0627 §4).
|
|
type Ask struct {
|
|
V int `json:"v"`
|
|
Ask json.RawMessage `json:"ask"`
|
|
Asking int `json:"asking,omitempty"`
|
|
}
|
|
|
|
func (a Ask) check(key string) error {
|
|
ask, err := ParseAnyAskKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if proof, _ := AskNeedsProof(ask.Kind); proof {
|
|
return fmt.Errorf("asks: the assigner asks no %s ask: it needs proof of who answered", ask.Kind)
|
|
}
|
|
if a.Asking < 0 {
|
|
return fmt.Errorf("asks: the ask's asking %d is negative", a.Asking)
|
|
}
|
|
var fields map[string]json.RawMessage
|
|
if len(bytes.TrimSpace(a.Ask)) == 0 || json.Unmarshal(a.Ask, &fields) != nil || fields == nil {
|
|
return fmt.Errorf("asks: the value carries no router's ask")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeAsk is a, the ask at key, as JSON at keys.Version; it refuses a key of a kind that needs proof or of no kind, a
|
|
// negative asking, and a value carrying no router's ask.
|
|
func EncodeAsk(key string, a Ask) ([]byte, error) {
|
|
if err := a.check(key); err != nil {
|
|
return nil, err
|
|
}
|
|
a.V = keys.Version
|
|
return json.Marshal(a)
|
|
}
|
|
|
|
// DecodeAsk reads the ask at key; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeAsk refuses.
|
|
func DecodeAsk(key string, data []byte) (Ask, error) {
|
|
var a Ask
|
|
if err := kept.Decode(data, &a); err != nil {
|
|
return Ask{}, err
|
|
}
|
|
if err := a.check(key); err != nil {
|
|
return Ask{}, err
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// SameAsk says whether two asks are the same value: the same version, asking and router's ask, its JSON compared compacted.
|
|
func SameAsk(a, b Ask) bool {
|
|
return a.V == b.V && a.Asking == b.Asking && bytes.Equal(compacted(a.Ask), compacted(b.Ask))
|
|
}
|
|
|
|
func compacted(raw json.RawMessage) []byte {
|
|
var b bytes.Buffer
|
|
if json.Compact(&b, raw) != nil {
|
|
return raw
|
|
}
|
|
return b.Bytes()
|
|
}
|