The licence manager binds a node reporting an account it already holds

Found going live: the other nodes report the adopted account with older
logins, which are never candidates, and the first binding was only made at
adoption — so a node reporting afterwards was never bound (ADR 0206 §7).
This commit is contained in:
jochen
2026-10-04 12:34:39 +02:00
parent a72df57214
commit af63f12129
2 changed files with 59 additions and 0 deletions
@@ -198,6 +198,12 @@ func (m *Manager) Consider(ctx context.Context, reports []Holdings) ([]string, e
accounts = append(accounts, a)
}
sort.Strings(accounts)
// A node reporting an account the manager already holds, and bound to nothing, is bound to it
// (ADR 0206 §7) — whenever its report arrives, not only when the licence is adopted: a node whose own
// login is older than the one adopted is never a candidate, and would otherwise never be bound.
if err := m.bindReporters(ctx, reports); err != nil {
return nil, err
}
var adopted []string
for _, account := range accounts {
list := byAccount[account]
@@ -225,6 +231,38 @@ func (m *Manager) Consider(ctx context.Context, reports []Holdings) ([]string, e
return adopted, nil
}
// bindReporters binds each reporting node that is bound to nothing to the licence its account already has.
func (m *Manager) bindReporters(ctx context.Context, reports []Holdings) error {
for _, rep := range reports {
if rep.Identity == nil || rep.Identity.AccountUUID == "" {
continue
}
if b, err := m.Store.Binding(ctx, rep.Node); err != nil || b != nil {
if err != nil {
return err
}
continue
}
l, err := m.Store.LicenceForAccount(ctx, rep.Identity.AccountUUID)
if err != nil {
return err
}
if l == nil {
continue
}
b, err := m.Store.Bind(ctx, rep.Node, l.Name)
if err != nil {
return err
}
_ = m.Store.Audit(ctx, "bound", map[string]any{"consumer": rep.Node, "licence": l.Name, "by": "its account's report"})
if err := m.PutBinding(ctx, rep.Node, BindingState{Licence: l.Name, Kind: l.Kind, Generation: b.Generation}); err != nil {
return err
}
m.Log("bound %s to %s, the licence its account already has", rep.Node, l.Name)
}
return nil
}
func (m *Manager) adoptOne(ctx context.Context, c Candidate, reports []Holdings) (string, error) {
answer, err := m.AskGrant(ctx, c.Node, m.Keys.PublicKey)
if err != nil {
@@ -336,3 +336,24 @@ func TestAReportIsReadAsTheAgentModuleWritesIt(t *testing.T) {
t.Fatalf("the report's time does not parse: %v", err)
}
}
// A node whose report arrives after its account was adopted — with an older login, so never a candidate —
// is still bound to that account's licence, once.
func TestANodeReportingAnAdoptedAccountLaterIsBoundToIt(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("novox", "rt-new", true, t0)})
late := mm.login("laptop", "rt-older", true, t0.Add(-24*time.Hour))
_, _ = mm.m.Consider(ctx, []Holdings{late})
if mm.state["laptop"].Licence != licence1 {
t.Fatalf("a node reporting the adopted account later was not bound: %v", mm.state)
}
if strings.Contains(strings.Join(mm.vendor.exchanged, ","), "rt-older") {
t.Fatal("the older login was exchanged")
}
g := mm.state["laptop"].Generation
_, _ = mm.m.Consider(ctx, []Holdings{late})
if mm.state["laptop"].Generation != g {
t.Fatal("a node already bound was bound again")
}
}