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  }