258 lines
10 KiB
Go
258 lines
10 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/contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/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"
|
|
)
|
|
|
|
// 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 and a backup'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:
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("asks: %q is not a kind of ask, %s, %s, %s, %s, %s or %s", kind, meshdeliveryclient.AskKindMerge, AskKindHold,
|
|
AskKindRestore, AskKindBackup, AskKindRecord, AskKindAssignments)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 := meshcatalogclient.KeyPart(fingerprint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
key, err := nodeengineclient.ComposedKey(node, module, part)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return AskKindBackup + "." + key, 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 or assignments 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 AskKindBackup:
|
|
node, module, part, isNode, err := nodeengineclient.ParseComposedKey(rest)
|
|
if err != nil || isNode {
|
|
return refused(fmt.Errorf("a backup ask is backup.<node>.<module>.<fingerprint hex>"))
|
|
}
|
|
fp, err := meshcatalogclient.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()
|
|
}
|