Do not remove keys or entries before the machines' keys are read (issue 955 F)
mesh/merge-gate head 6f45570d4c23 merged into trunk 017b729bc95d — fail: builds gitea, ssh-client → ace, g14, novox, shanks; no bus step; 4 problem(s) the …
mesh/repo-check head 6f45570d4c23 merged into trunk 017b729bc95d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery-group group feat/agents-write-git-ssh rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
2026-10-16 22:54:31 +02:00
parent 6fdf3629d3
commit 6f45570d4c
2 changed files with 53 additions and 3 deletions
@@ -62,9 +62,10 @@ type Service struct {
// Pause waits between asks of a watch the runtime does not answer yet. // Pause waits between asks of a watch the runtime does not answer yet.
Pause func(time.Duration) Pause func(time.Duration)
mu sync.Mutex mu sync.Mutex
keys map[string]machineKey keys map[string]machineKey
known string // the forge's host key line, as the machines write it into known_hosts loaded bool // the machines' keys have been read: until then a node with none has not reported none
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) bound provider.Bus // the bus the entries are written to (set by Provider)
@@ -117,6 +118,13 @@ func (s *Service) Make(ctx context.Context, c provider.Consumer) error {
if user == "" { if user == "" {
return fmt.Errorf("%q is not a machine's name an account can be named for", node) return fmt.Errorf("%q is not a machine's name an account can be named for", node)
} }
s.mu.Lock()
loaded := s.loaded
s.mu.Unlock()
if !loaded {
// After a restart the first pass can come before the keys arrive: nothing is removed or deleted on that.
return errors.New("the machines' keys are not read yet, so nothing is made, removed or deleted")
}
have := s.keysOf(node) have := s.keysOf(node)
if len(have) == 0 { 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 // A key the machine no longer reports is no longer wanted (ADR 0400 §2): the mesh's keys go, the operator's
@@ -253,6 +261,10 @@ func (s *Service) WatchKeys(ctx context.Context, src KeySource) error {
} }
}) })
if err == nil { if err == nil {
// The watch answers once the current values are all handled (stdio KeptState.Watch).
s.mu.Lock()
s.loaded = true
s.mu.Unlock()
return nil return nil
} }
pause := time.Minute pause := time.Minute
@@ -213,6 +213,7 @@ func newRig(t *testing.T) *rig {
svc := NewService(Config{Node: "novox", HostFile: host, OriginsFile: origins, Port: 222, svc := NewService(Config{Node: "novox", HostFile: host, OriginsFile: origins, Port: 222,
Forge: forgehttp.Client{URL: srv.URL, User: "mesh-admin", PasswordFile: pw}, Runner: runner, Forge: forgehttp.Client{URL: srv.URL, User: "mesh-admin", PasswordFile: pw}, Runner: runner,
Log: func(string, ...any) {}}) Log: func(string, ...any) {}})
svc.loaded = true // the machines' keys are read; a test that means otherwise says so
return &rig{t: t, bus: newBus(), forge: forge, svc: svc, runner: runner} return &rig{t: t, bus: newBus(), forge: forge, svc: svc, runner: runner}
} }
@@ -377,11 +378,15 @@ func TestKeysWatchedRetriesUntilItAnswersAndPassesOnChanges(t *testing.T) {
r := newRig(t) r := newRig(t)
src := &fakeKeys{failFirst: 2} src := &fakeKeys{failFirst: 2}
r.svc.Pause = func(time.Duration) {} r.svc.Pause = func(time.Duration) {}
r.svc.loaded = false
passes := 0 passes := 0
r.svc.OnKeys = func(string) { passes++ } r.svc.OnKeys = func(string) { passes++ }
if err := r.svc.WatchKeys(context.Background(), src); err != nil { if err := r.svc.WatchKeys(context.Background(), src); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !r.svc.loaded {
t.Fatal("the keys are not loaded once the watch has answered")
}
if src.calls != 3 { if src.calls != 3 {
t.Fatalf("asked %d times", src.calls) t.Fatalf("asked %d times", src.calls)
} }
@@ -433,3 +438,36 @@ func TestADeletedKeyEntryRemovesTheMeshKeyAndTheBoundEntryButNotTheOperatorsKey(
t.Fatal("the entry still serves a machine whose key is gone") t.Fatal("the entry still serves a machine whose key is gone")
} }
} }
func TestAPassBeforeTheKeysAreLoadedRemovesAndDeletesNothing(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()
// A restart: the keys are not read yet, so a node with none is not a node that reported none.
r.svc.mu.Lock()
r.svc.keys = map[string]machineKey{}
r.svc.loaded = false
r.svc.mu.Unlock()
out := r.pass()
if got := strings.Join(r.forge.titles("agent-g14"), ","); got != "mesh:g14.nox,my own" {
t.Fatalf("keys = %s", got)
}
if !r.bus.has("g14.ssh-client.forge-account") {
t.Fatal("the entry was deleted before the keys were read")
}
if len(out) != 1 || !strings.Contains(out[0].Why, "not read yet") {
t.Fatalf("the wait should be said: %+v", out)
}
// Loaded, and the node reports none: now its keys go.
r.svc.mu.Lock()
r.svc.loaded = true
r.svc.mu.Unlock()
r.pass()
if got := strings.Join(r.forge.titles("agent-g14"), ","); got != "my own" || r.bus.has("g14.ssh-client.forge-account") {
t.Fatalf("keys = %s", got)
}
}