Assigner: the operations of a change and the states of a proposal are the module assigner client's, which the enrolment builds with (B7f finding 4; ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -1233,7 +1233,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
t.Fatalf("must answer = %v, want both nodes", p.Must)
|
||||
}
|
||||
answerAll(p)
|
||||
p = proposalNow(p.ID, "asked", func(p mma.Proposal) bool { return p.State == plc.ProposalAsked })
|
||||
p = proposalNow(p.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
|
||||
akey, _ := delivery.PlacementAskKey(p.ID)
|
||||
b.answer(t, akey, assign.OptionYes)
|
||||
select {
|
||||
@@ -1277,7 +1277,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
return strings.Contains(p.Words, "laid again") && len(p.Must) > 0
|
||||
})
|
||||
answerAll(relaid)
|
||||
proposalNow(moved.ID, "asked", func(p mma.Proposal) bool { return p.State == plc.ProposalAsked })
|
||||
proposalNow(moved.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
|
||||
akey, _ = delivery.PlacementAskKey(moved.ID)
|
||||
b.answer(t, akey, assign.OptionNo)
|
||||
for deadline := time.Now().Add(15 * time.Second); b.hasKey(plc.ProposalsBucket, moved.ID); time.Sleep(50 * time.Millisecond) {
|
||||
@@ -1297,7 +1297,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
silent := q.(proposed).Proposal
|
||||
decided := proposalNow(silent.ID, "decided without its silent nodes", func(p mma.Proposal) bool { return p.State == plc.ProposalAsked })
|
||||
decided := proposalNow(silent.ID, "decided without its silent nodes", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
|
||||
if decided.Verdict == nil || decided.Verdict.State != delivery.VerdictPass ||
|
||||
!strings.Contains(strings.Join(decided.Verdict.Notes, "; "), "no answer from") || !strings.Contains(strings.Join(decided.Verdict.Notes, "; "), "laptop (offline)") {
|
||||
t.Errorf("the silent node's verdict = %+v", decided.Verdict)
|
||||
|
||||
@@ -258,13 +258,13 @@ func (p *passer) ask(ctx context.Context, prop mma.Proposal, akey, brief string)
|
||||
if pk, err := plc.PulseAskKey(prop.ID); err == nil {
|
||||
p.pulse(ctx, pk, assign.ExpiresAt(placed).Sub(p.clock()))
|
||||
}
|
||||
prop.State, prop.Ask = plc.ProposalAsked, placed.ID
|
||||
prop.State, prop.Ask = mmac.ProposalAsked, placed.ID
|
||||
return prop, true
|
||||
}
|
||||
|
||||
// stepProposal takes one proposal one step further.
|
||||
func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Placement, revs map[string]uint64) {
|
||||
if !p.saidProposed[prop.ID] && prop.State == plc.ProposalAnswering && prop.Verdict == nil {
|
||||
if !p.saidProposed[prop.ID] && prop.State == mmac.ProposalAnswering && prop.Verdict == nil {
|
||||
p.sayEvent(mma.EventPlacementProposed, mma.PlacementEvent{Proposal: prop}, prop.ID)
|
||||
p.saidProposed[prop.ID] = true
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
|
||||
}
|
||||
akey, _ := delivery.PlacementAskKey(prop.ID)
|
||||
again := prop.Again != nil && !prop.Again.IsZero() && (prop.Verdict == nil || prop.Again.After(prop.Verdict.At))
|
||||
if prop.State == plc.ProposalAsked {
|
||||
if prop.State == mmac.ProposalAsked {
|
||||
askPulse, _ := plc.PulseAskKey(prop.ID)
|
||||
switch {
|
||||
case p.answeredWith(akey, assign.OptionYes):
|
||||
@@ -327,11 +327,11 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
|
||||
prop.Verdict = &v
|
||||
switch v.State {
|
||||
case delivery.VerdictFail:
|
||||
prop.State, prop.Ask = plc.ProposalFailed, ""
|
||||
prop.State, prop.Ask = mmac.ProposalFailed, ""
|
||||
case plc.VerdictWaits:
|
||||
prop.State, prop.Ask = plc.ProposalAnswering, ""
|
||||
prop.State, prop.Ask = mmac.ProposalAnswering, ""
|
||||
case delivery.VerdictPass:
|
||||
if prop.State != plc.ProposalAsked || again {
|
||||
if prop.State != mmac.ProposalAsked || again {
|
||||
var ok bool
|
||||
if prop, ok = p.ask(ctx, prop, akey, judge.Brief(prop, v)); !ok {
|
||||
return
|
||||
@@ -535,7 +535,7 @@ func (p *passer) addNode(ctx context.Context, prop mma.Proposal, revs map[string
|
||||
laid, findings := plc.LayChanges(current, prop.Changes)
|
||||
findings = append(findings, assign.ControllerRefusals(prop.Changes, laid)...)
|
||||
node := prop.Changes[0].Node
|
||||
if prop.Changes[0].Op == plc.OpRemoveNode {
|
||||
if prop.Changes[0].Op == mmac.OpRemoveNode {
|
||||
p.removeNode(ctx, prop, current, findings)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range changes {
|
||||
if c.Op == plc.OpSeed {
|
||||
if c.Op == mmac.OpSeed {
|
||||
return nil, errors.New("the seed is gone with adopt: propose takes place, take-off, pin, unpin, hold, unhold, decide and config")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ type ProposeIn struct {
|
||||
// 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) {
|
||||
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: plc.ProposalAnswering}
|
||||
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: mmac.ProposalAnswering}
|
||||
var findings []string
|
||||
if p.By == "" {
|
||||
findings = append(findings, "the runtime names no caller: a proposal is made by someone")
|
||||
@@ -54,7 +54,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
findings = append(findings, "a proposal changes something: it names no change")
|
||||
}
|
||||
for i, c := range in.Changes {
|
||||
if c.Op == plc.OpSeed {
|
||||
if c.Op == mmac.OpSeed {
|
||||
findings = append(findings, fmt.Sprintf("change %d: the seed is gone with adopt, the controller's export being gone", i+1))
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
|
||||
// this one caller is the exception to a node with no record (ADR 0587 §7, changing ADR 0567 §2).
|
||||
joining := map[string]bool{}
|
||||
for _, c := range in.Changes {
|
||||
if c.Op == plc.OpAddNode {
|
||||
if c.Op == mmac.OpAddNode {
|
||||
joining[c.Node] = true
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placem
|
||||
var out []string
|
||||
nodes := map[string]bool{}
|
||||
for i, c := range changes {
|
||||
places := c.Module == ControllerModule && c.Op != plc.OpTakeOff
|
||||
places := c.Module == ControllerModule && c.Op != mmac.OpTakeOff
|
||||
if c.Whole != nil && slices.Contains(mmac.Assigned(*c.Whole), ControllerModule) {
|
||||
places = true
|
||||
}
|
||||
@@ -235,6 +235,6 @@ func Rest(p mma.Proposal, written map[string]*mma.Placement) (mma.Proposal, bool
|
||||
rest.Base[node] = base
|
||||
}
|
||||
}
|
||||
rest.Must, rest.Verdict, rest.Ask, rest.State = nil, nil, "", plc.ProposalAnswering
|
||||
rest.Must, rest.Verdict, rest.Ask, rest.State = nil, nil, "", mmac.ProposalAnswering
|
||||
return rest, len(rest.Changes) > 0
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
Held: func(m string) bool { return m != "ghost" },
|
||||
Recorded: func(n string) bool { return n == "anchor" || n == "laptop" }}
|
||||
}
|
||||
move := []mma.ProposalChange{{Op: plc.OpTakeOff, Node: "anchor", Module: "web"}, {Op: plc.OpPlace, Node: "laptop", Module: "web"}}
|
||||
move := []mma.ProposalChange{{Op: mmac.OpTakeOff, Node: "anchor", Module: "web"}, {Op: mmac.OpPlace, Node: "laptop", Module: "web"}}
|
||||
p, laid, findings := Propose(in(move...))
|
||||
if len(findings) != 0 || p.State != plc.ProposalAnswering || p.Base["anchor"] != 4 || p.Base["laptop"] != 9 ||
|
||||
if len(findings) != 0 || p.State != mmac.ProposalAnswering || p.Base["anchor"] != 4 || p.Base["laptop"] != 9 ||
|
||||
!slices.Equal(mmac.Assigned(*laid["laptop"]), []string{"shell", "web"}) || !slices.Equal(mmac.Assigned(*laid["anchor"]), []string{"store"}) {
|
||||
t.Fatalf("a move: %+v, %+v, %v", p, laid, findings)
|
||||
}
|
||||
@@ -41,18 +41,18 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
in ProposeIn
|
||||
want string
|
||||
}{
|
||||
"a module not held": {in(mma.ProposalChange{Op: plc.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
|
||||
"a node with no record": {in(mma.ProposalChange{Op: plc.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
|
||||
"nothing changes": {in(mma.ProposalChange{Op: plc.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
|
||||
"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"},
|
||||
"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: plc.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
|
||||
"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: plc.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
|
||||
"the controller seeded whole": {in(mma.ProposalChange{Op: plc.OpSeed, Node: "anchor", Whole: &mma.Placement{V: keys.Version, Node: "anchor",
|
||||
"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",
|
||||
Decided: anchor.Decided, Modules: []mma.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
|
||||
"a pin of nobody placed": {in(mma.ProposalChange{Op: plc.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
|
||||
"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"},
|
||||
} {
|
||||
if _, _, f := Propose(c.in); !slices.ContainsFunc(f, func(s string) bool { return strings.Contains(s, c.want) }) {
|
||||
t.Errorf("%s: findings %v, want one naming %q", name, f, c.want)
|
||||
@@ -61,12 +61,12 @@ 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).
|
||||
for _, c := range []mma.ProposalChange{
|
||||
{Op: plc.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
|
||||
{Op: plc.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}},
|
||||
{Op: plc.OpConfig, Node: "anchor", Module: "web", Layer: map[string]any{"places": []any{}}},
|
||||
{Op: plc.OpConfig, Node: "anchor", Module: "web", MeshWide: map[string]any{"accesses": []any{}}},
|
||||
{Op: plc.OpDecide, Node: "laptop", Field: "site", Value: json.RawMessage(`"attic"`)},
|
||||
{Op: plc.OpHold, Node: "anchor", Module: "store", Claim: "node-shell", Scope: "node"},
|
||||
{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"}}},
|
||||
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Layer: map[string]any{"places": []any{}}},
|
||||
{Op: mmac.OpConfig, Node: "anchor", Module: "web", MeshWide: map[string]any{"accesses": []any{}}},
|
||||
{Op: mmac.OpDecide, Node: "laptop", Field: "site", Value: json.RawMessage(`"attic"`)},
|
||||
{Op: mmac.OpHold, Node: "anchor", Module: "store", Claim: "node-shell", Scope: "node"},
|
||||
} {
|
||||
if _, _, f := Propose(in(c)); len(f) != 0 {
|
||||
t.Errorf("%s is refused: %v", c.Op, f)
|
||||
@@ -75,7 +75,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" ||
|
||||
len(rest.Base) != 1 || rest.State != plc.ProposalAnswering || rest.Must != nil {
|
||||
len(rest.Base) != 1 || rest.State != mmac.ProposalAnswering || rest.Must != nil {
|
||||
t.Errorf("the rest = %+v, %v", rest, ok)
|
||||
}
|
||||
if _, ok := Rest(p, laid); ok {
|
||||
@@ -100,7 +100,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
|
||||
t.Fatalf("the ask = %+v, %v", a, err)
|
||||
}
|
||||
q := p
|
||||
q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: plc.OpUnpin, Node: "laptop", Provision: "x"})
|
||||
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 {
|
||||
t.Error("the ask's yes binds changes it was not asked about")
|
||||
}
|
||||
|
||||
@@ -97,10 +97,10 @@ func TestPlacement(t *testing.T) {
|
||||
|
||||
t.Run("changes laid over the keys", func(t *testing.T) {
|
||||
laid, findings := LayChanges(current, []seat.ProposalChange{
|
||||
{Op: OpPlace, Node: "g14", Module: "new"},
|
||||
{Op: OpTakeOff, Node: "g14", Module: "zsh"},
|
||||
{Op: OpUnpin, Node: "g14", Provision: "shell"},
|
||||
{Op: OpDecide, Node: "novox", Field: "site", Value: "attic"},
|
||||
{Op: client.OpPlace, Node: "g14", Module: "new"},
|
||||
{Op: client.OpTakeOff, Node: "g14", Module: "zsh"},
|
||||
{Op: client.OpUnpin, Node: "g14", Provision: "shell"},
|
||||
{Op: client.OpDecide, Node: "novox", Field: "site", Value: "attic"},
|
||||
})
|
||||
if len(findings) != 0 || len(laid) != 2 || !reflect.DeepEqual(client.Assigned(laid["g14"]), []string{"app", "new"}) ||
|
||||
len(laid["g14"].Holds) != 0 || laid["g14"].Pins != nil || laid["novox"].Decided.Site != "attic" || !laid["novox"].Decided.Hub {
|
||||
@@ -109,21 +109,21 @@ func TestPlacement(t *testing.T) {
|
||||
if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) {
|
||||
t.Error("laying changed the current placement")
|
||||
}
|
||||
if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" {
|
||||
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)
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
change seat.ProposalChange
|
||||
want string
|
||||
}{
|
||||
"not placed": {seat.ProposalChange{Op: OpTakeOff, Node: "g14", Module: "nope"}, "not placed"},
|
||||
"no such field": {seat.ProposalChange{Op: OpDecide, Node: "g14", Field: "colour", Value: "x"}, "no decided field"},
|
||||
"wrong type": {seat.ProposalChange{Op: OpDecide, Node: "g14", Field: "hub", Value: "x"}, "does not take"},
|
||||
"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"},
|
||||
"pin of nobody": {seat.ProposalChange{Op: OpPin, Node: "g14", Provision: "x"}, "provider"},
|
||||
"not held": {seat.ProposalChange{Op: OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"},
|
||||
"bad port": {seat.ProposalChange{Op: OpPlace, Node: "g14", Module: "x", Ports: map[string]int64{"a": 1}}, "port"},
|
||||
"seed of another": {seat.ProposalChange{Op: OpSeed, Node: "g14", Whole: &novox}, "seed"},
|
||||
"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"},
|
||||
"seed of another": {seat.ProposalChange{Op: client.OpSeed, Node: "g14", Whole: &novox}, "seed"},
|
||||
} {
|
||||
if _, f := LayChanges(current, []seat.ProposalChange{c.change}); len(f) != 1 || !strings.Contains(f[0], c.want) {
|
||||
t.Errorf("%s: %v, want a finding naming %q", name, f, c.want)
|
||||
@@ -136,8 +136,8 @@ func TestPlacement(t *testing.T) {
|
||||
t.Errorf("NewProposalID = %q", got)
|
||||
}
|
||||
p := seat.Proposal{ID: id, By: "operator@anchor", Why: "move the app", At: time.Unix(1, 0).UTC(),
|
||||
Changes: []seat.ProposalChange{{Op: OpTakeOff, Node: "g14", Module: "zsh"}}, Base: map[string]int64{"g14": 4},
|
||||
State: ProposalAnswering}
|
||||
Changes: []seat.ProposalChange{{Op: client.OpTakeOff, Node: "g14", Module: "zsh"}}, Base: map[string]int64{"g14": 4},
|
||||
State: client.ProposalAnswering}
|
||||
raw, err := EncodeProposal(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -150,7 +150,7 @@ func TestPlacement(t *testing.T) {
|
||||
}
|
||||
seed := p
|
||||
seed.Seed, seed.ExportDigest = true, ExportDigest(current)
|
||||
seed.Changes = []seat.ProposalChange{{Op: OpSeed, Node: "g14", Whole: &g14}}
|
||||
seed.Changes = []seat.ProposalChange{{Op: client.OpSeed, Node: "g14", Whole: &g14}}
|
||||
if _, err := EncodeProposal(seed); err != nil {
|
||||
t.Errorf("the seed: %v", err)
|
||||
}
|
||||
@@ -161,17 +161,17 @@ func TestPlacement(t *testing.T) {
|
||||
}
|
||||
// The digests are what the earlier holder's code made of the same values (issue 1966: a proposal's ask binds its
|
||||
// changes' digest, and a seed its export's), whatever order the generated types keep their fields in.
|
||||
rich := seat.Proposal{ID: id, By: "operator@anchor", Why: "w", At: time.Unix(1, 0).UTC(), State: ProposalAnswering,
|
||||
rich := seat.Proposal{ID: id, By: "operator@anchor", Why: "w", At: time.Unix(1, 0).UTC(), State: client.ProposalAnswering,
|
||||
Base: map[string]int64{"g14": 4, "lab": 0, "novox": 2},
|
||||
Changes: []seat.ProposalChange{
|
||||
{Op: OpTakeOff, Node: "g14", Module: "zsh"},
|
||||
{Op: OpPlace, Node: "g14", Module: "new", Ports: map[string]int64{"80": 8080}, Bindings: map[string]seat.Provider{"pg": {Node: "novox", Module: "postgres"}}},
|
||||
{Op: OpPin, Node: "g14", Provision: "shell", Provider: &seat.Provider{Node: "g14", Module: "bash"}},
|
||||
{Op: OpHold, Node: "g14", Module: "app", Claim: "node-shell", Scope: "node", Kind: "k", Site: "s"},
|
||||
{Op: OpDecide, Node: "novox", Field: "site", Value: "attic"},
|
||||
{Op: OpDecide, Node: "novox", Field: "peer-names", Value: map[string]any{"k": "v"}},
|
||||
{Op: OpConfig, Node: "g14", Module: "app", MeshWide: map[string]any{"a": float64(1)}, Layer: map[string]any{"b": "c"}, MeshWideValues: map[string]any{"d": true}, Values: map[string]any{"e": []any{"f"}}},
|
||||
{Op: OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7", Account: "x", AgentAccountHome: "/h"}, Keys: &seat.ProposalChangeKeys{Overlay: "wg", Identity: "id", Sealing: "se"}},
|
||||
{Op: client.OpTakeOff, Node: "g14", Module: "zsh"},
|
||||
{Op: client.OpPlace, Node: "g14", Module: "new", Ports: map[string]int64{"80": 8080}, Bindings: map[string]seat.Provider{"pg": {Node: "novox", Module: "postgres"}}},
|
||||
{Op: client.OpPin, Node: "g14", Provision: "shell", Provider: &seat.Provider{Node: "g14", Module: "bash"}},
|
||||
{Op: client.OpHold, Node: "g14", Module: "app", Claim: "node-shell", Scope: "node", Kind: "k", Site: "s"},
|
||||
{Op: client.OpDecide, Node: "novox", Field: "site", Value: "attic"},
|
||||
{Op: client.OpDecide, Node: "novox", Field: "peer-names", Value: map[string]any{"k": "v"}},
|
||||
{Op: client.OpConfig, Node: "g14", Module: "app", MeshWide: map[string]any{"a": float64(1)}, Layer: map[string]any{"b": "c"}, MeshWideValues: map[string]any{"d": true}, Values: map[string]any{"e": []any{"f"}}},
|
||||
{Op: client.OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7", Account: "x", AgentAccountHome: "/h"}, Keys: &seat.ProposalChangeKeys{Overlay: "wg", Identity: "id", Sealing: "se"}},
|
||||
}}
|
||||
for name, c := range map[string]struct{ got, want string }{
|
||||
"export": {ExportDigest(current), "sha256:742f03d8790d7efa6abff76636f4a69ba2704d646233960f52ce08604b96e128"},
|
||||
@@ -190,10 +190,10 @@ func TestPlacement(t *testing.T) {
|
||||
// 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.
|
||||
// At the operator's yes it carries the tunnel key alone (the operator, 2026-11-02 01:05).
|
||||
joining := seat.ProposalChange{Op: OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7"},
|
||||
joining := seat.ProposalChange{Op: client.OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7"},
|
||||
Keys: &seat.ProposalChangeKeys{Overlay: "wg"}}
|
||||
add := seat.Proposal{ID: id, By: "ace.mesh-enrolment", Why: "lab joins", At: time.Unix(1, 0).UTC(),
|
||||
Changes: []seat.ProposalChange{joining}, Base: map[string]int64{"lab": 0}, State: ProposalAnswering,
|
||||
Changes: []seat.ProposalChange{joining}, Base: map[string]int64{"lab": 0}, State: client.ProposalAnswering,
|
||||
Warrant: &seat.Warrant{Node: "lab", Address: "10.0.0.7", Ask: "a-1", Warrant: "w-1"}}
|
||||
if raw, err := EncodeProposal(add); err != nil {
|
||||
t.Errorf("an add-node: %v", err)
|
||||
@@ -206,7 +206,7 @@ func TestPlacement(t *testing.T) {
|
||||
if laid, f := LayChanges(current, add.Changes); len(f) != 0 || laid["lab"].Decided.Address != "10.0.0.7" || len(laid["lab"].Modules) != 0 {
|
||||
t.Errorf("an add-node laid: %+v, %v", laid, f)
|
||||
}
|
||||
if _, f := LayChanges(current, []seat.ProposalChange{{Op: OpAddNode, Node: "g14", Decided: &seat.NodeDecided{Address: "10.0.0.9"},
|
||||
if _, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpAddNode, Node: "g14", Decided: &seat.NodeDecided{Address: "10.0.0.9"},
|
||||
Keys: &seat.ProposalChangeKeys{Overlay: "wg"}}}); len(f) != 1 || !strings.Contains(f[0], "placed already") {
|
||||
t.Errorf("an add-node of a placed node: %v", f)
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func TestPlacement(t *testing.T) {
|
||||
p.Changes = []seat.ProposalChange{c}
|
||||
},
|
||||
"beside another change": func(p *seat.Proposal) {
|
||||
p.Changes = append(p.Changes, seat.ProposalChange{Op: OpPlace, Node: "lab", Module: "zsh"})
|
||||
p.Changes = append(p.Changes, seat.ProposalChange{Op: client.OpPlace, Node: "lab", Module: "zsh"})
|
||||
},
|
||||
"the hub": func(p *seat.Proposal) {
|
||||
c := joining
|
||||
@@ -237,7 +237,7 @@ func TestPlacement(t *testing.T) {
|
||||
p.Changes = []seat.ProposalChange{c}
|
||||
},
|
||||
"a warrant on a move": func(p *seat.Proposal) {
|
||||
p.Changes, p.Base = []seat.ProposalChange{{Op: OpTakeOff, Node: "g14", Module: "zsh"}}, map[string]int64{"g14": 4}
|
||||
p.Changes, p.Base = []seat.ProposalChange{{Op: client.OpTakeOff, Node: "g14", Module: "zsh"}}, map[string]int64{"g14": 4}
|
||||
},
|
||||
} {
|
||||
q := add
|
||||
@@ -256,7 +256,7 @@ func TestPlacement(t *testing.T) {
|
||||
}
|
||||
// An enrolment's end takes its never-confirmed node off again: the enroller's alone, of a node running nothing.
|
||||
remove := add
|
||||
remove.Changes = []seat.ProposalChange{{Op: OpRemoveNode, Node: "lab"}}
|
||||
remove.Changes = []seat.ProposalChange{{Op: client.OpRemoveNode, Node: "lab"}}
|
||||
if _, err := EncodeProposal(remove); err != nil || AddNodeRefusal(remove, "mesh-delivery-assigner", enroller) == "" ||
|
||||
AddNodeRefusal(remove, enroller, enroller) != "" || !slices.Equal(RemovedNodes(remove), []string{"lab"}) {
|
||||
t.Errorf("a remove-node: %v, %q", err, AddNodeRefusal(remove, "mesh-delivery-assigner", enroller))
|
||||
@@ -265,7 +265,7 @@ func TestPlacement(t *testing.T) {
|
||||
if laid, f := LayChanges(joined, remove.Changes); len(f) != 0 || len(laid) != 0 {
|
||||
t.Errorf("a joining node removed: %v, %v", laid, f)
|
||||
}
|
||||
if _, f := LayChanges(current, []seat.ProposalChange{{Op: OpRemoveNode, Node: "g14"}}); len(f) != 1 ||
|
||||
if _, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpRemoveNode, Node: "g14"}}); len(f) != 1 ||
|
||||
!strings.Contains(f[0], "runs modules") {
|
||||
t.Errorf("a running node removed: %v", f)
|
||||
}
|
||||
@@ -280,7 +280,7 @@ func TestPlacement(t *testing.T) {
|
||||
"no change": func(p *seat.Proposal) { p.Changes = nil },
|
||||
"no base": func(p *seat.Proposal) { p.Base = map[string]int64{"novox": 1} },
|
||||
"no state": func(p *seat.Proposal) { p.State = "" },
|
||||
"asked, no ask": func(p *seat.Proposal) { p.State = ProposalAsked },
|
||||
"asked, no ask": func(p *seat.Proposal) { p.State = client.ProposalAsked },
|
||||
"seed mixed": func(p *seat.Proposal) { p.Seed, p.ExportDigest = true, "d" },
|
||||
"bad change": func(p *seat.Proposal) { p.Changes[0].Op = "swap" },
|
||||
"seed, no digest": func(p *seat.Proposal) { *p = seed; p.ExportDigest = "" },
|
||||
|
||||
@@ -17,37 +17,6 @@ import (
|
||||
// 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.
|
||||
|
||||
// The operations a proposal's change names (ADR 0567 §2).
|
||||
const (
|
||||
OpPlace = "place" // place a module on a node, with its host ports and bindings
|
||||
OpTakeOff = "take-off" // take a module off a node
|
||||
OpPin = "pin" // pin the provider of a provision on a node
|
||||
OpUnpin = "unpin" // remove a pin
|
||||
OpHold = "hold" // a module placed on the node holds a seat
|
||||
OpUnhold = "unhold" // it holds it no more
|
||||
OpDecide = "decide" // set a decided field of the node (NodeDecided by its JSON name), its address among them
|
||||
OpConfig = "config" // set a module's config placement or values on the node
|
||||
OpSeed = "seed" // the node's whole placement: the take-over (adopt, §7), never given by a caller
|
||||
// OpAddNode adds a joining node with its decided fields and the public halves of its keys, on the warrant of the
|
||||
// enrolment that issued its token (novox/hq ADR 0587 §7): the enroller's alone, the proposal's one change.
|
||||
OpAddNode = "add-node"
|
||||
// OpRemoveNode takes a joining node off placement again when its enrolment ends unconfirmed, revoked or expired
|
||||
// (the operator's decision of 2026-11-02 01:05): the enroller's alone, on the same warrant, the proposal's one
|
||||
// change, of a node with no module and no holding. The holder deletes its key (RemovedNodes).
|
||||
OpRemoveNode = "remove-node"
|
||||
)
|
||||
|
||||
// The states of an open proposal.
|
||||
const (
|
||||
// ProposalAnswering: its candidates are written and the nodes it must hear from have not all answered.
|
||||
ProposalAnswering = "answering"
|
||||
// ProposalFailed: its verdict failed; it stays open, for its proposer to read and withdraw, or for a node's later
|
||||
// answer to judge it again.
|
||||
ProposalFailed = "failed"
|
||||
// ProposalAsked: its verdict passed and the operator is asked to approve it.
|
||||
ProposalAsked = "asked"
|
||||
)
|
||||
|
||||
// VerdictWaits is a proposal's verdict while a node it must hear from has not answered within the bound; it passes or
|
||||
// fails as a check's verdict does (ADR 0567 §4).
|
||||
const VerdictWaits = "waits"
|
||||
@@ -65,7 +34,7 @@ func Nodes(p seat.Proposal) []string { return sortedKeys(p.Base) }
|
||||
func RemovedNodes(p seat.Proposal) []string {
|
||||
var out []string
|
||||
for _, c := range p.Changes {
|
||||
if c.Op == OpRemoveNode {
|
||||
if c.Op == client.OpRemoveNode {
|
||||
out = append(out, c.Node)
|
||||
}
|
||||
}
|
||||
@@ -84,22 +53,22 @@ func CheckChange(c seat.ProposalChange) error {
|
||||
return nil
|
||||
}
|
||||
switch c.Op {
|
||||
case OpPlace, OpTakeOff, OpConfig:
|
||||
case client.OpPlace, client.OpTakeOff, client.OpConfig:
|
||||
return needs("module", keys.CheckName("module", c.Module) == nil)
|
||||
case OpPin:
|
||||
case client.OpPin:
|
||||
return needs("provision or provider", c.Provision != "" && c.Provider != nil && c.Provider.Node != "" && c.Provider.Module != "")
|
||||
case OpUnpin:
|
||||
case client.OpUnpin:
|
||||
return needs("provision", c.Provision != "")
|
||||
case OpHold, OpUnhold:
|
||||
case client.OpHold, client.OpUnhold:
|
||||
return needs("module or claim", keys.CheckName("module", c.Module) == nil && c.Claim != "")
|
||||
case OpDecide:
|
||||
case client.OpDecide:
|
||||
return needs("field or value", c.Field != "" && c.Value != nil)
|
||||
case OpSeed:
|
||||
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 client.CheckPlacement(*c.Whole)
|
||||
case OpAddNode:
|
||||
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)
|
||||
}
|
||||
@@ -114,7 +83,7 @@ func CheckChange(c seat.ProposalChange) error {
|
||||
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 nil
|
||||
case OpRemoveNode:
|
||||
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)
|
||||
}
|
||||
@@ -157,16 +126,16 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
findings = append(findings, fmt.Sprintf("change %d: %s is not placed on %s", i+1, c.Module, c.Node))
|
||||
}
|
||||
switch c.Op {
|
||||
case OpSeed:
|
||||
case client.OpSeed:
|
||||
p = clonePlacement(*c.Whole)
|
||||
case OpAddNode:
|
||||
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{}}
|
||||
case OpRemoveNode:
|
||||
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).
|
||||
cur, has := current[c.Node]
|
||||
@@ -176,7 +145,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
findings = append(findings, fmt.Sprintf("change %d: %s runs modules or holds seats, so it is no joining node to remove", i+1, c.Node))
|
||||
}
|
||||
continue
|
||||
case OpPlace:
|
||||
case client.OpPlace:
|
||||
m := seat.PlacedModule{Module: c.Module, Ports: c.Ports, Bindings: c.Bindings}
|
||||
if at >= 0 {
|
||||
o := p.Modules[at]
|
||||
@@ -186,7 +155,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
} else {
|
||||
p.Modules = append(p.Modules, m)
|
||||
}
|
||||
case OpTakeOff:
|
||||
case client.OpTakeOff:
|
||||
if at < 0 {
|
||||
notPlaced()
|
||||
continue
|
||||
@@ -199,7 +168,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
}
|
||||
}
|
||||
p.Holds = holds
|
||||
case OpConfig:
|
||||
case client.OpConfig:
|
||||
if at < 0 {
|
||||
notPlaced()
|
||||
continue
|
||||
@@ -217,23 +186,23 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
if c.Values != nil {
|
||||
m.Values = c.Values
|
||||
}
|
||||
case OpPin:
|
||||
case client.OpPin:
|
||||
if p.Pins == nil {
|
||||
p.Pins = map[string]seat.Provider{}
|
||||
}
|
||||
p.Pins[c.Provision] = *c.Provider
|
||||
case OpUnpin:
|
||||
case client.OpUnpin:
|
||||
delete(p.Pins, c.Provision)
|
||||
if len(p.Pins) == 0 {
|
||||
p.Pins = nil
|
||||
}
|
||||
case OpHold:
|
||||
case client.OpHold:
|
||||
if at < 0 {
|
||||
notPlaced()
|
||||
continue
|
||||
}
|
||||
p.Holds = append(p.Holds, seat.Holding{Claim: c.Claim, Scope: c.Scope, Module: c.Module, Site: c.Site, Kind: c.Kind})
|
||||
case OpUnhold:
|
||||
case client.OpUnhold:
|
||||
kept := p.Holds[:0:0]
|
||||
for _, h := range p.Holds {
|
||||
if h.Module != c.Module || h.Claim != c.Claim {
|
||||
@@ -245,7 +214,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
|
||||
continue
|
||||
}
|
||||
p.Holds = kept
|
||||
case OpDecide:
|
||||
case client.OpDecide:
|
||||
d, err := decide(p.Decided, c.Field, c.Value)
|
||||
if err != nil {
|
||||
findings = append(findings, fmt.Sprintf("change %d: %v", i+1, err))
|
||||
@@ -339,7 +308,7 @@ func CheckProposal(p seat.Proposal) error {
|
||||
if err := CheckChange(c); err != nil {
|
||||
return fmt.Errorf("placement: the proposal %s, change %d: %w", p.ID, i+1, err)
|
||||
}
|
||||
if (c.Op == OpSeed) != p.Seed {
|
||||
if (c.Op == client.OpSeed) != p.Seed {
|
||||
return fmt.Errorf("placement: the proposal %s mixes the seed with other changes", p.ID)
|
||||
}
|
||||
if _, ok := p.Base[c.Node]; !ok {
|
||||
@@ -347,8 +316,8 @@ func CheckProposal(p seat.Proposal) error {
|
||||
}
|
||||
}
|
||||
switch p.State {
|
||||
case ProposalAnswering, ProposalFailed:
|
||||
case ProposalAsked:
|
||||
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)
|
||||
}
|
||||
@@ -367,7 +336,7 @@ func CheckProposal(p seat.Proposal) error {
|
||||
func checkAddNode(p seat.Proposal) error {
|
||||
adds := 0
|
||||
for _, c := range p.Changes {
|
||||
if c.Op == OpAddNode || c.Op == OpRemoveNode {
|
||||
if c.Op == client.OpAddNode || c.Op == client.OpRemoveNode {
|
||||
adds++
|
||||
}
|
||||
}
|
||||
@@ -384,7 +353,7 @@ func checkAddNode(p seat.Proposal) error {
|
||||
if w == nil || w.Ask == "" || w.Warrant == "" {
|
||||
return fmt.Errorf("placement: the add-node of %s carries no warrant of its enrolment", c.Node)
|
||||
}
|
||||
if c.Op == OpRemoveNode {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user