Files
cozystack/pkg/registry/utils_test.go
Simon KoudijsandClaude Opus 4.8 a28e77c9a2 fix(api): emit initial-events-end bookmark for core.cozystack.io watches
The aggregated apiserver resources in group core.cozystack.io
(TenantSecret, TenantModule, TenantNamespace) ignored the
SendInitialEvents field of ListOptions. Clients using the WatchList /
streaming-list protocol (client-go's WatchListClient feature, on by
default since v1.35) never received the required
k8s.io/initial-events-end bookmark, so their reflectors stayed stuck in
the initial-stream phase and logged "hasn't received required bookmark
event marking the end of initial events stream" every ~10s, never
reaching HasSynced.

apps.cozystack.io/Application already implemented this contract inline.
Extract that logic into a shared registry.InitialEventsBookmarker helper
(pkg/registry/utils.go) and wire it into all four translating Watch
implementations: after the initial ADDED events, the first backing
bookmark is annotated as the initial-events-end marker, with fallbacks
that emit it before the first live event and on watcher close. Application
is refactored onto the shared helper with its WorkloadMonitor
pending-event draining preserved.

Adds unit tests for the bookmarker and watch tests for each of the three
core resources asserting ADDED -> annotated Bookmark -> live event.

```release-note
[apiserver] core.cozystack.io resources (TenantSecret, TenantModule,
TenantNamespace) now emit the k8s.io/initial-events-end bookmark for
SendInitialEvents=true (WatchList) watches, so client-go informers reach
HasSynced instead of looping on the missing-bookmark warning.
```

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Simon Koudijs <simonkoudijs@gmail.com>
2026-06-02 16:17:53 +00:00

170 lines
5.6 KiB
Go

/*
Copyright 2024 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
)
// newTestBookmarkObject returns a minimal object for the bookmarker to stamp.
func newTestBookmarkObject() runtime.Object {
return &metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{APIVersion: "core.cozystack.io/v1alpha1", Kind: "TenantSecret"},
}
}
// assertInitialEventsEnd verifies ev is a Bookmark carrying
// metav1.InitialEventsAnnotationKey=true and a non-empty resourceVersion.
func assertInitialEventsEnd(t *testing.T, ev watch.Event, wantRV string) {
t.Helper()
if ev.Type != watch.Bookmark {
t.Fatalf("expected Bookmark event, got %s", ev.Type)
}
obj, ok := ev.Object.(*metav1.PartialObjectMetadata)
if !ok {
t.Fatalf("expected *PartialObjectMetadata, got %T", ev.Object)
}
if got := obj.Annotations[metav1.InitialEventsAnnotationKey]; got != "true" {
t.Fatalf("expected annotation %s=true, got %q", metav1.InitialEventsAnnotationKey, got)
}
if obj.ResourceVersion == "" {
t.Fatal("expected non-empty resourceVersion on terminating bookmark")
}
if wantRV != "" && obj.ResourceVersion != wantRV {
t.Fatalf("expected resourceVersion %q, got %q", wantRV, obj.ResourceVersion)
}
}
func TestInitialEventsBookmarker_Disabled(t *testing.T) {
b := NewInitialEventsBookmarker(false, "", newTestBookmarkObject)
if !b.Sent() {
t.Fatal("disabled bookmarker should report Sent()==true")
}
if _, ok := b.BeforeLiveEvent(watch.Modified); ok {
t.Fatal("disabled bookmarker should never need a before-live bookmark")
}
if _, ok := b.OnClose(); ok {
t.Fatal("disabled bookmarker should never need an on-close bookmark")
}
// A backing bookmark is still forwarded, but never carries the marker.
ev, end := b.OnBackingBookmark("7")
if end {
t.Fatal("disabled bookmarker should not mark a backing bookmark as initial-events-end")
}
if ev.Type != watch.Bookmark {
t.Fatalf("expected forwarded Bookmark, got %s", ev.Type)
}
if obj := ev.Object.(*metav1.PartialObjectMetadata); obj.Annotations[metav1.InitialEventsAnnotationKey] != "" {
t.Fatal("disabled bookmarker must not annotate forwarded bookmarks")
}
}
func TestInitialEventsBookmarker_BackingBookmarkTerminatesStream(t *testing.T) {
b := NewInitialEventsBookmarker(true, "", newTestBookmarkObject)
b.Observe("12")
ev, end := b.OnBackingBookmark("15")
if !end {
t.Fatal("first backing bookmark should terminate the initial stream")
}
assertInitialEventsEnd(t, ev, "15")
if !b.Sent() {
t.Fatal("bookmarker should report Sent()==true after terminating bookmark")
}
// Subsequent backing bookmarks are forwarded without the marker.
ev2, end2 := b.OnBackingBookmark("20")
if end2 {
t.Fatal("second backing bookmark must not re-terminate the stream")
}
if obj := ev2.Object.(*metav1.PartialObjectMetadata); obj.Annotations[metav1.InitialEventsAnnotationKey] != "" {
t.Fatal("subsequent bookmark must not carry the initial-events-end annotation")
}
}
func TestInitialEventsBookmarker_BeforeLiveEvent(t *testing.T) {
b := NewInitialEventsBookmarker(true, "", newTestBookmarkObject)
b.Observe("9")
// ADDED events are part of the initial stream and never trigger the marker.
if _, ok := b.BeforeLiveEvent(watch.Added); ok {
t.Fatal("ADDED event should not trigger the initial-events-end bookmark")
}
ev, ok := b.BeforeLiveEvent(watch.Modified)
if !ok {
t.Fatal("first non-ADDED event should trigger the initial-events-end bookmark")
}
assertInitialEventsEnd(t, ev, "9")
// Once sent, later live events do not re-trigger it.
if _, ok := b.BeforeLiveEvent(watch.Deleted); ok {
t.Fatal("initial-events-end bookmark should only be emitted once")
}
}
func TestInitialEventsBookmarker_OnClose(t *testing.T) {
b := NewInitialEventsBookmarker(true, "", newTestBookmarkObject)
b.Observe("42")
ev, ok := b.OnClose()
if !ok {
t.Fatal("closing before the marker was sent should flush a terminating bookmark")
}
assertInitialEventsEnd(t, ev, "42")
if _, ok := b.OnClose(); ok {
t.Fatal("on-close bookmark must not be emitted twice")
}
}
func TestInitialEventsBookmarker_ObserveIgnoresEmpty(t *testing.T) {
b := NewInitialEventsBookmarker(true, "", newTestBookmarkObject)
b.Observe("5")
b.Observe("") // must not clobber the tracked version
ev, _ := b.OnClose()
assertInitialEventsEnd(t, ev, "5")
}
func TestInitialEventsBookmarker_SeedsStartingResourceVersion(t *testing.T) {
// Watcher closes before any backing event is observed: the terminating
// bookmark falls back to the client's requested resourceVersion rather than
// an empty one.
b := NewInitialEventsBookmarker(true, "100", newTestBookmarkObject)
ev, ok := b.OnClose()
if !ok {
t.Fatal("expected a terminating bookmark on close")
}
assertInitialEventsEnd(t, ev, "100")
}
func TestInitialEventsBookmarker_ObserveOverridesStartingResourceVersion(t *testing.T) {
// Once a backing event is observed, its resourceVersion supersedes the seed.
b := NewInitialEventsBookmarker(true, "100", newTestBookmarkObject)
b.Observe("150")
ev, _ := b.OnClose()
assertInitialEventsEnd(t, ev, "150")
}