An apply of the bus module waits when the list on the machine cannot be read while the grants part's is followed, so the file is never removed as an orphan (review of the switch, issue 1945)
mesh/delivery merged: source.main moved to novox/mesh-host@7f04d1204765
mesh/delivery-group group feat/1945-user-list-switch merged: every member is merged
mesh/check being carried to e88ddab24170 on 7f04d1204765 | novox/mesh-host#145 in group feat/1945-user-list-switch

This commit is contained in:
2026-10-26 12:54:06 +01:00
parent 06f70e11ee
commit e88ddab241
3 changed files with 33 additions and 8 deletions
+7 -1
View File
@@ -451,7 +451,13 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, sess
actx, done := applyContext(session.WithLink(ctx)) actx, done := applyContext(session.WithLink(ctx))
// While the grants part's list is followed, the controller's list in the bus module's composition is not // While the grants part's list is followed, the controller's list in the bus module's composition is not
// written: the apply keeps the list on the machine (ADR 0547 clause 8, never both). // written: the apply keeps the list on the machine (ADR 0547 clause 8, never both).
res := applyComposition(actx, opts, users.Hold(c), others, sched, say) held, herr := users.Hold(c)
var res agent.Result
if herr != nil {
res = failedWith(herr.Error())
} else {
res = applyComposition(actx, opts, held, others, sched, say)
}
done() done()
// An apply of the bus module may write an older list than the newest composition holds: looked at again. // An apply of the bus module may write an older list than the newest composition holds: looked at again.
if module, _ := users.State(); module != "" && module == c.Module { if module, _ := users.State(); module != "" && module == c.Module {
+12 -6
View File
@@ -517,18 +517,24 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
// Hold is c as the apply may write it (C3 of the switch's review): while the grants part's list is followed, the bus // Hold is c as the apply may write it (C3 of the switch's review): while the grants part's list is followed, the bus
// module's composition from the controller carries the controller's list, so its user-list resource holds the list on the // module's composition from the controller carries the controller's list, so its user-list resource holds the list on the
// machine instead (made 0600), or is left out when none can be read. Any other composition, or the controller's list // machine instead (made 0600), or is left out when the machine holds none. A list on the machine that cannot be read
// followed, is c unchanged. // is an error: left out, the apply would remove the file as an orphan and the bus would refuse every connection at its
func (f *Follower) Hold(c delivery.Composed) delivery.Composed { // next reload, so the bus module's apply waits instead. Any other composition, or the controller's list followed, is c
// unchanged.
func (f *Follower) Hold(c delivery.Composed) (delivery.Composed, error) {
if f.Following() != delivery.FollowGrants { if f.Following() != delivery.FollowGrants {
return c return c, nil
} }
out := c out := c
out.Resources = nil out.Resources = nil
for _, raw := range c.Resources { for _, raw := range c.Resources {
if r, _, _, ok := userList([]json.RawMessage{raw}); ok { if r, _, _, ok := userList([]json.RawMessage{raw}); ok {
var held json.RawMessage var held json.RawMessage
if body, err := f.env.OnDisk(r); err == nil && body != nil { body, err := f.env.OnDisk(r)
if err != nil {
return c, fmt.Errorf("the user list on the machine could not be read, so the apply waits and the file stays: %w", err)
}
if body != nil {
var m map[string]any var m map[string]any
if json.Unmarshal(r, &m) == nil { if json.Unmarshal(r, &m) == nil {
delete(m, "digest") delete(m, "digest")
@@ -544,7 +550,7 @@ func (f *Follower) Hold(c delivery.Composed) delivery.Composed {
} }
out.Resources = append(out.Resources, raw) out.Resources = append(out.Resources, raw)
} }
return out return out, nil
} }
// mode0600 is a user-list resource made mode 0600, whatever its composition said (ADR 0547 clause 7). // mode0600 is a user-list resource made mode 0600, whatever its composition said (ADR 0547 clause 7).
+14 -1
View File
@@ -444,11 +444,24 @@ func TestTheBusNodeWritesTheNewestSignedUserListAlone(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
held := fl.Hold(cmp) held, err := fl.Hold(cmp)
if err != nil {
t.Fatal(err)
}
_, _, _, hasList := userList(held.Resources) _, _, _, hasList := userList(held.Resources)
if want := c.wantFollows == delivery.FollowController; hasList != want || len(held.Resources) < 1 { if want := c.wantFollows == delivery.FollowController; hasList != want || len(held.Resources) < 1 {
t.Fatalf("an apply while following %s holds the controller's list: %v", c.wantFollows, hasList) t.Fatalf("an apply while following %s holds the controller's list: %v", c.wantFollows, hasList)
} }
// A list on the machine that cannot be read stops the apply; it is never left out to be removed.
if c.wantFollows == delivery.FollowGrants {
readErr := New(Env{Node: node, Composed: f, OnDisk: func(json.RawMessage) ([]byte, error) {
return nil, errors.New("permission denied")
}})
readErr.grants = &delivery.GrantsUserList{Follow: delivery.FollowGrants}
if _, err := readErr.Hold(cmp); err == nil || !strings.Contains(err.Error(), "could not be read") {
t.Fatalf("an unreadable list on the machine was held as %v", err)
}
}
} }
if c.wantSource != "" && (st.Source != c.wantSource || st.Key != nodeKey.PublicKey || if c.wantSource != "" && (st.Source != c.wantSource || st.Key != nodeKey.PublicKey ||
(c.wantFollows != "" && st.Follows != c.wantFollows)) { (c.wantFollows != "" && st.Follows != c.wantFollows)) {