k8s.io/client-go@v0.31.1/rest/request_watchlist_test.go (about) 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 rest 18 19 import ( 20 "context" 21 "fmt" 22 "regexp" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 27 v1 "k8s.io/api/core/v1" 28 apiequality "k8s.io/apimachinery/pkg/api/equality" 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 "k8s.io/apimachinery/pkg/watch" 34 clientfeatures "k8s.io/client-go/features" 35 clientfeaturestesting "k8s.io/client-go/features/testing" 36 ) 37 38 func TestWatchListResult(t *testing.T) { 39 scenarios := []struct { 40 name string 41 target WatchListResult 42 result runtime.Object 43 44 expectedResult *v1.PodList 45 expectedErr error 46 }{ 47 { 48 name: "not a pointer", 49 result: fakeObj{}, 50 expectedErr: fmt.Errorf("rest.fakeObj is not a list: expected pointer, but got rest.fakeObj type"), 51 }, 52 { 53 name: "nil input won't panic", 54 result: nil, 55 expectedErr: fmt.Errorf("<nil> is not a list: expected pointer, but got invalid kind"), 56 }, 57 { 58 name: "not a list", 59 result: &v1.Pod{}, 60 expectedErr: fmt.Errorf("*v1.Pod is not a list: no Items field in this object"), 61 }, 62 { 63 name: "an err is always returned", 64 result: nil, 65 target: WatchListResult{err: fmt.Errorf("dummy err")}, 66 expectedErr: fmt.Errorf("dummy err"), 67 }, 68 { 69 name: "empty list", 70 result: &v1.PodList{}, 71 expectedResult: &v1.PodList{ 72 TypeMeta: metav1.TypeMeta{Kind: "PodList"}, 73 Items: []v1.Pod{}, 74 }, 75 }, 76 { 77 name: "gv is applied", 78 result: &v1.PodList{}, 79 target: WatchListResult{gv: schema.GroupVersion{Group: "g", Version: "v"}}, 80 expectedResult: &v1.PodList{ 81 TypeMeta: metav1.TypeMeta{Kind: "PodList", APIVersion: "g/v"}, 82 Items: []v1.Pod{}, 83 }, 84 }, 85 { 86 name: "gv is applied, empty group", 87 result: &v1.PodList{}, 88 target: WatchListResult{gv: schema.GroupVersion{Version: "v"}}, 89 expectedResult: &v1.PodList{ 90 TypeMeta: metav1.TypeMeta{Kind: "PodList", APIVersion: "v"}, 91 Items: []v1.Pod{}, 92 }, 93 }, 94 { 95 name: "rv is applied", 96 result: &v1.PodList{}, 97 target: WatchListResult{initialEventsEndBookmarkRV: "100"}, 98 expectedResult: &v1.PodList{ 99 TypeMeta: metav1.TypeMeta{Kind: "PodList"}, 100 ListMeta: metav1.ListMeta{ResourceVersion: "100"}, 101 Items: []v1.Pod{}, 102 }, 103 }, 104 { 105 name: "items are applied", 106 result: &v1.PodList{}, 107 target: WatchListResult{items: []runtime.Object{makePod(1), makePod(2)}}, 108 expectedResult: &v1.PodList{ 109 TypeMeta: metav1.TypeMeta{Kind: "PodList"}, 110 Items: []v1.Pod{*makePod(1), *makePod(2)}, 111 }, 112 }, 113 { 114 name: "type mismatch", 115 result: &v1.PodList{}, 116 target: WatchListResult{items: []runtime.Object{makeNamespace("1")}}, 117 expectedErr: fmt.Errorf("received object type = v1.Namespace at index = 0, doesn't match the list item type = v1.Pod"), 118 }, 119 } 120 for _, scenario := range scenarios { 121 t.Run(scenario.name, func(t *testing.T) { 122 err := scenario.target.Into(scenario.result) 123 if scenario.expectedErr != nil && err == nil { 124 t.Fatalf("expected an error = %v, got nil", scenario.expectedErr) 125 } 126 if scenario.expectedErr == nil && err != nil { 127 t.Fatalf("didn't expect an error, got = %v", err) 128 } 129 if err != nil { 130 if scenario.expectedErr.Error() != err.Error() { 131 t.Fatalf("unexpected err = %v, expected = %v", err, scenario.expectedErr) 132 } 133 return 134 } 135 if !apiequality.Semantic.DeepEqual(scenario.expectedResult, scenario.result) { 136 t.Errorf("diff: %v", cmp.Diff(scenario.expectedResult, scenario.result)) 137 } 138 }) 139 } 140 } 141 142 func TestWatchListSuccess(t *testing.T) { 143 scenarios := []struct { 144 name string 145 gv schema.GroupVersion 146 watchEvents []watch.Event 147 expectedResult *v1.PodList 148 }{ 149 { 150 name: "happy path", 151 // Note that the APIVersion for the core API group is "v1" (not "core/v1"). 152 // We fake "core/v1" here to test if the Group part is properly 153 // recognized and set on the resulting object. 154 gv: schema.GroupVersion{Group: "core", Version: "v1"}, 155 watchEvents: []watch.Event{ 156 {Type: watch.Added, Object: makePod(1)}, 157 {Type: watch.Added, Object: makePod(2)}, 158 {Type: watch.Bookmark, Object: makeBookmarkEvent(5)}, 159 }, 160 expectedResult: &v1.PodList{ 161 TypeMeta: metav1.TypeMeta{ 162 APIVersion: "core/v1", 163 Kind: "PodList", 164 }, 165 ListMeta: metav1.ListMeta{ResourceVersion: "5"}, 166 Items: []v1.Pod{*makePod(1), *makePod(2)}, 167 }, 168 }, 169 { 170 name: "APIVersion with only version provided is properly set", 171 gv: schema.GroupVersion{Version: "v1"}, 172 watchEvents: []watch.Event{ 173 {Type: watch.Added, Object: makePod(1)}, 174 {Type: watch.Bookmark, Object: makeBookmarkEvent(5)}, 175 }, 176 expectedResult: &v1.PodList{ 177 TypeMeta: metav1.TypeMeta{ 178 APIVersion: "v1", 179 Kind: "PodList", 180 }, 181 ListMeta: metav1.ListMeta{ResourceVersion: "5"}, 182 Items: []v1.Pod{*makePod(1)}, 183 }, 184 }, 185 { 186 name: "only the bookmark", 187 gv: schema.GroupVersion{Version: "v1"}, 188 watchEvents: []watch.Event{ 189 {Type: watch.Bookmark, Object: makeBookmarkEvent(5)}, 190 }, 191 expectedResult: &v1.PodList{ 192 TypeMeta: metav1.TypeMeta{ 193 APIVersion: "v1", 194 Kind: "PodList", 195 }, 196 ListMeta: metav1.ListMeta{ResourceVersion: "5"}, 197 Items: []v1.Pod{}, 198 }, 199 }, 200 } 201 for _, scenario := range scenarios { 202 t.Run(scenario.name, func(t *testing.T) { 203 ctx := context.Background() 204 fakeWatcher := watch.NewFake() 205 target := &Request{ 206 c: &RESTClient{ 207 content: ClientContentConfig{ 208 GroupVersion: scenario.gv, 209 }, 210 }, 211 } 212 213 go func(watchEvents []watch.Event) { 214 for _, watchEvent := range watchEvents { 215 fakeWatcher.Action(watchEvent.Type, watchEvent.Object) 216 } 217 }(scenario.watchEvents) 218 219 res := target.handleWatchList(ctx, fakeWatcher) 220 if res.err != nil { 221 t.Fatal(res.err) 222 } 223 224 result := &v1.PodList{} 225 if err := res.Into(result); err != nil { 226 t.Fatal(err) 227 } 228 if !apiequality.Semantic.DeepEqual(scenario.expectedResult, result) { 229 t.Errorf("diff: %v", cmp.Diff(scenario.expectedResult, result)) 230 } 231 if !fakeWatcher.IsStopped() { 232 t.Fatalf("the watcher wasn't stopped") 233 } 234 }) 235 } 236 } 237 238 func TestWatchListFailure(t *testing.T) { 239 scenarios := []struct { 240 name string 241 ctx context.Context 242 watcher *watch.FakeWatcher 243 watchEvents []watch.Event 244 245 expectedError error 246 }{ 247 { 248 name: "request stop", 249 ctx: func() context.Context { 250 ctx, ctxCancel := context.WithCancel(context.TODO()) 251 ctxCancel() 252 return ctx 253 }(), 254 watcher: watch.NewFake(), 255 expectedError: fmt.Errorf("context canceled"), 256 }, 257 { 258 name: "stop watcher", 259 ctx: context.TODO(), 260 watcher: func() *watch.FakeWatcher { 261 w := watch.NewFake() 262 w.Stop() 263 return w 264 }(), 265 expectedError: fmt.Errorf("unexpected watch close"), 266 }, 267 { 268 name: "stop on watch.Error", 269 ctx: context.TODO(), 270 watcher: watch.NewFake(), 271 watchEvents: []watch.Event{{Type: watch.Error, Object: &apierrors.NewInternalError(fmt.Errorf("dummy errror")).ErrStatus}}, 272 expectedError: fmt.Errorf("Internal error occurred: dummy errror"), 273 }, 274 { 275 name: "incorrect watch type (Deleted)", 276 ctx: context.TODO(), 277 watcher: watch.NewFake(), 278 watchEvents: []watch.Event{{Type: watch.Deleted, Object: makePod(1)}}, 279 expectedError: fmt.Errorf("unexpected watch event .*, expected to only receive watch.Added and watch.Bookmark events"), 280 }, 281 { 282 name: "incorrect watch type (Modified)", 283 ctx: context.TODO(), 284 watcher: watch.NewFake(), 285 watchEvents: []watch.Event{{Type: watch.Modified, Object: makePod(1)}}, 286 expectedError: fmt.Errorf("unexpected watch event .*, expected to only receive watch.Added and watch.Bookmark events"), 287 }, 288 { 289 name: "unordered input returns an error", 290 ctx: context.TODO(), 291 watcher: watch.NewFake(), 292 watchEvents: []watch.Event{{Type: watch.Added, Object: makePod(3)}, {Type: watch.Added, Object: makePod(1)}}, 293 expectedError: fmt.Errorf("cannot add the obj .* with the key = ns/pod-1, as it violates the ordering guarantees provided by the watchlist feature in beta phase, lastInsertedKey was = ns/pod-3"), 294 }, 295 } 296 297 for _, scenario := range scenarios { 298 t.Run(scenario.name, func(t *testing.T) { 299 target := &Request{} 300 go func(w *watch.FakeWatcher, watchEvents []watch.Event) { 301 for _, event := range watchEvents { 302 w.Action(event.Type, event.Object) 303 } 304 }(scenario.watcher, scenario.watchEvents) 305 306 res := target.handleWatchList(scenario.ctx, scenario.watcher) 307 resErr := res.Into(nil) 308 if resErr == nil { 309 t.Fatal("expected to get an error, got nil") 310 } 311 matched, err := regexp.MatchString(scenario.expectedError.Error(), resErr.Error()) 312 if err != nil { 313 t.Fatal(err) 314 } 315 if !matched { 316 t.Fatalf("unexpected err = %v, expected = %v", resErr, scenario.expectedError) 317 } 318 if !scenario.watcher.IsStopped() { 319 t.Fatalf("the watcher wasn't stopped") 320 } 321 }) 322 } 323 } 324 325 func TestWatchListWhenFeatureGateDisabled(t *testing.T) { 326 clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, false) 327 expectedError := fmt.Errorf("%q feature gate is not enabled", clientfeatures.WatchListClient) 328 target := &Request{} 329 330 res := target.WatchList(context.TODO()) 331 332 resErr := res.Into(nil) 333 if resErr == nil { 334 t.Fatal("expected to get an error, got nil") 335 } 336 if resErr.Error() != expectedError.Error() { 337 t.Fatalf("unexpected error: %v, expected: %v", resErr, expectedError) 338 } 339 } 340 341 func makePod(rv uint64) *v1.Pod { 342 return &v1.Pod{ 343 ObjectMeta: metav1.ObjectMeta{ 344 Name: fmt.Sprintf("pod-%d", rv), 345 Namespace: "ns", 346 ResourceVersion: fmt.Sprintf("%d", rv), 347 Annotations: map[string]string{}, 348 }, 349 } 350 } 351 352 func makeNamespace(name string) *v1.Namespace { 353 return &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}} 354 } 355 356 func makeBookmarkEvent(rv uint64) *v1.Pod { 357 return &v1.Pod{ 358 ObjectMeta: metav1.ObjectMeta{ 359 ResourceVersion: fmt.Sprintf("%d", rv), 360 Annotations: map[string]string{metav1.InitialEventsAnnotationKey: "true"}, 361 }, 362 } 363 } 364 365 type fakeObj struct { 366 } 367 368 func (f fakeObj) GetObjectKind() schema.ObjectKind { 369 return schema.EmptyObjectKind 370 } 371 372 func (f fakeObj) DeepCopyObject() runtime.Object { 373 return fakeObj{} 374 }