k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/internal/generic/controller_test.go (about) 1 /* 2 Copyright 2022 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 generic_test 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "reflect" 24 "sync" 25 "sync/atomic" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/stretchr/testify/require" 31 32 k8serrors "k8s.io/apimachinery/pkg/api/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 35 "k8s.io/apimachinery/pkg/labels" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/apimachinery/pkg/runtime/serializer" 39 "k8s.io/apimachinery/pkg/util/wait" 40 "k8s.io/apimachinery/pkg/watch" 41 42 "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" 43 44 clienttesting "k8s.io/client-go/testing" 45 "k8s.io/client-go/tools/cache" 46 ) 47 48 type testInformer struct { 49 cache.SharedIndexInformer 50 51 lock sync.Mutex 52 registrations map[interface{}]struct{} 53 } 54 55 func (t *testInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) { 56 res, err := t.SharedIndexInformer.AddEventHandler(handler) 57 if err != nil { 58 return res, err 59 } 60 61 func() { 62 t.lock.Lock() 63 defer t.lock.Unlock() 64 if t.registrations == nil { 65 t.registrations = make(map[interface{}]struct{}) 66 } 67 t.registrations[res] = struct{}{} 68 }() 69 70 return res, err 71 } 72 73 func (t *testInformer) RemoveEventHandler(registration cache.ResourceEventHandlerRegistration) error { 74 func() { 75 t.lock.Lock() 76 defer t.lock.Unlock() 77 78 if _, ok := t.registrations[registration]; !ok { 79 panic("removing unknown event handler?") 80 } 81 delete(t.registrations, registration) 82 }() 83 84 return t.SharedIndexInformer.RemoveEventHandler(registration) 85 } 86 87 var ( 88 scheme *runtime.Scheme = runtime.NewScheme() 89 codecs serializer.CodecFactory = serializer.NewCodecFactory(scheme) 90 fakeGVR schema.GroupVersionResource = schema.GroupVersionResource{ 91 Group: "fake.example.com", 92 Version: "v1", 93 Resource: "fakes", 94 } 95 fakeGVK schema.GroupVersionKind = fakeGVR.GroupVersion().WithKind("Fake") 96 fakeGVKList schema.GroupVersionKind = fakeGVR.GroupVersion().WithKind("FakeList") 97 ) 98 99 func init() { 100 scheme.AddKnownTypeWithName(fakeGVK, &unstructured.Unstructured{}) 101 scheme.AddKnownTypeWithName(fakeGVKList, &unstructured.UnstructuredList{}) 102 } 103 104 func setupTest(ctx context.Context, customReconciler func(string, string, runtime.Object) error) ( 105 tracker clienttesting.ObjectTracker, 106 controller generic.Controller[*unstructured.Unstructured], 107 informer *testInformer, 108 waitForReconcile func(runtime.Object) error, 109 verifyNoMoreEvents func() bool, 110 ) { 111 tracker = clienttesting.NewObjectTracker(scheme, codecs.UniversalDecoder()) 112 reconciledObjects := make(chan runtime.Object) 113 114 // Set up fake informers that return instances of mock Policy definitoins 115 // and mock policy bindings 116 informer = &testInformer{SharedIndexInformer: cache.NewSharedIndexInformer(&cache.ListWatch{ 117 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 118 return tracker.List(fakeGVR, fakeGVK, "") 119 }, 120 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 121 return tracker.Watch(fakeGVR, "") 122 }, 123 }, &unstructured.Unstructured{}, 30*time.Second, nil)} 124 125 reconciler := func(namespace, name string, newObj *unstructured.Unstructured) error { 126 var err error 127 copied := newObj.DeepCopyObject() 128 if customReconciler != nil { 129 err = customReconciler(namespace, name, newObj) 130 } 131 select { 132 case reconciledObjects <- copied: 133 case <-ctx.Done(): 134 panic("timed out attempting to deliver reconcile event") 135 } 136 return err 137 } 138 139 waitForReconcile = func(obj runtime.Object) error { 140 select { 141 case reconciledObj := <-reconciledObjects: 142 if reflect.DeepEqual(obj, reconciledObj) { 143 return nil 144 } 145 return fmt.Errorf("expected equal objects: %v", cmp.Diff(obj, reconciledObj)) 146 case <-ctx.Done(): 147 return fmt.Errorf("context done before reconcile: %w", ctx.Err()) 148 } 149 } 150 151 myController := generic.NewController( 152 generic.NewInformer[*unstructured.Unstructured](informer), 153 reconciler, 154 generic.ControllerOptions{}, 155 ) 156 157 verifyNoMoreEvents = func() bool { 158 close(reconciledObjects) // closing means that a future attempt to send will crash 159 for leftover := range reconciledObjects { 160 panic(fmt.Errorf("leftover object which was not anticipated by test: %v", leftover)) 161 } 162 // TODO(alexzielenski): this effectively doesn't test anything since the 163 // controller drops any pending events when it shuts down. 164 return true 165 } 166 167 return tracker, myController, informer, waitForReconcile, verifyNoMoreEvents 168 } 169 170 func TestReconcile(t *testing.T) { 171 testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second) 172 defer testCancel() 173 174 tracker, myController, informer, waitForReconcile, verifyNoMoreEvents := setupTest(testContext, nil) 175 176 // Add object to informer 177 initialObject := &unstructured.Unstructured{} 178 initialObject.SetUnstructuredContent(map[string]interface{}{ 179 "metadata": map[string]interface{}{ 180 "name": "object1", 181 "resourceVersion": "1", 182 }, 183 }) 184 initialObject.SetGroupVersionKind(fakeGVK) 185 186 require.NoError(t, tracker.Add(initialObject)) 187 188 wg := sync.WaitGroup{} 189 190 // Start informer 191 wg.Add(1) 192 go func() { 193 defer wg.Done() 194 informer.Run(testContext.Done()) 195 }() 196 197 // Start controller 198 wg.Add(1) 199 go func() { 200 defer wg.Done() 201 stopReason := myController.Run(testContext) 202 require.ErrorIs(t, stopReason, context.Canceled) 203 }() 204 205 // The controller is blocked because the reconcile function sends on an 206 // unbuffered channel. 207 require.False(t, myController.HasSynced()) 208 209 // Wait for all enqueued reconciliations 210 require.NoError(t, waitForReconcile(initialObject)) 211 212 // Now it is safe to wait for it to Sync 213 require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced)) 214 215 // Updated object 216 updatedObject := &unstructured.Unstructured{} 217 updatedObject.SetUnstructuredContent(map[string]interface{}{ 218 "metadata": map[string]interface{}{ 219 "name": "object1", 220 "resourceVersion": "2", 221 }, 222 "newKey": "a key", 223 }) 224 updatedObject.SetGroupVersionKind(fakeGVK) 225 require.NoError(t, tracker.Update(fakeGVR, updatedObject, "")) 226 227 // Wait for all enqueued reconciliations 228 require.NoError(t, waitForReconcile(updatedObject)) 229 require.NoError(t, tracker.Delete(fakeGVR, updatedObject.GetNamespace(), updatedObject.GetName())) 230 require.NoError(t, waitForReconcile(nil)) 231 232 testCancel() 233 wg.Wait() 234 235 verifyNoMoreEvents() 236 } 237 238 func TestShutdown(t *testing.T) { 239 testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second) 240 defer testCancel() 241 242 _, myController, informer, _, verifyNoMoreEvents := setupTest(testContext, nil) 243 244 wg := sync.WaitGroup{} 245 246 // Start informer 247 wg.Add(1) 248 go func() { 249 defer wg.Done() 250 informer.Run(testContext.Done()) 251 }() 252 253 // Start controller 254 wg.Add(1) 255 go func() { 256 defer wg.Done() 257 stopReason := myController.Run(testContext) 258 require.ErrorIs(t, stopReason, context.Canceled) 259 }() 260 261 // Wait for controller and informer to start up 262 require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced)) 263 264 // Stop the controller and informer 265 testCancel() 266 267 // Wait for controller and informer to stop 268 wg.Wait() 269 270 // Ensure the event handler was cleaned up 271 require.Empty(t, informer.registrations) 272 273 verifyNoMoreEvents() 274 } 275 276 // Show an error is thrown informer isn't started when the controller runs 277 func TestInformerNeverStarts(t *testing.T) { 278 testContext, testCancel := context.WithTimeout(context.Background(), 400*time.Millisecond) 279 defer testCancel() 280 281 _, myController, informer, _, verifyNoMoreEvents := setupTest(testContext, nil) 282 283 wg := sync.WaitGroup{} 284 285 // Start controller 286 wg.Add(1) 287 go func() { 288 defer wg.Done() 289 stopReason := myController.Run(testContext) 290 require.ErrorIs(t, stopReason, context.DeadlineExceeded) 291 }() 292 293 // Wait for deadline to pass without syncing the cache 294 require.False(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced)) 295 296 // Wait for controller to stop (or context deadline will pass quickly) 297 wg.Wait() 298 299 // Ensure there are no event handlers 300 require.Empty(t, informer.registrations) 301 302 verifyNoMoreEvents() 303 } 304 305 // Shows that if RV does not change, the reconciler does not get called 306 func TestIgnoredUpdate(t *testing.T) { 307 testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second) 308 defer testCancel() 309 310 tracker, myController, informer, waitForReconcile, verifyNoMoreEvents := setupTest(testContext, nil) 311 312 // Add object to informer 313 initialObject := &unstructured.Unstructured{} 314 initialObject.SetUnstructuredContent(map[string]interface{}{ 315 "metadata": map[string]interface{}{ 316 "name": "object1", 317 "resourceVersion": "1", 318 }, 319 }) 320 initialObject.SetGroupVersionKind(fakeGVK) 321 322 require.NoError(t, tracker.Add(initialObject)) 323 324 wg := sync.WaitGroup{} 325 326 // Start informer 327 wg.Add(1) 328 go func() { 329 defer wg.Done() 330 informer.Run(testContext.Done()) 331 }() 332 333 // Start controller 334 wg.Add(1) 335 go func() { 336 defer wg.Done() 337 stopReason := myController.Run(testContext) 338 require.ErrorIs(t, stopReason, context.Canceled) 339 }() 340 341 // The controller is blocked because the reconcile function sends on an 342 // unbuffered channel. 343 require.False(t, myController.HasSynced()) 344 345 // Wait for all enqueued reconciliations 346 require.NoError(t, waitForReconcile(initialObject)) 347 348 // Now it is safe to wait for it to Sync 349 require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced)) 350 351 // Send update with the same object 352 require.NoError(t, tracker.Update(fakeGVR, initialObject, "")) 353 354 // Don't wait for it to be reconciled 355 356 testCancel() 357 wg.Wait() 358 359 // TODO(alexzielenski): Find a better way to test this since the 360 // controller drops any pending events when it shuts down. 361 verifyNoMoreEvents() 362 } 363 364 // Shows that an object which fails reconciliation will retry 365 func TestReconcileRetry(t *testing.T) { 366 testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second) 367 defer testCancel() 368 369 calls := atomic.Uint64{} 370 success := atomic.Bool{} 371 tracker, myController, _, waitForReconcile, verifyNoMoreEvents := setupTest(testContext, func(s1, s2 string, o runtime.Object) error { 372 373 if calls.Add(1) > 2 { 374 // Suddenly start liking the object 375 success.Store(true) 376 return nil 377 } 378 return errors.New("i dont like this object") 379 }) 380 381 // Start informer 382 wg := sync.WaitGroup{} 383 384 wg.Add(1) 385 go func() { 386 defer wg.Done() 387 myController.Informer().Run(testContext.Done()) 388 }() 389 390 // Start controller 391 wg.Add(1) 392 go func() { 393 defer wg.Done() 394 stopReason := myController.Run(testContext) 395 require.ErrorIs(t, stopReason, context.Canceled) 396 }() 397 398 // Add object to informer 399 initialObject := &unstructured.Unstructured{} 400 initialObject.SetUnstructuredContent(map[string]interface{}{ 401 "metadata": map[string]interface{}{ 402 "name": "object1", 403 "resourceVersion": "1", 404 }, 405 }) 406 initialObject.SetGroupVersionKind(fakeGVK) 407 require.NoError(t, tracker.Add(initialObject)) 408 409 require.NoError(t, waitForReconcile(initialObject), "initial reconcile") 410 require.NoError(t, waitForReconcile(initialObject), "previous reconcile failed, should retry quickly") 411 require.NoError(t, waitForReconcile(initialObject), "previous reconcile failed, should retry quickly") 412 // Will not try again since calls > 2 for last reconcile 413 require.True(t, success.Load(), "last call to reconcile should return success") 414 testCancel() 415 wg.Wait() 416 417 verifyNoMoreEvents() 418 } 419 420 func TestInformerList(t *testing.T) { 421 testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second) 422 423 tracker, myController, _, _, _ := setupTest(testContext, nil) 424 425 wg := sync.WaitGroup{} 426 427 wg.Add(1) 428 go func() { 429 defer wg.Done() 430 myController.Informer().Run(testContext.Done()) 431 }() 432 433 defer func() { 434 testCancel() 435 wg.Wait() 436 }() 437 438 require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.Informer().HasSynced)) 439 440 object1 := &unstructured.Unstructured{} 441 object1.SetUnstructuredContent(map[string]interface{}{ 442 "metadata": map[string]interface{}{ 443 "name": "object1", 444 "resourceVersion": "object1", 445 }, 446 }) 447 object1.SetGroupVersionKind(fakeGVK) 448 449 object1v2 := &unstructured.Unstructured{} 450 object1v2.SetUnstructuredContent(map[string]interface{}{ 451 "metadata": map[string]interface{}{ 452 "name": "object1", 453 "resourceVersion": "object1v2", 454 }, 455 }) 456 object1v2.SetGroupVersionKind(fakeGVK) 457 458 object2 := &unstructured.Unstructured{} 459 object2.SetUnstructuredContent(map[string]interface{}{ 460 "metadata": map[string]interface{}{ 461 "name": "object2", 462 "resourceVersion": "object2", 463 }, 464 }) 465 object2.SetGroupVersionKind(fakeGVK) 466 467 object3 := &unstructured.Unstructured{} 468 object3.SetUnstructuredContent(map[string]interface{}{ 469 "metadata": map[string]interface{}{ 470 "name": "object3", 471 "resourceVersion": "object3", 472 }, 473 }) 474 object3.SetGroupVersionKind(fakeGVK) 475 476 namespacedObject1 := &unstructured.Unstructured{} 477 namespacedObject1.SetUnstructuredContent(map[string]interface{}{ 478 "metadata": map[string]interface{}{ 479 "name": "namespacedObject1", 480 "namespace": "test", 481 "resourceVersion": "namespacedObject1", 482 }, 483 }) 484 namespacedObject1.SetGroupVersionKind(fakeGVK) 485 486 namespacedObject2 := &unstructured.Unstructured{} 487 namespacedObject2.SetUnstructuredContent(map[string]interface{}{ 488 "metadata": map[string]interface{}{ 489 "name": "namespacedObject2", 490 "namespace": "test", 491 "resourceVersion": "namespacedObject2", 492 }, 493 }) 494 namespacedObject2.SetGroupVersionKind(fakeGVK) 495 496 require.NoError(t, tracker.Add(object1)) 497 require.NoError(t, tracker.Add(object2)) 498 499 require.NoError(t, wait.PollUntilContextTimeout(testContext, 100*time.Millisecond, 500*time.Millisecond, false, func(ctx context.Context) (done bool, err error) { 500 return myController.Informer().LastSyncResourceVersion() == object2.GetResourceVersion(), nil 501 })) 502 503 values, err := myController.Informer().List(labels.Everything()) 504 require.NoError(t, err) 505 require.ElementsMatch(t, []*unstructured.Unstructured{object1, object2}, values) 506 507 require.NoError(t, tracker.Update(fakeGVR, object1v2, object1v2.GetNamespace())) 508 require.NoError(t, tracker.Delete(fakeGVR, object2.GetNamespace(), object2.GetName())) 509 require.NoError(t, tracker.Add(object3)) 510 511 require.NoError(t, wait.PollUntilContextTimeout(testContext, 100*time.Millisecond, 500*time.Millisecond, false, func(ctx context.Context) (done bool, err error) { 512 return myController.Informer().LastSyncResourceVersion() == object3.GetResourceVersion(), nil 513 })) 514 515 values, err = myController.Informer().List(labels.Everything()) 516 require.NoError(t, err) 517 require.ElementsMatch(t, []*unstructured.Unstructured{object1v2, object3}, values) 518 519 require.NoError(t, tracker.Add(namespacedObject1)) 520 require.NoError(t, tracker.Add(namespacedObject2)) 521 522 require.NoError(t, wait.PollUntilContextTimeout(testContext, 100*time.Millisecond, 500*time.Millisecond, false, func(ctx context.Context) (done bool, err error) { 523 return myController.Informer().LastSyncResourceVersion() == namespacedObject2.GetResourceVersion(), nil 524 })) 525 values, err = myController.Informer().Namespaced(namespacedObject1.GetNamespace()).List(labels.Everything()) 526 require.NoError(t, err) 527 require.ElementsMatch(t, []*unstructured.Unstructured{namespacedObject1, namespacedObject2}, values) 528 529 value, err := myController.Informer().Get(object3.GetName()) 530 require.NoError(t, err) 531 require.Equal(t, value, object3) 532 533 value, err = myController.Informer().Namespaced(namespacedObject1.GetNamespace()).Get(namespacedObject1.GetName()) 534 require.NoError(t, err) 535 require.Equal(t, value, namespacedObject1) 536 537 _, err = myController.Informer().Get("fakeobjectname") 538 require.True(t, k8serrors.IsNotFound(err)) 539 540 _, err = myController.Informer().Namespaced("test").Get("fakeobjectname") 541 require.True(t, k8serrors.IsNotFound(err)) 542 543 _, err = myController.Informer().Namespaced("fakenamespace").Get("fakeobjectname") 544 require.True(t, k8serrors.IsNotFound(err)) 545 }