From 9a760efea1165bcc5797da47491f6b9843e8af8f Mon Sep 17 00:00:00 2001 From: Lukasz Szaszkiewicz Date: Mon, 10 Jun 2024 18:03:47 +0200 Subject: [PATCH] client-go/util/watchlist: intro CanUseWatchListForListRequest Kubernetes-commit: 38fae9b799393f6fe17d07fb8148f05b1110859b --- util/watchlist/watch_list.go | 82 ++++++++++++ util/watchlist/watch_list_test.go | 200 ++++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 util/watchlist/watch_list.go create mode 100644 util/watchlist/watch_list_test.go diff --git a/util/watchlist/watch_list.go b/util/watchlist/watch_list.go new file mode 100644 index 00000000..84106458 --- /dev/null +++ b/util/watchlist/watch_list.go @@ -0,0 +1,82 @@ +/* +Copyright 2024 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 watchlist + +import ( + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metainternalversionvalidation "k8s.io/apimachinery/pkg/apis/meta/internalversion/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientfeatures "k8s.io/client-go/features" + "k8s.io/utils/ptr" +) + +var scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(metainternalversion.AddToScheme(scheme)) +} + +// PrepareWatchListOptionsFromListOptions creates a new ListOptions +// that can be used for a watch-list request from the given listOptions. +// +// This function also determines if the given listOptions can be used to form a watch-list request, +// which would result in streaming semantically equivalent data from the server. +func PrepareWatchListOptionsFromListOptions(listOptions metav1.ListOptions) (metav1.ListOptions, bool, error) { + if !clientfeatures.FeatureGates().Enabled(clientfeatures.WatchListClient) { + return metav1.ListOptions{}, false, nil + } + + internalListOptions := &metainternalversion.ListOptions{} + if err := scheme.Convert(&listOptions, internalListOptions, nil); err != nil { + return metav1.ListOptions{}, false, err + } + if errs := metainternalversionvalidation.ValidateListOptions(internalListOptions, true); len(errs) > 0 { + return metav1.ListOptions{}, false, nil + } + + watchListOptions := listOptions + // this is our legacy case, the cache ignores LIMIT for + // ResourceVersion == 0 and RVM=unset|NotOlderThan + if listOptions.Limit > 0 && listOptions.ResourceVersion != "0" { + return metav1.ListOptions{}, false, nil + } + watchListOptions.Limit = 0 + + // to ensure that we can create a watch-list request that returns + // semantically equivalent data for the given listOptions, + // we need to validate that the RVM for the list is supported by watch-list requests. + if listOptions.ResourceVersionMatch == metav1.ResourceVersionMatchExact { + return metav1.ListOptions{}, false, nil + } + watchListOptions.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan + + watchListOptions.Watch = true + watchListOptions.AllowWatchBookmarks = true + watchListOptions.SendInitialEvents = ptr.To(true) + + internalWatchListOptions := &metainternalversion.ListOptions{} + if err := scheme.Convert(&watchListOptions, internalWatchListOptions, nil); err != nil { + return metav1.ListOptions{}, false, err + } + if errs := metainternalversionvalidation.ValidateListOptions(internalWatchListOptions, true); len(errs) > 0 { + return metav1.ListOptions{}, false, nil + } + + return watchListOptions, true, nil +} diff --git a/util/watchlist/watch_list_test.go b/util/watchlist/watch_list_test.go new file mode 100644 index 00000000..b3cc10e1 --- /dev/null +++ b/util/watchlist/watch_list_test.go @@ -0,0 +1,200 @@ +/* +Copyright 2024 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 watchlist + +import ( + "testing" + + "github.com/stretchr/testify/require" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientfeatures "k8s.io/client-go/features" + clientfeaturestesting "k8s.io/client-go/features/testing" + "k8s.io/utils/ptr" +) + +// TestPrepareWatchListOptionsFromListOptions test the following cases: +// +// +--------------------------+-----------------+---------+-----------------+ +// | ResourceVersionMatch | ResourceVersion | Limit | Continuation | +// +--------------------------+-----------------+---------+-----------------+ +// | unset/NotOlderThan/Exact | unset/0/100 | unset/4 | unset/FakeToken | +// +--------------------------+-----------------+---------+-----------------+ +func TestPrepareWatchListOptionsFromListOptions(t *testing.T) { + scenarios := []struct { + name string + listOptions metav1.ListOptions + enableWatchListFG bool + + expectToPrepareWatchListOptions bool + expectedWatchListOptions metav1.ListOptions + }{ + + { + name: "can't enable watch list for: WatchListClient=off, RVM=unset, RV=unset, Limit=unset, Continuation=unset", + enableWatchListFG: false, + expectToPrepareWatchListOptions: false, + }, + // +----------------------+-----------------+-------+--------------+ + // | ResourceVersionMatch | ResourceVersion | Limit | Continuation | + // +----------------------+-----------------+-------+--------------+ + // | unset | unset | unset | unset | + // | unset | 0 | unset | unset | + // | unset | 100 | unset | unset | + // | unset | 0 | 4 | unset | + // | unset | 0 | unset | FakeToken | + // +----------------------+-----------------+-------+--------------+ + { + name: "can enable watch list for: RVM=unset, RV=unset, Limit=unset, Continuation=unset", + enableWatchListFG: true, + expectToPrepareWatchListOptions: true, + expectedWatchListOptions: expectedWatchListOptionsFor(""), + }, + { + name: "can enable watch list for: RVM=unset, RV=0, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersion: "0"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: true, + expectedWatchListOptions: expectedWatchListOptionsFor("0"), + }, + { + name: "can enable watch list for: RVM=unset, RV=100, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersion: "100"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: true, + expectedWatchListOptions: expectedWatchListOptionsFor("100"), + }, + { + name: "legacy: can enable watch list for: RVM=unset, RV=0, Limit=4, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersion: "0", Limit: 4}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: true, + expectedWatchListOptions: expectedWatchListOptionsFor("0"), + }, + { + name: "can't enable watch list for: RVM=unset, RV=0, Limit=unset, Continuation=FakeToken", + listOptions: metav1.ListOptions{ResourceVersion: "0", Continue: "FakeToken"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + // +----------------------+-----------------+-------+--------------+ + // | ResourceVersionMatch | ResourceVersion | Limit | Continuation | + // +----------------------+-----------------+-------+--------------+ + // | NotOlderThan | unset | unset | unset | + // | NotOlderThan | 0 | unset | unset | + // | NotOlderThan | 100 | unset | unset | + // | NotOlderThan | 0 | 4 | unset | + // | NotOlderThan | 0 | unset | FakeToken | + // +----------------------+-----------------+-------+--------------+ + { + name: "can't enable watch list for: RVM=NotOlderThan, RV=unset, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + { + name: "can enable watch list for: RVM=NotOlderThan, RV=0, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "0"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: true, + expectedWatchListOptions: expectedWatchListOptionsFor("0"), + }, + { + name: "can enable watch list for: RVM=NotOlderThan, RV=100, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "100"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: true, + expectedWatchListOptions: expectedWatchListOptionsFor("100"), + }, + { + name: "legacy: can enable watch list for: RVM=NotOlderThan, RV=0, Limit=4, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "0", Limit: 4}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: true, + expectedWatchListOptions: expectedWatchListOptionsFor("0"), + }, + { + name: "can't enable watch list for: RVM=NotOlderThan, RV=0, Limit=unset, Continuation=FakeToken", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "0", Continue: "FakeToken"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + + // +----------------------+-----------------+-------+--------------+ + // | ResourceVersionMatch | ResourceVersion | Limit | Continuation | + // +----------------------+-----------------+-------+--------------+ + // | Exact | unset | unset | unset | + // | Exact | 0 | unset | unset | + // | Exact | 100 | unset | unset | + // | Exact | 0 | 4 | unset | + // | Exact | 0 | unset | FakeToken | + // +----------------------+-----------------+-------+--------------+ + { + name: "can't enable watch list for: RVM=Exact, RV=unset, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + { + name: "can enable watch list for: RVM=Exact, RV=0, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "0"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + { + name: "can enable watch list for: RVM=Exact, RV=100, Limit=unset, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "100"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + { + name: "can't enable watch list for: RVM=Exact, RV=0, Limit=4, Continuation=unset", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "0", Limit: 4}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + { + name: "can't enable watch list for: RVM=Exact, RV=0, Limit=unset, Continuation=FakeToken", + listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "0", Continue: "FakeToken"}, + enableWatchListFG: true, + expectToPrepareWatchListOptions: false, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, scenario.enableWatchListFG) + + watchListOptions, hasWatchListOptionsPrepared, err := PrepareWatchListOptionsFromListOptions(scenario.listOptions) + + require.NoError(t, err) + require.Equal(t, scenario.expectToPrepareWatchListOptions, hasWatchListOptionsPrepared) + require.Equal(t, scenario.expectedWatchListOptions, watchListOptions) + }) + } +} + +func expectedWatchListOptionsFor(rv string) metav1.ListOptions { + var watchListOptions metav1.ListOptions + + watchListOptions.ResourceVersion = rv + watchListOptions.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan + watchListOptions.Watch = true + watchListOptions.AllowWatchBookmarks = true + watchListOptions.SendInitialEvents = ptr.To(true) + + return watchListOptions +}