github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/dataprotection/restore_controller_test.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package dataprotection 21 22 import ( 23 "fmt" 24 "strconv" 25 "strings" 26 27 . "github.com/onsi/ginkgo/v2" 28 . "github.com/onsi/gomega" 29 "k8s.io/apimachinery/pkg/types" 30 31 batchv1 "k8s.io/api/batch/v1" 32 corev1 "k8s.io/api/core/v1" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 35 dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1" 36 "github.com/1aal/kubeblocks/pkg/constant" 37 dprestore "github.com/1aal/kubeblocks/pkg/dataprotection/restore" 38 dputils "github.com/1aal/kubeblocks/pkg/dataprotection/utils" 39 "github.com/1aal/kubeblocks/pkg/generics" 40 testapps "github.com/1aal/kubeblocks/pkg/testutil/apps" 41 testdp "github.com/1aal/kubeblocks/pkg/testutil/dataprotection" 42 ) 43 44 var _ = Describe("Restore Controller test", func() { 45 cleanEnv := func() { 46 // must wait till resources deleted and no longer existed before the testcases start, 47 // otherwise if later it needs to create some new resource objects with the same name, 48 // in race conditions, it will find the existence of old objects, resulting failure to 49 // create the new objects. 50 By("clean resources") 51 52 // delete rest mocked objects 53 inNS := client.InNamespace(testCtx.DefaultNamespace) 54 ml := client.HasLabels{testCtx.TestObjLabelKey} 55 56 // namespaced 57 testapps.ClearResources(&testCtx, generics.ClusterSignature, inNS, ml) 58 testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml) 59 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupSignature, true, inNS) 60 61 // wait all backup to be deleted, otherwise the controller maybe create 62 // job to delete the backup between the ClearResources function delete 63 // the job and get the job list, resulting the ClearResources panic. 64 Eventually(testapps.List(&testCtx, generics.BackupSignature, inNS)).Should(HaveLen(0)) 65 66 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.JobSignature, true, inNS) 67 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.RestoreSignature, true, inNS) 68 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeClaimSignature, true, inNS) 69 testapps.ClearResources(&testCtx, generics.SecretSignature, inNS, ml) 70 71 // non-namespaced 72 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.ActionSetSignature, true, ml) 73 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.StorageClassSignature, true, ml) 74 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupRepoSignature, true, ml) 75 testapps.ClearResources(&testCtx, generics.StorageProviderSignature, ml) 76 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeSignature, true, ml) 77 } 78 79 BeforeEach(func() { 80 cleanEnv() 81 }) 82 83 AfterEach(func() { 84 cleanEnv() 85 }) 86 87 When("restore controller test", func() { 88 var ( 89 repoPVCName string 90 actionSet *dpv1alpha1.ActionSet 91 nodeName = "minikube" 92 ) 93 94 BeforeEach(func() { 95 By("creating an actionSet") 96 actionSet = testdp.NewFakeActionSet(&testCtx) 97 98 By("creating storage provider") 99 _ = testdp.NewFakeStorageProvider(&testCtx, nil) 100 101 By("creating a backupRepo") 102 _, repoPVCName = testdp.NewFakeBackupRepo(&testCtx, nil) 103 }) 104 105 initResourcesAndWaitRestore := func( 106 mockBackupCompleted, 107 useVolumeSnapshot, 108 isSerialPolicy bool, 109 expectRestorePhase dpv1alpha1.RestorePhase, 110 change func(f *testdp.MockRestoreFactory)) *dpv1alpha1.Restore { 111 By("create a completed backup") 112 backup := mockBackupForRestore(actionSet.Name, repoPVCName, mockBackupCompleted, useVolumeSnapshot) 113 114 By("create restore ") 115 schedulingSpec := dpv1alpha1.SchedulingSpec{ 116 NodeName: nodeName, 117 } 118 restoreFactory := testdp.NewRestoreactory(testCtx.DefaultNamespace, testdp.RestoreName). 119 SetBackup(backup.Name, testCtx.DefaultNamespace). 120 SetSchedulingSpec(schedulingSpec) 121 122 change(restoreFactory) 123 124 if isSerialPolicy { 125 restoreFactory.SetVolumeClaimRestorePolicy(dpv1alpha1.VolumeClaimRestorePolicySerial) 126 } 127 restore := restoreFactory.Create(&testCtx).GetObject() 128 129 By(fmt.Sprintf("wait for restore is %s", expectRestorePhase)) 130 restoreKey := client.ObjectKeyFromObject(restore) 131 Eventually(testapps.CheckObj(&testCtx, restoreKey, func(g Gomega, r *dpv1alpha1.Restore) { 132 g.Expect(r.Status.Phase).Should(Equal(expectRestorePhase)) 133 })).Should(Succeed()) 134 return restore 135 } 136 137 checkJobAndPVCSCount := func(restore *dpv1alpha1.Restore, jobReplicas, pvcReplicas, startingIndex int) { 138 Eventually(testapps.List(&testCtx, generics.JobSignature, 139 client.MatchingLabels{dprestore.DataProtectionLabelRestoreKey: restore.Name}, 140 client.InNamespace(testCtx.DefaultNamespace))).Should(HaveLen(jobReplicas)) 141 142 pvcMatchingLabels := client.MatchingLabels{constant.AppManagedByLabelKey: "restore"} 143 Eventually(testapps.List(&testCtx, generics.PersistentVolumeClaimSignature, pvcMatchingLabels, 144 client.InNamespace(testCtx.DefaultNamespace))).Should(HaveLen(pvcReplicas)) 145 146 By(fmt.Sprintf("pvc index should greater than or equal to %d", startingIndex)) 147 pvcList := &corev1.PersistentVolumeClaimList{} 148 Expect(k8sClient.List(ctx, pvcList, pvcMatchingLabels, 149 client.InNamespace(testCtx.DefaultNamespace))).Should(Succeed()) 150 for _, v := range pvcList.Items { 151 indexStr := string(v.Name[len(v.Name)-1]) 152 index, _ := strconv.Atoi(indexStr) 153 Expect(index >= startingIndex).Should(BeTrue()) 154 } 155 } 156 157 mockRestoreJobsCompleted := func(restore *dpv1alpha1.Restore) { 158 jobList := &batchv1.JobList{} 159 Expect(k8sClient.List(ctx, jobList, 160 client.MatchingLabels{dprestore.DataProtectionLabelRestoreKey: restore.Name}, 161 client.InNamespace(testCtx.DefaultNamespace))).Should(Succeed()) 162 for _, v := range jobList.Items { 163 testdp.PatchK8sJobStatus(&testCtx, client.ObjectKeyFromObject(&v), batchv1.JobComplete) 164 } 165 } 166 167 testRestoreWithVolumeClaimsTemplate := func(replicas, startingIndex int) { 168 restore := initResourcesAndWaitRestore(true, false, false, dpv1alpha1.RestorePhaseRunning, 169 func(f *testdp.MockRestoreFactory) { 170 f.SetVolumeClaimsTemplate(testdp.MysqlTemplateName, testdp.DataVolumeName, 171 testdp.DataVolumeMountPath, "", int32(replicas), int32(startingIndex)) 172 }) 173 174 By("expect restore jobs and pvcs are created") 175 checkJobAndPVCSCount(restore, replicas, replicas, startingIndex) 176 177 By("mock jobs are completed") 178 mockRestoreJobsCompleted(restore) 179 180 By("wait for restore is completed") 181 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(restore), func(g Gomega, r *dpv1alpha1.Restore) { 182 g.Expect(r.Status.Phase).Should(Equal(dpv1alpha1.RestorePhaseCompleted)) 183 })).Should(Succeed()) 184 } 185 186 Context("with restore fails", func() { 187 It("test restore is Failed when backup is not completed", func() { 188 By("expect for restore is Failed ") 189 restore := initResourcesAndWaitRestore(false, false, true, dpv1alpha1.RestorePhaseRunning, 190 func(f *testdp.MockRestoreFactory) { 191 f.SetVolumeClaimsTemplate(testdp.MysqlTemplateName, testdp.DataVolumeName, 192 testdp.DataVolumeMountPath, "", int32(3), int32(0)) 193 }) 194 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(restore), func(g Gomega, r *dpv1alpha1.Restore) { 195 g.Expect(r.Status.Phase).Should(Equal(dpv1alpha1.RestorePhaseFailed)) 196 })).Should(Succeed()) 197 }) 198 199 It("test restore is Failed when restore job is not Failed", func() { 200 By("expect for restore is Failed ") 201 restore := initResourcesAndWaitRestore(true, false, true, dpv1alpha1.RestorePhaseRunning, 202 func(f *testdp.MockRestoreFactory) { 203 f.SetVolumeClaimsTemplate(testdp.MysqlTemplateName, testdp.DataVolumeName, 204 testdp.DataVolumeMountPath, "", int32(3), int32(0)) 205 }) 206 207 By("wait for creating first job and pvc") 208 checkJobAndPVCSCount(restore, 1, 1, 0) 209 210 By("mock restore job is Failed") 211 jobList := &batchv1.JobList{} 212 Expect(k8sClient.List(ctx, jobList, 213 client.MatchingLabels{dprestore.DataProtectionLabelRestoreKey: restore.Name}, 214 client.InNamespace(testCtx.DefaultNamespace))).Should(Succeed()) 215 216 for _, v := range jobList.Items { 217 testdp.PatchK8sJobStatus(&testCtx, client.ObjectKeyFromObject(&v), batchv1.JobFailed) 218 } 219 220 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(restore), func(g Gomega, r *dpv1alpha1.Restore) { 221 g.Expect(r.Status.Phase).Should(Equal(dpv1alpha1.RestorePhaseFailed)) 222 })).Should(Succeed()) 223 }) 224 }) 225 226 Context("test prepareData stage", func() { 227 It("test volumeClaimsTemplate when startingIndex is 0", func() { 228 testRestoreWithVolumeClaimsTemplate(3, 0) 229 }) 230 231 It("test volumeClaimsTemplate when startingIndex is 1", func() { 232 testRestoreWithVolumeClaimsTemplate(2, 1) 233 }) 234 235 It("test volumeClaimsTemplate when volumeClaimRestorePolicy is Serial", func() { 236 replicas := 2 237 startingIndex := 1 238 restore := initResourcesAndWaitRestore(true, false, true, dpv1alpha1.RestorePhaseRunning, 239 func(f *testdp.MockRestoreFactory) { 240 f.SetVolumeClaimsTemplate(testdp.MysqlTemplateName, testdp.DataVolumeName, 241 testdp.DataVolumeMountPath, "", int32(replicas), int32(startingIndex)) 242 }) 243 244 By("wait for creating first job and pvc") 245 checkJobAndPVCSCount(restore, 1, 1, startingIndex) 246 247 By("mock jobs are completed") 248 mockRestoreJobsCompleted(restore) 249 250 var firstJobName string 251 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(restore), func(g Gomega, r *dpv1alpha1.Restore) { 252 g.Expect(r.Status.Actions.PrepareData).ShouldNot(BeEmpty()) 253 g.Expect(r.Status.Actions.PrepareData[0].Status).Should(Equal(dpv1alpha1.RestoreActionCompleted)) 254 firstJobName = strings.ReplaceAll(r.Status.Actions.PrepareData[0].ObjectKey, "Job/", "") 255 })).Should(Succeed()) 256 257 By("wait for deleted first job") 258 Eventually(testapps.CheckObjExists(&testCtx, 259 types.NamespacedName{Name: firstJobName, Namespace: testCtx.DefaultNamespace}, &batchv1.Job{}, false)).Should(Succeed()) 260 261 By("after the first job is completed, next job will be created") 262 checkJobAndPVCSCount(restore, 1, replicas, startingIndex) 263 264 jobList := &batchv1.JobList{} 265 Expect(k8sClient.List(ctx, jobList, 266 client.MatchingLabels{dprestore.DataProtectionLabelRestoreKey: restore.Name}, 267 client.InNamespace(testCtx.DefaultNamespace))).Should(Succeed()) 268 269 for _, v := range jobList.Items { 270 finished, _, _ := dputils.IsJobFinished(&v) 271 Expect(finished).Should(BeFalse()) 272 } 273 274 By("mock jobs are completed") 275 mockRestoreJobsCompleted(restore) 276 277 By("wait for restore is completed") 278 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(restore), func(g Gomega, r *dpv1alpha1.Restore) { 279 g.Expect(r.Status.Phase).Should(Equal(dpv1alpha1.RestorePhaseCompleted)) 280 })).Should(Succeed()) 281 }) 282 283 It("test dataSourceRef", func() { 284 initResourcesAndWaitRestore(true, true, false, dpv1alpha1.RestorePhaseAsDataSource, 285 func(f *testdp.MockRestoreFactory) { 286 f.SetDataSourceRef(testdp.DataVolumeName, testdp.DataVolumeMountPath) 287 }) 288 }) 289 290 }) 291 292 Context("test postReady stage", func() { 293 var _ *testdp.BackupClusterInfo 294 BeforeEach(func() { 295 By("fake a new cluster") 296 _ = testdp.NewFakeCluster(&testCtx) 297 }) 298 299 It("test post ready actions", func() { 300 By("remove the prepareData stage for testing post ready actions") 301 Expect(testapps.ChangeObj(&testCtx, actionSet, func(set *dpv1alpha1.ActionSet) { 302 set.Spec.Restore.PrepareData = nil 303 })).Should(Succeed()) 304 305 matchLabels := map[string]string{ 306 constant.AppInstanceLabelKey: testdp.ClusterName, 307 } 308 restore := initResourcesAndWaitRestore(true, false, false, dpv1alpha1.RestorePhaseRunning, 309 func(f *testdp.MockRestoreFactory) { 310 f.SetConnectCredential(testdp.ClusterName).SetJobActionConfig(matchLabels).SetExecActionConfig(matchLabels) 311 }) 312 313 By("wait for creating two exec jobs with the matchLabels") 314 Eventually(testapps.List(&testCtx, generics.JobSignature, 315 client.MatchingLabels{dprestore.DataProtectionLabelRestoreKey: restore.Name}, 316 client.InNamespace(testCtx.DefaultNamespace))).Should(HaveLen(2)) 317 318 By("mock exec jobs are completed") 319 mockRestoreJobsCompleted(restore) 320 321 By("wait for creating a job of jobAction with the matchLabels, expect jobs count is 3(2+1)") 322 Eventually(testapps.List(&testCtx, generics.JobSignature, 323 client.MatchingLabels{dprestore.DataProtectionLabelRestoreKey: restore.Name}, 324 client.InNamespace(testCtx.DefaultNamespace))).Should(HaveLen(3)) 325 326 By("mock jobs are completed") 327 mockRestoreJobsCompleted(restore) 328 329 By("wait for restore is completed") 330 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(restore), func(g Gomega, r *dpv1alpha1.Restore) { 331 g.Expect(r.Status.Phase).Should(Equal(dpv1alpha1.RestorePhaseCompleted)) 332 })).Should(Succeed()) 333 334 By("test deleting restore") 335 Expect(k8sClient.Delete(ctx, restore)).Should(Succeed()) 336 Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(restore), restore, false)).Should(Succeed()) 337 }) 338 }) 339 }) 340 }) 341 342 func mockBackupForRestore(actionSetName, backupPVCName string, mockBackupCompleted, useVolumeSnapshotBackup bool) *dpv1alpha1.Backup { 343 backup := testdp.NewFakeBackup(&testCtx, nil) 344 // wait for backup is failed by backup controller. 345 // it will be failed if the backupPolicy is not created. 346 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(backup), func(g Gomega, tmpBackup *dpv1alpha1.Backup) { 347 g.Expect(tmpBackup.Status.Phase).Should(Equal(dpv1alpha1.BackupPhaseFailed)) 348 })).Should(Succeed()) 349 350 if mockBackupCompleted { 351 // then mock backup to completed 352 backupMethodName := testdp.BackupMethodName 353 if useVolumeSnapshotBackup { 354 backupMethodName = testdp.VSBackupMethodName 355 } 356 Expect(testapps.ChangeObjStatus(&testCtx, backup, func() { 357 backup.Status.Phase = dpv1alpha1.BackupPhaseCompleted 358 backup.Status.PersistentVolumeClaimName = backupPVCName 359 testdp.MockBackupStatusMethod(backup, backupMethodName, testdp.DataVolumeName, actionSetName) 360 })).Should(Succeed()) 361 } 362 return backup 363 }