Skip to content

Commit 813d3f3

Browse files
authored
Merge pull request kubernetes#125440 from p0lyn0mial/upstream-client-go-watchlist-can-use-watchlist-for-list-rq
client-go/util/watchlist: intro CanUseWatchListForListRequest(
2 parents 6c556cb + 38fae9b commit 813d3f3

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package watchlist
18+
19+
import (
20+
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
21+
metainternalversionvalidation "k8s.io/apimachinery/pkg/apis/meta/internalversion/validation"
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
25+
clientfeatures "k8s.io/client-go/features"
26+
"k8s.io/utils/ptr"
27+
)
28+
29+
var scheme = runtime.NewScheme()
30+
31+
func init() {
32+
utilruntime.Must(metainternalversion.AddToScheme(scheme))
33+
}
34+
35+
// PrepareWatchListOptionsFromListOptions creates a new ListOptions
36+
// that can be used for a watch-list request from the given listOptions.
37+
//
38+
// This function also determines if the given listOptions can be used to form a watch-list request,
39+
// which would result in streaming semantically equivalent data from the server.
40+
func PrepareWatchListOptionsFromListOptions(listOptions metav1.ListOptions) (metav1.ListOptions, bool, error) {
41+
if !clientfeatures.FeatureGates().Enabled(clientfeatures.WatchListClient) {
42+
return metav1.ListOptions{}, false, nil
43+
}
44+
45+
internalListOptions := &metainternalversion.ListOptions{}
46+
if err := scheme.Convert(&listOptions, internalListOptions, nil); err != nil {
47+
return metav1.ListOptions{}, false, err
48+
}
49+
if errs := metainternalversionvalidation.ValidateListOptions(internalListOptions, true); len(errs) > 0 {
50+
return metav1.ListOptions{}, false, nil
51+
}
52+
53+
watchListOptions := listOptions
54+
// this is our legacy case, the cache ignores LIMIT for
55+
// ResourceVersion == 0 and RVM=unset|NotOlderThan
56+
if listOptions.Limit > 0 && listOptions.ResourceVersion != "0" {
57+
return metav1.ListOptions{}, false, nil
58+
}
59+
watchListOptions.Limit = 0
60+
61+
// to ensure that we can create a watch-list request that returns
62+
// semantically equivalent data for the given listOptions,
63+
// we need to validate that the RVM for the list is supported by watch-list requests.
64+
if listOptions.ResourceVersionMatch == metav1.ResourceVersionMatchExact {
65+
return metav1.ListOptions{}, false, nil
66+
}
67+
watchListOptions.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan
68+
69+
watchListOptions.Watch = true
70+
watchListOptions.AllowWatchBookmarks = true
71+
watchListOptions.SendInitialEvents = ptr.To(true)
72+
73+
internalWatchListOptions := &metainternalversion.ListOptions{}
74+
if err := scheme.Convert(&watchListOptions, internalWatchListOptions, nil); err != nil {
75+
return metav1.ListOptions{}, false, err
76+
}
77+
if errs := metainternalversionvalidation.ValidateListOptions(internalWatchListOptions, true); len(errs) > 0 {
78+
return metav1.ListOptions{}, false, nil
79+
}
80+
81+
return watchListOptions, true, nil
82+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package watchlist
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
clientfeatures "k8s.io/client-go/features"
26+
clientfeaturestesting "k8s.io/client-go/features/testing"
27+
"k8s.io/utils/ptr"
28+
)
29+
30+
// TestPrepareWatchListOptionsFromListOptions test the following cases:
31+
//
32+
// +--------------------------+-----------------+---------+-----------------+
33+
// | ResourceVersionMatch | ResourceVersion | Limit | Continuation |
34+
// +--------------------------+-----------------+---------+-----------------+
35+
// | unset/NotOlderThan/Exact | unset/0/100 | unset/4 | unset/FakeToken |
36+
// +--------------------------+-----------------+---------+-----------------+
37+
func TestPrepareWatchListOptionsFromListOptions(t *testing.T) {
38+
scenarios := []struct {
39+
name string
40+
listOptions metav1.ListOptions
41+
enableWatchListFG bool
42+
43+
expectToPrepareWatchListOptions bool
44+
expectedWatchListOptions metav1.ListOptions
45+
}{
46+
47+
{
48+
name: "can't enable watch list for: WatchListClient=off, RVM=unset, RV=unset, Limit=unset, Continuation=unset",
49+
enableWatchListFG: false,
50+
expectToPrepareWatchListOptions: false,
51+
},
52+
// +----------------------+-----------------+-------+--------------+
53+
// | ResourceVersionMatch | ResourceVersion | Limit | Continuation |
54+
// +----------------------+-----------------+-------+--------------+
55+
// | unset | unset | unset | unset |
56+
// | unset | 0 | unset | unset |
57+
// | unset | 100 | unset | unset |
58+
// | unset | 0 | 4 | unset |
59+
// | unset | 0 | unset | FakeToken |
60+
// +----------------------+-----------------+-------+--------------+
61+
{
62+
name: "can enable watch list for: RVM=unset, RV=unset, Limit=unset, Continuation=unset",
63+
enableWatchListFG: true,
64+
expectToPrepareWatchListOptions: true,
65+
expectedWatchListOptions: expectedWatchListOptionsFor(""),
66+
},
67+
{
68+
name: "can enable watch list for: RVM=unset, RV=0, Limit=unset, Continuation=unset",
69+
listOptions: metav1.ListOptions{ResourceVersion: "0"},
70+
enableWatchListFG: true,
71+
expectToPrepareWatchListOptions: true,
72+
expectedWatchListOptions: expectedWatchListOptionsFor("0"),
73+
},
74+
{
75+
name: "can enable watch list for: RVM=unset, RV=100, Limit=unset, Continuation=unset",
76+
listOptions: metav1.ListOptions{ResourceVersion: "100"},
77+
enableWatchListFG: true,
78+
expectToPrepareWatchListOptions: true,
79+
expectedWatchListOptions: expectedWatchListOptionsFor("100"),
80+
},
81+
{
82+
name: "legacy: can enable watch list for: RVM=unset, RV=0, Limit=4, Continuation=unset",
83+
listOptions: metav1.ListOptions{ResourceVersion: "0", Limit: 4},
84+
enableWatchListFG: true,
85+
expectToPrepareWatchListOptions: true,
86+
expectedWatchListOptions: expectedWatchListOptionsFor("0"),
87+
},
88+
{
89+
name: "can't enable watch list for: RVM=unset, RV=0, Limit=unset, Continuation=FakeToken",
90+
listOptions: metav1.ListOptions{ResourceVersion: "0", Continue: "FakeToken"},
91+
enableWatchListFG: true,
92+
expectToPrepareWatchListOptions: false,
93+
},
94+
// +----------------------+-----------------+-------+--------------+
95+
// | ResourceVersionMatch | ResourceVersion | Limit | Continuation |
96+
// +----------------------+-----------------+-------+--------------+
97+
// | NotOlderThan | unset | unset | unset |
98+
// | NotOlderThan | 0 | unset | unset |
99+
// | NotOlderThan | 100 | unset | unset |
100+
// | NotOlderThan | 0 | 4 | unset |
101+
// | NotOlderThan | 0 | unset | FakeToken |
102+
// +----------------------+-----------------+-------+--------------+
103+
{
104+
name: "can't enable watch list for: RVM=NotOlderThan, RV=unset, Limit=unset, Continuation=unset",
105+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan},
106+
enableWatchListFG: true,
107+
expectToPrepareWatchListOptions: false,
108+
},
109+
{
110+
name: "can enable watch list for: RVM=NotOlderThan, RV=0, Limit=unset, Continuation=unset",
111+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "0"},
112+
enableWatchListFG: true,
113+
expectToPrepareWatchListOptions: true,
114+
expectedWatchListOptions: expectedWatchListOptionsFor("0"),
115+
},
116+
{
117+
name: "can enable watch list for: RVM=NotOlderThan, RV=100, Limit=unset, Continuation=unset",
118+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "100"},
119+
enableWatchListFG: true,
120+
expectToPrepareWatchListOptions: true,
121+
expectedWatchListOptions: expectedWatchListOptionsFor("100"),
122+
},
123+
{
124+
name: "legacy: can enable watch list for: RVM=NotOlderThan, RV=0, Limit=4, Continuation=unset",
125+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "0", Limit: 4},
126+
enableWatchListFG: true,
127+
expectToPrepareWatchListOptions: true,
128+
expectedWatchListOptions: expectedWatchListOptionsFor("0"),
129+
},
130+
{
131+
name: "can't enable watch list for: RVM=NotOlderThan, RV=0, Limit=unset, Continuation=FakeToken",
132+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, ResourceVersion: "0", Continue: "FakeToken"},
133+
enableWatchListFG: true,
134+
expectToPrepareWatchListOptions: false,
135+
},
136+
137+
// +----------------------+-----------------+-------+--------------+
138+
// | ResourceVersionMatch | ResourceVersion | Limit | Continuation |
139+
// +----------------------+-----------------+-------+--------------+
140+
// | Exact | unset | unset | unset |
141+
// | Exact | 0 | unset | unset |
142+
// | Exact | 100 | unset | unset |
143+
// | Exact | 0 | 4 | unset |
144+
// | Exact | 0 | unset | FakeToken |
145+
// +----------------------+-----------------+-------+--------------+
146+
{
147+
name: "can't enable watch list for: RVM=Exact, RV=unset, Limit=unset, Continuation=unset",
148+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact},
149+
enableWatchListFG: true,
150+
expectToPrepareWatchListOptions: false,
151+
},
152+
{
153+
name: "can enable watch list for: RVM=Exact, RV=0, Limit=unset, Continuation=unset",
154+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "0"},
155+
enableWatchListFG: true,
156+
expectToPrepareWatchListOptions: false,
157+
},
158+
{
159+
name: "can enable watch list for: RVM=Exact, RV=100, Limit=unset, Continuation=unset",
160+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "100"},
161+
enableWatchListFG: true,
162+
expectToPrepareWatchListOptions: false,
163+
},
164+
{
165+
name: "can't enable watch list for: RVM=Exact, RV=0, Limit=4, Continuation=unset",
166+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "0", Limit: 4},
167+
enableWatchListFG: true,
168+
expectToPrepareWatchListOptions: false,
169+
},
170+
{
171+
name: "can't enable watch list for: RVM=Exact, RV=0, Limit=unset, Continuation=FakeToken",
172+
listOptions: metav1.ListOptions{ResourceVersionMatch: metav1.ResourceVersionMatchExact, ResourceVersion: "0", Continue: "FakeToken"},
173+
enableWatchListFG: true,
174+
expectToPrepareWatchListOptions: false,
175+
},
176+
}
177+
for _, scenario := range scenarios {
178+
t.Run(scenario.name, func(t *testing.T) {
179+
clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, scenario.enableWatchListFG)
180+
181+
watchListOptions, hasWatchListOptionsPrepared, err := PrepareWatchListOptionsFromListOptions(scenario.listOptions)
182+
183+
require.NoError(t, err)
184+
require.Equal(t, scenario.expectToPrepareWatchListOptions, hasWatchListOptionsPrepared)
185+
require.Equal(t, scenario.expectedWatchListOptions, watchListOptions)
186+
})
187+
}
188+
}
189+
190+
func expectedWatchListOptionsFor(rv string) metav1.ListOptions {
191+
var watchListOptions metav1.ListOptions
192+
193+
watchListOptions.ResourceVersion = rv
194+
watchListOptions.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan
195+
watchListOptions.Watch = true
196+
watchListOptions.AllowWatchBookmarks = true
197+
watchListOptions.SendInitialEvents = ptr.To(true)
198+
199+
return watchListOptions
200+
}

0 commit comments

Comments
 (0)