add generics tests

84.1% coverage
This commit is contained in:
Alexander Zielenski 2022-10-13 13:44:03 -07:00
parent 2286501e22
commit 74b103cd52
No known key found for this signature in database
GPG Key ID: 754BC11B447F7843

View File

@ -0,0 +1,516 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package generic_test
import (
"context"
"errors"
"fmt"
"reflect"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/admission/plugin/cel/internal/generic"
clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
)
type testInformer struct {
cache.SharedIndexInformer
lock sync.Mutex
registrations map[interface{}]struct{}
}
func (t *testInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) {
res, err := t.SharedIndexInformer.AddEventHandler(handler)
if err != nil {
return res, err
}
func() {
t.lock.Lock()
defer t.lock.Unlock()
if t.registrations == nil {
t.registrations = make(map[interface{}]struct{})
}
t.registrations[res] = struct{}{}
}()
return res, err
}
func (t *testInformer) RemoveEventHandler(registration cache.ResourceEventHandlerRegistration) error {
func() {
t.lock.Lock()
defer t.lock.Unlock()
if _, ok := t.registrations[registration]; !ok {
panic("removing unknown event handler?")
}
delete(t.registrations, registration)
}()
return t.SharedIndexInformer.RemoveEventHandler(registration)
}
var (
scheme *runtime.Scheme = runtime.NewScheme()
codecs serializer.CodecFactory = serializer.NewCodecFactory(scheme)
fakeGVR schema.GroupVersionResource = schema.GroupVersionResource{
Group: "fake.example.com",
Version: "v1",
Resource: "fakes",
}
fakeGVK schema.GroupVersionKind = fakeGVR.GroupVersion().WithKind("Fake")
fakeGVKList schema.GroupVersionKind = fakeGVR.GroupVersion().WithKind("FakeList")
)
func init() {
scheme.AddKnownTypeWithName(fakeGVK, &unstructured.Unstructured{})
scheme.AddKnownTypeWithName(fakeGVKList, &unstructured.UnstructuredList{})
}
func setupTest(ctx context.Context, customReconciler func(string, string, runtime.Object) error) (
tracker clienttesting.ObjectTracker,
controller generic.Controller[*unstructured.Unstructured],
informer *testInformer,
waitForReconcile func(runtime.Object) error,
) {
tracker = clienttesting.NewObjectTracker(scheme, codecs.UniversalDecoder())
reconciledObjects := make(chan runtime.Object)
// Set up fake informers that return instances of mock Policy definitoins
// and mock policy bindings
informer = &testInformer{SharedIndexInformer: cache.NewSharedIndexInformer(&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return tracker.List(fakeGVR, fakeGVK, "")
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return tracker.Watch(fakeGVR, "")
},
}, &unstructured.Unstructured{}, 30*time.Second, nil)}
reconciler := func(namespace, name string, newObj *unstructured.Unstructured) error {
var err error
copied := newObj.DeepCopyObject()
if customReconciler != nil {
err = customReconciler(namespace, name, newObj)
}
reconciledObjects <- copied
return err
}
waitForReconcile = func(obj runtime.Object) error {
select {
case reconciledObj := <-reconciledObjects:
if reflect.DeepEqual(obj, reconciledObj) {
return nil
}
return fmt.Errorf("expected equal objects: %v", cmp.Diff(obj, reconciledObj))
case <-ctx.Done():
return fmt.Errorf("context done before reconcile: %w", ctx.Err())
}
}
myController := generic.NewController(
generic.NewInformer[*unstructured.Unstructured](informer),
reconciler,
generic.ControllerOptions{},
)
go func() {
<-ctx.Done()
close(reconciledObjects)
for leftover := range reconciledObjects {
panic(fmt.Errorf("leftover object which was not anticipated by test: %v", leftover))
}
}()
return tracker, myController, informer, waitForReconcile
}
func TestReconcile(t *testing.T) {
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer testCancel()
tracker, myController, informer, waitForReconcile := setupTest(testContext, nil)
// Add object to informer
initialObject := &unstructured.Unstructured{}
initialObject.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object1",
"resourceVersion": "1",
},
})
initialObject.SetGroupVersionKind(fakeGVK)
require.NoError(t, tracker.Add(initialObject))
wg := sync.WaitGroup{}
// Start informer
wg.Add(1)
go func() {
defer wg.Done()
informer.Run(testContext.Done())
}()
// Start controller
wg.Add(1)
go func() {
defer wg.Done()
stopReason := myController.Run(testContext)
require.ErrorIs(t, stopReason, context.Canceled)
}()
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
// Wait for all enqueued reconciliations
require.NoError(t, waitForReconcile(initialObject))
// Updated object
updatedObject := &unstructured.Unstructured{}
updatedObject.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object1",
"resourceVersion": "2",
},
"newKey": "a key",
})
updatedObject.SetGroupVersionKind(fakeGVK)
require.NoError(t, tracker.Update(fakeGVR, updatedObject, ""))
// Wait for all enqueued reconciliations
require.NoError(t, waitForReconcile(updatedObject))
require.NoError(t, tracker.Delete(fakeGVR, updatedObject.GetNamespace(), updatedObject.GetName()))
require.NoError(t, waitForReconcile(nil))
testCancel()
wg.Wait()
}
func TestShutdown(t *testing.T) {
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer testCancel()
_, myController, informer, _ := setupTest(testContext, nil)
wg := sync.WaitGroup{}
// Start informer
wg.Add(1)
go func() {
defer wg.Done()
informer.Run(testContext.Done())
}()
// Start controller
wg.Add(1)
go func() {
defer wg.Done()
stopReason := myController.Run(testContext)
require.ErrorIs(t, stopReason, context.Canceled)
}()
// Wait for controller and informer to start up
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
// Stop the controller and informer
testCancel()
// Wait for controller and informer to stop
wg.Wait()
// Ensure the event handler was cleaned up
require.Empty(t, informer.registrations)
}
// Show an error is thrown informer isn't started when the controller runs
func TestInformerNeverStarts(t *testing.T) {
testContext, testCancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
defer testCancel()
_, myController, informer, _ := setupTest(testContext, nil)
wg := sync.WaitGroup{}
// Start controller
wg.Add(1)
go func() {
defer wg.Done()
stopReason := myController.Run(testContext)
require.ErrorIs(t, stopReason, context.DeadlineExceeded)
}()
// Wait for deadline to pass without syncing the cache
require.False(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
// Wait for controller to stop (or context deadline will pass quickly)
wg.Wait()
// Ensure there are no event handlers
require.Empty(t, informer.registrations)
}
// Shows that if RV does not change, the reconciler does not get called
func TestIgnoredUpdate(t *testing.T) {
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer testCancel()
tracker, myController, informer, waitForReconcile := setupTest(testContext, nil)
// Add object to informer
initialObject := &unstructured.Unstructured{}
initialObject.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object1",
"resourceVersion": "1",
},
})
initialObject.SetGroupVersionKind(fakeGVK)
require.NoError(t, tracker.Add(initialObject))
wg := sync.WaitGroup{}
// Start informer
wg.Add(1)
go func() {
defer wg.Done()
informer.Run(testContext.Done())
}()
// Start controller
wg.Add(1)
go func() {
defer wg.Done()
stopReason := myController.Run(testContext)
require.ErrorIs(t, stopReason, context.Canceled)
}()
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
// Wait for all enqueued reconciliations
require.NoError(t, waitForReconcile(initialObject))
// Send update with the same object
require.NoError(t, tracker.Update(fakeGVR, initialObject, ""))
// Don't wait for it to be reconciled
testCancel()
wg.Wait()
// Test infrastructure has logic to panic if there are any reconciled objects
// that weren't "expected"
}
// Shows that an object which fails reconciliation will retry
func TestReconcileRetry(t *testing.T) {
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer testCancel()
calls := atomic.Uint64{}
success := atomic.Bool{}
tracker, myController, _, waitForReconcile := setupTest(testContext, func(s1, s2 string, o runtime.Object) error {
if calls.Add(1) > 2 {
// Suddenly start liking the object
success.Store(true)
return nil
}
return errors.New("i dont like this object")
})
// Start informer
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
myController.Informer().Run(testContext.Done())
}()
// Start controller
wg.Add(1)
go func() {
defer wg.Done()
stopReason := myController.Run(testContext)
require.ErrorIs(t, stopReason, context.Canceled)
}()
// Add object to informer
initialObject := &unstructured.Unstructured{}
initialObject.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object1",
"resourceVersion": "1",
},
})
initialObject.SetGroupVersionKind(fakeGVK)
require.NoError(t, tracker.Add(initialObject))
require.NoError(t, waitForReconcile(initialObject), "initial reconcile")
require.NoError(t, waitForReconcile(initialObject), "previous reconcile failed, should retry quickly")
require.NoError(t, waitForReconcile(initialObject), "previous reconcile failed, should retry quickly")
// Will not try again since calls > 2 for last reconcile
require.True(t, success.Load(), "last call to reconcile should return success")
testCancel()
wg.Wait()
}
func TestInformerList(t *testing.T) {
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer testCancel()
tracker, myController, _, _ := setupTest(testContext, nil)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
myController.Informer().Run(testContext.Done())
}()
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
object1 := &unstructured.Unstructured{}
object1.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object1",
"resourceVersion": "object1",
},
})
object1.SetGroupVersionKind(fakeGVK)
object1v2 := &unstructured.Unstructured{}
object1v2.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object1",
"resourceVersion": "object1v2",
},
})
object1v2.SetGroupVersionKind(fakeGVK)
object2 := &unstructured.Unstructured{}
object2.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object2",
"resourceVersion": "object2",
},
})
object2.SetGroupVersionKind(fakeGVK)
object3 := &unstructured.Unstructured{}
object3.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "object3",
"resourceVersion": "object3",
},
})
object3.SetGroupVersionKind(fakeGVK)
namespacedObject1 := &unstructured.Unstructured{}
namespacedObject1.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "namespacedObject1",
"namespace": "test",
"resourceVersion": "namespacedObject1",
},
})
namespacedObject1.SetGroupVersionKind(fakeGVK)
namespacedObject2 := &unstructured.Unstructured{}
namespacedObject2.SetUnstructuredContent(map[string]interface{}{
"metadata": map[string]interface{}{
"name": "namespacedObject2",
"namespace": "test",
"resourceVersion": "namespacedObject2",
},
})
namespacedObject2.SetGroupVersionKind(fakeGVK)
require.NoError(t, tracker.Add(object1))
require.NoError(t, tracker.Add(object2))
require.NoError(t, wait.PollWithContext(testContext, 100*time.Millisecond, 500*time.Millisecond, func(ctx context.Context) (done bool, err error) {
return myController.Informer().LastSyncResourceVersion() == object2.GetResourceVersion(), nil
}))
values, err := myController.Informer().List(labels.Everything())
require.NoError(t, err)
require.ElementsMatch(t, []*unstructured.Unstructured{object1, object2}, values)
require.NoError(t, tracker.Update(fakeGVR, object1v2, object1v2.GetNamespace()))
require.NoError(t, tracker.Delete(fakeGVR, object2.GetNamespace(), object2.GetName()))
require.NoError(t, tracker.Add(object3))
require.NoError(t, wait.PollWithContext(testContext, 100*time.Millisecond, 500*time.Millisecond, func(ctx context.Context) (done bool, err error) {
return myController.Informer().LastSyncResourceVersion() == object3.GetResourceVersion(), nil
}))
values, err = myController.Informer().List(labels.Everything())
require.NoError(t, err)
require.ElementsMatch(t, []*unstructured.Unstructured{object1v2, object3}, values)
require.NoError(t, tracker.Add(namespacedObject1))
require.NoError(t, tracker.Add(namespacedObject2))
require.NoError(t, wait.PollWithContext(testContext, 100*time.Millisecond, 500*time.Millisecond, func(ctx context.Context) (done bool, err error) {
return myController.Informer().LastSyncResourceVersion() == namespacedObject2.GetResourceVersion(), nil
}))
values, err = myController.Informer().Namespaced(namespacedObject1.GetNamespace()).List(labels.Everything())
require.NoError(t, err)
require.ElementsMatch(t, []*unstructured.Unstructured{namespacedObject1, namespacedObject2}, values)
value, err := myController.Informer().Get(object3.GetName())
require.NoError(t, err)
require.Equal(t, value, object3)
value, err = myController.Informer().Namespaced(namespacedObject1.GetNamespace()).Get(namespacedObject1.GetName())
require.NoError(t, err)
require.Equal(t, value, namespacedObject1)
_, err = myController.Informer().Get("fakeobjectname")
require.True(t, k8serrors.IsNotFound(err))
_, err = myController.Informer().Namespaced("test").Get("fakeobjectname")
require.True(t, k8serrors.IsNotFound(err))
_, err = myController.Informer().Namespaced("fakenamespace").Get("fakeobjectname")
require.True(t, k8serrors.IsNotFound(err))
}