diff --git a/controllers/dataprotection/backup_controller_test.go b/controllers/dataprotection/backup_controller_test.go index da5b0fb2315..de4280d0d30 100644 --- a/controllers/dataprotection/backup_controller_test.go +++ b/controllers/dataprotection/backup_controller_test.go @@ -23,6 +23,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "time" + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -194,11 +196,58 @@ var _ = Describe("Backup Controller test", func() { }) backupKey := client.ObjectKeyFromObject(backup) + By("check backup failed and its expiration when retentionPeriod is not set") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed)) + g.Expect(fetched.Status.Expiration).Should(BeNil()) + })).Should(Succeed()) + }) + }) + + Context("creates a backup with retentionPeriod", func() { + It("create an valid backup", func() { + By("creating a backup from backupPolicy " + testdp.BackupPolicyName) + backup := testdp.NewFakeBackup(&testCtx, func(backup *dpv1alpha1.Backup) { + backup.Spec.RetentionPeriod = "1h" + }) + backupKey := client.ObjectKeyFromObject(backup) + + getJobKey := func() client.ObjectKey { + return client.ObjectKey{ + Name: dpbackup.GenerateBackupJobName(backup, dpbackup.BackupDataJobNamePrefix), + Namespace: backup.Namespace, + } + } + + By("check backup expiration is set by start time when backup is running") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).Should(Equal(dpv1alpha1.BackupPhaseRunning)) + g.Expect(fetched.Status.Expiration.Second()).Should(Equal(fetched.Status.StartTimestamp.Add(time.Hour).Second())) + })).Should(Succeed()) + + testdp.PatchK8sJobStatus(&testCtx, getJobKey(), batchv1.JobComplete) + + By("check backup expiration is update by completion time when backup is completed") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseCompleted)) + g.Expect(fetched.Status.CompletionTimestamp).ShouldNot(BeNil()) + g.Expect(fetched.Status.Expiration.Second()).Should(Equal(fetched.Status.CompletionTimestamp.Add(time.Hour).Second())) + })).Should(Succeed()) + }) + + It("create an invalid backup", func() { + By("creating a backup using a not found backupPolicy") + backup := testdp.NewFakeBackup(&testCtx, func(backup *dpv1alpha1.Backup) { + backup.Spec.BackupPolicyName = "not-found" + backup.Spec.RetentionPeriod = "1h" + }) + backupKey := client.ObjectKeyFromObject(backup) + By("check backup failed and its expiration is set") Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed)) g.Expect(fetched.Status.Expiration).ShouldNot(BeNil()) - })) + })).Should(Succeed()) }) }) @@ -361,6 +410,10 @@ var _ = Describe("Backup Controller test", func() { }) When("with exceptional settings", func() { + var ( + backupPolicy *dpv1alpha1.BackupPolicy + ) + Context("creates a backup with non-existent backup policy", func() { var backupKey types.NamespacedName BeforeEach(func() { @@ -375,6 +428,76 @@ var _ = Describe("Backup Controller test", func() { })).Should(Succeed()) }) }) + + Context("creates a backup using non-existent backup method", func() { + BeforeEach(func() { + By("creating a backupPolicy without backup method") + backupPolicy = testdp.NewFakeBackupPolicy(&testCtx, nil) + }) + + It("should fail because of no-existent backup method", func() { + backup := testdp.NewFakeBackup(&testCtx, func(backup *dpv1alpha1.Backup) { + backup.Spec.BackupPolicyName = backupPolicy.Name + backup.Spec.BackupMethod = "non-existent" + }) + backupKey := client.ObjectKeyFromObject(backup) + + By("check backup status failed") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed)) + })).Should(Succeed()) + }) + }) + + Context("creates a backup with invalid backup method", func() { + BeforeEach(func() { + backupPolicy = testdp.NewFakeBackupPolicy(&testCtx, func(backupPolicy *dpv1alpha1.BackupPolicy) { + backupPolicy.Spec.BackupMethods = append(backupPolicy.Spec.BackupMethods, dpv1alpha1.BackupMethod{ + Name: "invalid", + ActionSetName: "", + }) + }) + }) + + It("should fail because backup method doesn't specify snapshotVolumes with empty actionSet", func() { + backup := testdp.NewFakeBackup(&testCtx, func(backup *dpv1alpha1.Backup) { + backup.Spec.BackupPolicyName = backupPolicy.Name + backup.Spec.BackupMethod = "invalid" + }) + backupKey := client.ObjectKeyFromObject(backup) + + By("check backup status failed") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed)) + })).Should(Succeed()) + }) + + It("should fail because of no-existing actionSet", func() { + backup := testdp.NewFakeBackup(&testCtx, nil) + backupKey := client.ObjectKeyFromObject(backup) + + By("check backup status failed") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed)) + })).Should(Succeed()) + }) + + It("should fail because actionSet's backup type isn't Full", func() { + actionSet := testdp.NewFakeActionSet(&testCtx) + actionSetKey := client.ObjectKeyFromObject(actionSet) + Eventually(testapps.GetAndChangeObj(&testCtx, actionSetKey, func(fetched *dpv1alpha1.ActionSet) { + fetched.Spec.BackupType = dpv1alpha1.BackupTypeIncremental + })) + + backup := testdp.NewFakeBackup(&testCtx, nil) + backupKey := client.ObjectKeyFromObject(backup) + + By("check backup status failed") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed)) + })).Should(Succeed()) + }) + }) }) When("with backup repo", func() { diff --git a/pkg/dataprotection/backup/deleter_test.go b/pkg/dataprotection/backup/deleter_test.go index 5ea0bbeea2b..72877c8d75e 100644 --- a/pkg/dataprotection/backup/deleter_test.go +++ b/pkg/dataprotection/backup/deleter_test.go @@ -122,6 +122,37 @@ var _ = Describe("Backup Deleter Test", func() { job := &batchv1.Job{} key := BuildDeleteBackupFilesJobKey(backup) Eventually(testapps.CheckObjExists(&testCtx, key, job, true)).Should(Succeed()) + + By("delete backup with job running") + backupKey := client.ObjectKeyFromObject(backup) + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + status, err := deleter.DeleteBackupFiles(fetched) + Expect(err).ShouldNot(HaveOccurred()) + Expect(status).Should(Equal(DeletionStatusDeleting)) + })).Should(Succeed()) + + By("delete backup with job succeed") + testdp.ReplaceK8sJobStatus(&testCtx, key, batchv1.JobComplete) + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + status, err := deleter.DeleteBackupFiles(fetched) + Expect(err).ShouldNot(HaveOccurred()) + Expect(status).Should(Equal(DeletionStatusSucceeded)) + })).Should(Succeed()) + + By("delete backup with job failed") + testdp.ReplaceK8sJobStatus(&testCtx, key, batchv1.JobFailed) + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dpv1alpha1.Backup) { + status, err := deleter.DeleteBackupFiles(fetched) + Expect(err).Should(HaveOccurred()) + Expect(status).Should(Equal(DeletionStatusFailed)) + })).Should(Succeed()) + }) + + It("delete backup with backup repo", func() { + backup.Status.BackupRepoName = testdp.BackupRepoName + status, err := deleter.DeleteBackupFiles(backup) + Expect(err).ShouldNot(HaveOccurred()) + Expect(status).Should(Equal(DeletionStatusSucceeded)) }) }) diff --git a/pkg/dataprotection/backup/request_test.go b/pkg/dataprotection/backup/request_test.go index 03c19330a9b..02213b477b5 100644 --- a/pkg/dataprotection/backup/request_test.go +++ b/pkg/dataprotection/backup/request_test.go @@ -18,3 +18,130 @@ along with this program. If not, see . */ package backup + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/pkg/constant" + ctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" + "github.com/apecloud/kubeblocks/pkg/dataprotection/utils/boolptr" + "github.com/apecloud/kubeblocks/pkg/generics" + testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" + testdp "github.com/apecloud/kubeblocks/pkg/testutil/dataprotection" +) + +var _ = Describe("Request Test", func() { + buildRequest := func() *Request { + return &Request{ + RequestCtx: ctrlutil.RequestCtx{ + Log: logger, + Ctx: testCtx.Ctx, + Recorder: recorder, + }, + Client: testCtx.Cli, + } + } + + cleanEnv := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + + // delete rest mocked objects + inNS := client.InNamespace(testCtx.DefaultNamespace) + ml := client.HasLabels{testCtx.TestObjLabelKey} + + // namespaced + testapps.ClearResources(&testCtx, generics.ClusterSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupSignature, true, inNS) + + // wait all backup to be deleted, otherwise the controller maybe create + // job to delete the backup between the ClearResources function delete + // the job and get the job list, resulting the ClearResources panic. + Eventually(testapps.List(&testCtx, generics.BackupSignature, inNS)).Should(HaveLen(0)) + + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupPolicySignature, true, inNS) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.JobSignature, true, inNS) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeClaimSignature, true, inNS) + + // non-namespaced + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.ActionSetSignature, true, ml) + testapps.ClearResources(&testCtx, generics.StorageClassSignature, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupRepoSignature, true, ml) + testapps.ClearResources(&testCtx, generics.StorageProviderSignature, ml) + testapps.ClearResources(&testCtx, generics.VolumeSnapshotClassSignature, ml) + } + + var clusterInfo *testdp.BackupClusterInfo + + BeforeEach(func() { + cleanEnv() + clusterInfo = testdp.NewFakeCluster(&testCtx) + }) + + AfterEach(func() { + cleanEnv() + }) + + When("with default settings", func() { + var ( + backup *dpv1alpha1.Backup + actionSet *dpv1alpha1.ActionSet + backupPolicy *dpv1alpha1.BackupPolicy + backupRepo *dpv1alpha1.BackupRepo + + targetPod *corev1.Pod + request *Request + ) + + BeforeEach(func() { + actionSet = testapps.CreateCustomizedObj(&testCtx, "backup/actionset.yaml", + &dpv1alpha1.ActionSet{}, testapps.WithName(testdp.ActionSetName)) + backup = testdp.NewFakeBackup(&testCtx, nil) + backupRepo = testapps.CreateCustomizedObj(&testCtx, "backup/backuprepo.yaml", &dpv1alpha1.BackupRepo{}, nil) + backupPolicy = testdp.NewBackupPolicyFactory(testCtx.DefaultNamespace, testdp.BackupPolicyName). + SetBackupRepoName(testdp.BackupRepoName). + SetPathPrefix(testdp.BackupPathPrefix). + SetTarget(constant.AppInstanceLabelKey, testdp.ClusterName, + constant.KBAppComponentLabelKey, testdp.ComponentName, + constant.RoleLabelKey, constant.Leader). + AddBackupMethod(testdp.BackupMethodName, false, testdp.ActionSetName). + Create(&testCtx).GetObject() + + targetPod = clusterInfo.TargetPod + request = buildRequest() + }) + + Context("build action", func() { + It("should build action", func() { + request.Backup = backup + request.ActionSet = actionSet + request.TargetPods = []*corev1.Pod{targetPod} + request.BackupPolicy = backupPolicy + request.BackupMethod = &backupPolicy.Spec.BackupMethods[0] + request.BackupRepo = backupRepo + _, err := request.BuildActions() + Expect(err).NotTo(HaveOccurred()) + }) + + It("build create volume snapshot action", func() { + request.TargetPods = []*corev1.Pod{targetPod} + request.BackupMethod = &dpv1alpha1.BackupMethod{ + Name: testdp.VSBackupMethodName, + SnapshotVolumes: boolptr.True(), + } + _, err := request.buildCreateVolumeSnapshotAction() + Expect(err).Should(HaveOccurred()) + }) + + }) + }) +}) diff --git a/pkg/dataprotection/backup/scheduler_test.go b/pkg/dataprotection/backup/scheduler_test.go index 03c19330a9b..74ff4366ae6 100644 --- a/pkg/dataprotection/backup/scheduler_test.go +++ b/pkg/dataprotection/backup/scheduler_test.go @@ -18,3 +18,93 @@ along with this program. If not, see . */ package backup + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/client" + + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/pkg/constant" + ctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" + "github.com/apecloud/kubeblocks/pkg/generics" + testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" + testdp "github.com/apecloud/kubeblocks/pkg/testutil/dataprotection" +) + +var _ = Describe("Scheduler Test", func() { + buildScheduler := func() *Scheduler { + return &Scheduler{ + RequestCtx: ctrlutil.RequestCtx{ + Log: logger, + Ctx: testCtx.Ctx, + Recorder: recorder, + }, + Client: testCtx.Cli, + } + } + + cleanEnv := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + + // delete rest mocked objects + inNS := client.InNamespace(testCtx.DefaultNamespace) + + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupPolicySignature, true, inNS) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupScheduleSignature, true, inNS) + } + + BeforeEach(func() { + cleanEnv() + }) + + AfterEach(func() { + cleanEnv() + }) + + When("with default settings", func() { + var ( + backupPolicy *dpv1alpha1.BackupPolicy + backupSchedule *dpv1alpha1.BackupSchedule + + scheduler *Scheduler + ) + + BeforeEach(func() { + backupPolicy = testdp.NewBackupPolicyFactory(testCtx.DefaultNamespace, testdp.BackupPolicyName). + SetBackupRepoName(testdp.BackupRepoName). + SetPathPrefix(testdp.BackupPathPrefix). + SetTarget(constant.AppInstanceLabelKey, testdp.ClusterName, + constant.KBAppComponentLabelKey, testdp.ComponentName, + constant.RoleLabelKey, constant.Leader). + AddBackupMethod(testdp.BackupMethodName, false, testdp.ActionSetName). + AddBackupMethod(testdp.VSBackupMethodName, true, ""). + Create(&testCtx).GetObject() + backupSchedule = testdp.NewFakeBackupSchedule(&testCtx, nil) + + scheduler = buildScheduler() + }) + + Context("test Schedule", func() { + It("should schedule", func() { + scheduler.BackupSchedule = backupSchedule + scheduler.BackupPolicy = backupPolicy + Expect(scheduler.Schedule()).Should(Succeed()) + }) + + It("schedule should fail if invalid backup policy", func() { + scheduler.BackupSchedule = backupSchedule + scheduler.BackupPolicy = backupPolicy + for i := range scheduler.BackupPolicy.Spec.BackupMethods { + scheduler.BackupPolicy.Spec.BackupMethods[i].Name = "not-exist" + } + Expect(scheduler.Schedule()).ShouldNot(Succeed()) + }) + }) + }) +}) diff --git a/pkg/dataprotection/backup/suite_test.go b/pkg/dataprotection/backup/suite_test.go index 8bdc4dbe763..2fae8a912cf 100644 --- a/pkg/dataprotection/backup/suite_test.go +++ b/pkg/dataprotection/backup/suite_test.go @@ -42,6 +42,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + storagev1alpha1 "github.com/apecloud/kubeblocks/apis/storage/v1alpha1" ctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" "github.com/apecloud/kubeblocks/pkg/testutil" viper "github.com/apecloud/kubeblocks/pkg/viperx" @@ -109,6 +110,12 @@ var _ = BeforeSuite(func() { err = dpv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = storagev1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = vsv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})