mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-03 18:47:04 +09:00
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>
170 lines
5.6 KiB
Go
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")
|
|
}
|