knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/configmap/informer/informed_watcher_test.go (about) 1 /* 2 Copyright 2018 The Knative 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 informer 18 19 import ( 20 "context" 21 "sync" 22 "testing" 23 24 corev1 "k8s.io/api/core/v1" 25 "k8s.io/apimachinery/pkg/api/equality" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/labels" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/selection" 30 fakekubeclientset "k8s.io/client-go/kubernetes/fake" 31 "k8s.io/client-go/tools/cache" 32 ) 33 34 type counter struct { 35 name string 36 mu sync.RWMutex 37 cfg []*corev1.ConfigMap 38 wg *sync.WaitGroup 39 } 40 41 func (c *counter) callback(cm *corev1.ConfigMap) { 42 c.mu.Lock() 43 defer c.mu.Unlock() 44 45 c.cfg = append(c.cfg, cm) 46 if c.wg != nil { 47 c.wg.Done() 48 } 49 } 50 51 func (c *counter) count() int { 52 c.mu.RLock() 53 defer c.mu.RUnlock() 54 return len(c.cfg) 55 } 56 57 func TestInformedWatcher(t *testing.T) { 58 fooCM := &corev1.ConfigMap{ 59 ObjectMeta: metav1.ObjectMeta{ 60 Namespace: "default", 61 Name: "foo", 62 }, 63 Data: map[string]string{"key": "val"}, 64 } 65 barCM := &corev1.ConfigMap{ 66 ObjectMeta: metav1.ObjectMeta{ 67 Namespace: "default", 68 Name: "bar", 69 }, 70 Data: map[string]string{"key2": "val3"}, 71 } 72 kc := fakekubeclientset.NewSimpleClientset(fooCM, barCM) 73 cmw := NewInformedWatcher(kc, "default") 74 75 foo1 := &counter{name: "foo1"} 76 foo2 := &counter{name: "foo2"} 77 bar := &counter{name: "bar"} 78 cmw.Watch("foo", foo1.callback) 79 cmw.Watch("foo", foo2.callback) 80 cmw.Watch("bar", bar.callback) 81 82 stopCh := make(chan struct{}) 83 defer close(stopCh) 84 if err := cmw.Start(stopCh); err != nil { 85 t.Fatal("cm.Start() =", err) 86 } 87 88 // When Start returns the callbacks should have been called with the 89 // version of the objects that is available. 90 for _, obj := range []*counter{foo1, foo2, bar} { 91 if got, want := obj.count(), 1; got != want { 92 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 93 } 94 } 95 96 // After a "foo" event, the "foo" watchers should have 2, 97 // and the "bar" watchers should still have 1 98 cmw.updateConfigMapEvent(nil, fooCM) 99 for _, obj := range []*counter{foo1, foo2} { 100 if got, want := obj.count(), 2; got != want { 101 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 102 } 103 } 104 105 for _, obj := range []*counter{bar} { 106 if got, want := obj.count(), 1; got != want { 107 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 108 } 109 } 110 111 // After a "foo" and "bar" event, the "foo" watchers should have 3, 112 // and the "bar" watchers should still have 2 113 cmw.updateConfigMapEvent(nil, fooCM) 114 cmw.updateConfigMapEvent(nil, barCM) 115 for _, obj := range []*counter{foo1, foo2} { 116 if got, want := obj.count(), 3; got != want { 117 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 118 } 119 } 120 for _, obj := range []*counter{bar} { 121 if got, want := obj.count(), 2; got != want { 122 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 123 } 124 } 125 126 // After a "bar" event, all watchers should have 3 127 nbarCM := barCM.DeepCopy() 128 nbarCM.Data["wow"] = "now!" 129 cmw.updateConfigMapEvent(barCM, nbarCM) 130 for _, obj := range []*counter{foo1, foo2, bar} { 131 if got, want := obj.count(), 3; got != want { 132 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 133 } 134 } 135 136 // After an idempotent event no changes should be recorded. 137 cmw.updateConfigMapEvent(barCM, barCM) 138 cmw.updateConfigMapEvent(fooCM, fooCM) 139 for _, obj := range []*counter{foo1, foo2, bar} { 140 if got, want := obj.count(), 3; got != want { 141 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 142 } 143 } 144 145 // After an unwatched ConfigMap update, no change. 146 147 cmw.updateConfigMapEvent(nil, &corev1.ConfigMap{ 148 ObjectMeta: metav1.ObjectMeta{ 149 Namespace: "default", 150 Name: "not-watched", 151 }, 152 }) 153 for _, obj := range []*counter{foo1, foo2, bar} { 154 if got, want := obj.count(), 3; got != want { 155 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 156 } 157 } 158 159 // After a change in an unrelated namespace, no change. 160 cmw.updateConfigMapEvent(nil, &corev1.ConfigMap{ 161 ObjectMeta: metav1.ObjectMeta{ 162 Namespace: "not-default", 163 Name: "foo", 164 }, 165 }) 166 for _, obj := range []*counter{foo1, foo2, bar} { 167 if got, want := obj.count(), 3; got != want { 168 t.Errorf("%v.count = %d, want %d", obj.name, got, want) 169 } 170 } 171 } 172 173 func TestFilterConfigByLabelExists(t *testing.T) { 174 testCases := map[string]struct { 175 input string 176 expectOutStr string 177 expectErr bool 178 }{ 179 "Valid input": { 180 input: "test/label", 181 expectOutStr: "test/label", 182 }, 183 "Invalid input": { 184 input: "invalid,", 185 expectErr: true, 186 }, 187 "Empty input": { 188 input: "", 189 expectErr: true, 190 }, 191 } 192 193 for name, tc := range testCases { 194 t.Run(name, func(t *testing.T) { 195 req, err := FilterConfigByLabelExists(tc.input) 196 197 if gotError := err != nil; gotError != tc.expectErr { 198 t.Fatalf("[error] expected/got: %t/%t (msg: %v)", tc.expectErr, gotError, err) 199 } 200 if gotReq, wantReq := req != nil, tc.expectOutStr != ""; gotReq != wantReq { 201 t.Fatalf("[output] expected/got: %t/%t (req: %q)", wantReq, gotReq, req) 202 } 203 204 if req != nil && req.String() != tc.expectOutStr { 205 t.Errorf("Expected %q, got %q", tc.expectOutStr, req.String()) 206 } 207 }) 208 } 209 } 210 211 func TestWatchMissingFailsOnStart(t *testing.T) { 212 const ( 213 labelKey = "test/label" 214 labelVal = "test" 215 ) 216 217 cmWithLabel := &corev1.ConfigMap{ 218 ObjectMeta: metav1.ObjectMeta{ 219 Namespace: "default", 220 Name: "with-label", 221 Labels: map[string]string{labelKey: labelVal}, 222 }, 223 } 224 cmWithoutLabel := &corev1.ConfigMap{ 225 ObjectMeta: metav1.ObjectMeta{ 226 Namespace: "default", 227 Name: "without-label", 228 Labels: map[string]string{}, 229 }, 230 } 231 232 testCases := map[string]struct { 233 initialObj []runtime.Object 234 watchNames []string 235 expectErr string 236 labelReq string 237 }{ 238 "ConfigMap does not exist": { 239 initialObj: nil, 240 watchNames: []string{"foo"}, 241 expectErr: `configmap "foo" not found`, 242 }, 243 "ConfigMap is missing required label": { 244 initialObj: []runtime.Object{cmWithLabel, cmWithoutLabel}, 245 watchNames: []string{"with-label", "without-label"}, 246 expectErr: `configmap "without-label" not found`, 247 labelReq: labelKey, 248 }, 249 } 250 251 for name, tc := range testCases { 252 t.Run(name, func(t *testing.T) { 253 var cmw *InformedWatcher 254 255 kc := fakekubeclientset.NewSimpleClientset(tc.initialObj...) 256 257 if tc.labelReq != "" { 258 req, _ := labels.NewRequirement(labelKey, selection.Equals, []string{labelVal}) 259 cmw = NewInformedWatcher(kc, "default", *req) 260 } else { 261 cmw = NewInformedWatcher(kc, "default") 262 } 263 264 for _, cmName := range tc.watchNames { 265 cmw.Watch(cmName) 266 } 267 268 stopCh := make(chan struct{}) 269 defer close(stopCh) 270 err := cmw.Start(stopCh) 271 272 switch { 273 case err == nil: 274 t.Fatal("Failed to start InformedWatcher =", err) 275 case err.Error() != tc.expectErr: 276 t.Fatal("Unexpected error =", err) 277 } 278 }) 279 } 280 } 281 282 func TestWatchMissingOKWithDefaultOnStart(t *testing.T) { 283 fooCM := &corev1.ConfigMap{ 284 ObjectMeta: metav1.ObjectMeta{ 285 Namespace: "default", 286 Name: "foo", 287 }, 288 } 289 290 kc := fakekubeclientset.NewSimpleClientset() 291 cmw := NewInformedWatcher(kc, "default") 292 293 foo1 := &counter{name: "foo1"} 294 cmw.WatchWithDefault(*fooCM, foo1.callback) 295 296 stopCh := make(chan struct{}) 297 defer close(stopCh) 298 299 // This shouldn't error because we don't have a ConfigMap named "foo", but we do have a default. 300 err := cmw.Start(stopCh) 301 if err != nil { 302 t.Fatal("cm.Start() failed =", err) 303 } 304 305 if foo1.count() != 1 { 306 t.Errorf("foo1.count = %d, want 1", foo1.count()) 307 } 308 } 309 310 func TestErrorOnMultipleStarts(t *testing.T) { 311 fooCM := &corev1.ConfigMap{ 312 ObjectMeta: metav1.ObjectMeta{ 313 Namespace: "default", 314 Name: "foo", 315 }, 316 } 317 kc := fakekubeclientset.NewSimpleClientset(fooCM) 318 cmw := NewInformedWatcher(kc, "default") 319 320 foo1 := &counter{name: "foo1"} 321 cmw.Watch("foo", foo1.callback) 322 323 stopCh := make(chan struct{}) 324 defer close(stopCh) 325 326 // This should succeed because the watched resource exists. 327 if err := cmw.Start(stopCh); err != nil { 328 t.Fatal("cm.Start() =", err) 329 } 330 331 // This should error because we already called Start() 332 if err := cmw.Start(stopCh); err == nil { 333 t.Fatal("cm.Start() succeeded, wanted error") 334 } 335 } 336 337 func TestDefaultObserved(t *testing.T) { 338 defaultFooCM := &corev1.ConfigMap{ 339 ObjectMeta: metav1.ObjectMeta{ 340 Namespace: "default", 341 Name: "foo", 342 }, 343 Data: map[string]string{ 344 "default": "from code", 345 }, 346 } 347 fooCM := &corev1.ConfigMap{ 348 ObjectMeta: metav1.ObjectMeta{ 349 Namespace: "default", 350 Name: "foo", 351 }, 352 Data: map[string]string{ 353 "from": "k8s", 354 }, 355 } 356 357 kc := fakekubeclientset.NewSimpleClientset(fooCM) 358 cmw := NewInformedWatcher(kc, "default") 359 360 foo1 := &counter{name: "foo1"} 361 cmw.WatchWithDefault(*defaultFooCM, foo1.callback) 362 363 stopCh := make(chan struct{}) 364 defer close(stopCh) 365 err := cmw.Start(stopCh) 366 if err != nil { 367 t.Fatal("cm.Start() =", err) 368 } 369 // We expect: 370 // 1. The default to be seen once during startup. 371 // 2. The real K8s version during the initial pass. 372 expected := []*corev1.ConfigMap{defaultFooCM, fooCM} 373 if got, want := foo1.count(), len(expected); got != want { 374 t.Fatalf("foo1.count = %d, want %d", got, want) 375 } 376 for i, cfg := range expected { 377 if got, want := foo1.cfg[i].Data, cfg.Data; !equality.Semantic.DeepEqual(want, got) { 378 t.Errorf("%d config seen should have been '%v', actually '%v'", i, want, got) 379 } 380 } 381 } 382 383 func TestDefaultConfigMapDeleted(t *testing.T) { 384 defaultFooCM := &corev1.ConfigMap{ 385 ObjectMeta: metav1.ObjectMeta{ 386 Namespace: "default", 387 Name: "foo", 388 }, 389 Data: map[string]string{ 390 "default": "from code", 391 }, 392 } 393 fooCM := &corev1.ConfigMap{ 394 ObjectMeta: metav1.ObjectMeta{ 395 Namespace: "default", 396 Name: "foo", 397 }, 398 Data: map[string]string{ 399 "from": "k8s", 400 }, 401 } 402 403 kc := fakekubeclientset.NewSimpleClientset(fooCM) 404 cmw := NewInformedWatcher(kc, "default") 405 406 foo1 := &counter{ 407 name: "foo1", 408 wg: &sync.WaitGroup{}, 409 } 410 foo1.wg.Add(3) // We expect 3 invocations. 411 cmw.WatchWithDefault(*defaultFooCM, foo1.callback) 412 413 stopCh := make(chan struct{}) 414 defer close(stopCh) 415 416 if err := cmw.Start(stopCh); err != nil { 417 t.Fatal("cm.Start() =", err) 418 } 419 420 // Delete the real ConfigMap in K8s, which should cause the default to be processed again. 421 // Because this happens asynchronously via a watcher, use a sync.WaitGroup to wait until it has 422 // occurred. 423 if err := kc.CoreV1().ConfigMaps(fooCM.Namespace).Delete( 424 context.Background(), fooCM.Name, metav1.DeleteOptions{}); err != nil { 425 t.Fatal("Error deleting fooCM:", err) 426 } 427 foo1.wg.Wait() 428 429 // We expect: 430 // 1. The default to be seen once during startup. 431 // 2. The real K8s version during the initial pass. 432 // 3. The default again, when the real K8s version is deleted. 433 expected := []*corev1.ConfigMap{defaultFooCM, fooCM, defaultFooCM} 434 if got, want := foo1.count(), len(expected); got != want { 435 t.Fatalf("foo1.count = %v, want %d", got, want) 436 } 437 for i, cfg := range expected { 438 if got, want := foo1.cfg[i].Data, cfg.Data; !equality.Semantic.DeepEqual(want, got) { 439 t.Errorf("%d config seen should have been '%v', actually '%v'", i, want, got) 440 } 441 } 442 } 443 444 func TestWatchWithDefaultAfterStart(t *testing.T) { 445 defaultFooCM := &corev1.ConfigMap{ 446 ObjectMeta: metav1.ObjectMeta{ 447 Namespace: "default", 448 Name: "foo", 449 }, 450 Data: map[string]string{ 451 "default": "from code", 452 }, 453 } 454 fooCM := &corev1.ConfigMap{ 455 ObjectMeta: metav1.ObjectMeta{ 456 Namespace: "default", 457 Name: "foo", 458 }, 459 Data: map[string]string{ 460 "from": "k8s", 461 }, 462 } 463 464 kc := fakekubeclientset.NewSimpleClientset(fooCM) 465 cmw := NewInformedWatcher(kc, "default") 466 467 stopCh := make(chan struct{}) 468 defer close(stopCh) 469 // Start before adding the WatchWithDefault. 470 if err := cmw.Start(stopCh); err != nil { 471 t.Fatal("cm.Start() =", err) 472 } 473 474 foo1 := &counter{name: "foo1"} 475 476 // Add the WatchWithDefault. This should panic because the InformedWatcher has already started. 477 func() { 478 defer func() { 479 recover() 480 }() 481 cmw.WatchWithDefault(*defaultFooCM, foo1.callback) 482 t.Fatal("WatchWithDefault should have panicked") 483 }() 484 485 // We expect nothing. 486 var expected []*corev1.ConfigMap 487 if got, want := foo1.count(), len(expected); got != want { 488 t.Fatalf("foo1.count = %v, want %d", got, want) 489 } 490 } 491 492 func TestDeleteConfigMapEventWithDeletedFinalStateUnknown(t *testing.T) { 493 defaultFooCM := &corev1.ConfigMap{ 494 ObjectMeta: metav1.ObjectMeta{ 495 Namespace: "default", 496 Name: "foo", 497 }, 498 Data: map[string]string{ 499 "default": "from code", 500 }, 501 } 502 fooCM := &corev1.ConfigMap{ 503 ObjectMeta: metav1.ObjectMeta{ 504 Namespace: "default", 505 Name: "foo", 506 }, 507 Data: map[string]string{ 508 "from": "k8s", 509 }, 510 } 511 512 kc := fakekubeclientset.NewSimpleClientset(fooCM) 513 cmw := NewInformedWatcher(kc, "default") 514 515 foo1 := &counter{name: "foo1"} 516 cmw.WatchWithDefault(*defaultFooCM, foo1.callback) 517 518 stopCh := make(chan struct{}) 519 defer close(stopCh) 520 if err := cmw.Start(stopCh); err != nil { 521 t.Fatal("cm.Start() =", err) 522 } 523 524 // Test that deleteConfigMapEvent does NOT panic when given cache.DeletedFinalStateUnknown with a ConfigMap 525 // This simulates the scenario where the informer receives a DeletedFinalStateUnknown tombstone. 526 tombstone := cache.DeletedFinalStateUnknown{ 527 Key: "default/foo", 528 Obj: fooCM, 529 } 530 531 // This should not panic and should trigger the default ConfigMap 532 cmw.deleteConfigMapEvent(tombstone) 533 534 // We expect: 535 // 1. The default to be seen once during startup. 536 // 2. The real K8s version during the initial pass. 537 // 3. The default again, when the real K8s version is deleted via tombstone. 538 expected := []*corev1.ConfigMap{defaultFooCM, fooCM, defaultFooCM} 539 if got, want := foo1.count(), len(expected); got != want { 540 t.Fatalf("foo1.count = %v, want %d", got, want) 541 } 542 for i, cfg := range expected { 543 if got, want := foo1.cfg[i].Data, cfg.Data; !equality.Semantic.DeepEqual(want, got) { 544 t.Errorf("%d config seen should have been '%v', actually '%v'", i, want, got) 545 } 546 } 547 } 548 549 func TestDeleteConfigMapEventWithDeletedFinalStateUnknownInvalidObject(t *testing.T) { 550 defaultFooCM := &corev1.ConfigMap{ 551 ObjectMeta: metav1.ObjectMeta{ 552 Namespace: "default", 553 Name: "foo", 554 }, 555 Data: map[string]string{ 556 "default": "from code", 557 }, 558 } 559 560 kc := fakekubeclientset.NewSimpleClientset() 561 cmw := NewInformedWatcher(kc, "default") 562 563 foo1 := &counter{name: "foo1"} 564 cmw.WatchWithDefault(*defaultFooCM, foo1.callback) 565 566 stopCh := make(chan struct{}) 567 defer close(stopCh) 568 if err := cmw.Start(stopCh); err != nil { 569 t.Fatal("cm.Start() =", err) 570 } 571 572 initialCount := foo1.count() 573 574 // Test that deleteConfigMapEvent does NOT panic when given cache.DeletedFinalStateUnknown with an invalid object 575 // This simulates the scenario where the tombstone contains a non-ConfigMap object. 576 tombstone := cache.DeletedFinalStateUnknown{ 577 Key: "default/foo", 578 Obj: "not-a-configmap", // Invalid object type 579 } 580 581 // This should not panic and should gracefully return without changing the count 582 cmw.deleteConfigMapEvent(tombstone) 583 584 // The count should remain unchanged since the object is invalid 585 if got, want := foo1.count(), initialCount; got != want { 586 t.Fatalf("foo1.count = %v, want %d (should not change)", got, want) 587 } 588 } 589 590 func TestDeleteConfigMapEventWithInvalidObject(t *testing.T) { 591 defaultFooCM := &corev1.ConfigMap{ 592 ObjectMeta: metav1.ObjectMeta{ 593 Namespace: "default", 594 Name: "foo", 595 }, 596 Data: map[string]string{ 597 "default": "from code", 598 }, 599 } 600 601 kc := fakekubeclientset.NewSimpleClientset() 602 cmw := NewInformedWatcher(kc, "default") 603 604 foo1 := &counter{name: "foo1"} 605 cmw.WatchWithDefault(*defaultFooCM, foo1.callback) 606 607 stopCh := make(chan struct{}) 608 defer close(stopCh) 609 if err := cmw.Start(stopCh); err != nil { 610 t.Fatal("cm.Start() =", err) 611 } 612 613 initialCount := foo1.count() 614 615 // Test that deleteConfigMapEvent does NOT panic when given a completely invalid object 616 // This simulates the scenario where the informer receives an unexpected object type. 617 invalidObj := "not-a-configmap" 618 619 // This should not panic and should gracefully return without changing the count 620 cmw.deleteConfigMapEvent(invalidObj) 621 622 // The count should remain unchanged since the object is invalid 623 if got, want := foo1.count(), initialCount; got != want { 624 t.Fatalf("foo1.count = %v, want %d (should not change)", got, want) 625 } 626 }