Files
cozystack/pkg/registry/utils.go
T
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

146 lines
4.8 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 (
"strconv"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
)
// MaxResourceVersion returns the maximum resourceVersion from all items in a list.
// This is useful when the list's ResourceVersion is empty (e.g., from controller-runtime cache).
func MaxResourceVersion(list runtime.Object) (string, error) {
var max uint64
err := meta.EachListItem(list, func(obj runtime.Object) error {
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}
rvStr := accessor.GetResourceVersion()
if rvStr == "" {
return nil
}
rv, err := strconv.ParseUint(rvStr, 10, 64)
if err != nil {
return err
}
if rv > max {
max = rv
}
return nil
})
if err != nil {
return "", err
}
return strconv.FormatUint(max, 10), nil
}
// InitialEventsBookmarker implements the WatchList initial-events-end contract
// for a Watch that translates events from a backing controller-runtime watcher.
// It annotates the first backing Bookmark as the terminating marker, falling
// back to emitting one before the first live event or when the watcher closes.
//
// Construct with NewInitialEventsBookmarker. Methods are not safe for concurrent
// use; call them from a single watch goroutine.
type InitialEventsBookmarker struct {
// newBookmark returns an empty, TypeMeta-populated object of the translated
// resource type; the bookmarker fills in the resourceVersion and annotation.
newBookmark func() runtime.Object
sent bool
lastRV string
}
// NewInitialEventsBookmarker returns a bookmarker for a watch. When
// sendInitialEvents is false every method is a no-op. startingRV seeds the
// resourceVersion (the client's requested ResourceVersion) so the terminating
// bookmark carries a valid version even if the watcher closes before any
// backing event is observed.
func NewInitialEventsBookmarker(sendInitialEvents bool, startingRV string, newBookmark func() runtime.Object) *InitialEventsBookmarker {
return &InitialEventsBookmarker{
newBookmark: newBookmark,
lastRV: startingRV,
sent: !sendInitialEvents,
}
}
// Sent reports whether the initial-events-end bookmark has been emitted (or was
// never required). Callers buffering live events use it to decide when to start
// delivering them.
func (b *InitialEventsBookmarker) Sent() bool { return b.sent }
// Observe records the resourceVersion of the latest backing event so the
// bookmark carries an up-to-date version. Empty versions are ignored.
func (b *InitialEventsBookmarker) Observe(resourceVersion string) {
if resourceVersion != "" {
b.lastRV = resourceVersion
}
}
// bookmarkEvent builds a Bookmark at the tracked resourceVersion, annotated as
// the terminating marker when end is true.
func (b *InitialEventsBookmarker) bookmarkEvent(end bool) watch.Event {
obj := b.newBookmark()
if accessor, err := meta.Accessor(obj); err == nil {
accessor.SetResourceVersion(b.lastRV)
if end {
accessor.SetAnnotations(map[string]string{metav1.InitialEventsAnnotationKey: "true"})
}
}
return watch.Event{Type: watch.Bookmark, Object: obj}
}
// OnBackingBookmark forwards the backing watcher's Bookmark at rv. The first one
// after the initial events becomes the terminating marker; the returned bool
// reports whether this call emitted it, so callers can flush buffered events.
func (b *InitialEventsBookmarker) OnBackingBookmark(rv string) (event watch.Event, initialEventsEnd bool) {
b.Observe(rv)
if b.sent {
return b.bookmarkEvent(false), false
}
b.sent = true
return b.bookmarkEvent(true), true
}
// BeforeLiveEvent returns the terminating bookmark and true when a live
// (non-ADDED) event is about to be delivered and the marker is still pending.
func (b *InitialEventsBookmarker) BeforeLiveEvent(eventType watch.EventType) (event watch.Event, needed bool) {
if b.sent || eventType == watch.Added {
return watch.Event{}, false
}
b.sent = true
return b.bookmarkEvent(true), true
}
// OnClose returns the terminating bookmark and true if the backing watcher
// closed before the marker was emitted.
func (b *InitialEventsBookmarker) OnClose() (event watch.Event, needed bool) {
if b.sent {
return watch.Event{}, false
}
b.sent = true
return b.bookmarkEvent(true), true
}