diff --git a/changelogs/unreleased/6368-Lyndon-Li b/changelogs/unreleased/6368-Lyndon-Li new file mode 100644 index 000000000..dfb815699 --- /dev/null +++ b/changelogs/unreleased/6368-Lyndon-Li @@ -0,0 +1 @@ +Add UT for pkg/util \ No newline at end of file diff --git a/pkg/util/kube/pod_test.go b/pkg/util/kube/pod_test.go index 8131b02c7..6f39c0b23 100644 --- a/pkg/util/kube/pod_test.go +++ b/pkg/util/kube/pod_test.go @@ -30,6 +30,8 @@ import ( "k8s.io/client-go/kubernetes/fake" clientTesting "k8s.io/client-go/testing" + + velerotest "github.com/vmware-tanzu/velero/pkg/test" ) func TestEnsureDeletePod(t *testing.T) { @@ -91,3 +93,253 @@ func TestEnsureDeletePod(t *testing.T) { }) } } + +func TestIsPodRunning(t *testing.T) { + tests := []struct { + name string + pod *corev1api.Pod + err string + }{ + { + name: "pod is nil", + err: "invalid input pod", + }, + { + name: "pod is not scheduled", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + }, + Status: corev1api.PodStatus{ + Phase: "fake-phase", + }, + }, + err: "pod is not scheduled, name=fake-pod, namespace=fake-ns, phase=fake-phase", + }, + { + name: "pod is not running", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + }, + Spec: corev1api.PodSpec{ + NodeName: "fake-node", + }, + Status: corev1api.PodStatus{ + Phase: "fake-phase", + }, + }, + err: "pod is not in the expected status, name=fake-pod, namespace=fake-ns, phase=fake-phase: pod is not running", + }, + { + name: "pod is being deleted", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }, + Spec: corev1api.PodSpec{ + NodeName: "fake-node", + }, + Status: corev1api.PodStatus{ + Phase: corev1api.PodRunning, + }, + }, + err: "pod is being terminated, name=fake-pod, namespace=fake-ns, phase=Running", + }, + { + name: "success", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + }, + Spec: corev1api.PodSpec{ + NodeName: "fake-node", + }, + Status: corev1api.PodStatus{ + Phase: corev1api.PodRunning, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsPodRunning(test.pod) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestIsPodScheduled(t *testing.T) { + tests := []struct { + name string + pod *corev1api.Pod + err string + }{ + { + name: "pod is nil", + err: "invalid input pod", + }, + { + name: "pod is not scheduled", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + }, + Status: corev1api.PodStatus{ + Phase: "fake-phase", + }, + }, + err: "pod is not scheduled, name=fake-pod, namespace=fake-ns, phase=fake-phase", + }, + { + name: "pod is not running or pending", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + }, + Spec: corev1api.PodSpec{ + NodeName: "fake-node", + }, + Status: corev1api.PodStatus{ + Phase: "fake-phase", + }, + }, + err: "pod is not in the expected status, name=fake-pod, namespace=fake-ns, phase=fake-phase: pod is not running or pending", + }, + { + name: "pod is being deleted", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }, + Spec: corev1api.PodSpec{ + NodeName: "fake-node", + }, + Status: corev1api.PodStatus{ + Phase: corev1api.PodRunning, + }, + }, + err: "pod is being terminated, name=fake-pod, namespace=fake-ns, phase=Running", + }, + { + name: "success on running", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + }, + Spec: corev1api.PodSpec{ + NodeName: "fake-node", + }, + Status: corev1api.PodStatus{ + Phase: corev1api.PodRunning, + }, + }, + }, + { + name: "success on pending", + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pod", + }, + Spec: corev1api.PodSpec{ + NodeName: "fake-node", + }, + Status: corev1api.PodStatus{ + Phase: corev1api.PodPending, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := IsPodScheduled(test.pod) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestDeletePodIfAny(t *testing.T) { + tests := []struct { + name string + podName string + podNamespace string + kubeClientObj []runtime.Object + kubeReactors []reactor + logMessage string + logLevel string + logError string + }{ + { + name: "get fail", + podName: "fake-pod", + podNamespace: "fake-namespace", + logMessage: "Abort deleting pod, it doesn't exist fake-namespace/fake-pod", + logLevel: "level=debug", + }, + { + name: "delete fail", + podName: "fake-pod", + podNamespace: "fake-namespace", + kubeReactors: []reactor{ + { + verb: "delete", + resource: "pods", + reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("fake-delete-error") + }, + }, + }, + logMessage: "Failed to delete pod fake-namespace/fake-pod", + logLevel: "level=error", + logError: "error=fake-delete-error", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) + + for _, reactor := range test.kubeReactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + var kubeClient kubernetes.Interface = fakeKubeClient + + logMessage := "" + DeletePodIfAny(context.Background(), kubeClient.CoreV1(), test.podName, test.podNamespace, velerotest.NewSingleLogger(&logMessage)) + + if len(test.logMessage) > 0 { + assert.Contains(t, logMessage, test.logMessage) + } + + if len(test.logLevel) > 0 { + assert.Contains(t, logMessage, test.logLevel) + } + + if len(test.logError) > 0 { + assert.Contains(t, logMessage, test.logError) + } + }) + } +} diff --git a/pkg/util/kube/pvc_pv_test.go b/pkg/util/kube/pvc_pv_test.go index bd96643fe..d31c5d57d 100644 --- a/pkg/util/kube/pvc_pv_test.go +++ b/pkg/util/kube/pvc_pv_test.go @@ -352,3 +352,485 @@ func TestDeletePVCIfAny(t *testing.T) { }) } } + +func TestDeletePVIfAny(t *testing.T) { + tests := []struct { + name string + pvName string + kubeClientObj []runtime.Object + kubeReactors []reactor + logMessage string + logLevel string + logError string + }{ + { + name: "get fail", + pvName: "fake-pv", + logMessage: "Abort deleting PV, it doesn't exist, fake-pv", + logLevel: "level=debug", + }, + { + name: "delete fail", + pvName: "fake-pv", + kubeReactors: []reactor{ + { + verb: "delete", + resource: "persistentvolumes", + reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("fake-delete-error") + }, + }, + }, + logMessage: "Failed to delete PV fake-pv", + logLevel: "level=error", + logError: "error=fake-delete-error", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) + + for _, reactor := range test.kubeReactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + var kubeClient kubernetes.Interface = fakeKubeClient + + logMessage := "" + DeletePVIfAny(context.Background(), kubeClient.CoreV1(), test.pvName, velerotest.NewSingleLogger(&logMessage)) + + if len(test.logMessage) > 0 { + assert.Contains(t, logMessage, test.logMessage) + } + + if len(test.logLevel) > 0 { + assert.Contains(t, logMessage, test.logLevel) + } + + if len(test.logError) > 0 { + assert.Contains(t, logMessage, test.logError) + } + }) + } +} + +func TestEnsureDeletePVC(t *testing.T) { + pvcObject := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pvc", + }, + } + + tests := []struct { + name string + clientObj []runtime.Object + pvcName string + namespace string + reactors []reactor + err string + }{ + { + name: "delete fail", + pvcName: "fake-pvc", + namespace: "fake-ns", + err: "error to delete pvc fake-pvc: persistentvolumeclaims \"fake-pvc\" not found", + }, + { + name: "wait fail", + pvcName: "fake-pvc", + namespace: "fake-ns", + clientObj: []runtime.Object{pvcObject}, + reactors: []reactor{ + { + verb: "get", + resource: "persistentvolumeclaims", + reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("fake-get-error") + }, + }, + }, + err: "error to retrieve pvc info for fake-pvc: error to get pvc fake-pvc: fake-get-error", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.clientObj...) + + for _, reactor := range test.reactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + var kubeClient kubernetes.Interface = fakeKubeClient + + err := EnsureDeletePVC(context.Background(), kubeClient.CoreV1(), test.pvcName, test.namespace, time.Millisecond) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestRebindPVC(t *testing.T) { + pvcObject := &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pvc", + Annotations: map[string]string{ + KubeAnnBindCompleted: "true", + KubeAnnBoundByController: "true", + }, + }, + } + + tests := []struct { + name string + clientObj []runtime.Object + pvc *corev1api.PersistentVolumeClaim + pv string + reactors []reactor + result *corev1api.PersistentVolumeClaim + err string + }{ + { + name: "path fail", + pvc: pvcObject, + pv: "fake-pv", + clientObj: []runtime.Object{pvcObject}, + reactors: []reactor{ + { + verb: "patch", + resource: "persistentvolumeclaims", + reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("fake-patch-error") + }, + }, + }, + err: "error patching PVC: fake-patch-error", + }, + { + name: "succeed", + pvc: pvcObject, + pv: "fake-pv", + clientObj: []runtime.Object{pvcObject}, + result: &corev1api.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-ns", + Name: "fake-pvc", + }, + Spec: corev1api.PersistentVolumeClaimSpec{ + VolumeName: "fake-pv", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.clientObj...) + + for _, reactor := range test.reactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + var kubeClient kubernetes.Interface = fakeKubeClient + + result, err := RebindPVC(context.Background(), kubeClient.CoreV1(), test.pvc, test.pv) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, test.result, result) + }) + } +} + +func TestResetPVBinding(t *testing.T) { + pvObject := &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + Annotations: map[string]string{ + KubeAnnBoundByController: "true", + }, + }, + Spec: corev1api.PersistentVolumeSpec{ + ClaimRef: &corev1api.ObjectReference{ + Kind: "fake-kind", + Namespace: "fake-ns", + Name: "fake-pvc", + }, + }, + } + + tests := []struct { + name string + clientObj []runtime.Object + pv *corev1api.PersistentVolume + labels map[string]string + reactors []reactor + result *corev1api.PersistentVolume + err string + }{ + { + name: "path fail", + pv: pvObject, + clientObj: []runtime.Object{pvObject}, + reactors: []reactor{ + { + verb: "patch", + resource: "persistentvolumes", + reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("fake-patch-error") + }, + }, + }, + err: "error patching PV: fake-patch-error", + }, + { + name: "succeed", + pv: pvObject, + labels: map[string]string{ + "fake-label-1": "fake-value-1", + "fake-label-2": "fake-value-2", + }, + clientObj: []runtime.Object{pvObject}, + result: &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + Labels: map[string]string{ + "fake-label-1": "fake-value-1", + "fake-label-2": "fake-value-2", + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.clientObj...) + + for _, reactor := range test.reactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + var kubeClient kubernetes.Interface = fakeKubeClient + + result, err := ResetPVBinding(context.Background(), kubeClient.CoreV1(), test.pv, test.labels) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, test.result, result) + }) + } +} + +func TestSetPVReclaimPolicy(t *testing.T) { + pvObject := &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimRetain, + }, + } + + tests := []struct { + name string + clientObj []runtime.Object + pv *corev1api.PersistentVolume + policy corev1api.PersistentVolumeReclaimPolicy + reactors []reactor + result *corev1api.PersistentVolume + err string + }{ + { + name: "policy not changed", + pv: pvObject, + policy: corev1api.PersistentVolumeReclaimRetain, + }, + { + name: "path fail", + pv: pvObject, + policy: corev1api.PersistentVolumeReclaimDelete, + clientObj: []runtime.Object{pvObject}, + reactors: []reactor{ + { + verb: "patch", + resource: "persistentvolumes", + reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.New("fake-patch-error") + }, + }, + }, + err: "error patching PV: fake-patch-error", + }, + { + name: "succeed", + pv: pvObject, + policy: corev1api.PersistentVolumeReclaimDelete, + clientObj: []runtime.Object{pvObject}, + result: &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.clientObj...) + + for _, reactor := range test.reactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + var kubeClient kubernetes.Interface = fakeKubeClient + + result, err := SetPVReclaimPolicy(context.Background(), kubeClient.CoreV1(), test.pv, test.policy) + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, test.result, result) + }) + } +} + +func TestWaitPVBound(t *testing.T) { + tests := []struct { + name string + pvName string + pvcName string + pvcNamespace string + kubeClientObj []runtime.Object + kubeReactors []reactor + expectedPV *corev1api.PersistentVolume + err string + }{ + { + name: "get pv error", + pvName: "fake-pv", + err: "error to wait for bound of PV: failed to get pv fake-pv: persistentvolumes \"fake-pv\" not found", + }, + { + name: "pvc claimRef miss", + pvName: "fake-pv", + kubeClientObj: []runtime.Object{ + &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + }, + }, + err: "error to wait for bound of PV: timed out waiting for the condition", + }, + { + name: "pvc claimRef pvc name mismatch", + pvName: "fake-pv", + pvcName: "fake-pvc", + kubeClientObj: []runtime.Object{ + &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + ClaimRef: &corev1api.ObjectReference{ + Kind: "fake-kind", + }, + }, + }, + }, + err: "error to wait for bound of PV: timed out waiting for the condition", + }, + { + name: "pvc claimRef pvc namespace mismatch", + pvName: "fake-pv", + pvcName: "fake-pvc", + pvcNamespace: "fake-ns", + kubeClientObj: []runtime.Object{ + &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + ClaimRef: &corev1api.ObjectReference{ + Kind: "fake-kind", + Name: "fake-pvc", + }, + }, + }, + }, + err: "error to wait for bound of PV: timed out waiting for the condition", + }, + { + name: "success", + pvName: "fake-pv", + pvcName: "fake-pvc", + pvcNamespace: "fake-ns", + kubeClientObj: []runtime.Object{ + &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + ClaimRef: &corev1api.ObjectReference{ + Kind: "fake-kind", + Name: "fake-pvc", + Namespace: "fake-ns", + }, + }, + }, + }, + expectedPV: &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + ClaimRef: &corev1api.ObjectReference{ + Kind: "fake-kind", + Name: "fake-pvc", + Namespace: "fake-ns", + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) + + for _, reactor := range test.kubeReactors { + fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc) + } + + var kubeClient kubernetes.Interface = fakeKubeClient + + pv, err := WaitPVBound(context.Background(), kubeClient.CoreV1(), test.pvName, test.pvcName, test.pvcNamespace, time.Millisecond) + + if err != nil { + assert.EqualError(t, err, test.err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, test.expectedPV, pv) + }) + } +}