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  }