k8s.io/client-go@v0.31.1/util/consistencydetector/data_consistency_detector_test.go (about) 1 /* 2 Copyright 2023 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 consistencydetector 18 19 import ( 20 "context" 21 "fmt" 22 "testing" 23 24 "github.com/stretchr/testify/require" 25 26 v1 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/types" 31 "k8s.io/utils/ptr" 32 ) 33 34 func TestDataConsistencyChecker(t *testing.T) { 35 scenarios := []struct { 36 name string 37 38 lastSyncedResourceVersion string 39 listResponse runtime.Object 40 retrievedItems []runtime.Object 41 requestOptions metav1.ListOptions 42 43 expectedRequestOptions []metav1.ListOptions 44 expectedListRequests int 45 expectPanic bool 46 }{ 47 { 48 name: "data consistency check won't panic when data is consistent", 49 lastSyncedResourceVersion: "2", 50 listResponse: &v1.PodList{ 51 ListMeta: metav1.ListMeta{ResourceVersion: "2"}, 52 Items: []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2")}, 53 }, 54 requestOptions: metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))}, 55 retrievedItems: []runtime.Object{makePod("p1", "1"), makePod("p2", "2")}, 56 expectedListRequests: 1, 57 expectedRequestOptions: []metav1.ListOptions{ 58 { 59 ResourceVersion: "2", 60 ResourceVersionMatch: metav1.ResourceVersionMatchExact, 61 TimeoutSeconds: ptr.To(int64(39)), 62 }, 63 }, 64 }, 65 66 { 67 name: "data consistency check works with unstructured data (dynamic client)", 68 lastSyncedResourceVersion: "2", 69 listResponse: &unstructured.UnstructuredList{ 70 Object: map[string]interface{}{ 71 "apiVersion": "vTest", 72 "kind": "rTestList", 73 }, 74 Items: []unstructured.Unstructured{ 75 *makeUnstructuredObject("vTest", "rTest", "item1"), 76 *makeUnstructuredObject("vTest", "rTest", "item2"), 77 }, 78 }, 79 requestOptions: metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))}, 80 retrievedItems: []runtime.Object{ 81 makeUnstructuredObject("vTest", "rTest", "item1"), 82 makeUnstructuredObject("vTest", "rTest", "item2"), 83 }, 84 expectedListRequests: 1, 85 expectedRequestOptions: []metav1.ListOptions{ 86 { 87 ResourceVersion: "2", 88 ResourceVersionMatch: metav1.ResourceVersionMatchExact, 89 TimeoutSeconds: ptr.To(int64(39)), 90 }, 91 }, 92 }, 93 94 { 95 name: "legacy, the limit is removed from the list options when it wasn't honored by the watch cache", 96 lastSyncedResourceVersion: "2", 97 listResponse: &v1.PodList{ 98 ListMeta: metav1.ListMeta{ResourceVersion: "2"}, 99 Items: []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")}, 100 }, 101 requestOptions: metav1.ListOptions{ResourceVersion: "0", Limit: 2}, 102 retrievedItems: []runtime.Object{makePod("p1", "1"), makePod("p2", "2"), makePod("p3", "3")}, 103 expectedListRequests: 1, 104 expectedRequestOptions: []metav1.ListOptions{ 105 { 106 ResourceVersion: "2", 107 ResourceVersionMatch: metav1.ResourceVersionMatchExact, 108 }, 109 }, 110 }, 111 112 { 113 name: "the limit is NOT removed from the list options for non-legacy request", 114 lastSyncedResourceVersion: "2", 115 listResponse: &v1.PodList{ 116 ListMeta: metav1.ListMeta{ResourceVersion: "2"}, 117 Items: []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")}, 118 }, 119 requestOptions: metav1.ListOptions{ResourceVersion: "2", Limit: 2}, 120 retrievedItems: []runtime.Object{makePod("p1", "1"), makePod("p2", "2"), makePod("p3", "3")}, 121 expectedListRequests: 1, 122 expectedRequestOptions: []metav1.ListOptions{ 123 { 124 Limit: 2, 125 ResourceVersion: "2", 126 ResourceVersionMatch: metav1.ResourceVersionMatchExact, 127 }, 128 }, 129 }, 130 131 { 132 name: "legacy, the limit is NOT removed from the list options when the watch cache is disabled", 133 lastSyncedResourceVersion: "2", 134 listResponse: &v1.PodList{ 135 ListMeta: metav1.ListMeta{ResourceVersion: "2"}, 136 Items: []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")}, 137 }, 138 requestOptions: metav1.ListOptions{ResourceVersion: "0", Limit: 5}, 139 retrievedItems: []runtime.Object{makePod("p1", "1"), makePod("p2", "2"), makePod("p3", "3")}, 140 expectedListRequests: 1, 141 expectedRequestOptions: []metav1.ListOptions{ 142 { 143 Limit: 5, 144 ResourceVersion: "2", 145 ResourceVersionMatch: metav1.ResourceVersionMatchExact, 146 }, 147 }, 148 }, 149 150 { 151 name: "data consistency check won't panic when there is no data", 152 lastSyncedResourceVersion: "2", 153 listResponse: &v1.PodList{ 154 ListMeta: metav1.ListMeta{ResourceVersion: "2"}, 155 }, 156 requestOptions: metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))}, 157 expectedListRequests: 1, 158 expectedRequestOptions: []metav1.ListOptions{ 159 { 160 ResourceVersion: "2", 161 ResourceVersionMatch: metav1.ResourceVersionMatchExact, 162 TimeoutSeconds: ptr.To(int64(39)), 163 }, 164 }, 165 }, 166 167 { 168 name: "data consistency check won't be performed when Continuation was set", 169 requestOptions: metav1.ListOptions{Continue: "fake continuation token"}, 170 expectedListRequests: 0, 171 }, 172 173 { 174 name: "data consistency check won't be performed when ResourceVersion was set to 0", 175 lastSyncedResourceVersion: "0", 176 listResponse: &v1.PodList{ 177 ListMeta: metav1.ListMeta{ResourceVersion: "0"}, 178 Items: []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2")}, 179 }, 180 requestOptions: metav1.ListOptions{}, 181 expectedListRequests: 0, 182 }, 183 184 { 185 name: "data consistency panics when data is inconsistent", 186 lastSyncedResourceVersion: "2", 187 listResponse: &v1.PodList{ 188 ListMeta: metav1.ListMeta{ResourceVersion: "2"}, 189 Items: []v1.Pod{*makePod("p1", "1"), *makePod("p2", "2"), *makePod("p3", "3")}, 190 }, 191 requestOptions: metav1.ListOptions{TimeoutSeconds: ptr.To(int64(39))}, 192 retrievedItems: []runtime.Object{makePod("p1", "1"), makePod("p2", "2")}, 193 expectedListRequests: 1, 194 expectedRequestOptions: []metav1.ListOptions{ 195 { 196 ResourceVersion: "2", 197 ResourceVersionMatch: metav1.ResourceVersionMatchExact, 198 TimeoutSeconds: ptr.To(int64(39)), 199 }, 200 }, 201 expectPanic: true, 202 }, 203 } 204 205 for _, scenario := range scenarios { 206 t.Run(scenario.name, func(t *testing.T) { 207 ctx := context.TODO() 208 if scenario.listResponse == nil { 209 scenario.listResponse = &v1.PodList{} 210 } 211 fakeLister := &listWrapper{response: scenario.listResponse} 212 retrievedItemsFunc := func() []runtime.Object { 213 return scenario.retrievedItems 214 } 215 216 if scenario.expectPanic { 217 require.Panics(t, func() { 218 CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, scenario.requestOptions, retrievedItemsFunc) 219 }) 220 } else { 221 CheckDataConsistency(ctx, "", scenario.lastSyncedResourceVersion, fakeLister.List, scenario.requestOptions, retrievedItemsFunc) 222 } 223 224 require.Equal(t, fakeLister.counter, scenario.expectedListRequests) 225 require.Equal(t, fakeLister.requestOptions, scenario.expectedRequestOptions) 226 }) 227 } 228 } 229 230 func TestDataConsistencyCheckerRetry(t *testing.T) { 231 ctx := context.TODO() 232 retrievedItemsFunc := func() []*v1.Pod { 233 return nil 234 } 235 stopListErrorAfter := 5 236 fakeErrLister := &errorLister{stopErrorAfter: stopListErrorAfter} 237 238 CheckDataConsistency(ctx, "", "", fakeErrLister.List, metav1.ListOptions{}, retrievedItemsFunc) 239 require.Equal(t, fakeErrLister.listCounter, fakeErrLister.stopErrorAfter) 240 } 241 242 type errorLister struct { 243 listCounter int 244 stopErrorAfter int 245 } 246 247 func (lw *errorLister) List(_ context.Context, _ metav1.ListOptions) (runtime.Object, error) { 248 lw.listCounter++ 249 if lw.listCounter == lw.stopErrorAfter { 250 return &v1.PodList{}, nil 251 } 252 return nil, fmt.Errorf("nasty error") 253 } 254 255 type listWrapper struct { 256 counter int 257 requestOptions []metav1.ListOptions 258 response runtime.Object 259 } 260 261 func (lw *listWrapper) List(_ context.Context, opts metav1.ListOptions) (runtime.Object, error) { 262 lw.counter++ 263 lw.requestOptions = append(lw.requestOptions, opts) 264 return lw.response, nil 265 } 266 267 func makePod(name, rv string) *v1.Pod { 268 return &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: name, ResourceVersion: rv, UID: types.UID(name)}} 269 } 270 271 func makeUnstructuredObject(version, kind, name string) *unstructured.Unstructured { 272 return &unstructured.Unstructured{ 273 Object: map[string]interface{}{ 274 "apiVersion": version, 275 "kind": kind, 276 "metadata": map[string]interface{}{ 277 "name": name, 278 }, 279 }, 280 } 281 }