github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controller/plan/restore_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 plan
    21  
    22  import (
    23  	"fmt"
    24  	"time"
    25  
    26  	. "github.com/onsi/ginkgo/v2"
    27  	. "github.com/onsi/gomega"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/client-go/kubernetes/scheme"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  
    35  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    36  	dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1"
    37  	"github.com/1aal/kubeblocks/pkg/constant"
    38  	"github.com/1aal/kubeblocks/pkg/controller/component"
    39  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    40  	"github.com/1aal/kubeblocks/pkg/generics"
    41  	testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    42  	testdp "github.com/1aal/kubeblocks/pkg/testutil/dataprotection"
    43  )
    44  
    45  var _ = Describe("Restore", func() {
    46  	const defaultTTL = "7d"
    47  	const backupName = "test-backup-job"
    48  	const sourceCluster = "source-cluster"
    49  
    50  	var (
    51  		randomStr   = testCtx.GetRandomStr()
    52  		clusterName = "cluster-" + randomStr
    53  
    54  		now       = metav1.Now()
    55  		startTime = metav1.Time{Time: now.Add(-time.Hour * 2)}
    56  	)
    57  
    58  	cleanEnv := func() {
    59  		// must wait till resources deleted and no longer existed before the testcases start,
    60  		// otherwise if later it needs to create some new resource objects with the same name,
    61  		// in race conditions, it will find the existence of old objects, resulting failure to
    62  		// create the new objects.
    63  		By("clean resources")
    64  
    65  		// delete cluster(and all dependent sub-resources), clusterversion and clusterdef
    66  		testapps.ClearClusterResources(&testCtx)
    67  		inNS := client.InNamespace(testCtx.DefaultNamespace)
    68  		ml := client.HasLabels{testCtx.TestObjLabelKey}
    69  
    70  		deletionPropagation := metav1.DeletePropagationBackground
    71  		deletionGracePeriodSeconds := int64(0)
    72  		opts := client.DeleteAllOfOptions{
    73  			DeleteOptions: client.DeleteOptions{
    74  				GracePeriodSeconds: &deletionGracePeriodSeconds,
    75  				PropagationPolicy:  &deletionPropagation,
    76  			},
    77  		}
    78  		testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml, &opts)
    79  		testapps.ClearResources(&testCtx, generics.BackupSignature, inNS, ml)
    80  		testapps.ClearResources(&testCtx, generics.BackupPolicySignature, inNS, ml)
    81  		testapps.ClearResources(&testCtx, generics.RestoreSignature, inNS, ml)
    82  		testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeClaimSignature, true, inNS, ml)
    83  		//
    84  		// non-namespaced
    85  		testapps.ClearResources(&testCtx, generics.BackupPolicyTemplateSignature, ml)
    86  	}
    87  
    88  	BeforeEach(cleanEnv)
    89  
    90  	AfterEach(cleanEnv)
    91  
    92  	Context("Cluster Restore", func() {
    93  		const (
    94  			clusterDefName     = "test-clusterdef"
    95  			clusterVersionName = "test-clusterversion"
    96  			mysqlCompType      = "replicasets"
    97  			mysqlCompName      = "mysql"
    98  			nginxCompType      = "proxy"
    99  			topologyKey        = "testTopologyKey"
   100  			labelKey           = "testNodeLabelKey"
   101  			labelValue         = "testLabelValue"
   102  		)
   103  
   104  		var (
   105  			clusterDef              *appsv1alpha1.ClusterDefinition
   106  			clusterVersion          *appsv1alpha1.ClusterVersion
   107  			cluster                 *appsv1alpha1.Cluster
   108  			synthesizedComponent    *component.SynthesizedComponent
   109  			pvc                     *corev1.PersistentVolumeClaim
   110  			backup                  *dpv1alpha1.Backup
   111  			fullBackupActionSet     *dpv1alpha1.ActionSet
   112  			fullBackupActionSetName string
   113  		)
   114  
   115  		BeforeEach(func() {
   116  			clusterDef = testapps.NewClusterDefFactory(clusterDefName).
   117  				AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompType).
   118  				AddComponentDef(testapps.StatelessNginxComponent, nginxCompType).
   119  				Create(&testCtx).GetObject()
   120  			clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName).
   121  				AddComponentVersion(mysqlCompType).
   122  				AddContainerShort("mysql", testapps.ApeCloudMySQLImage).
   123  				AddComponentVersion(nginxCompType).
   124  				AddInitContainerShort("nginx-init", testapps.NginxImage).
   125  				AddContainerShort("nginx", testapps.NginxImage).
   126  				Create(&testCtx).GetObject()
   127  			pvcSpec := testapps.NewPVCSpec("1Gi")
   128  			cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName,
   129  				clusterDef.Name, clusterVersion.Name).
   130  				AddComponent(mysqlCompName, mysqlCompType).
   131  				SetReplicas(3).
   132  				SetClusterAffinity(&appsv1alpha1.Affinity{
   133  					PodAntiAffinity: appsv1alpha1.Required,
   134  					TopologyKeys:    []string{topologyKey},
   135  					NodeLabels: map[string]string{
   136  						labelKey: labelValue,
   137  					},
   138  				}).
   139  				AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec).
   140  				Create(&testCtx).GetObject()
   141  
   142  			By("By mocking a pvc")
   143  			pvc = testapps.NewPersistentVolumeClaimFactory(
   144  				testCtx.DefaultNamespace, "data-"+clusterName+"-"+mysqlCompName+"-0", clusterName, mysqlCompName, "data").
   145  				SetStorage("1Gi").
   146  				Create(&testCtx).GetObject()
   147  
   148  			By("By mocking a pod")
   149  			volume := corev1.Volume{Name: pvc.Name, VolumeSource: corev1.VolumeSource{
   150  				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: pvc.Name}}}
   151  			_ = testapps.NewPodFactory(testCtx.DefaultNamespace, clusterName+"-"+mysqlCompName+"-0").
   152  				AddAppInstanceLabel(clusterName).
   153  				AddAppComponentLabel(mysqlCompName).
   154  				AddAppManagedByLabel().
   155  				AddVolume(volume).
   156  				AddLabels(constant.ConsensusSetAccessModeLabelKey, string(appsv1alpha1.ReadWrite)).
   157  				AddContainer(corev1.Container{Name: testapps.DefaultMySQLContainerName, Image: testapps.ApeCloudMySQLImage}).
   158  				AddNodeName("fake-node-name").
   159  				Create(&testCtx).GetObject()
   160  
   161  			By("create actionset of full backup")
   162  			fullBackupActionSet = testapps.CreateCustomizedObj(&testCtx, "backup/actionset.yaml", &dpv1alpha1.ActionSet{}, testapps.RandomizedObjName())
   163  			fullBackupActionSetName = fullBackupActionSet.Name
   164  
   165  			By("By creating backup policyTemplate: ")
   166  			backupTplLabels := map[string]string{
   167  				constant.ClusterDefLabelKey: clusterDefName,
   168  			}
   169  			_ = testapps.NewBackupPolicyTemplateFactory("backup-policy-template").
   170  				WithRandomName().SetLabels(backupTplLabels).
   171  				AddBackupPolicy(mysqlCompName).
   172  				SetClusterDefRef(clusterDefName).
   173  				SetRetentionPeriod(defaultTTL).
   174  				AddBackupMethod(testdp.BackupMethodName, false, fullBackupActionSetName).
   175  				SetBackupMethodVolumeMounts(testapps.DataVolumeName, "/data")
   176  
   177  			clusterCompDefObj := clusterDef.Spec.ComponentDefs[0]
   178  			synthesizedComponent = &component.SynthesizedComponent{
   179  				WorkloadType:          appsv1alpha1.Consensus,
   180  				PodSpec:               clusterCompDefObj.PodSpec,
   181  				Probes:                clusterCompDefObj.Probes,
   182  				LogConfigs:            clusterCompDefObj.LogConfigs,
   183  				HorizontalScalePolicy: clusterCompDefObj.HorizontalScalePolicy,
   184  				VolumeClaimTemplates:  cluster.Spec.ComponentSpecs[0].ToVolumeClaimTemplates(),
   185  				Name:                  mysqlCompName,
   186  				VolumeTypes:           []appsv1alpha1.VolumeTypeSpec{{Name: testapps.DataVolumeName, Type: appsv1alpha1.VolumeTypeData}},
   187  				Replicas:              1,
   188  			}
   189  			By("By creating remote pvc: ")
   190  			remotePVC := testapps.NewPersistentVolumeClaimFactory(
   191  				testCtx.DefaultNamespace, "remote-pvc", clusterName, mysqlCompName, "log").
   192  				SetStorage("1Gi").
   193  				Create(&testCtx).GetObject()
   194  
   195  			By("By creating base backup: ")
   196  			backupLabels := map[string]string{
   197  				constant.AppInstanceLabelKey:    sourceCluster,
   198  				constant.KBAppComponentLabelKey: mysqlCompName,
   199  			}
   200  			backup = testdp.NewBackupFactory(testCtx.DefaultNamespace, backupName).
   201  				WithRandomName().SetLabels(backupLabels).
   202  				SetBackupPolicyName("test-fake").
   203  				SetBackupMethod(testdp.VSBackupMethodName).
   204  				Create(&testCtx).GetObject()
   205  			baseStartTime := &startTime
   206  			baseStopTime := &now
   207  			backup.Status = dpv1alpha1.BackupStatus{
   208  				Phase:                     dpv1alpha1.BackupPhaseCompleted,
   209  				StartTimestamp:            baseStartTime,
   210  				CompletionTimestamp:       baseStopTime,
   211  				PersistentVolumeClaimName: remotePVC.Name,
   212  			}
   213  			testdp.MockBackupStatusMethod(backup, testdp.VSBackupMethodName, testapps.DataVolumeName, testdp.ActionSetName)
   214  			patchBackupStatus(backup.Status, client.ObjectKeyFromObject(backup))
   215  		})
   216  
   217  		It("Test restore", func() {
   218  			By("restore from backup")
   219  			restoreFromBackup := fmt.Sprintf(`{"%s": {"name":"%s"}}`, mysqlCompName, backup.Name)
   220  			Expect(testapps.ChangeObj(&testCtx, cluster, func(tmpCluster *appsv1alpha1.Cluster) {
   221  				tmpCluster.Annotations = map[string]string{
   222  					constant.RestoreFromBackupAnnotationKey: restoreFromBackup,
   223  				}
   224  			})).Should(Succeed())
   225  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).Should(Succeed())
   226  			restoreMGR := NewRestoreManager(ctx, k8sClient, cluster, scheme.Scheme, nil, 3, 0)
   227  			err := restoreMGR.DoRestore(synthesizedComponent)
   228  			Expect(intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeNeedWaiting)).Should(BeTrue())
   229  
   230  			By("mock restore of prepareData stage to Completed")
   231  			restoreMeta := restoreMGR.GetRestoreObjectMeta(synthesizedComponent, dpv1alpha1.PrepareData)
   232  			namedspace := types.NamespacedName{Name: restoreMeta.Name, Namespace: restoreMeta.Namespace}
   233  			Expect(testapps.GetAndChangeObjStatus(&testCtx, namedspace, func(restore *dpv1alpha1.Restore) {
   234  				restore.Status.Phase = dpv1alpha1.RestorePhaseCompleted
   235  			})()).ShouldNot(HaveOccurred())
   236  
   237  			By("mock cluster phase to Running")
   238  			Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() {
   239  				cluster.Status.Phase = appsv1alpha1.RunningClusterPhase
   240  				cluster.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{
   241  					mysqlCompName: {
   242  						Phase: appsv1alpha1.RunningClusterCompPhase,
   243  					},
   244  				}
   245  			})).Should(Succeed())
   246  
   247  			By("wait for postReady restore created and mock it to Completed")
   248  			restoreMGR.Cluster = cluster
   249  			_ = restoreMGR.DoRestore(synthesizedComponent)
   250  
   251  			// check if restore CR of postReady stage is created.
   252  			restoreMeta = restoreMGR.GetRestoreObjectMeta(synthesizedComponent, dpv1alpha1.PostReady)
   253  			namedspace = types.NamespacedName{Name: restoreMeta.Name, Namespace: restoreMeta.Namespace}
   254  			Eventually(testapps.CheckObjExists(&testCtx, namedspace,
   255  				&dpv1alpha1.Restore{}, true)).Should(Succeed())
   256  			// set restore to Completed
   257  			Expect(testapps.GetAndChangeObjStatus(&testCtx, namedspace, func(restore *dpv1alpha1.Restore) {
   258  				restore.Status.Phase = dpv1alpha1.RestorePhaseCompleted
   259  			})()).ShouldNot(HaveOccurred())
   260  
   261  			By("clean up annotations after cluster running")
   262  			_ = restoreMGR.DoRestore(synthesizedComponent)
   263  			Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) {
   264  				g.Expect(tmpCluster.Annotations[constant.RestoreFromBackupAnnotationKey]).Should(BeEmpty())
   265  			})).Should(Succeed())
   266  		})
   267  
   268  		It("unsupported restore to different namespace", func() {
   269  			const fakeNamespace = "fake-namespace"
   270  			restoreFromBackup := fmt.Sprintf(`{"%s": {"name":"%s", "namespace":"%s"}}`, mysqlCompName, backup.Name, fakeNamespace)
   271  			Expect(testapps.ChangeObj(&testCtx, cluster, func(tmpCluster *appsv1alpha1.Cluster) {
   272  				tmpCluster.Annotations = map[string]string{
   273  					constant.RestoreFromBackupAnnotationKey: restoreFromBackup,
   274  				}
   275  			})).Should(Succeed())
   276  			Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).Should(Succeed())
   277  			restoreMGR := NewRestoreManager(ctx, k8sClient, cluster, scheme.Scheme, nil, 3, 0)
   278  			err := restoreMGR.DoRestore(synthesizedComponent)
   279  			Expect(intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeRestoreFailed)).Should(BeTrue())
   280  		})
   281  	})
   282  })
   283  
   284  func patchBackupStatus(status dpv1alpha1.BackupStatus, key types.NamespacedName) {
   285  	Eventually(testapps.GetAndChangeObjStatus(&testCtx, key, func(fetched *dpv1alpha1.Backup) {
   286  		fetched.Status = status
   287  	})).Should(Succeed())
   288  }