The assigner's state and verb placement are assignments, with its events, tools, views and every caller; the assigner copies the retired state at its start (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -2,14 +2,14 @@ package assign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
@@ -69,7 +69,7 @@ func RestoreAsk(key, node, module, fp, words string, now time.Time) asks.Ask {
|
||||
// words, under the assigner's own name (ADR 0437 clause 9), keyed by the held fingerprint. needsYou is true for a module holding
|
||||
// data, whose restore ask waits for the operator.
|
||||
func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Condition {
|
||||
key := placement.Module + ".held." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
|
||||
key := assignment.Module + ".held." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
|
||||
if !raised {
|
||||
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared}
|
||||
@@ -92,7 +92,7 @@ func HeldCondition(module, fp, node, words string, raised, needsYou bool) oc.Con
|
||||
// the node keeps its want. It names the node, the module, fp and the have's words, keyed by node and module, and is cleared
|
||||
// (raised false) once the node no longer fails alone.
|
||||
func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc.Condition {
|
||||
key := placement.Module + ".failed-alone." + node + "." + module
|
||||
key := assignment.Module + ".failed-alone." + node + "." + module
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
|
||||
if !raised {
|
||||
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared,
|
||||
@@ -115,7 +115,7 @@ func NodeFailedCondition(node, module, fp string, f NodeFailure, raised bool) oc
|
||||
// naming the module, the node and the rule holding its first want back with the rule's words, under the assigner's own name keyed by
|
||||
// node and module, and cleared (raised false) when the node has a want for it again.
|
||||
func NeitherCondition(node, module string, h Holding, raised bool) oc.Condition {
|
||||
key := placement.Module + ".neither." + node + "." + module
|
||||
key := assignment.Module + ".neither." + node + "." + module
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
|
||||
if !raised {
|
||||
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared,
|
||||
@@ -237,7 +237,7 @@ func BackupAsk(key, node, module, fp, digest, words string, asking int, now time
|
||||
// BackupCondition is the condition raised when a backup before an apply fails and the module is held on the node (ADR 0539 §16),
|
||||
// naming the node, the module and the backup's words; cleared under the same key.
|
||||
func BackupCondition(node, module, fp, words string, raised bool) oc.Condition {
|
||||
key := placement.Module + ".backup." + node + "." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
|
||||
key := assignment.Module + ".backup." + node + "." + module + "." + strings.TrimPrefix(fp, meshcatalogclient.SourcePrefix2)
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: module, Machine: node}
|
||||
if !raised {
|
||||
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared}
|
||||
|
||||
@@ -2,16 +2,16 @@ package assign
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
|
||||
@@ -148,7 +148,7 @@ func saidConditionsAreFindings(t *testing.T) {
|
||||
{HeldCondition("mail", fNew, "ace", "refused", true, true), LiftedCondition("mail", fNew), "module.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".held"},
|
||||
{NodeFailedCondition("ace", "mail", fNew, NodeFailure{Words: "x", Others: []string{"g14"}}, true), NodeFailedCondition("ace", "mail", fNew, NodeFailure{}, false), "module.ace.mail.failed-alone"},
|
||||
{NeitherCondition("ace", "mail", Holding{Rule: 3, Words: "w"}, true), NeitherCondition("ace", "mail", Holding{}, false), "module.ace.mail.neither"},
|
||||
{SeatWaitsCondition([]string{"placement"}), SeatWaitsCondition(nil), "module.mesh-delivery-assigner.seat-states"},
|
||||
{SeatWaitsCondition([]string{"assignments"}), SeatWaitsCondition(nil), "module.mesh-delivery-assigner.seat-states"},
|
||||
{BackupCondition("ace", "mail", fNew, "disk full", true), BackupCondition("ace", "mail", fNew, "", false), "module.ace.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".backup"},
|
||||
} {
|
||||
puts, deletes, said := map[string]mcseat.FindingsByKeyValue{}, []string{}, 0
|
||||
@@ -162,7 +162,7 @@ func saidConditionsAreFindings(t *testing.T) {
|
||||
t.Fatalf("%s: %v", c.key, err)
|
||||
}
|
||||
got, ok := puts[c.key]
|
||||
if !ok || got.Severity != c.raised.Severity || got.Headline != c.raised.Headline || got.Observer != placement.Module ||
|
||||
if !ok || got.Severity != c.raised.Severity || got.Headline != c.raised.Headline || got.Observer != assignment.Module ||
|
||||
got.Subject.Machine != c.raised.Subject.Machine || said != 0 {
|
||||
t.Fatalf("%s: finding %+v (written %v), said %d", c.key, got, puts, said)
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package assign
|
||||
|
||||
import (
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"strings"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
// SeatWaitsCondition is the assigner's word on the seat mesh-module-assigner while a state of the seat is not bound (ADR 0567
|
||||
// §1): raised naming each, cleared when none is left.
|
||||
func SeatWaitsCondition(unbound []string) oc.Condition {
|
||||
key := placement.Module + ".seat-states"
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: placement.Module}
|
||||
key := assignment.Module + ".seat-states"
|
||||
subject := oc.ConditionSubject{Scope: "module", ID: assignment.Module}
|
||||
if len(unbound) == 0 {
|
||||
return oc.Condition{Key: key, Subject: subject, State: occ.StateCleared, Resolved: "the seat mesh-module-assigner holds all its states."}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package assign
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,13 +21,13 @@ import (
|
||||
// assigner no longer says it straight (never both).
|
||||
|
||||
// FindingsBucket is the assigner's findings.
|
||||
var FindingsBucket = mcc.FindingsBucket(placement.Module)
|
||||
var FindingsBucket = mcc.FindingsBucket(assignment.Module)
|
||||
|
||||
// FindingOf is a said condition as a finding: its key `<scope>.<id>.<kind>`, from the condition's own key
|
||||
// `mesh-delivery-assigner.<kind>.<id…>` (the subject's id when the key names none), its words unchanged. raised is false for
|
||||
// a clear, whose key is to be deleted; value is nil then.
|
||||
func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised bool, err error) {
|
||||
rest, ok := strings.CutPrefix(c.Key, placement.Module+".")
|
||||
rest, ok := strings.CutPrefix(c.Key, assignment.Module+".")
|
||||
if !ok || rest == "" {
|
||||
return "", nil, false, fmt.Errorf("the condition %q is not the assigner's", c.Key)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func FindingOf(c oc.Condition, at time.Time) (key string, value []byte, raised b
|
||||
}
|
||||
f := mcc.Observation{Scope: scope, ID: id, Kind: kind, Machine: c.Subject.Machine, Severity: string(c.Severity), Summary: summary,
|
||||
Headline: c.Headline, Explanation: c.Explanation, Resolved: c.Resolved, Needs: c.Needs, Actions: actions,
|
||||
Source: "assigner." + kind}.Finding(placement.Module, "", at)
|
||||
Source: "assigner." + kind}.Finding(assignment.Module, "", at)
|
||||
value, err = mcc.EncodeFinding(f)
|
||||
return key, value, true, err
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package assign
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// catalogue.Upgrade: a person's choice over the module's word, its irreplaceable data and the default, the bus as any module (ADR 0601 §1).
|
||||
|
||||
// PoliciesBucket is the assigner's state `policies`.
|
||||
var PoliciesBucket = placement.Module + "_" + buckets.PoliciesState
|
||||
var PoliciesBucket = assignment.Module + "_" + buckets.PoliciesState
|
||||
|
||||
// DecodePolicies reads the policies state's values by key (the module); a value that cannot be read is left out, named.
|
||||
func DecodePolicies(values map[string][]byte) (map[string]catalogue.PersonPolicy, []string) {
|
||||
@@ -76,7 +76,7 @@ func ProvisionFailedFinding(key string, p meshprovisionerclient.Provision, at ti
|
||||
|
||||
// encode is the observation as the assigner's finding at at, in the seat's layout.
|
||||
func encode(o mcc.Observation, at time.Time) ([]byte, error) {
|
||||
return mcc.EncodeFinding(o.Finding(placement.Module, "", at))
|
||||
return mcc.EncodeFinding(o.Finding(assignment.Module, "", at))
|
||||
}
|
||||
|
||||
func also(provider, consumer string) []string {
|
||||
@@ -109,7 +109,7 @@ func ProvisionMissingFinding(key string, bound time.Duration, at time.Time) ([]b
|
||||
Resolved: provision + " for " + module + " on " + node + " has its key", Source: "assigner." + KindProvisionMissing}, at)
|
||||
}
|
||||
|
||||
// KindUnplaced is, from the lease write, a node that runs but no placement names (ADR 0571 §6).
|
||||
// KindUnplaced is, from the lease write, a node that runs but no assignments names (ADR 0571 §6).
|
||||
const KindUnplaced = "unplaced"
|
||||
|
||||
// UnplacedKey is its finding's key, `machine.<node>.unplaced`.
|
||||
@@ -117,12 +117,12 @@ func UnplacedKey(node string) string {
|
||||
return mcc.Key(mcc.ScopeMachine, node, KindUnplaced)
|
||||
}
|
||||
|
||||
// UnplacedFinding is the finding of a node no placement names since the lease write.
|
||||
// UnplacedFinding is the finding of a node no assignments names since the lease write.
|
||||
func UnplacedFinding(node string, at time.Time) ([]byte, error) {
|
||||
summary := fmt.Sprintf("%s runs, but no placement names it: since the controller handed its lease away nothing composes it, "+
|
||||
summary := fmt.Sprintf("%s runs, but no assignments names it: since the controller handed its lease away nothing composes it, "+
|
||||
"so the assigner writes it no want", node)
|
||||
return encode(mcc.Observation{Scope: mcc.ScopeMachine, ID: node, Kind: KindUnplaced, Machine: node, Summary: summary,
|
||||
Headline: clipHeadline(node + " has no placement"), Needs: "place it with mesh-module-assigner.propose.",
|
||||
Headline: clipHeadline(node + " has no assignments"), Needs: "place it with mesh-module-assigner.propose.",
|
||||
Explanation: occ.NeedsYou + "place it with mesh-module-assigner.propose. " + summary + ".",
|
||||
Resolved: node + " is placed", Source: "assigner." + KindUnplaced}, at)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
@@ -75,7 +75,7 @@ func StepPut(w PutWorld) PutNext {
|
||||
}
|
||||
fp := short(p.FP)
|
||||
if !w.Assigned {
|
||||
return say(puts.Refused, "%s is not assigned to %s: a put moves a module where it stands assigned (propose its placement first)", w.Module, w.Node)
|
||||
return say(puts.Refused, "%s is not assigned to %s: a put moves a module where it stands assigned (propose its assignments first)", w.Module, w.Node)
|
||||
}
|
||||
switch w.Build {
|
||||
case builds.BuildDone:
|
||||
|
||||
@@ -10,27 +10,27 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
|
||||
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
|
||||
"mesh-delivery-assigner/internal/placement"
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
"mesh-delivery-assigner/internal/assignment"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
)
|
||||
|
||||
// The seat mesh-module-assigner (novox/hq ADR 0567), held by the assigner beside its wants: placement is its state
|
||||
// `placement`, changed only by a proposal (§2) the nodes it touches answer (§3), judged (§4) and written on the
|
||||
// The seat mesh-module-assigner (novox/hq ADR 0567), held by the assigner beside its wants: assignments is its state
|
||||
// `assignments`, changed only by a proposal (§2) the nodes it touches answer (§3), judged (§4) and written on the
|
||||
// operator's Approve at the revision it was laid over (§5). What here decides; the tools and the passes do.
|
||||
|
||||
// ProposeIn is what a proposal is laid over: its id, who and why, its changes; every node's current placement and the
|
||||
// ProposeIn is what a proposal is laid over: its id, who and why, its changes; every node's current assignments and the
|
||||
// revision of its key; which modules the indexer holds and which nodes have a record; and, for the seed, the export's
|
||||
// digest.
|
||||
type ProposeIn struct {
|
||||
ID, By, Why string
|
||||
At time.Time
|
||||
Changes []mma.ProposalChange
|
||||
Current map[string]mma.Placement
|
||||
Current map[string]mma.Assignments
|
||||
Revisions map[string]uint64
|
||||
Held func(module string) bool
|
||||
Recorded func(node string) bool
|
||||
@@ -39,11 +39,11 @@ type ProposeIn struct {
|
||||
}
|
||||
|
||||
// Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision),
|
||||
// the laid placement of every node whose key it changes, and every finding, which refuses it whole: a change that does not fit its node, and what
|
||||
// CheckPlacements refuses of the result laid over every placement (two modules on one host port, a provider not placed,
|
||||
// the laid assignments of every node whose key it changes, and every finding, which refuses it whole: a change that does not fit its node, and what
|
||||
// CheckAssignments refuses of the result laid over every assignments (two modules on one host port, a provider not placed,
|
||||
// a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no
|
||||
// record). A key whose value the proposal leaves as it is changes nothing and is not laid.
|
||||
func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Assignments, []string) {
|
||||
p := mma.Proposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(),
|
||||
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: mmac.ProposalAnswering}
|
||||
var findings []string
|
||||
@@ -64,13 +64,13 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
laid, lay := plc.LayChanges(in.Current, in.Changes)
|
||||
findings = append(findings, lay...)
|
||||
findings = append(findings, ControllerRefusals(in.Changes, laid)...)
|
||||
all := map[string]mma.Placement{}
|
||||
all := map[string]mma.Assignments{}
|
||||
for node, pl := range in.Current {
|
||||
all[node] = pl
|
||||
}
|
||||
out := map[string]*mma.Placement{}
|
||||
out := map[string]*mma.Assignments{}
|
||||
for node, pl := range laid {
|
||||
if before, ok := in.Current[node]; ok && mmac.SamePlacement(before, pl) {
|
||||
if before, ok := in.Current[node]; ok && mmac.SameAssignments(before, pl) {
|
||||
continue
|
||||
}
|
||||
pl := pl
|
||||
@@ -79,7 +79,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
p.Base[node] = int64(in.Revisions[node])
|
||||
}
|
||||
// A remove-node deletes the key of a joining node that never confirmed (the operator's decision of 2026-11-02 01:05): its
|
||||
// candidate is no placement, and the checks across nodes read the mesh without it. LayChanges said why one cannot go.
|
||||
// candidate is no assignments, and the checks across nodes read the mesh without it. LayChanges said why one cannot go.
|
||||
for _, node := range plc.RemovedNodes(p) {
|
||||
if _, ok := in.Current[node]; !ok {
|
||||
continue
|
||||
@@ -89,7 +89,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
p.Base[node] = int64(in.Revisions[node])
|
||||
}
|
||||
if len(findings) == 0 && len(out) == 0 {
|
||||
findings = append(findings, "the changes leave every placement as it is")
|
||||
findings = append(findings, "the changes leave every assignments as it is")
|
||||
}
|
||||
held, recorded := in.Held, in.Recorded
|
||||
if held == nil {
|
||||
@@ -110,7 +110,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
was := recorded
|
||||
recorded = func(node string) bool { return joining[node] || was(node) }
|
||||
}
|
||||
for _, r := range plc.CheckPlacements(all, held, recorded) {
|
||||
for _, r := range plc.CheckAssignments(all, held, recorded) {
|
||||
findings = append(findings, r)
|
||||
}
|
||||
return p, out, findings
|
||||
@@ -119,14 +119,14 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
// ControllerModule is the controller's module, which nothing places again (issue 1126).
|
||||
const ControllerModule = "mesh-controller"
|
||||
|
||||
// ControllerEnded is why a placement of the controller is refused: the parts that acted only once its lease was handed
|
||||
// ControllerEnded is why assignments of the controller is refused: the parts that acted only once its lease was handed
|
||||
// (consumers, the user list, enrolment, dead letters) now act without reading the lease, so a controller placed again
|
||||
// would write beside them, silently.
|
||||
const ControllerEnded = "the controller ended by its unassignment on 2026-11-02; placing it again would make two writers (issue 1126)"
|
||||
|
||||
// ControllerRefusals names every change, and every node as laid, that places the controller: by a place, by a seed's
|
||||
// whole placement, or in what the changes lay. None when nothing does.
|
||||
func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placement) []string {
|
||||
// whole assignments, or in what the changes lay. None when nothing does.
|
||||
func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Assignments) []string {
|
||||
var out []string
|
||||
nodes := map[string]bool{}
|
||||
for i, c := range changes {
|
||||
@@ -148,16 +148,16 @@ func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placem
|
||||
}
|
||||
|
||||
// Write is one node a warrant writes (§5): the node, its node key `<node>.node` (the commit of the node's keys,
|
||||
// mesh-sdk mmac.Keys.Writes), the revision of the node's placement it is written at (0: it has none, so
|
||||
// mesh-sdk mmac.Keys.Writes), the revision of the node's assignments it is written at (0: it has none, so
|
||||
// it is created) and its value (nil: it is deleted).
|
||||
type Write struct {
|
||||
Node, Key string
|
||||
Base uint64
|
||||
Value *mma.Placement
|
||||
Value *mma.Assignments
|
||||
}
|
||||
|
||||
// Writes is every key a proposal writes, by node order.
|
||||
func Writes(p mma.Proposal, laid map[string]*mma.Placement) ([]Write, error) {
|
||||
func Writes(p mma.Proposal, laid map[string]*mma.Assignments) ([]Write, error) {
|
||||
var out []Write
|
||||
for _, node := range plc.Nodes(p) {
|
||||
key, err := mmac.NodeKey(node)
|
||||
@@ -179,40 +179,40 @@ const (
|
||||
// CallClearedCondition clears the condition of a refused controller call a previous process raised, for its pair (node and
|
||||
// module, or node and provision for a pin).
|
||||
func CallClearedCondition(kind, node, module string) oc.Condition {
|
||||
return oc.Condition{Key: placement.Module + "." + kind + "." + node + "." + module,
|
||||
return oc.Condition{Key: assignment.Module + "." + kind + "." + node + "." + module,
|
||||
Subject: oc.ConditionSubject{Scope: "module", ID: module, Machine: node}, State: occ.StateCleared,
|
||||
Resolved: "the assigner calls no verb of the controller any more: placement is the one source."}
|
||||
Resolved: "the assigner calls no verb of the controller any more: assignments is the one source."}
|
||||
}
|
||||
|
||||
// PlacementAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions.
|
||||
func PlacementAct(p mma.Proposal) occ.Act {
|
||||
return occ.Act{"placement": "write", "proposal": p.ID, "changes": plc.ChangesDigest(p)}
|
||||
// AssignmentsAct is the act an approved proposal's yes binds: writing exactly its changes at its base revisions.
|
||||
func AssignmentsAct(p mma.Proposal) occ.Act {
|
||||
return occ.Act{"assignments": "write", "proposal": p.ID, "changes": plc.ChangesDigest(p)}
|
||||
}
|
||||
|
||||
// PlacementAsking is a proposal's asking number: each judging that raises its ask again (again, a key moved) is
|
||||
// AssignmentsAsking is a proposal's asking number: each judging that raises its ask again (again, a key moved) is
|
||||
// another asking, under an id of its own.
|
||||
func PlacementAsking(p mma.Proposal, at time.Time) int {
|
||||
func AssignmentsAsking(p mma.Proposal, at time.Time) int {
|
||||
return int(at.Unix())
|
||||
}
|
||||
|
||||
// PlacementAsk is the authorising ask about a proposal (§5): an Approve that binds its act, so only a channel that
|
||||
// AssignmentsAsk is the authorising ask about a proposal (§5): an Approve that binds its act, so only a channel that
|
||||
// proves who answered takes it, with the brief of the verdict by node (judge.Brief) as its explanation.
|
||||
func PlacementAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) {
|
||||
binds, err := occ.ActDigest(PlacementAct(p))
|
||||
func AssignmentsAsk(key string, p mma.Proposal, brief string, asking int, now time.Time) (asks.Ask, error) {
|
||||
binds, err := occ.ActDigest(AssignmentsAct(p))
|
||||
if err != nil {
|
||||
return asks.Ask{}, err
|
||||
}
|
||||
nodes := plc.Nodes(p)
|
||||
headline := fmt.Sprintf("Change placement on %s?", strings.Join(nodes, ", "))
|
||||
headline := fmt.Sprintf("Change assignments on %s?", strings.Join(nodes, ", "))
|
||||
if n := len([]rune(headline)); n > occ.HeadlineLength {
|
||||
headline = fmt.Sprintf("Change placement on %d machines?", len(nodes))
|
||||
headline = fmt.Sprintf("Change assignments on %d machines?", len(nodes))
|
||||
}
|
||||
return assignerAsk(asking, oc.Ask{
|
||||
ID: meshdeliveryclient.AskingID(key, asking),
|
||||
Headline: headline,
|
||||
Explanation: brief,
|
||||
Options: []oc.Option{
|
||||
{ID: OptionYes, Label: "Approve", Does: "the placement of " + strings.Join(nodes, ", ") + " is written as proposed",
|
||||
{ID: OptionYes, Label: "Approve", Does: "the assignments of " + strings.Join(nodes, ", ") + " is written as proposed",
|
||||
Level: occ.Approve, Binds: binds},
|
||||
{ID: OptionNo, Label: "Decline", Does: "nothing is written, and the proposal ends", Level: occ.Acknowledge},
|
||||
},
|
||||
@@ -225,7 +225,7 @@ func PlacementAsk(key string, p mma.Proposal, brief string, asking int, now time
|
||||
|
||||
// Rest is what a proposal leaves to write once the keys of written are: its changes of the other nodes, to be laid again
|
||||
// over the keys as the write left them (§5: a key moved while it was written). False when nothing is left.
|
||||
func Rest(p mma.Proposal, written map[string]*mma.Placement) (mma.Proposal, bool) {
|
||||
func Rest(p mma.Proposal, written map[string]*mma.Assignments) (mma.Proposal, bool) {
|
||||
rest := p
|
||||
rest.Changes, rest.Base = nil, map[string]int64{}
|
||||
for _, c := range p.Changes {
|
||||
|
||||
@@ -8,23 +8,23 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
mmac "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
)
|
||||
|
||||
// ADR 0567 §2, §5, §7, §8: what the seat's holder decides of a proposal: every refusal named at once, a valid one laid
|
||||
// with the base revision of each key it changes, the writes at those revisions, the controller's calls that keep its
|
||||
// assignments in step, the authorising ask bound to the changes, and the export read as the controller answers it.
|
||||
func TestThePlacementSeatDecides(t *testing.T) {
|
||||
anchor := mma.Placement{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"},
|
||||
func TestTheAssignmentsSeatDecides(t *testing.T) {
|
||||
anchor := mma.Assignments{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"},
|
||||
Modules: []mma.PlacedModule{{Module: "store", Ports: map[string]int64{"5432": 5432}}, {Module: "web"}}}
|
||||
laptop := mma.Placement{V: keys.Version, Node: "laptop", Decided: mma.NodeDecided{Address: "10.0.0.2"}, Modules: []mma.PlacedModule{{Module: "shell"}}}
|
||||
laptop := mma.Assignments{V: keys.Version, Node: "laptop", Decided: mma.NodeDecided{Address: "10.0.0.2"}, Modules: []mma.PlacedModule{{Module: "shell"}}}
|
||||
in := func(changes ...mma.ProposalChange) ProposeIn {
|
||||
return ProposeIn{ID: "p-one", By: "operator@laptop", Why: "move web", At: time.Unix(1, 0), Changes: changes,
|
||||
Current: map[string]mma.Placement{"anchor": anchor, "laptop": laptop}, Revisions: map[string]uint64{"anchor": 4, "laptop": 9},
|
||||
Current: map[string]mma.Assignments{"anchor": anchor, "laptop": laptop}, Revisions: map[string]uint64{"anchor": 4, "laptop": 9},
|
||||
Held: func(m string) bool { return m != "ghost" },
|
||||
Recorded: func(n string) bool { return n == "anchor" || n == "laptop" }}
|
||||
}
|
||||
@@ -43,14 +43,14 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
}{
|
||||
"a module not held": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
|
||||
"a node with no record": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
|
||||
"nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
|
||||
"nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every assignments as it is"},
|
||||
"no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"},
|
||||
"no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"},
|
||||
"a seed by hand": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
|
||||
// 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that
|
||||
// act without its lease.
|
||||
"the controller placed again": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
|
||||
"the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Placement{V: keys.Version, Node: "anchor",
|
||||
"the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Assignments{V: keys.Version, Node: "anchor",
|
||||
Decided: anchor.Decided, Modules: []mma.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
|
||||
"a pin of nobody placed": {in(mma.ProposalChange{Op: mmac.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
|
||||
} {
|
||||
@@ -59,7 +59,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every kind of change is taken: the placement is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
|
||||
// Every kind of change is taken: the assignments is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
|
||||
for _, c := range []mma.ProposalChange{
|
||||
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
|
||||
{Op: mmac.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}},
|
||||
@@ -74,7 +74,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
}
|
||||
|
||||
// A key moved while the warrant wrote: what is left to write is the other nodes' changes, to be laid and asked again.
|
||||
if rest, ok := Rest(p, map[string]*mma.Placement{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
|
||||
if rest, ok := Rest(p, map[string]*mma.Assignments{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
|
||||
len(rest.Base) != 1 || rest.State != mmac.ProposalAnswering || rest.Must != nil {
|
||||
t.Errorf("the rest = %+v, %v", rest, ok)
|
||||
}
|
||||
@@ -95,13 +95,13 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
if err != nil || len(writes) != 2 || writes[0].Key != "anchor.node" || writes[0].Base != 4 || writes[1].Base != 9 {
|
||||
t.Fatalf("writes = %+v, %v", writes, err)
|
||||
}
|
||||
a, err := PlacementAsk("placement.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0))
|
||||
if err != nil || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], PlacementAct(p)) != nil {
|
||||
a, err := AssignmentsAsk("assignments.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0))
|
||||
if err != nil || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(p)) != nil {
|
||||
t.Fatalf("the ask = %+v, %v", a, err)
|
||||
}
|
||||
q := p
|
||||
q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: mmac.OpUnpin, Node: "laptop", Provision: "x"})
|
||||
if occ.Performs(mustAsk(t, a).Options[0], PlacementAct(q)) == nil {
|
||||
if occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(q)) == nil {
|
||||
t.Error("the ask's yes binds changes it was not asked about")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
"maps"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
@@ -90,9 +90,9 @@ type Module struct {
|
||||
// needs, ADR 0618 §1): a node's want is held only while that node's engine reads none of them (ADR 0624 §2). Nil: the
|
||||
// build names none, and holds nothing.
|
||||
FormNeeds []string
|
||||
// Self says whether node composes the module itself: the catalogue's placement names the node (ADR 0547 §2). It is the one
|
||||
// Self says whether node composes the module itself: the catalogue's assignments names the node (ADR 0547 §2). It is the one
|
||||
// source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while
|
||||
// the node was placed does not outlive the placement (issue 1126). Nil: no node does.
|
||||
// the node was placed does not outlive the assignments (issue 1126). Nil: no node does.
|
||||
Self func(node string) bool
|
||||
}
|
||||
|
||||
@@ -587,11 +587,11 @@ func Wants(m Module, n Nodes, wants map[string]mwc.Want, lastHealthy map[string]
|
||||
return out
|
||||
}
|
||||
|
||||
// selfDerived is next with its Self derived from the placement now (Module.Self), whichever rule made it, and whether it is
|
||||
// selfDerived is next with its Self derived from the assignments now (Module.Self), whichever rule made it, and whether it is
|
||||
// written. A placed node's want names the build alone (ADR 0547 §2). An unplaced node's want that a rule kept from the want
|
||||
// written while the node was placed (held at F, held back by rule 3 or 5, a data module asked about) names the controller's
|
||||
// composition of the same fingerprint, asked as any move of a record module is (ADR 0539 §14): on 2026-10-27, after the
|
||||
// placement seed's revert, g14's systemd-resolved (held) and forticlient (rule 5), and shanks's forticlient (rule 3), kept
|
||||
// assignments seed's revert, g14's systemd-resolved (held) and forticlient (rule 5), and shanks's forticlient (rule 3), kept
|
||||
// their Self wants, which their node agents, no longer placed, could not compose (issue 1126). With no such composition, or
|
||||
// while its ask waits, the want written stays and the node's holding says why.
|
||||
func (m Module) selfDerived(node string, before mwc.Want, had bool, next mwc.Want, n Nodes, out *Outcome) (mwc.Want, bool) {
|
||||
|
||||
@@ -3,8 +3,8 @@ package assign
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
mwc "git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
@@ -348,7 +348,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the placement is gone, names
|
||||
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the assignments is gone, names
|
||||
// the controller's composition of the fingerprint the rule keeps (on 2026-10-27 g14's systemd-resolved, held, and forticlient,
|
||||
// rule 5, kept their Self wants, which g14 could no longer compose); with no such composition, or while a record ask waits, it
|
||||
// stays and is said; a placed node's want is Self with no composition.
|
||||
|
||||
+23
-23
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -9,16 +9,16 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// ADR 0567 §2, §4: what a change says, the holder's check of every placement, a proposal's changes laid over the keys,
|
||||
// ADR 0567 §2, §4: what a change says, the holder's check of every assignments, a proposal's changes laid over the keys,
|
||||
// its digests (held to the values the earlier holder's code made, in the field order it wrote), its refusals, and who holds
|
||||
// which seat.
|
||||
func TestPlacement(t *testing.T) {
|
||||
g14 := seat.Placement{Node: "g14", Revision: "abc",
|
||||
func TestAssignments(t *testing.T) {
|
||||
g14 := seat.Assignments{Node: "g14", Revision: "abc",
|
||||
Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"},
|
||||
Tunnel: &seat.FoundTunnel{Interface: "wg0", Config: "c", Port: 51820, Mtu: 1420, PublicKey: "K", Peers: []seat.FoundTunnelPeer{{PublicKey: "P", Address: "10.0.0.3"}}},
|
||||
PeerNames: map[string]string{"P": "pi"}},
|
||||
@@ -29,13 +29,13 @@ func TestPlacement(t *testing.T) {
|
||||
Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}},
|
||||
Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
|
||||
}
|
||||
novox := seat.Placement{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}},
|
||||
novox := seat.Assignments{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}},
|
||||
Holds: []seat.Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}}
|
||||
current := map[string]seat.Placement{"g14": g14, "novox": novox}
|
||||
current := map[string]seat.Assignments{"g14": g14, "novox": novox}
|
||||
const id = "p-20261028t183000-1a2b3c"
|
||||
// the enroller is whichever module holds the seat mesh-node-enroller: no module is named for it
|
||||
const enroller = "enroll-lab"
|
||||
enrollers := EnrollersOf(map[string]seat.Placement{"ace": {Node: "ace",
|
||||
enrollers := EnrollersOf(map[string]seat.Assignments{"ace": {Node: "ace",
|
||||
Holds: []seat.Holding{{Claim: "mesh-node-enroller", Scope: "mesh", Module: enroller}}}})
|
||||
if !slices.Equal(enrollers, []string{enroller}) {
|
||||
t.Fatalf("the holders of the enroller seat = %v", enrollers)
|
||||
@@ -54,7 +54,7 @@ func TestPlacement(t *testing.T) {
|
||||
if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) {
|
||||
t.Errorf("a pin change changes every module on the node: %+v", got)
|
||||
}
|
||||
if got := ChangeOf(seat.Placement{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
|
||||
if got := ChangeOf(seat.Assignments{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
|
||||
t.Errorf("a new node: %+v", got)
|
||||
}
|
||||
c := []Change{{Node: "novox"}, {Node: "g14", Before: 1, After: 2}}
|
||||
@@ -64,18 +64,18 @@ func TestPlacement(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the check of every placement", func(t *testing.T) {
|
||||
t.Run("the check of every assignments", func(t *testing.T) {
|
||||
known := func(m string) bool { return m != "ghost" }
|
||||
enrolled := func(n string) bool { return n != "stranger" }
|
||||
if r := CheckPlacements(current, known, enrolled); len(r) != 0 {
|
||||
t.Fatalf("a sound placement is refused: %v", r)
|
||||
if r := CheckAssignments(current, known, enrolled); len(r) != 0 {
|
||||
t.Fatalf("a sound assignments is refused: %v", r)
|
||||
}
|
||||
// A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there.
|
||||
for name, refused := range map[string]bool{"mesh-dns-resolver": false, "mesh-broker": true} {
|
||||
a := seat.Placement{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: name, Scope: "mesh", Module: "dns"}}}
|
||||
a := seat.Assignments{Node: "ace", Modules: []seat.PlacedModule{{Module: "dns"}}, Holds: []seat.Holding{{Claim: name, Scope: "mesh", Module: "dns"}}}
|
||||
b := a
|
||||
b.Node = "novox"
|
||||
r := CheckPlacements(map[string]seat.Placement{"ace": a, "novox": b}, known, enrolled)
|
||||
r := CheckAssignments(map[string]seat.Assignments{"ace": a, "novox": b}, known, enrolled)
|
||||
if (len(r) != 0) != refused {
|
||||
t.Errorf("%s on two nodes: refusals %v, want refused %v", name, r, refused)
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func TestPlacement(t *testing.T) {
|
||||
twice := novox
|
||||
twice.Node = "stranger"
|
||||
twice.Decided.Address = "10.77.0.2"
|
||||
r := CheckPlacements(map[string]seat.Placement{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled)
|
||||
r := CheckAssignments(map[string]seat.Assignments{"g14": clash, "novox": novox, "stranger": twice}, known, enrolled)
|
||||
for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080",
|
||||
"bound to ace/postgres", "pin of shell names g14/bash", "stranger is a node with no record",
|
||||
"mesh-store, held once for the mesh, is held on novox and stranger",
|
||||
@@ -113,7 +113,7 @@ func TestPlacement(t *testing.T) {
|
||||
t.Fatalf("laid %+v, %v", laid, findings)
|
||||
}
|
||||
if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) {
|
||||
t.Error("laying changed the current placement")
|
||||
t.Error("laying changed the current assignments")
|
||||
}
|
||||
if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" {
|
||||
t.Errorf("a node with no key: %+v, %v", fresh, f)
|
||||
@@ -125,7 +125,7 @@ func TestPlacement(t *testing.T) {
|
||||
"not placed": {seat.ProposalChange{Op: client.OpTakeOff, Node: "g14", Module: "nope"}, "not placed"},
|
||||
"no such field": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "colour", Value: "x"}, "no decided field"},
|
||||
"wrong type": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "hub", Value: "x"}, "does not take"},
|
||||
"unknown op": {seat.ProposalChange{Op: "move", Node: "g14"}, "no change of placement"},
|
||||
"unknown op": {seat.ProposalChange{Op: "move", Node: "g14"}, "no change of assignments"},
|
||||
"pin of nobody": {seat.ProposalChange{Op: client.OpPin, Node: "g14", Provision: "x"}, "provider"},
|
||||
"not held": {seat.ProposalChange{Op: client.OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"},
|
||||
"bad port": {seat.ProposalChange{Op: client.OpPlace, Node: "g14", Module: "x", Ports: map[string]int64{"a": 1}}, "port"},
|
||||
@@ -190,8 +190,8 @@ func TestPlacement(t *testing.T) {
|
||||
t.Errorf("the digest of %s is %s, and the earlier holder made %s", name, c.got, c.want)
|
||||
}
|
||||
}
|
||||
if ExportDigest(current) != ExportDigest(map[string]seat.Placement{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
|
||||
t.Error("the export digest depends on order or not on the placements")
|
||||
if ExportDigest(current) != ExportDigest(map[string]seat.Assignments{"novox": novox, "g14": g14}) || ExportDigest(current) == ExportDigest(nil) {
|
||||
t.Error("the export digest depends on order or not on the assignments")
|
||||
}
|
||||
// 774/S42/AC1 (ADR 0587 §7): an add-node carries its enrolment's warrant naming its node and address, as its one
|
||||
// change, from the enroller alone; a warrant naming another node or address is refused.
|
||||
@@ -267,7 +267,7 @@ func TestPlacement(t *testing.T) {
|
||||
AddNodeRefusal(remove, enroller, enrollers) != "" || !slices.Equal(RemovedNodes(remove), []string{"lab"}) {
|
||||
t.Errorf("a remove-node: %v, %q", err, AddNodeRefusal(remove, "mesh-delivery-assigner", enrollers))
|
||||
}
|
||||
joined := map[string]seat.Placement{"lab": {V: keys.Version, Node: "lab", Decided: seat.NodeDecided{Address: "10.0.0.7"}, Modules: []seat.PlacedModule{}}}
|
||||
joined := map[string]seat.Assignments{"lab": {V: keys.Version, Node: "lab", Decided: seat.NodeDecided{Address: "10.0.0.7"}, Modules: []seat.PlacedModule{}}}
|
||||
if laid, f := LayChanges(joined, remove.Changes); len(f) != 0 || len(laid) != 0 {
|
||||
t.Errorf("a joining node removed: %v, %v", laid, f)
|
||||
}
|
||||
@@ -301,9 +301,9 @@ func TestPlacement(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// ADR 0571 clause 4: who holds which seat, from placement; a node seat by its node.
|
||||
// ADR 0571 clause 4: who holds which seat, from assignments; a node seat by its node.
|
||||
t.Run("who holds which seat", func(t *testing.T) {
|
||||
held := SeatHoldersOf(map[string]seat.Placement{
|
||||
held := SeatHoldersOf(map[string]seat.Assignments{
|
||||
"ace": {Node: "ace", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "node-files", Scope: "node", Module: "mesh-files"}}},
|
||||
"novox": {Node: "novox", Holds: []seat.Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "mesh-broker", Scope: "mesh", Module: "nats"},
|
||||
{Claim: "photo-gallery", Scope: "mesh", Module: "photos"}}},
|
||||
+13
-13
@@ -1,17 +1,17 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// Placement candidates (novox/hq ADR 0567 §3): for each node a proposal must hear from, the holder writes one key
|
||||
// `<proposal>.<node>` in its state `candidates`: the node's placement with the proposal laid over it for a node whose key
|
||||
// Assignments candidates (novox/hq ADR 0567 §3): for each node a proposal must hear from, the holder writes one key
|
||||
// `<proposal>.<node>` in its state `candidates`: the node's assignments with the proposal laid over it for a node whose key
|
||||
// it changes, or the node's current key for one whose composition it changes only through another node (Reached). The
|
||||
// layouts are the seat's contract's (CandidatesByProposalNodeValue); what refuses a candidate is here.
|
||||
|
||||
@@ -31,7 +31,7 @@ func CandidateKey(proposal, node string) (string, error) {
|
||||
func ParseCandidateKey(key string) (proposal, node string, err error) {
|
||||
proposal, node, ok := strings.Cut(key, ".")
|
||||
if !ok || keys.CheckName("node", node) != nil || client.CheckProposalID(proposal) != nil {
|
||||
return "", "", fmt.Errorf("placement: %q is not a placement candidate key, <proposal>.<node>", key)
|
||||
return "", "", fmt.Errorf("assignments: %q is not an assignments candidate key, <proposal>.<node>", key)
|
||||
}
|
||||
return proposal, node, nil
|
||||
}
|
||||
@@ -40,8 +40,8 @@ func checkCandidate(c seat.CandidatesByProposalNodeValue) error {
|
||||
if err := client.CheckProposalID(c.Proposal); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Placement != nil {
|
||||
return client.CheckPlacement(*c.Placement)
|
||||
if c.Assignments != nil {
|
||||
return client.CheckAssignments(*c.Assignments)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -52,15 +52,15 @@ func EncodeCandidate(c seat.CandidatesByProposalNodeValue) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
c.V = keys.Version
|
||||
if c.Placement != nil {
|
||||
p := *c.Placement
|
||||
if c.Assignments != nil {
|
||||
p := *c.Assignments
|
||||
p.V = keys.Version
|
||||
c.Placement = &p
|
||||
c.Assignments = &p
|
||||
}
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// DecodeCandidate reads a candidate; it refuses an unknown version, a proposal id that is not one, and a placement the
|
||||
// DecodeCandidate reads a candidate; it refuses an unknown version, a proposal id that is not one, and assignments the
|
||||
// client refuses.
|
||||
func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
|
||||
if err := keys.CheckVersion(data); err != nil {
|
||||
@@ -68,7 +68,7 @@ func DecodeCandidate(data []byte) (seat.CandidatesByProposalNodeValue, error) {
|
||||
}
|
||||
c, err := seat.DecodeCandidatesByProposalNodeValue(data)
|
||||
if err != nil {
|
||||
return seat.CandidatesByProposalNodeValue{}, fmt.Errorf("placement: a candidate does not fit its shape: %w", err)
|
||||
return seat.CandidatesByProposalNodeValue{}, fmt.Errorf("assignments: a candidate does not fit its shape: %w", err)
|
||||
}
|
||||
if err := checkCandidate(c); err != nil {
|
||||
return seat.CandidatesByProposalNodeValue{}, err
|
||||
+21
-21
@@ -1,9 +1,9 @@
|
||||
// Package placement is the assigner's own check of a placement and of a proposal that changes it (novox/hq ADR 0567 §2,
|
||||
// Package assignments is the assigner's own check of assignments and of a proposal that changes it (novox/hq ADR 0567 §2,
|
||||
// ADR 0634 §1 on where a seat's code goes): what the holder of the seat mesh-module-assigner decides before it writes
|
||||
// anything. The layouts it works on are the seat's contract's (package meshmoduleassigner); what every reader of the
|
||||
// placement shares (the keys assembled into a node's placement, the node key's digest, who holds a seat) is the seat's
|
||||
// assignments shares (the keys assembled into a node's assignments, the node key's digest, who holds a seat) is the seat's
|
||||
// client's (package meshmoduleassignerclient). Nothing here is read by another module.
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
@@ -25,19 +25,19 @@ var KeptApart = []struct{ A, B, Record string }{
|
||||
{A: "mesh-watcher", B: "mesh-self-check", Record: "ADR 0227 §6, ADR 0564 clause 6"},
|
||||
}
|
||||
|
||||
// CheckPlacements is the holder's check of a proposal laid over every placement (ADR 0567 §2), over the placements by
|
||||
// CheckAssignments is the holder's check of a proposal laid over every assignments (ADR 0567 §2), over the assignments by
|
||||
// node, the modules that are known and the nodes with a record. It returns every refusal, in node order, never only the
|
||||
// first: a placement naming a node not enrolled or a module that is not known (held by the indexer, or provided by the
|
||||
// first: assignments naming a node not enrolled or a module that is not known (held by the indexer, or provided by the
|
||||
// mesh itself: the caller's `known` says both); two modules of one node on the same host port; a pin, binding or holding
|
||||
// naming a provider that is not placed where it says; a seat of scope mesh held twice, unless the seat is replicated
|
||||
// (catalogue.Seat's Replicated, ADR 0223), which is held once per node; two nodes with one address on the private
|
||||
// network; more than one hub; two modules KeptApart on one node. A host port a manifest declares without a number and a
|
||||
// requirement left unbound are the composition's to find (its ForeseenPorts and Unbound).
|
||||
func CheckPlacements(placements map[string]seat.Placement, known func(module string) bool, enrolled func(node string) bool) []string {
|
||||
func CheckAssignments(assignments map[string]seat.Assignments, known func(module string) bool, enrolled func(node string) bool) []string {
|
||||
var refusals []string
|
||||
nodes := sortedKeys(placements)
|
||||
nodes := sortedKeys(assignments)
|
||||
placed := func(p seat.Provider) bool {
|
||||
other, ok := placements[p.Node]
|
||||
other, ok := assignments[p.Node]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
@@ -52,9 +52,9 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
|
||||
addresses := map[string]string{}
|
||||
var hubs []string
|
||||
for _, node := range nodes {
|
||||
p := placements[node]
|
||||
p := assignments[node]
|
||||
if p.Node != node {
|
||||
refusals = append(refusals, fmt.Sprintf("the placement of %s names the node %q", node, p.Node))
|
||||
refusals = append(refusals, fmt.Sprintf("the assignments of %s names the node %q", node, p.Node))
|
||||
}
|
||||
if a := strings.TrimSpace(p.Decided.Address); a != "" {
|
||||
if other, twice := addresses[a]; twice {
|
||||
@@ -122,8 +122,8 @@ func CheckPlacements(placements map[string]seat.Placement, known func(module str
|
||||
return refusals
|
||||
}
|
||||
|
||||
// Change is what `placement-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had
|
||||
// none) and after (0: the key was deleted), and its modules added, removed or changed against the placement before.
|
||||
// Change is what `assignments-changed` says of one node (ADR 0567 §6): the node, the revision of its key before (0: it had
|
||||
// none) and after (0: the key was deleted), and its modules added, removed or changed against the assignments before.
|
||||
type Change struct {
|
||||
Node string `json:"node"`
|
||||
Before uint64 `json:"before"`
|
||||
@@ -141,10 +141,10 @@ func (c Change) Body() map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
// ChangeOf is what changed from before to after on one node; before is the zero Placement for a node new to placement.
|
||||
// ChangeOf is what changed from before to after on one node; before is the zero Assignments for a node new to assignments.
|
||||
// A change of the node's pins or holdings counts as a change of every module placed on it both times, since the
|
||||
// composition of each may read them.
|
||||
func ChangeOf(before, after seat.Placement) Change {
|
||||
func ChangeOf(before, after seat.Assignments) Change {
|
||||
c := Change{Node: after.Node}
|
||||
if c.Node == "" {
|
||||
c.Node = before.Node
|
||||
@@ -176,7 +176,7 @@ func ChangeOf(before, after seat.Placement) Change {
|
||||
return c
|
||||
}
|
||||
|
||||
// SortChanges orders a placement-changed event's nodes by node.
|
||||
// SortChanges orders assignments-changed event's nodes by node.
|
||||
func SortChanges(c []Change) {
|
||||
sort.Slice(c, func(i, j int) bool { return c[i].Node < c[j].Node })
|
||||
}
|
||||
@@ -187,14 +187,14 @@ func jsonEqual(a, b any) bool {
|
||||
return string(ja) == string(jb)
|
||||
}
|
||||
|
||||
// SeatHoldersOf is the state composed from every node's placement (ADR 0567 §1): each holding by the seat it claims,
|
||||
// SeatHoldersOf is the state composed from every node's assignments (ADR 0567 §1): each holding by the seat it claims,
|
||||
// keyed as the client's SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name
|
||||
// is keyed by the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes
|
||||
// only a key that differs.
|
||||
func SeatHoldersOf(placements map[string]seat.Placement) map[string]seat.SeatHolders {
|
||||
func SeatHoldersOf(assignments map[string]seat.Assignments) map[string]seat.SeatHolders {
|
||||
out := map[string]seat.SeatHolders{}
|
||||
for _, n := range sortedKeys(placements) {
|
||||
for _, h := range placements[n].Holds {
|
||||
for _, n := range sortedKeys(assignments) {
|
||||
for _, h := range assignments[n].Holds {
|
||||
node := ""
|
||||
if h.Scope == "node" {
|
||||
node = n
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ type tunnelPeerForm struct {
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
type placementForm struct {
|
||||
type assignmentsForm struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
Revision string `json:"revision,omitempty"`
|
||||
@@ -107,7 +107,7 @@ type changeForm struct {
|
||||
Layer map[string]any `json:"layer,omitempty"`
|
||||
MeshValues map[string]any `json:"mesh-wide-values,omitempty"`
|
||||
Values map[string]any `json:"values,omitempty"`
|
||||
Whole *placementForm `json:"whole,omitempty"`
|
||||
Whole *assignmentsForm `json:"whole,omitempty"`
|
||||
Decided *decidedForm `json:"decided,omitempty"`
|
||||
Keys *joiningForm `json:"keys,omitempty"`
|
||||
}
|
||||
@@ -142,8 +142,8 @@ func decidedOf(d seat.NodeDecided) decidedForm {
|
||||
Adopted: d.Adopted, Taken: d.Taken, Tunnel: tunnelOf(d.Tunnel), PeerNames: d.PeerNames}
|
||||
}
|
||||
|
||||
func placementOf(p seat.Placement) placementForm {
|
||||
out := placementForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided),
|
||||
func assignmentsOf(p seat.Assignments) assignmentsForm {
|
||||
out := assignmentsForm{V: int(p.V), Node: p.Node, Revision: p.Revision, Decided: decidedOf(p.Decided),
|
||||
Pins: providersOf(p.Pins)}
|
||||
if p.Modules != nil {
|
||||
out.Modules = []placedForm{}
|
||||
@@ -170,7 +170,7 @@ func changeOf(c seat.ProposalChange) changeForm {
|
||||
out.Value, _ = json.Marshal(c.Value)
|
||||
}
|
||||
if c.Whole != nil {
|
||||
w := placementOf(*c.Whole)
|
||||
w := assignmentsOf(*c.Whole)
|
||||
out.Whole = &w
|
||||
}
|
||||
if c.Decided != nil {
|
||||
@@ -183,14 +183,14 @@ func changeOf(c seat.ProposalChange) changeForm {
|
||||
return out
|
||||
}
|
||||
|
||||
// ExportDigest is the digest of an export's answer, every node's placement, as the seed proposes it: the same
|
||||
// placements give the same digest, whatever order they came in.
|
||||
func ExportDigest(placements map[string]seat.Placement) string {
|
||||
// ExportDigest is the digest of an export's answer, every node's assignments, as the seed proposes it: the same
|
||||
// assignments give the same digest, whatever order they came in.
|
||||
func ExportDigest(assignments map[string]seat.Assignments) string {
|
||||
h := sha256.New()
|
||||
for _, node := range sortedKeys(placements) {
|
||||
p := placements[node]
|
||||
p.V = keys.Version // the export was made with the placement encoded at the keys' version
|
||||
raw, _ := json.Marshal(placementOf(p))
|
||||
for _, node := range sortedKeys(assignments) {
|
||||
p := assignments[node]
|
||||
p.V = keys.Version // the export was made with the assignments encoded at the keys' version
|
||||
raw, _ := json.Marshal(assignmentsOf(p))
|
||||
fmt.Fprintf(h, "%s\n%s\n", node, raw)
|
||||
}
|
||||
return "sha256:" + hex.EncodeToString(h.Sum(nil))
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"os"
|
||||
+43
-43
@@ -1,4 +1,4 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
client "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
enroller "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
|
||||
client "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// A placement changes only by a proposal (novox/hq ADR 0567 §2): `propose` takes why and a list of changes; the holder
|
||||
// Assignments changes only by a proposal (novox/hq ADR 0567 §2): `propose` takes why and a list of changes; the holder
|
||||
// lays them over the current keys, names the revision of each key it laid them over, and either refuses at once with every
|
||||
// finding or writes the proposal under its id in its state `proposals` and a candidate for each node it must hear from.
|
||||
|
||||
@@ -50,7 +50,7 @@ func CheckChange(c seat.ProposalChange) error {
|
||||
}
|
||||
needs := func(what string, ok bool) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("placement: a %s change of %s names no %s", c.Op, c.Node, what)
|
||||
return fmt.Errorf("assignments: a %s change of %s names no %s", c.Op, c.Node, what)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -67,50 +67,50 @@ func CheckChange(c seat.ProposalChange) error {
|
||||
return needs("field or value", c.Field != "" && c.Value != nil)
|
||||
case client.OpSeed:
|
||||
if c.Whole == nil || c.Whole.Node != c.Node {
|
||||
return fmt.Errorf("placement: the seed of %s names no placement of that node", c.Node)
|
||||
return fmt.Errorf("assignments: the seed of %s names no assignments of that node", c.Node)
|
||||
}
|
||||
return client.CheckPlacement(*c.Whole)
|
||||
return client.CheckAssignments(*c.Whole)
|
||||
case client.OpAddNode:
|
||||
if c.Decided == nil || c.Decided.Address == "" || c.Keys == nil || c.Keys.Overlay == "" {
|
||||
return fmt.Errorf("placement: the add-node of %s names no address or no tunnel key", c.Node)
|
||||
return fmt.Errorf("assignments: the add-node of %s names no address or no tunnel key", c.Node)
|
||||
}
|
||||
// The identity keys come at redeem, whole or not at all: an identity with no sealing key, or the reverse.
|
||||
if (c.Keys.Identity == "") != (c.Keys.Sealing == "") || (c.Keys.Identity == "" && c.Keys.Serving != "") {
|
||||
return fmt.Errorf("placement: the add-node of %s names part of its identity keys; they come whole at redeem", c.Node)
|
||||
return fmt.Errorf("assignments: the add-node of %s names part of its identity keys; they come whole at redeem", c.Node)
|
||||
}
|
||||
if c.Decided.Hub {
|
||||
return fmt.Errorf("placement: the add-node of %s makes it the hub, which a joining node never is", c.Node)
|
||||
return fmt.Errorf("assignments: the add-node of %s makes it the hub, which a joining node never is", c.Node)
|
||||
}
|
||||
if c.Module != "" || len(c.Ports)+len(c.Bindings) > 0 || c.Whole != nil {
|
||||
return fmt.Errorf("placement: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node)
|
||||
return fmt.Errorf("assignments: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node)
|
||||
}
|
||||
return nil
|
||||
case client.OpRemoveNode:
|
||||
if c.Module != "" || c.Decided != nil || c.Keys != nil || c.Whole != nil {
|
||||
return fmt.Errorf("placement: the remove-node of %s names more than its node", c.Node)
|
||||
return fmt.Errorf("assignments: the remove-node of %s names more than its node", c.Node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("placement: %q is no change of placement: place, take-off, pin, unpin, hold, unhold, decide, config, add-node or remove-node", c.Op)
|
||||
return fmt.Errorf("assignments: %q is no change of assignments: place, take-off, pin, unpin, hold, unhold, decide, config, add-node or remove-node", c.Op)
|
||||
}
|
||||
|
||||
// LayChanges is the placements of the nodes the changes name with the changes laid over current, in the order given;
|
||||
// current is every node's placement now (absent: the node has no key). It returns, by node, the laid placement, and
|
||||
// LayChanges is the assignments of the nodes the changes name with the changes laid over current, in the order given;
|
||||
// current is every node's assignments now (absent: the node has no key). It returns, by node, the laid assignments, and
|
||||
// every finding: a change that does not fit its node (a module taken off that is not placed, configured or holding
|
||||
// there; a field NodeDecided does not have) or a placement the client refuses. What CheckPlacements refuses across nodes
|
||||
// there; a field NodeDecided does not have) or assignments the client refuses. What CheckAssignments refuses across nodes
|
||||
// is the caller's to ask of the result laid over current.
|
||||
func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange) (map[string]seat.Placement, []string) {
|
||||
laid := map[string]seat.Placement{}
|
||||
func LayChanges(current map[string]seat.Assignments, changes []seat.ProposalChange) (map[string]seat.Assignments, []string) {
|
||||
laid := map[string]seat.Assignments{}
|
||||
var findings []string
|
||||
of := func(node string) seat.Placement {
|
||||
of := func(node string) seat.Assignments {
|
||||
if p, ok := laid[node]; ok {
|
||||
return p
|
||||
}
|
||||
p, ok := current[node]
|
||||
if !ok {
|
||||
return seat.Placement{V: keys.Version, Node: node, Modules: []seat.PlacedModule{}}
|
||||
return seat.Assignments{V: keys.Version, Node: node, Modules: []seat.PlacedModule{}}
|
||||
}
|
||||
return clonePlacement(p)
|
||||
return cloneAssignments(p)
|
||||
}
|
||||
for i, c := range changes {
|
||||
if err := CheckChange(c); err != nil {
|
||||
@@ -129,14 +129,14 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
}
|
||||
switch c.Op {
|
||||
case client.OpSeed:
|
||||
p = clonePlacement(*c.Whole)
|
||||
p = cloneAssignments(*c.Whole)
|
||||
case client.OpAddNode:
|
||||
// A joining node has no key yet (ADR 0587 §7): one that has is no joining node.
|
||||
if _, has := current[c.Node]; has {
|
||||
findings = append(findings, fmt.Sprintf("change %d: %s is placed already, so it does not join", i+1, c.Node))
|
||||
continue
|
||||
}
|
||||
p = seat.Placement{V: keys.Version, Node: c.Node, Decided: *c.Decided, Modules: []seat.PlacedModule{}}
|
||||
p = seat.Assignments{V: keys.Version, Node: c.Node, Decided: *c.Decided, Modules: []seat.PlacedModule{}}
|
||||
case client.OpRemoveNode:
|
||||
// Only a joining node never confirmed goes this way: one with no module and no holding (a node that runs
|
||||
// anything is taken off module by module, by a proposal the operator approves).
|
||||
@@ -227,7 +227,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
laid[c.Node] = p
|
||||
}
|
||||
for _, node := range sortedKeys(laid) {
|
||||
if err := client.CheckPlacement(laid[node]); err != nil {
|
||||
if err := client.CheckAssignments(laid[node]); err != nil {
|
||||
findings = append(findings, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -275,9 +275,9 @@ var decidedFields = func() map[string]bool {
|
||||
return out
|
||||
}()
|
||||
|
||||
func clonePlacement(p seat.Placement) seat.Placement {
|
||||
func cloneAssignments(p seat.Assignments) seat.Assignments {
|
||||
raw, _ := json.Marshal(p)
|
||||
var out seat.Placement
|
||||
var out seat.Assignments
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
if out.Modules == nil {
|
||||
out.Modules = []seat.PlacedModule{}
|
||||
@@ -301,33 +301,33 @@ func CheckProposal(p seat.Proposal) error {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(p.By) == "" || strings.TrimSpace(p.Why) == "" {
|
||||
return fmt.Errorf("placement: the proposal %s names no proposer or no why", p.ID)
|
||||
return fmt.Errorf("assignments: the proposal %s names no proposer or no why", p.ID)
|
||||
}
|
||||
if len(p.Base) == 0 || (len(p.Changes) == 0) != p.HandBack || (p.HandBack && p.Seed) {
|
||||
return fmt.Errorf("placement: the proposal %s changes nothing, or is a hand-back with changes", p.ID)
|
||||
return fmt.Errorf("assignments: the proposal %s changes nothing, or is a hand-back with changes", p.ID)
|
||||
}
|
||||
for i, c := range p.Changes {
|
||||
if err := CheckChange(c); err != nil {
|
||||
return fmt.Errorf("placement: the proposal %s, change %d: %w", p.ID, i+1, err)
|
||||
return fmt.Errorf("assignments: the proposal %s, change %d: %w", p.ID, i+1, err)
|
||||
}
|
||||
if (c.Op == client.OpSeed) != p.Seed {
|
||||
return fmt.Errorf("placement: the proposal %s mixes the seed with other changes", p.ID)
|
||||
return fmt.Errorf("assignments: the proposal %s mixes the seed with other changes", p.ID)
|
||||
}
|
||||
if _, ok := p.Base[c.Node]; !ok {
|
||||
return fmt.Errorf("placement: the proposal %s changes %s and names no base revision of its key", p.ID, c.Node)
|
||||
return fmt.Errorf("assignments: the proposal %s changes %s and names no base revision of its key", p.ID, c.Node)
|
||||
}
|
||||
}
|
||||
switch p.State {
|
||||
case client.ProposalAnswering, client.ProposalFailed:
|
||||
case client.ProposalAsked:
|
||||
if p.Ask == "" || p.Verdict == nil || p.Verdict.State != VerdictPass {
|
||||
return fmt.Errorf("placement: the proposal %s is asked with no ask or no passing verdict", p.ID)
|
||||
return fmt.Errorf("assignments: the proposal %s is asked with no ask or no passing verdict", p.ID)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("placement: the proposal %s is in no state %q", p.ID, p.State)
|
||||
return fmt.Errorf("assignments: the proposal %s is in no state %q", p.ID, p.State)
|
||||
}
|
||||
if p.Seed && p.ExportDigest == "" {
|
||||
return fmt.Errorf("placement: the seed %s names no export digest", p.ID)
|
||||
return fmt.Errorf("assignments: the seed %s names no export digest", p.ID)
|
||||
}
|
||||
return checkAddNode(p)
|
||||
}
|
||||
@@ -344,25 +344,25 @@ func checkAddNode(p seat.Proposal) error {
|
||||
}
|
||||
if adds == 0 {
|
||||
if p.Warrant != nil {
|
||||
return fmt.Errorf("placement: the proposal %s carries an enrolment's warrant and adds no node", p.ID)
|
||||
return fmt.Errorf("assignments: the proposal %s carries an enrolment's warrant and adds no node", p.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if adds != 1 || len(p.Changes) != 1 || p.HandBack || p.Seed {
|
||||
return fmt.Errorf("placement: the proposal %s adds a node beside other changes; an add-node is a proposal's one change", p.ID)
|
||||
return fmt.Errorf("assignments: the proposal %s adds a node beside other changes; an add-node is a proposal's one change", p.ID)
|
||||
}
|
||||
c, w := p.Changes[0], p.Warrant
|
||||
if w == nil || w.Ask == "" || w.Warrant == "" {
|
||||
return fmt.Errorf("placement: the add-node of %s carries no warrant of its enrolment", c.Node)
|
||||
return fmt.Errorf("assignments: the add-node of %s carries no warrant of its enrolment", c.Node)
|
||||
}
|
||||
if c.Op == client.OpRemoveNode {
|
||||
if w.Node != c.Node {
|
||||
return fmt.Errorf("placement: the remove-node of %s carries the warrant of %s", c.Node, w.Node)
|
||||
return fmt.Errorf("assignments: the remove-node of %s carries the warrant of %s", c.Node, w.Node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if w.Node != c.Node || w.Address != c.Decided.Address {
|
||||
return fmt.Errorf("placement: the add-node of %s at %s carries the warrant of %s at %s", c.Node, c.Decided.Address, w.Node, w.Address)
|
||||
return fmt.Errorf("assignments: the add-node of %s at %s carries the warrant of %s at %s", c.Node, c.Decided.Address, w.Node, w.Address)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -383,10 +383,10 @@ func AddNodeRefusal(p seat.Proposal, caller string, enrollers []string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// EnrollersOf is the modules that hold the seat mesh-node-enroller on the placements given, sorted: the state seat-holders
|
||||
// EnrollersOf is the modules that hold the seat mesh-node-enroller on the assignments given, sorted: the state seat-holders
|
||||
// composed from them (SeatHoldersOf), read by the client's HoldersOf.
|
||||
func EnrollersOf(placements map[string]seat.Placement) []string {
|
||||
holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(placements)), enroller.Seat, "")
|
||||
func EnrollersOf(assignments map[string]seat.Assignments) []string {
|
||||
holders, _ := client.HoldersOf(rawHolders(SeatHoldersOf(assignments)), enroller.Seat, "")
|
||||
var out []string
|
||||
for _, h := range holders {
|
||||
if !slices.Contains(out, h.Module) {
|
||||
@@ -422,7 +422,7 @@ func DecodeProposal(data []byte) (seat.Proposal, error) {
|
||||
}
|
||||
p, err := seat.DecodeProposalsByProposalValue(data)
|
||||
if err != nil {
|
||||
return seat.Proposal{}, fmt.Errorf("placement: a proposal does not fit its shape: %w", err)
|
||||
return seat.Proposal{}, fmt.Errorf("assignments: a proposal does not fit its shape: %w", err)
|
||||
}
|
||||
if err := CheckProposal(p); err != nil {
|
||||
return seat.Proposal{}, err
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
package placement
|
||||
package assignment
|
||||
|
||||
import seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
import seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
|
||||
// Module is the module that holds the seat mesh-module-assigner: the states of the seat are named after it, as every
|
||||
// module state is (a bucket is `<module>_<state>`), and its manifest declares them.
|
||||
@@ -8,7 +8,7 @@ const Module = "mesh-delivery-assigner"
|
||||
|
||||
// The buckets of the seat's states, which only the holder writes (ADR 0567 §1).
|
||||
const (
|
||||
PlacementBucket = Module + "_" + seat.StatePlacement
|
||||
AssignmentsBucket = Module + "_" + seat.StateAssignments
|
||||
ProposalsBucket = Module + "_" + seat.StateProposals
|
||||
CandidatesBucket = Module + "_" + seat.StateCandidates
|
||||
PulsesBucket = Module + "_" + seat.StatePulses
|
||||
@@ -16,8 +16,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
// PlacementHistory is the revisions the placement bucket keeps per key: the bus's most (§6).
|
||||
PlacementHistory = 64
|
||||
// AssignmentsHistory is the revisions the assignments bucket keeps per key: the bus's most (§6).
|
||||
AssignmentsHistory = 64
|
||||
// EventDays is how long the seat's events are kept: they ride the shared events stream on the seat's own subject
|
||||
// (the seat's family, 7 days).
|
||||
EventDays = 7
|
||||
@@ -7,13 +7,13 @@ package buckets
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
|
||||
wants "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
catalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
wants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
)
|
||||
|
||||
// The states the assigner keeps: the wants seat's, and its own asks, puts and policies.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package judge is how the seat mesh-module-assigner's holder judges a proposal (novox/hq ADR 0567 §3, §4, as ADR 0593 §8
|
||||
// changes them): what it changes on each node by its placement (Intended), every node answering it, and the verdict of
|
||||
// changes them): what it changes on each node by its assignments (Intended), every node answering it, and the verdict of
|
||||
// their answers (JudgeAnswers). The assigner's own (ADR 0593 §1), moved back from mesh-sdk's compose/judge: the holder
|
||||
// composes nothing, since every composition is its node's engine's, and a node answers through the candidates path
|
||||
// with what it composed. It holds no bus client and no timer: the holder gives it what it read.
|
||||
@@ -17,24 +17,24 @@ import (
|
||||
"time"
|
||||
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
)
|
||||
|
||||
// AnswerBound is how long the holder waits for a node's answer after its candidate was written: the time to live of the
|
||||
// node's pulse key (placement.PulseAnswerKey), whose expiry names it as giving no answer, never passed. The same answer
|
||||
// node's pulse key (assignment.PulseAnswerKey), whose expiry names it as giving no answer, never passed. The same answer
|
||||
// bound a check gives a node (ADR 0593 §8, §7: 60 seconds, nec.AnswerBound). A variable so a test on the bus
|
||||
// can shorten it.
|
||||
var AnswerBound = nec.AnswerBound
|
||||
|
||||
// Intended is, by node, the modules whose placed entry the proposal changes there, and the nodes that answer it: every
|
||||
// node (ADR 0593 §8, changing ADR 0567 §4's list of the nodes that must answer), sorted. current is the placement as it
|
||||
// stands, laid the proposal's placements by node (nil removes the node's key) and nodes every node with a record. A
|
||||
// node (ADR 0593 §8, changing ADR 0567 §4's list of the nodes that must answer), sorted. current is the assignments as it
|
||||
// stands, laid the proposal's assignments by node (nil removes the node's key) and nodes every node with a record. A
|
||||
// node's set holds the modules the proposal adds, removes or whose placed entry differs on that node; a node with no
|
||||
// key now gets an empty set (§7: the seed is answered as the same). What a node's compositions change beyond its own
|
||||
// placement it says itself in its answer, marked in the brief as not computed.
|
||||
func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement, nodes []string) (intended map[string]map[string]bool, must []string) {
|
||||
// assignments it says itself in its answer, marked in the brief as not computed.
|
||||
func Intended(current map[string]mma.Assignments, laid map[string]*mma.Assignments, nodes []string) (intended map[string]map[string]bool, must []string) {
|
||||
intended = map[string]map[string]bool{}
|
||||
all := map[string]bool{}
|
||||
for _, n := range nodes {
|
||||
@@ -50,7 +50,7 @@ func Intended(current map[string]mma.Placement, laid map[string]*mma.Placement,
|
||||
before, placed := current[node]
|
||||
after, changesKey := laid[node]
|
||||
if !changesKey || !placed {
|
||||
continue // untouched, or new to placement: every answer must be the same
|
||||
continue // untouched, or new to assignments: every answer must be the same
|
||||
}
|
||||
var afterModules []mma.PlacedModule
|
||||
if after != nil {
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
mma "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
|
||||
plc "mesh-delivery-assigner/internal/placement"
|
||||
plc "mesh-delivery-assigner/internal/assignment"
|
||||
)
|
||||
|
||||
// ADR 0567 §3, §4 (rows moved from the assigner's check of a pull request's placement, ADR 0559 §5): what a proposal
|
||||
// ADR 0567 §3, §4 (rows moved from the assigner's check of a pull request's assignments, ADR 0559 §5): what a proposal
|
||||
// changes by node, and the verdict of the answers: differences shown and failing nothing, a refusal and a silent node
|
||||
// failing, a later answer judging again, the seed failing on any difference.
|
||||
func TestJudge(t *testing.T) {
|
||||
current := map[string]mma.Placement{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}}
|
||||
laid := map[string]*mma.Placement{
|
||||
current := map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail"}}}, "server": {Node: "server"}}
|
||||
laid := map[string]*mma.Assignments{
|
||||
"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "mail", Ports: map[string]int64{"25": 2525}}, {Module: "web"}}},
|
||||
"laptop": {Node: "laptop", Modules: []mma.PlacedModule{{Module: "web"}}}}
|
||||
// ADR 0593 §8: every node answers; what the placement changes is read from the placement alone.
|
||||
// ADR 0593 §8: every node answers; what the assignments changes is read from the assignments alone.
|
||||
got, must := Intended(current, laid, []string{"anchor", "laptop", "server"})
|
||||
if !got["anchor"]["mail"] || !got["anchor"]["web"] || len(got["laptop"]) != 0 || len(got["server"]) != 0 ||
|
||||
!slices.Equal(must, []string{"anchor", "laptop", "server"}) {
|
||||
|
||||
Reference in New Issue
Block a user