From 6fdf3629d3a4da6e62c0cae77cc1bd91285fd217 Mon Sep 17 00:00:00 2001 From: "fix-955-forge@novox" Date: Fri, 16 Oct 2026 22:53:22 +0200 Subject: [PATCH] Remove a machine's mesh keys and entry when its key is no longer reported (issue 955 F) --- .../cmd/forge-account-provider/service.go | 16 +++++++++++++++- .../cmd/forge-account-provider/service_test.go | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/modules/gitea/cmd/forge-account-provider/service.go b/modules/gitea/cmd/forge-account-provider/service.go index b9975ea6..2be1deec 100644 --- a/modules/gitea/cmd/forge-account-provider/service.go +++ b/modules/gitea/cmd/forge-account-provider/service.go @@ -66,6 +66,8 @@ type Service struct { keys map[string]machineKey known string // the forge's host key line, as the machines write it into known_hosts + bound provider.Bus // the bus the entries are written to (set by Provider) + making sync.Mutex // one Make at a time: they register keys, and two at once would race each other } @@ -85,10 +87,12 @@ func NewService(cfg Config) *Service { // Provider is the provider helper answering the consumers over bus. Every consumer is made again every ten minutes, // which puts back an account, its team or a key changed by hand. func (s *Service) Provider(bus provider.Bus) *provider.Provider { + vb := &valuesBus{Bus: bus, s: s, written: map[string][]byte{}} + s.bound = vb return provider.New(provider.Options{ Node: s.cfg.Node, Module: Module, Offers: map[string]provider.Offer{ProvisionName: {Credential: provider.NoCredential}}, - Bus: &valuesBus{Bus: bus, s: s, written: map[string][]byte{}}, + Bus: vb, Make: s.Make, Gone: s.Gone, Every: 10 * time.Minute, Logf: s.cfg.Log, @@ -115,6 +119,16 @@ func (s *Service) Make(ctx context.Context, c provider.Consumer) error { } have := s.keysOf(node) if len(have) == 0 { + // A key the machine no longer reports is no longer wanted (ADR 0400 ยง2): the mesh's keys go, the operator's + // stay, and the entry that served the machine goes with them. + if err := s.accounts.RemoveKeysTitled(ctx, user, forgeaccount.TitlePrefix+node+"."); err != nil { + return err + } + if s.bound != nil { + if err := s.bound.Delete(ctx, provider.BucketOf(Module), c.Entry.Key()); err != nil { + return err + } + } return errNoKey{node} } if _, err := s.hostName(); err != nil { diff --git a/modules/gitea/cmd/forge-account-provider/service_test.go b/modules/gitea/cmd/forge-account-provider/service_test.go index fdfdf949..fa5d7a8a 100644 --- a/modules/gitea/cmd/forge-account-provider/service_test.go +++ b/modules/gitea/cmd/forge-account-provider/service_test.go @@ -415,3 +415,21 @@ func (f *fakeKeys) Watch(handle func(key, op string, value []byte, current bool) return nil } func (f *fakeKeys) send(key, op string, v []byte, current bool) { f.handle(key, op, v, current) } + +func TestADeletedKeyEntryRemovesTheMeshKeyAndTheBoundEntryButNotTheOperatorsKey(t *testing.T) { + r := newRig(t) + r.bus.bindClient("g14") + r.key("g14", "nox", pubA+" nox@g14 (mesh)") + r.pass() + r.forge.mu.Lock() + r.forge.held["agent-g14"] = append(r.forge.held["agent-g14"], map[string]any{"id": int64(99), "title": "my own", "key": pubB}) + r.forge.mu.Unlock() + r.svc.KeyGone("g14.nox") + r.pass() + if got := r.forge.titles("agent-g14"); len(got) != 1 || got[0] != "my own" { + t.Fatalf("keys = %v", got) + } + if r.bus.has("g14.ssh-client.forge-account") { + t.Fatal("the entry still serves a machine whose key is gone") + } +}