The assigner's seat with put, release and puts, the controller's compose verb, a put's key and value, and the view answers no option that authorises (ADR 0546)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package catalogue
|
||||
|
||||
// The assigner's seat (novox/hq ADR 0546): putting a version of a module on a node is one verb. The module
|
||||
// mesh-delivery-assigner, which writes every want, holds it once for the mesh and serves its verbs on its own process's
|
||||
// credential; a put is a key of its own state, `mesh-delivery-assigner_puts`, so where it stands is read and watched there.
|
||||
|
||||
// AssignerSeat is the seat mesh-delivery-assigner holds.
|
||||
const AssignerSeat = "mesh-delivery-assigner"
|
||||
|
||||
// assignerVerbs are the assigner seat's verbs: a put, its release, and the puts read (ADR 0546 clauses 1, 6 and 9).
|
||||
func assignerVerbs() []Verb {
|
||||
return []Verb{
|
||||
{Name: "put", Description: "Put one version of a module on one machine (novox/hq ADR 0546): the build at that " +
|
||||
"fingerprint, the module's bus accounts and grant there, its composition, then the machine's want naming " +
|
||||
"it, held there until released when hold is true. A module whose moves a person takes (the bus, a module " +
|
||||
"of policy record) is asked of the operator first, and the bus has its streams snapshotted after the yes. " +
|
||||
"Answers step by step what was done, or what it waits on and why; the put goes on after the answer, and " +
|
||||
"`puts` says where it stands.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine, where the module is assigned",
|
||||
"module": "the module",
|
||||
"version": "a fingerprint (src2: and 64 hex digits, or at least its first 12 hex digits), or newest",
|
||||
"hold": "\"true\": the want stays at this version until released; \"false\" (the default): the rules move it again once it runs",
|
||||
"why": "why, kept with the put and said in any ask",
|
||||
}, []string{"node", "module", "version", "why"}, "hold")},
|
||||
{Name: "release", Description: "End a put on one machine (novox/hq ADR 0546): its hold is taken away and the " +
|
||||
"assigner's rules write the machine's want again from their next pass. Nothing changes on the machine " +
|
||||
"until they move it.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine",
|
||||
"module": "the module",
|
||||
"why": "why, kept with the put",
|
||||
}, []string{"node", "module", "why"})},
|
||||
{Name: "puts", Description: "Every put and where it stands: its version, hold, why, each step with its time " +
|
||||
"and words, and what it waits on. Narrowed by machine or module. Read-only.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "one machine",
|
||||
"module": "one module",
|
||||
}, nil)},
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,8 @@ var defaultSeats = append([]Seat{
|
||||
// machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act.
|
||||
// What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it.
|
||||
{Name: DeliverySeat, Scope: ScopeMesh, Serves: deliveryVerbs(), Decision: "novox/hq ADR 0239"},
|
||||
// Putting a version of a module on a node is one verb, the assigner's, which writes every want (novox/hq ADR 0546).
|
||||
{Name: AssignerSeat, Scope: ScopeMesh, Serves: assignerVerbs(), Decision: "novox/hq ADR 0546"},
|
||||
// Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a
|
||||
// delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight.
|
||||
// They keep their names until that migration is done deliberately, apart from the node-* pass.
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
delivered[s.Delivers] = s.Name
|
||||
}
|
||||
}
|
||||
// Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq
|
||||
// Forty-seven with mesh-delivery-assigner (novox/hq ADR 0546); forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq
|
||||
// ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes.
|
||||
// Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
|
||||
// into node-uplink (novox/hq ADR 0223); thirty-seven
|
||||
@@ -56,8 +56,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
|
||||
// ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired
|
||||
// mesh-build-machine row goes, when no registered manifest claims it.
|
||||
if len(Seats()) != 46 {
|
||||
t.Errorf("the mesh defines %d seats rather than 46; the set is closed, so a change here is "+
|
||||
if len(Seats()) != 47 {
|
||||
t.Errorf("the mesh defines %d seats rather than 47; the set is closed, so a change here is "+
|
||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +248,19 @@ var ControllerVerbs = []Verb{
|
||||
"node": "the machine that runs the module",
|
||||
"module": "the module's name",
|
||||
}, []string{"node", "module"})},
|
||||
// One module composed for one node at one fingerprint, on demand (novox/hq ADR 0546 clause 4): what the assigner's put
|
||||
// asks when the composition or the grant it needs is not there. Optional until every controller serves it.
|
||||
{Name: "compose", Description: "Compose one module for one machine at one fingerprint now, and answer it (novox/hq " +
|
||||
"ADR 0546): the module's bus accounts on that machine that are not minted are issued and sealed (never one minted " +
|
||||
"before), the bus's user list and the grants are written, then the composition is written into the machine's " +
|
||||
"compositions with its digest and the judgement of the move to it. Answers the composition's key, digest and " +
|
||||
"judgement, and whether the grant at that fingerprint names the machine's account. Under the lease only.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine",
|
||||
"module": "the module, assigned to that machine",
|
||||
"fp": "the fingerprint, src2: and 64 hex digits; the one the module is registered at when absent",
|
||||
}, []string{"node", "module"}),
|
||||
Optional: true},
|
||||
{Name: "token", Description: "Refused through a verb since novox/hq ADR 0266: the one-time token a machine " +
|
||||
"joins with is answered to its caller, and whoever may call a verb includes agents. Issue it at the " +
|
||||
"controller's terminal: `mesh-controller token issue --new <name> --overlay-key <public half>`.",
|
||||
|
||||
+12
-2
@@ -181,15 +181,25 @@ type Want struct {
|
||||
Rule int `json:"rule,omitempty"`
|
||||
Backup string `json:"backup,omitempty"`
|
||||
Words string `json:"words,omitempty"`
|
||||
// Put says the want was written by a put (ADR 0546), and Hold that the put holds it: the rules pass over it until it
|
||||
// is released. A put's want names no newest fingerprint or rule.
|
||||
Put bool `json:"put,omitempty"`
|
||||
Hold bool `json:"hold,omitempty"`
|
||||
}
|
||||
|
||||
func (w Want) check() error {
|
||||
if w.Removed {
|
||||
if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" {
|
||||
return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule or backup answer")
|
||||
if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold {
|
||||
return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule, backup answer or put")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if w.Hold && !w.Put {
|
||||
return fmt.Errorf("delivery: only a put holds a want")
|
||||
}
|
||||
if w.Put && (w.Newest != "" || w.Rule != 0) {
|
||||
return fmt.Errorf("delivery: a put's want is held back by no rule")
|
||||
}
|
||||
if err := checkFingerprint(w.FP, "a want's fingerprint"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 1297/B1a: a want key round-trips through its parser; a held key, a have key and a node's own key are refused,
|
||||
@@ -77,6 +78,7 @@ func TestAssignerValuesRoundTrip(t *testing.T) {
|
||||
{FP: fp2of('a'), Composed: hashOf('1')},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"},
|
||||
{Removed: true, Words: "the module left g14"},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Put: true, Hold: true, Words: "put by the operator: the bus's user list"},
|
||||
}
|
||||
for _, w := range wants {
|
||||
data, err := EncodeWant(w)
|
||||
@@ -89,6 +91,24 @@ func TestAssignerValuesRoundTrip(t *testing.T) {
|
||||
t.Errorf("want %+v came back %+v, %v", w, got, err)
|
||||
}
|
||||
}
|
||||
at := time.Date(2026, 10, 25, 21, 0, 0, 0, time.UTC)
|
||||
p := Put{FP: fp2of('a'), Hold: true, Why: "the bus's user list", By: "shanks.node-tools", At: at}.Said(at, PutAsked, "put asked")
|
||||
p = p.Said(at, PutAsked, "put asked").Said(at.Add(time.Second), PutWanted, "want written")
|
||||
p.Composed = hashOf('1')
|
||||
if len(p.Steps) != 2 || p.Step != PutWanted || PutEnded(p.Step) || !PutEnded(PutReleased) {
|
||||
t.Errorf("a put's steps are %+v at %s", p.Steps, p.Step)
|
||||
}
|
||||
pkey, _ := PutKey("novox", "nats")
|
||||
if n, m, err := ParsePutKey(pkey); err != nil || n != "novox" || m != "nats" {
|
||||
t.Errorf("the put key %s reads %s %s, %v", pkey, n, m, err)
|
||||
}
|
||||
pdata, err := EncodePut(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotP, err := DecodePut(pdata); err != nil || gotP.FP != p.FP || gotP.Step != PutWanted || len(gotP.Steps) != 2 || !gotP.Hold {
|
||||
t.Errorf("the put came back %+v, %v", gotP, err)
|
||||
}
|
||||
h := Held{Node: "ace", Words: "its health check failed: connection refused"}
|
||||
data, err := EncodeHeld(h)
|
||||
if err != nil {
|
||||
@@ -128,6 +148,9 @@ func TestAssignerValuesRefused(t *testing.T) {
|
||||
{FP: fp2of('a'), Newest: fp2of('b')},
|
||||
{FP: fp2of('a'), Newest: fp2of('a'), Rule: 3},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 8},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Hold: true},
|
||||
{FP: fp2of('a'), Composed: hashOf('1'), Put: true, Newest: fp2of('b'), Rule: 3},
|
||||
{Removed: true, Put: true},
|
||||
} {
|
||||
if _, err := EncodeWant(w); err == nil {
|
||||
t.Errorf("EncodeWant(%+v) accepted", w)
|
||||
@@ -148,6 +171,19 @@ func TestAssignerValuesRefused(t *testing.T) {
|
||||
t.Errorf("EncodeDifference(%+v) accepted", d)
|
||||
}
|
||||
}
|
||||
for _, p := range []Put{
|
||||
{FP: fp2of('a'), Why: "w", Step: "walking"},
|
||||
{FP: fp2of('a'), Step: PutAsked},
|
||||
{FP: "src2:abc", Why: "w", Step: PutAsked},
|
||||
{FP: fp2of('a'), Why: "w", Step: PutWanted, Composed: "sha256:x"},
|
||||
} {
|
||||
if _, err := EncodePut(p); err == nil {
|
||||
t.Errorf("EncodePut(%+v) accepted", p)
|
||||
}
|
||||
}
|
||||
if _, _, err := ParsePutKey("ace.node"); err == nil {
|
||||
t.Errorf("a node's own key was read as a put")
|
||||
}
|
||||
if _, err := DecodeWant([]byte(`{"v":2,"fp":"` + fp2of('a') + `"}`)); !errors.Is(err, ErrUnknownVersion) {
|
||||
t.Errorf("a want of version 2 was read: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A put (novox/hq ADR 0546): one version of a module put on one node by the assigner seat's verb `put`. Each put is the
|
||||
// key `<node>.<module>` of the assigner's state puts, which holds the version asked, the hold, why, and each step the
|
||||
// assigner took or waits on, so a reader watches the key to follow it. The want the put writes carries Put (and Hold),
|
||||
// so the assigner's rules pass over that key while the put holds it.
|
||||
|
||||
// AssignerPutsBucket is the assigner's bucket of puts, `<owner module>_puts`.
|
||||
const AssignerPutsBucket = AssignerModule + "_puts"
|
||||
|
||||
// AssignerPutsHistory is the revisions the puts bucket keeps per key: each step of a put is one.
|
||||
const AssignerPutsHistory = 20
|
||||
|
||||
// A put's steps, in the order they are taken (ADR 0546 clauses 3 to 9). A put stands at the last step written.
|
||||
const (
|
||||
// PutAsked is a put written by the verb, not taken up by a pass yet.
|
||||
PutAsked = "asked"
|
||||
// PutBuild waits for the builder's build at the fingerprint.
|
||||
PutBuild = "build"
|
||||
// PutCompose waits for the controller's composition (and grant) at the fingerprint.
|
||||
PutCompose = "compose"
|
||||
// PutPerson waits for the operator's answer to the ask about the move.
|
||||
PutPerson = "person"
|
||||
// PutSnapshot waits for the bus's streams to be snapshotted after the operator's yes.
|
||||
PutSnapshot = "snapshot"
|
||||
// PutWanted is the want written: the node's have has not reached it yet.
|
||||
PutWanted = "wanted"
|
||||
// PutHolding is a held put whose have runs it healthy: the want stays until released.
|
||||
PutHolding = "holding"
|
||||
// PutDone is a put without hold whose have ran it healthy: the rules write the want again.
|
||||
PutDone = "done"
|
||||
// PutReleased is a put released by the verb `release`.
|
||||
PutReleased = "released"
|
||||
// PutRefused is a put that cannot go on: a build that failed, an answer no, a snapshot not taken. Its words say why.
|
||||
PutRefused = "refused"
|
||||
)
|
||||
|
||||
// PutEnded says a put at this step is over: nothing more is done for it and the rules own the want.
|
||||
func PutEnded(step string) bool {
|
||||
return step == PutDone || step == PutReleased || step == PutRefused
|
||||
}
|
||||
|
||||
// PutStep is one step a put took: when, which, and what was done or waits, in a sentence.
|
||||
type PutStep struct {
|
||||
At time.Time `json:"at"`
|
||||
Step string `json:"step"`
|
||||
Words string `json:"words"`
|
||||
}
|
||||
|
||||
// PutStepsKept is how many steps a put keeps, newest last.
|
||||
const PutStepsKept = 16
|
||||
|
||||
// Put is the value of a key of AssignerPutsBucket.
|
||||
type Put struct {
|
||||
V int `json:"v"`
|
||||
// FP is the fingerprint put, of the new form.
|
||||
FP string `json:"fp"`
|
||||
// Hold keeps the want at FP until released.
|
||||
Hold bool `json:"hold,omitempty"`
|
||||
Why string `json:"why"`
|
||||
// By is who asked, as the bus named the caller; "unproven" when it named none.
|
||||
By string `json:"by,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
// Step is where the put stands now, one of the Put* steps; Words say it in a sentence.
|
||||
Step string `json:"step"`
|
||||
Words string `json:"words,omitempty"`
|
||||
// Composed is the digest of the composition the want names, once known.
|
||||
Composed string `json:"composed,omitempty"`
|
||||
// Ask is the key, in AssignerAsksBucket, of the ask about the move, once asked.
|
||||
Ask string `json:"ask,omitempty"`
|
||||
// Snapshot says where the bus's streams were snapshotted, once they were.
|
||||
Snapshot string `json:"snapshot,omitempty"`
|
||||
// Steps are the steps taken, oldest first, at most PutStepsKept.
|
||||
Steps []PutStep `json:"steps,omitempty"`
|
||||
}
|
||||
|
||||
// Said is p at step with words, the step added to its steps when it differs from the last one.
|
||||
func (p Put) Said(at time.Time, step, words string) Put {
|
||||
if p.Step == step && p.Words == words {
|
||||
return p
|
||||
}
|
||||
p.Step, p.Words = step, words
|
||||
p.Steps = append(append([]PutStep(nil), p.Steps...), PutStep{At: at.UTC(), Step: step, Words: words})
|
||||
if len(p.Steps) > PutStepsKept {
|
||||
p.Steps = p.Steps[len(p.Steps)-PutStepsKept:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p Put) check() error {
|
||||
if err := checkFingerprint(p.FP, "a put's fingerprint"); err != nil {
|
||||
return err
|
||||
}
|
||||
if p.Why == "" {
|
||||
return fmt.Errorf("delivery: a put says why")
|
||||
}
|
||||
switch p.Step {
|
||||
case PutAsked, PutBuild, PutCompose, PutPerson, PutSnapshot, PutWanted, PutHolding, PutDone, PutReleased, PutRefused:
|
||||
default:
|
||||
return fmt.Errorf("delivery: %q is not a step of a put", p.Step)
|
||||
}
|
||||
if p.Composed != "" {
|
||||
return checkDigest(p.Composed, "a put's composition digest")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutKey is the key, in AssignerPutsBucket, of the put of module on node: `<node>.<module>`.
|
||||
func PutKey(node, module string) (string, error) {
|
||||
return HaveKey(node, module)
|
||||
}
|
||||
|
||||
// ParsePutKey reads a PutKey.
|
||||
func ParsePutKey(key string) (node, module string, err error) {
|
||||
node, module, isNode, err := ParseHaveKey(key)
|
||||
if err == nil && isNode {
|
||||
err = fmt.Errorf("delivery: %q is a node's own key, and no put", key)
|
||||
}
|
||||
return node, module, err
|
||||
}
|
||||
|
||||
// EncodePut is p as JSON at Version; it refuses a fingerprint not of the new form, a put that says no why and an
|
||||
// unknown step.
|
||||
func EncodePut(p Put) ([]byte, error) {
|
||||
if err := p.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.V = Version
|
||||
return json.Marshal(p)
|
||||
}
|
||||
|
||||
// DecodePut reads a put; it refuses an unknown version (ErrUnknownVersion) and what EncodePut refuses.
|
||||
func DecodePut(data []byte) (Put, error) {
|
||||
var p Put
|
||||
if err := decode(data, &p); err != nil {
|
||||
return Put{}, err
|
||||
}
|
||||
if err := p.check(); err != nil {
|
||||
return Put{}, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -283,9 +283,15 @@ func ViewAnswerFits(a ViewAnswer, ask AssignerAsk) error {
|
||||
if a.Asking != ask.Ask.ID {
|
||||
return fmt.Errorf("delivery: the view's answer is to the asking %s, and the ask stands at the asking %s", a.Asking, ask.Ask.ID)
|
||||
}
|
||||
if _, ok := ask.Ask.Option(a.Option); !ok {
|
||||
o, ok := ask.Ask.Option(a.Option)
|
||||
if !ok {
|
||||
return fmt.Errorf("delivery: the view's answer chose %q, which the asking %s does not offer", a.Option, ask.Ask.ID)
|
||||
}
|
||||
if o.Level != "" && o.Level != asks.Acknowledge {
|
||||
// An option that authorises is answered on a channel that proves who answered (ADR 0259 §5, ADR 0546 clause 8),
|
||||
// never in the view, which proves nobody.
|
||||
return fmt.Errorf("delivery: the view's answer chose %q, an option of level %s, which only a channel that proves who answered takes", a.Option, o.Level)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -158,4 +158,11 @@ func TestViewAnswerFits(t *testing.T) {
|
||||
if err := ViewAnswerFits(ViewAnswer{Asking: first.Ask.ID, Option: "restore"}, first); err == nil {
|
||||
t.Errorf("an option the ask does not offer fits")
|
||||
}
|
||||
// An option that authorises is never answered in the view (ADR 0546 clause 8).
|
||||
approving := first
|
||||
approving.Ask.Options = append([]asks.Option(nil), first.Ask.Options...)
|
||||
approving.Ask.Options[0].Level = asks.Approve
|
||||
if err := ViewAnswerFits(answer, approving); err == nil || !strings.Contains(err.Error(), "approve") {
|
||||
t.Errorf("the view answered an option of level approve: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user