mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-26 21:17:32 +09:00
The apiserver defaults SendInitialEvents on for every watch whose resourceVersion is empty or "0", and leaves AllowWatchBookmarks off. The aggregated storages read SendInitialEvents alone as a WatchList request, so a plain `kubectl get -w` on applications, tenant modules, namespaces, secrets or security groups got a bookmark annotated as the end of initial events. The k8s.io/apiserver v0.35 watch handler lets that through. From v0.36 it treats the annotated bookmark as the end of a WatchList and calls the WatchList completion hook, which it only sets up when SendInitialEvents and AllowWatchBookmarks are both on, so the handler dereferences nil and the watch dies. The handler is the one compiled into cozystack-api, so bumping the library is what breaks it, whatever the cluster version. The marker now requires both flags, the same condition the watch cache uses, and the backing watches ask for bookmarks only when the client did, since every backing bookmark is forwarded. Security groups still forward SendInitialEvents to their backing watch so it replays existing objects as ADDED. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
191 lines
6.5 KiB
Go
191 lines
6.5 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"
|
|
|
|
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
|
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")
|
|
}
|
|
|
|
func TestInitialEventsEndBookmarkRequested(t *testing.T) {
|
|
yes, no := true, false
|
|
for _, tc := range []struct {
|
|
name string
|
|
opts metainternalversion.ListOptions
|
|
want bool
|
|
}{
|
|
{"watch list", metainternalversion.ListOptions{SendInitialEvents: &yes, AllowWatchBookmarks: true}, true},
|
|
{"plain watch defaulted by the apiserver", metainternalversion.ListOptions{SendInitialEvents: &yes}, false},
|
|
{"bookmarks without initial events", metainternalversion.ListOptions{SendInitialEvents: &no, AllowWatchBookmarks: true}, false},
|
|
{"neither", metainternalversion.ListOptions{}, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := InitialEventsEndBookmarkRequested(&tc.opts); got != tc.want {
|
|
t.Fatalf("InitialEventsEndBookmarkRequested = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|