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 }