diff --git a/tools/cache/listwatch.go b/tools/cache/listwatch.go index f5b04a19b..2c4065f01 100644 --- a/tools/cache/listwatch.go +++ b/tools/cache/listwatch.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" restclient "k8s.io/client-go/rest" + "k8s.io/client-go/util/watchlist" ) // Lister is any object that knows how to perform an initial list. @@ -130,6 +131,35 @@ type listerWatcherWrapper struct { ListerWithContext WatcherWithContext } +type listWatcherWithWatchListSemanticsWrapper struct { + *ListWatch + + // unsupportedWatchListSemantics indicates whether a client explicitly does NOT support + // WatchList semantics. + // + // Over the years, unit tests in kube have been written in many different ways. + // After enabling the WatchListClient feature by default, existing tests started failing. + // To avoid breaking lots of existing client-go users after upgrade, + // we introduced this field as an opt-in. + // + // When true, the reflector disables WatchList even if the feature gate is enabled. + unsupportedWatchListSemantics bool +} + +func (lw *listWatcherWithWatchListSemanticsWrapper) IsWatchListSemanticsUnSupported() bool { + return lw.unsupportedWatchListSemantics +} + +// ToListWatcherWithWatchListSemantics returns a ListerWatcher +// that knows whether the provided client explicitly +// does NOT support the WatchList semantics. This allows Reflectors +// to adapt their behavior based on client capabilities. +func ToListWatcherWithWatchListSemantics(lw *ListWatch, client any) ListerWatcher { + return &listWatcherWithWatchListSemanticsWrapper{ + lw, + watchlist.DoesClientNotSupportWatchListSemantics(client), + } +} // ListFunc knows how to list resources // diff --git a/tools/cache/listwatch_test.go b/tools/cache/listwatch_test.go new file mode 100644 index 000000000..70bee04c0 --- /dev/null +++ b/tools/cache/listwatch_test.go @@ -0,0 +1,65 @@ +/* +Copyright 2025 The Kubernetes 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 cache + +import ( + "testing" + + "k8s.io/client-go/util/watchlist" +) + +type fakeWatchListClient struct { + unSupportedWatchListSemantics bool +} + +func (f fakeWatchListClient) IsWatchListSemanticsUnSupported() bool { + return f.unSupportedWatchListSemantics +} + +func TestToListWatcherWithWatchListSemantics(t *testing.T) { + scenarios := []struct { + name string + client any + expectUnSupportedWatchListSemantics bool + }{ + { + name: "client which doesn't implement the interface supports WatchList semantics", + client: nil, + expectUnSupportedWatchListSemantics: false, + }, + { + name: "client does not support WatchList semantics", + client: fakeWatchListClient{unSupportedWatchListSemantics: true}, + expectUnSupportedWatchListSemantics: true, + }, + { + name: "client supports WatchList semantics", + client: fakeWatchListClient{unSupportedWatchListSemantics: false}, + expectUnSupportedWatchListSemantics: false, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + target := ToListWatcherWithWatchListSemantics(&ListWatch{}, scenario.client) + + if got := watchlist.DoesClientNotSupportWatchListSemantics(target); got != scenario.expectUnSupportedWatchListSemantics { + t.Fatalf("DoesClientNotSupportWatchListSemantics returned: %v, want: %v", got, scenario.expectUnSupportedWatchListSemantics) + } + }) + } +}