github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/dataprotection_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 cluster 21 22 import ( 23 "bytes" 24 "context" 25 "fmt" 26 "strings" 27 "time" 28 29 . "github.com/onsi/ginkgo/v2" 30 . "github.com/onsi/gomega" 31 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime" 35 k8sapitypes "k8s.io/apimachinery/pkg/types" 36 "k8s.io/cli-runtime/pkg/genericiooptions" 37 "k8s.io/client-go/dynamic" 38 clientfake "k8s.io/client-go/rest/fake" 39 cmdtesting "k8s.io/kubectl/pkg/cmd/testing" 40 41 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 42 dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1" 43 "github.com/1aal/kubeblocks/pkg/cli/cluster" 44 "github.com/1aal/kubeblocks/pkg/cli/create" 45 "github.com/1aal/kubeblocks/pkg/cli/delete" 46 "github.com/1aal/kubeblocks/pkg/cli/list" 47 "github.com/1aal/kubeblocks/pkg/cli/testing" 48 "github.com/1aal/kubeblocks/pkg/cli/types" 49 "github.com/1aal/kubeblocks/pkg/cli/util" 50 "github.com/1aal/kubeblocks/pkg/constant" 51 dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types" 52 ) 53 54 var _ = Describe("DataProtection", func() { 55 const policyName = "policy" 56 const repoName = "repo" 57 var streams genericiooptions.IOStreams 58 var tf *cmdtesting.TestFactory 59 var out *bytes.Buffer 60 BeforeEach(func() { 61 streams, _, out, _ = genericiooptions.NewTestIOStreams() 62 tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) 63 tf.Client = &clientfake.RESTClient{} 64 }) 65 66 AfterEach(func() { 67 tf.Cleanup() 68 }) 69 70 Context("backup", func() { 71 initClient := func(policies ...*dpv1alpha1.BackupPolicy) { 72 clusterDef := testing.FakeClusterDef() 73 cluster := testing.FakeCluster(testing.ClusterName, testing.Namespace) 74 clusterDefLabel := map[string]string{ 75 constant.ClusterDefLabelKey: clusterDef.Name, 76 } 77 cluster.SetLabels(clusterDefLabel) 78 pods := testing.FakePods(1, testing.Namespace, testing.ClusterName) 79 objects := []runtime.Object{ 80 cluster, clusterDef, &pods.Items[0], 81 } 82 for _, v := range policies { 83 objects = append(objects, v) 84 } 85 tf.FakeDynamicClient = testing.FakeDynamicClient(objects...) 86 } 87 88 It("list-backup-policy", func() { 89 By("fake client") 90 defaultBackupPolicy := testing.FakeBackupPolicy(policyName, testing.ClusterName) 91 policy2 := testing.FakeBackupPolicy("policy1", testing.ClusterName) 92 policy3 := testing.FakeBackupPolicy("policy2", testing.ClusterName) 93 policy3.Namespace = "policy" 94 initClient(defaultBackupPolicy, policy2, policy3) 95 96 By("test list-backup-policy cmd") 97 cmd := NewListBackupPolicyCmd(tf, streams) 98 Expect(cmd).ShouldNot(BeNil()) 99 cmd.Run(cmd, nil) 100 Expect(out.String()).Should(ContainSubstring(defaultBackupPolicy.Name)) 101 Expect(out.String()).Should(ContainSubstring("true")) 102 Expect(len(strings.Split(strings.Trim(out.String(), "\n"), "\n"))).Should(Equal(3)) 103 104 By("test list all namespace") 105 out.Reset() 106 _ = cmd.Flags().Set("all-namespaces", "true") 107 cmd.Run(cmd, nil) 108 fmt.Println(out.String()) 109 Expect(out.String()).Should(ContainSubstring(policy2.Name)) 110 Expect(len(strings.Split(strings.Trim(out.String(), "\n"), "\n"))).Should(Equal(4)) 111 }) 112 113 It("edit-backup-policy", func() { 114 By("fake client") 115 defaultBackupPolicy := testing.FakeBackupPolicy(policyName, testing.ClusterName) 116 repo := testing.FakeBackupRepo(repoName, false) 117 tf.FakeDynamicClient = testing.FakeDynamicClient(defaultBackupPolicy, repo) 118 119 By("test edit backup policy function") 120 o := editBackupPolicyOptions{Factory: tf, IOStreams: streams, GVR: types.BackupPolicyGVR()} 121 Expect(o.complete([]string{policyName})).Should(Succeed()) 122 o.values = []string{"backupRepoName=repo"} 123 Expect(o.runEditBackupPolicy()).Should(Succeed()) 124 125 By("test backup repo not exists") 126 o.values = []string{"backupRepoName=repo1"} 127 Expect(o.runEditBackupPolicy()).Should(MatchError(ContainSubstring(`"repo1" not found`))) 128 129 By("test with vim editor") 130 o.values = []string{} 131 o.isTest = true 132 Expect(o.runEditBackupPolicy()).Should(Succeed()) 133 }) 134 135 It("validate create backup", func() { 136 By("without cluster name") 137 o := &CreateBackupOptions{ 138 CreateOptions: create.CreateOptions{ 139 Dynamic: testing.FakeDynamicClient(), 140 IOStreams: streams, 141 Factory: tf, 142 }, 143 } 144 Expect(o.Validate()).To(MatchError("missing cluster name")) 145 146 By("test without default backupPolicy") 147 o.Name = testing.ClusterName 148 o.Namespace = testing.Namespace 149 initClient() 150 o.Dynamic = tf.FakeDynamicClient 151 Expect(o.Validate()).Should(MatchError(fmt.Errorf(`not found any backup policy for cluster "%s"`, testing.ClusterName))) 152 153 By("test with two default backupPolicy") 154 defaultBackupPolicy := testing.FakeBackupPolicy(policyName, testing.ClusterName) 155 initClient(defaultBackupPolicy, testing.FakeBackupPolicy("policy2", testing.ClusterName)) 156 o.Dynamic = tf.FakeDynamicClient 157 Expect(o.Validate()).Should(MatchError(fmt.Errorf(`cluster "%s" has multiple default backup policies`, o.Name))) 158 159 By("test without method") 160 initClient(defaultBackupPolicy) 161 o.Dynamic = tf.FakeDynamicClient 162 Expect(o.Validate().Error()).Should(ContainSubstring("backup method can not be empty, you can specify it by --method")) 163 164 By("test with one default backupPolicy") 165 initClient(defaultBackupPolicy) 166 o.Dynamic = tf.FakeDynamicClient 167 o.BackupMethod = testing.BackupMethodName 168 Expect(o.Validate()).Should(Succeed()) 169 }) 170 171 It("run backup command", func() { 172 defaultBackupPolicy := testing.FakeBackupPolicy(policyName, testing.ClusterName) 173 otherBackupPolicy := testing.FakeBackupPolicy("otherPolicy", testing.ClusterName) 174 otherBackupPolicy.Annotations = map[string]string{} 175 initClient(defaultBackupPolicy, otherBackupPolicy) 176 By("test backup with default backupPolicy") 177 cmd := NewCreateBackupCmd(tf, streams) 178 Expect(cmd).ShouldNot(BeNil()) 179 _ = cmd.Flags().Set("method", testing.BackupMethodName) 180 cmd.Run(cmd, []string{testing.ClusterName}) 181 182 By("test with specified backupMethod and backupPolicy") 183 o := &CreateBackupOptions{ 184 CreateOptions: create.CreateOptions{ 185 IOStreams: streams, 186 Factory: tf, 187 GVR: types.BackupGVR(), 188 CueTemplateName: "backup_template.cue", 189 Name: testing.ClusterName, 190 }, 191 BackupPolicy: otherBackupPolicy.Name, 192 BackupMethod: testing.BackupMethodName, 193 } 194 Expect(o.CompleteBackup()).Should(Succeed()) 195 err := o.Validate() 196 Expect(err).Should(Succeed()) 197 }) 198 }) 199 200 It("delete-backup", func() { 201 By("test delete-backup cmd") 202 cmd := NewDeleteBackupCmd(tf, streams) 203 Expect(cmd).ShouldNot(BeNil()) 204 205 args := []string{"test1"} 206 clusterLabel := util.BuildLabelSelectorByNames("", args) 207 208 By("test delete-backup with cluster") 209 o := delete.NewDeleteOptions(tf, streams, types.BackupGVR()) 210 Expect(completeForDeleteBackup(o, args)).Should(HaveOccurred()) 211 212 By("test delete-backup with cluster and force") 213 o.Force = true 214 Expect(completeForDeleteBackup(o, args)).Should(Succeed()) 215 Expect(o.LabelSelector == clusterLabel).Should(BeTrue()) 216 217 By("test delete-backup with cluster and force and labels") 218 o.Force = true 219 customLabel := "test=test" 220 o.LabelSelector = customLabel 221 Expect(completeForDeleteBackup(o, args)).Should(Succeed()) 222 Expect(o.LabelSelector == customLabel+","+clusterLabel).Should(BeTrue()) 223 }) 224 225 It("list-backup", func() { 226 cmd := NewListBackupCmd(tf, streams) 227 Expect(cmd).ShouldNot(BeNil()) 228 By("test list-backup cmd with no backup") 229 tf.FakeDynamicClient = testing.FakeDynamicClient() 230 o := ListBackupOptions{ListOptions: list.NewListOptions(tf, streams, types.BackupGVR())} 231 Expect(PrintBackupList(o)).Should(Succeed()) 232 Expect(o.ErrOut.(*bytes.Buffer).String()).Should(ContainSubstring("No backups found")) 233 234 By("test list-backup") 235 backup1 := testing.FakeBackup("test1") 236 backup1.Labels = map[string]string{ 237 constant.AppInstanceLabelKey: "apecloud-mysql", 238 } 239 backup1.Status.Phase = dpv1alpha1.BackupPhaseRunning 240 backup2 := testing.FakeBackup("test1") 241 backup2.Namespace = "backup" 242 tf.FakeDynamicClient = testing.FakeDynamicClient(backup1, backup2) 243 Expect(PrintBackupList(o)).Should(Succeed()) 244 Expect(o.Out.(*bytes.Buffer).String()).Should(ContainSubstring("test1")) 245 Expect(o.Out.(*bytes.Buffer).String()).Should(ContainSubstring("apecloud-mysql")) 246 247 By("test list all namespace") 248 o.Out.(*bytes.Buffer).Reset() 249 o.AllNamespaces = true 250 Expect(PrintBackupList(o)).Should(Succeed()) 251 Expect(len(strings.Split(strings.Trim(o.Out.(*bytes.Buffer).String(), "\n"), "\n"))).Should(Equal(3)) 252 }) 253 254 It("restore", func() { 255 timestamp := time.Now().Format("20060102150405") 256 backupName := "backup-test-" + timestamp 257 clusterName := "source-cluster-" + timestamp 258 newClusterName := "new-cluster-" + timestamp 259 secrets := testing.FakeSecrets(testing.Namespace, clusterName) 260 clusterDef := testing.FakeClusterDef() 261 clusterObj := testing.FakeCluster(clusterName, testing.Namespace) 262 clusterDefLabel := map[string]string{ 263 constant.ClusterDefLabelKey: clusterDef.Name, 264 } 265 clusterObj.SetLabels(clusterDefLabel) 266 backupPolicy := testing.FakeBackupPolicy("backPolicy", clusterObj.Name) 267 268 pods := testing.FakePods(1, testing.Namespace, clusterName) 269 tf.FakeDynamicClient = testing.FakeDynamicClient(&secrets.Items[0], 270 &pods.Items[0], clusterDef, clusterObj, backupPolicy) 271 tf.Client = &clientfake.RESTClient{} 272 // create backup 273 cmd := NewCreateBackupCmd(tf, streams) 274 Expect(cmd).ShouldNot(BeNil()) 275 _ = cmd.Flags().Set("method", testing.BackupMethodName) 276 _ = cmd.Flags().Set("name", backupName) 277 cmd.Run(nil, []string{clusterName}) 278 279 By("restore new cluster from source cluster which is not deleted") 280 // mock backup is ok 281 mockBackupInfo(tf.FakeDynamicClient, backupName, clusterName, nil, "") 282 cmdRestore := NewCreateRestoreCmd(tf, streams) 283 Expect(cmdRestore != nil).To(BeTrue()) 284 _ = cmdRestore.Flags().Set("backup", backupName) 285 cmdRestore.Run(nil, []string{newClusterName}) 286 newClusterObj := &appsv1alpha1.Cluster{} 287 Expect(cluster.GetK8SClientObject(tf.FakeDynamicClient, newClusterObj, types.ClusterGVR(), testing.Namespace, newClusterName)).Should(Succeed()) 288 Expect(clusterObj.Spec.ComponentSpecs[0].Replicas).Should(Equal(int32(1))) 289 // check if cluster contains the annotation for restoring 290 Expect(newClusterObj.Annotations[constant.RestoreFromBackupAnnotationKey]).Should(ContainSubstring(constant.ConnectionPassword)) 291 Expect(newClusterObj.Annotations[constant.RestoreFromBackupAnnotationKey]).Should(ContainSubstring(constant.BackupNamespaceKeyForRestore)) 292 By("restore new cluster from source cluster which is deleted") 293 // mock cluster is not lived in kubernetes 294 mockBackupInfo(tf.FakeDynamicClient, backupName, "deleted-cluster", nil, "") 295 cmdRestore.Run(nil, []string{newClusterName + "1"}) 296 297 By("run restore cmd with cluster spec.affinity=nil") 298 patchCluster := []byte(`{"spec":{"affinity":null}}`) 299 _, _ = tf.FakeDynamicClient.Resource(types.ClusterGVR()).Namespace(testing.Namespace).Patch(context.TODO(), clusterName, 300 k8sapitypes.MergePatchType, patchCluster, metav1.PatchOptions{}) 301 cmdRestore.Run(nil, []string{newClusterName + "-with-nil-affinity"}) 302 }) 303 304 // It("restore-to-time", func() { 305 // timestamp := time.Now().Format("20060102150405") 306 // backupName := "backup-test-" + timestamp 307 // backupName1 := backupName + "1" 308 // clusterName := "source-cluster-" + timestamp 309 // secrets := testing.FakeSecrets(testing.Namespace, clusterName) 310 // clusterDef := testing.FakeClusterDef() 311 // cluster := testing.FakeCluster(clusterName, testing.Namespace) 312 // clusterDefLabel := map[string]string{ 313 // constant.ClusterDefLabelKey: clusterDef.Name, 314 // } 315 // cluster.SetLabels(clusterDefLabel) 316 // backupPolicy := testing.FakeBackupPolicy("backPolicy", cluster.Name) 317 // backupTypeMeta := testing.FakeBackup("backup-none").TypeMeta 318 // backupLabels := map[string]string{ 319 // constant.AppInstanceLabelKey: clusterName, 320 // constant.KBAppComponentLabelKey: "test", 321 // dptypes.DataProtectionLabelClusterUIDKey: string(cluster.UID), 322 // } 323 // now := metav1.Now() 324 // baseBackup := testapps.NewBackupFactory(testing.Namespace, "backup-base"). 325 // SetBackupMethod(dpv1alpha1.BackupTypeSnapshot). 326 // SetBackupTimeRange(now.Add(-time.Minute), now.Add(-time.Second)). 327 // SetLabels(backupLabels).GetObject() 328 // baseBackup.TypeMeta = backupTypeMeta 329 // baseBackup.Status.Phase = dpv1alpha1.BackupPhaseCompleted 330 // logfileBackup := testapps.NewBackupFactory(testing.Namespace, backupName). 331 // SetBackupMethod(dpv1alpha1.BackupTypeLogFile). 332 // SetBackupTimeRange(now.Add(-time.Minute), now.Add(time.Minute)). 333 // SetLabels(backupLabels).GetObject() 334 // logfileBackup.TypeMeta = backupTypeMeta 335 // 336 // logfileBackup1 := testapps.NewBackupFactory(testing.Namespace, backupName1). 337 // SetBackupMethod(dpv1alpha1.BackupTypeLogFile). 338 // SetBackupTimeRange(now.Add(-time.Minute), now.Add(2*time.Minute)).GetObject() 339 // uid := string(cluster.UID) 340 // logfileBackup1.Labels = map[string]string{ 341 // constant.AppInstanceLabelKey: clusterName, 342 // constant.KBAppComponentLabelKey: "test", 343 // constant.DataProtectionLabelClusterUIDKey: uid[:30] + "00", 344 // } 345 // logfileBackup1.TypeMeta = backupTypeMeta 346 // 347 // pods := testing.FakePods(1, testing.Namespace, clusterName) 348 // tf.FakeDynamicClient = fake.NewSimpleDynamicClient( 349 // scheme.Scheme, &secrets.Items[0], &pods.Items[0], cluster, backupPolicy, baseBackup, logfileBackup, logfileBackup1) 350 // tf.Client = &clientfake.RESTClient{} 351 // 352 // By("restore new cluster from source cluster which is not deleted") 353 // cmdRestore := NewCreateRestoreCmd(tf, streams) 354 // Expect(cmdRestore != nil).To(BeTrue()) 355 // _ = cmdRestore.Flags().Set("restore-to-time", util.TimeFormatWithDuration(&now, time.Second)) 356 // _ = cmdRestore.Flags().Set("source-cluster", clusterName) 357 // cmdRestore.Run(nil, []string{}) 358 // 359 // // test with RFC3339 format 360 // _ = cmdRestore.Flags().Set("restore-to-time", now.Format(time.RFC3339)) 361 // _ = cmdRestore.Flags().Set("source-cluster", clusterName) 362 // cmdRestore.Run(nil, []string{"new-cluster"}) 363 // 364 // By("restore should be failed when backups belong to different source clusters") 365 // o := &CreateRestoreOptions{CreateOptions: create.CreateOptions{ 366 // IOStreams: streams, 367 // Factory: tf, 368 // }} 369 // restoreTime := time.Now().Add(90 * time.Second) 370 // o.RestoreTimeStr = util.TimeFormatWithDuration(&metav1.Time{Time: restoreTime}, time.Second) 371 // o.SourceCluster = clusterName 372 // Expect(o.Complete()).Should(Succeed()) 373 // Expect(o.validateRestoreTime().Error()).Should(ContainSubstring("restore-to-time is out of time range")) 374 // }) 375 376 It("describe-backup", func() { 377 cmd := NewDescribeBackupCmd(tf, streams) 378 Expect(cmd).ShouldNot(BeNil()) 379 By("test describe-backup cmd with no backup") 380 tf.FakeDynamicClient = testing.FakeDynamicClient() 381 o := DescribeBackupOptions{ 382 Factory: tf, 383 IOStreams: streams, 384 Gvr: types.BackupGVR(), 385 } 386 args := []string{} 387 Expect(o.Complete(args)).Should(HaveOccurred()) 388 389 By("test describe-backup") 390 backupName := "test1" 391 backup1 := testing.FakeBackup(backupName) 392 args = append(args, backupName) 393 backup1.Status.Phase = dpv1alpha1.BackupPhaseCompleted 394 logNow := metav1.Now() 395 backup1.Status.StartTimestamp = &logNow 396 backup1.Status.CompletionTimestamp = &logNow 397 backup1.Status.Expiration = &logNow 398 backup1.Status.Duration = &metav1.Duration{Duration: logNow.Sub(logNow.Time)} 399 tf.FakeDynamicClient = testing.FakeDynamicClient(backup1) 400 Expect(o.Complete(args)).Should(Succeed()) 401 o.client = testing.FakeClientSet() 402 Expect(o.Run()).Should(Succeed()) 403 }) 404 405 It("describe-backup-policy", func() { 406 cmd := NewDescribeBackupPolicyCmd(tf, streams) 407 Expect(cmd).ShouldNot(BeNil()) 408 By("test describe-backup-policy cmd with cluster and backupPolicy") 409 tf.FakeDynamicClient = testing.FakeDynamicClient() 410 o := DescribeBackupPolicyOptions{ 411 Factory: tf, 412 IOStreams: streams, 413 } 414 Expect(o.Complete()).Should(Succeed()) 415 Expect(o.Validate()).Should(HaveOccurred()) 416 417 By("test describe-backup-policy with cluster") 418 policyName := "test1" 419 policy1 := testing.FakeBackupPolicy(policyName, testing.ClusterName) 420 tf.FakeDynamicClient = testing.FakeDynamicClient(policy1) 421 o.client = testing.FakeClientSet() 422 o.ClusterNames = []string{testing.ClusterName} 423 Expect(o.Complete()).Should(Succeed()) 424 Expect(o.Validate()).Should(Succeed()) 425 Expect(o.Run()).Should(Succeed()) 426 427 By("test describe-backup-policy with backupPolicy") 428 o = DescribeBackupPolicyOptions{ 429 Factory: tf, 430 IOStreams: streams, 431 } 432 o.Names = []string{policyName} 433 o.client = testing.FakeClientSet() 434 Expect(o.Complete()).Should(Succeed()) 435 Expect(o.Validate()).Should(Succeed()) 436 Expect(o.Run()).Should(Succeed()) 437 }) 438 439 }) 440 441 func mockBackupInfo(dynamic dynamic.Interface, backupName, clusterName string, timeRange map[string]any, backupMethod string) { 442 clusterString := fmt.Sprintf(`{"metadata":{"name":"deleted-cluster","namespace":"%s"},"spec":{"clusterDefinitionRef":"apecloud-mysql","clusterVersionRef":"ac-mysql-8.0.30","componentSpecs":[{"name":"mysql","componentDefRef":"mysql","replicas":1}]}}`, testing.Namespace) 443 backupStatus := &unstructured.Unstructured{ 444 Object: map[string]any{ 445 "status": map[string]any{ 446 "phase": "Completed", 447 "timeRange": timeRange, 448 }, 449 "metadata": map[string]any{ 450 "name": backupName, 451 "annotations": map[string]any{ 452 constant.ClusterSnapshotAnnotationKey: clusterString, 453 dptypes.ConnectionPasswordKey: "test-password", 454 }, 455 "labels": map[string]any{ 456 constant.AppInstanceLabelKey: clusterName, 457 constant.KBAppComponentLabelKey: "test", 458 }, 459 }, 460 "spec": map[string]any{ 461 "backupMethod": backupMethod, 462 }, 463 }, 464 } 465 _, err := dynamic.Resource(types.BackupGVR()).Namespace(testing.Namespace).UpdateStatus(context.TODO(), 466 backupStatus, metav1.UpdateOptions{}) 467 Expect(err).Should(Succeed()) 468 }