package retryclient import ( "context" "fmt" "testing" "github.com/go-logr/logr/testr" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kruntime "k8s.io/apimachinery/pkg/runtime" k8stypes "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "edge-infra.dev/pkg/sds/lib/k8s/retryclient/types" ) var keyObj = corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", Namespace: "test-namespace", }, } func testPodList(pods ...corev1.Pod) *corev1.PodList { return &corev1.PodList{ Items: pods, } } func testPod(variant string) *corev1.Pod { return &corev1.Pod{TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", Namespace: "test-namespace", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: variant, Image: variant, }}, }, } } func SetupTestCtx(t *testing.T) context.Context { logOptions := testr.Options{ LogTimestamp: true, Verbosity: -1, } ctx := ctrl.LoggerInto(context.Background(), testr.NewWithOptions(t, logOptions)) return ctx } func TestWithRetry(t *testing.T) { var count int testCases := map[string]struct { client types.Retrier fn func(ctx context.Context) error expectError bool expectedAttempts int }{ "Success": { client: New(getMockKubeClient(), getMockKubeReader(), Config{}), fn: func(_ context.Context) error { count++ return nil }, expectError: false, expectedAttempts: 1, }, "SuccessAfterRetry": { client: New(getMockKubeClient(), getMockKubeReader(), Config{}), fn: func(_ context.Context) error { count++ if count < 3 { return fmt.Errorf("force retry") } return nil }, expectError: false, expectedAttempts: 3, }, "FailureAfterRetry": { client: New(getMockKubeClient(), getMockKubeReader(), Config{ MaxRetries: 2, }), fn: func(_ context.Context) error { count++ return fmt.Errorf("example failure") }, expectError: true, expectedAttempts: 3, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { count = 0 cli := tc.client.(*RetryClient) err := cli.withRetry(SetupTestCtx(t), tc.fn) switch tc.expectError { case true: assert.Error(t, err) case false: assert.NoError(t, err) } assert.Equal(t, tc.expectedAttempts, count) }) } } func TestSafeGet(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() cli := New( getMockKubeClient(testPod("cached")), getMockKubeReader(testPod("uncached")), Config{}) testCases := map[string]struct { fn func(context.Context, client.Object) error expected *corev1.Pod }{ "UseCache": { fn: func(ctx context.Context, obj client.Object) error { return cli.SafeGet(ctx, client.ObjectKeyFromObject(obj), obj) }, expected: testPod("cached"), }, "IgnoreCache": { fn: func(ctx context.Context, obj client.Object) error { return cli.IgnoreCache().SafeGet(ctx, client.ObjectKeyFromObject(obj), obj) }, expected: testPod("uncached"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { obj := keyObj.DeepCopy() require.NoError(t, tc.fn(SetupTestCtx(t), obj)) obj.ResourceVersion = "" assert.Equal(t, tc.expected, obj) }) } } func TestSafeList(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() cli := New( getMockKubeClient(testPod("cached")), getMockKubeReader(testPod("uncached")), Config{}) testCases := map[string]struct { fn func(context.Context, *corev1.PodList) error expected *corev1.PodList }{ "UseCache": { fn: func(ctx context.Context, podList *corev1.PodList) error { return cli.SafeList(ctx, podList) }, expected: testPodList(*testPod("cached")), }, "IgnoreCache": { fn: func(ctx context.Context, podList *corev1.PodList) error { return cli.IgnoreCache().SafeList(ctx, podList) }, expected: testPodList(*testPod("uncached")), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { podList := &corev1.PodList{ TypeMeta: metav1.TypeMeta{ Kind: "PodList", APIVersion: "v1", }, } require.NoError(t, tc.fn(SetupTestCtx(t), podList)) for i := range podList.Items { podList.Items[i].ResourceVersion = "" } assert.Equal(t, tc.expected, podList) }) } } func TestSafeCreate(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() cli := New(getMockKubeClient(), getMockKubeReader(), Config{}) require.NoError(t, cli.SafeCreate(SetupTestCtx(t), testPod("test-image"))) p := keyObj.DeepCopy() err := cli.Client().Get(SetupTestCtx(t), client.ObjectKeyFromObject(keyObj.DeepCopy()), p) require.NoError(t, err) p.ResourceVersion = "" assert.Equal(t, testPod("test-image"), p) } func TestSafeDelete(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() cli := New( getMockKubeClient(testPod("test-image")), getMockKubeReader(), Config{}) require.NoError(t, cli.SafeDelete(SetupTestCtx(t), testPod("test-image"))) err := cli.Client().Get(SetupTestCtx(t), client.ObjectKeyFromObject(keyObj.DeepCopy()), keyObj.DeepCopy()) assert.True(t, apierrors.IsNotFound(err)) } func TestSafeUpdate(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() cli := New( getMockKubeClient(testPod("test-image-1")), getMockKubeReader(testPod("test-image-2")), Config{}) testCases := map[string]struct { fn func(context.Context, k8stypes.NamespacedName, client.Object, func(context.Context, client.Object) error, ...client.UpdateOption) error updateFn func(context.Context, client.Object) error expected *corev1.Pod }{ "UseCache": { fn: cli.SafeUpdate, updateFn: func(_ context.Context, obj client.Object) error { testPod("test-image-2").DeepCopyInto(obj.(*corev1.Pod)) return nil }, expected: testPod("test-image-2"), }, "IgnoreCache": { fn: cli.IgnoreCache().SafeUpdate, updateFn: func(_ context.Context, obj client.Object) error { testPod("test-image-1").DeepCopyInto(obj.(*corev1.Pod)) return nil }, expected: testPod("test-image-1"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { pod := &corev1.Pod{} err := tc.fn(SetupTestCtx(t), client.ObjectKeyFromObject(keyObj.DeepCopy()), pod, tc.updateFn) require.NoError(t, err) pod.ResourceVersion = "" assert.Equal(t, tc.expected, pod) }) } } func TestIgnoreCache(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() cli := New( getMockKubeClient(), getMockKubeReader(), Config{}) rcli := cli.(*RetryClient) // Test that useCache is True before and after IgnoreCache() assert.True(t, rcli.useCache) assert.False(t, rcli.IgnoreCache().(*RetryClient).useCache) assert.True(t, rcli.useCache) } // Gets a fake K8s client with given K8s objects func getMockKubeClient(initObjs ...client.Object) client.Client { return fake.NewClientBuilder().WithScheme(createScheme()).WithObjects(initObjs...).Build() } func getMockKubeReader(initObjs ...client.Object) client.Reader { return fake.NewClientBuilder().WithScheme(createScheme()).WithObjects(initObjs...).Build() } // Creates scheme for the fake client from client-go and IENode schema func createScheme() *kruntime.Scheme { scheme := kruntime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) return scheme }