github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/dataprotection/backuprepo_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 "time" 24 25 . "github.com/onsi/ginkgo/v2" 26 . "github.com/onsi/gomega" 27 batchv1 "k8s.io/api/batch/v1" 28 corev1 "k8s.io/api/core/v1" 29 storagev1 "k8s.io/api/storage/v1" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/meta" 32 "k8s.io/apimachinery/pkg/api/resource" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/types" 35 "k8s.io/client-go/kubernetes" 36 "k8s.io/utils/clock/testing" 37 "sigs.k8s.io/controller-runtime/pkg/client" 38 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 39 40 dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1" 41 storagev1alpha1 "github.com/1aal/kubeblocks/apis/storage/v1alpha1" 42 "github.com/1aal/kubeblocks/pkg/constant" 43 dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types" 44 "github.com/1aal/kubeblocks/pkg/generics" 45 testapps "github.com/1aal/kubeblocks/pkg/testutil/apps" 46 viper "github.com/1aal/kubeblocks/pkg/viperx" 47 ) 48 49 var _ = Describe("BackupRepo controller", func() { 50 const namespace2 = "namespace2" 51 const pvcProtectionFinalizer = "kubernetes.io/pvc-protection" 52 53 cleanEnv := func() { 54 // must wait till resources deleted and no longer existed before the testcases start, 55 // otherwise if later it needs to create some new resource objects with the same name, 56 // in race conditions, it will find the existence of old objects, resulting failure to 57 // create the new objects. 58 By("clean resources") 59 // non-namespaced 60 ml := client.HasLabels{testCtx.TestObjLabelKey} 61 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupRepoSignature, true, ml) 62 testapps.ClearResources(&testCtx, generics.StorageProviderSignature, ml) 63 testapps.ClearResources(&testCtx, generics.CSIDriverSignature, ml) 64 testapps.ClearResources(&testCtx, generics.StorageClassSignature, ml) 65 66 // namespaced 67 inNS := client.InNamespace(viper.GetString(constant.CfgKeyCtrlrMgrNS)) 68 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeClaimSignature, true, inNS, ml) 69 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupSignature, true, inNS, ml) 70 testapps.ClearResources(&testCtx, generics.SecretSignature, inNS, ml) 71 testapps.ClearResources(&testCtx, generics.JobSignature, inNS, ml) 72 73 // namespace2 74 inNS2 := client.InNamespace(namespace2) 75 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeClaimSignature, true, inNS2, ml) 76 testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupSignature, true, inNS2, ml) 77 testapps.ClearResources(&testCtx, generics.SecretSignature, inNS2, ml) 78 testapps.ClearResources(&testCtx, generics.JobSignature, inNS2, ml) 79 80 // delete namespace2 81 Eventually(func(g Gomega) { 82 // from https://github.com/kubernetes-sigs/controller-runtime/issues/880#issuecomment-749742403 83 namespaceObj := &corev1.Namespace{} 84 err := testCtx.Cli.Get(testCtx.Ctx, types.NamespacedName{Name: namespace2}, namespaceObj) 85 if apierrors.IsNotFound(err) { 86 return 87 } 88 namespaceObj.Spec.Finalizers = []corev1.FinalizerName{} 89 // We have to use the k8s.io/client-go library here to expose 90 // ability to patch the /finalize subresource on the namespace 91 clientGo, err := kubernetes.NewForConfig(testEnv.Config) 92 Expect(err).Should(Succeed()) 93 _, err = clientGo.CoreV1().Namespaces().Finalize(testCtx.Ctx, namespaceObj, metav1.UpdateOptions{}) 94 Expect(err).Should(Succeed()) 95 }).Should(Succeed()) 96 97 // By("deleting the Namespace to perform the tests") 98 // Eventually(func(g Gomega) { 99 // namespace := testCtx.GetNamespaceObj() 100 // err := testCtx.Cli.Delete(testCtx.Ctx, &namespace) 101 // g.Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) 102 // g.Expect(client.IgnoreNotFound(testCtx.Cli.Get( 103 // testCtx.Ctx, testCtx.GetNamespaceKey(), &namespace))).To(Not(HaveOccurred())) 104 // }).Should(Succeed()) 105 } 106 107 ensureNamespace := func(name string) { 108 Eventually(func(g Gomega) { 109 obj := &corev1.Namespace{} 110 obj.Name = name 111 err := testCtx.Cli.Get(testCtx.Ctx, client.ObjectKeyFromObject(obj), &corev1.Namespace{}) 112 if err == nil { 113 return 114 } 115 g.Expect(client.IgnoreNotFound(err)).Should(Succeed()) 116 err = testCtx.Cli.Create(testCtx.Ctx, obj) 117 g.Expect(err).Should(Succeed()) 118 }).Should(Succeed()) 119 } 120 121 BeforeEach(func() { 122 cleanEnv() 123 ensureNamespace(namespace2) 124 }) 125 126 AfterEach(func() { 127 cleanEnv() 128 }) 129 130 Context("BackupRepo controller test", func() { 131 const defaultCSIDriverName = "default.csi.driver" 132 var credentialSecretKey types.NamespacedName 133 var repoKey types.NamespacedName 134 var providerKey types.NamespacedName 135 var repo *dpv1alpha1.BackupRepo 136 137 createCredentialSecretSpec := func() { 138 obj := &corev1.Secret{} 139 obj.GenerateName = "credential-" 140 obj.Namespace = testCtx.DefaultNamespace 141 obj.StringData = map[string]string{ 142 "cred-key1": "cred-val1", 143 "cred-key2": "cred-val2", 144 } 145 secret := testapps.CreateK8sResource(&testCtx, obj) 146 credentialSecretKey = client.ObjectKeyFromObject(secret) 147 } 148 149 createBackupRepoSpec := func(mutateFunc func(repo *dpv1alpha1.BackupRepo)) *dpv1alpha1.BackupRepo { 150 obj := &dpv1alpha1.BackupRepo{} 151 obj.GenerateName = "backuprepo-" 152 obj.Spec = dpv1alpha1.BackupRepoSpec{ 153 StorageProviderRef: providerKey.Name, 154 VolumeCapacity: resource.MustParse("100Gi"), 155 PVReclaimPolicy: corev1.PersistentVolumeReclaimRetain, 156 Config: map[string]string{ 157 "key1": "val1", 158 "key2": "val2", 159 }, 160 Credential: &corev1.SecretReference{ 161 Name: credentialSecretKey.Name, 162 Namespace: credentialSecretKey.Namespace, 163 }, 164 } 165 if mutateFunc != nil { 166 mutateFunc(obj) 167 } 168 repo = testapps.CreateK8sResource(&testCtx, obj).(*dpv1alpha1.BackupRepo) 169 repoKey = client.ObjectKeyFromObject(repo) 170 return repo 171 } 172 173 createStorageProviderSpec := func(mutateFunc func(provider *storagev1alpha1.StorageProvider)) { 174 obj := &storagev1alpha1.StorageProvider{} 175 obj.GenerateName = "storageprovider-" 176 obj.Spec.CSIDriverName = defaultCSIDriverName 177 obj.Spec.CSIDriverSecretTemplate = ` 178 value-of-key1: {{ index .Parameters "key1" }} 179 value-of-key2: {{ index .Parameters "key2" }} 180 value-of-cred-key1: {{ index .Parameters "cred-key1" }} 181 value-of-cred-key2: {{ index .Parameters "cred-key2" }} 182 ` 183 obj.Spec.StorageClassTemplate = ` 184 provisioner: default.csi.driver 185 parameters: 186 value-of-key1: {{ index .Parameters "key1" }} 187 value-of-key2: {{ index .Parameters "key2" }} 188 value-of-cred-key1: {{ index .Parameters "cred-key1" }} 189 value-of-cred-key2: {{ index .Parameters "cred-key2" }} 190 secret-name: {{ .CSIDriverSecretRef.Name }} 191 secret-namespace: {{ .CSIDriverSecretRef.Namespace }} 192 ` 193 obj.Status.Phase = storagev1alpha1.StorageProviderReady 194 meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{ 195 Type: storagev1alpha1.ConditionTypeCSIDriverInstalled, 196 Status: metav1.ConditionTrue, 197 Reason: "CSIDriverInstalled", 198 }) 199 if mutateFunc != nil { 200 mutateFunc(obj) 201 } 202 provider := testapps.CreateK8sResource(&testCtx, obj.DeepCopy()) 203 providerKey = client.ObjectKeyFromObject(provider) 204 // update status 205 newObj := provider.(*storagev1alpha1.StorageProvider) 206 patch := client.MergeFrom(newObj.DeepCopy()) 207 newObj.Status = obj.Status 208 Expect(testCtx.Cli.Status().Patch(testCtx.Ctx, newObj, patch)).NotTo(HaveOccurred()) 209 } 210 211 createCSIDriverObjectSpec := func(driverName string) { 212 obj := &storagev1.CSIDriver{} 213 obj.Name = driverName 214 testapps.CreateK8sResource(&testCtx, obj) 215 } 216 217 createBackupSpec := func(mutateFunc func(backup *dpv1alpha1.Backup)) *dpv1alpha1.Backup { 218 obj := &dpv1alpha1.Backup{} 219 obj.GenerateName = "backup-" 220 obj.Namespace = testCtx.DefaultNamespace 221 obj.Labels = map[string]string{ 222 dataProtectionBackupRepoKey: repoKey.Name, 223 dataProtectionWaitRepoPreparationKey: trueVal, 224 } 225 obj.Spec.BackupMethod = "test-backup-method" 226 obj.Spec.BackupPolicyName = "default" 227 if mutateFunc != nil { 228 mutateFunc(obj) 229 } 230 backup := testapps.CreateK8sResource(&testCtx, obj).(*dpv1alpha1.Backup) 231 // updating the status of the Backup to COMPLETED, backup repo controller only 232 // handles for non-failed backups. 233 Eventually(func(g Gomega) { 234 obj := &dpv1alpha1.Backup{} 235 err := testCtx.Cli.Get(testCtx.Ctx, client.ObjectKeyFromObject(backup), obj) 236 g.Expect(err).ShouldNot(HaveOccurred()) 237 if obj.Status.Phase == dpv1alpha1.BackupPhaseFailed { 238 // the controller will set the status to failed because 239 // essential objects (e.g. backup policy) are missed. 240 // we set the status to completed after that, to avoid conflict. 241 obj.Status.Phase = dpv1alpha1.BackupPhaseCompleted 242 err = testCtx.Cli.Status().Update(testCtx.Ctx, obj) 243 g.Expect(err).ShouldNot(HaveOccurred()) 244 } else { 245 // check again 246 g.Expect(false).Should(BeTrue()) 247 } 248 }).Should(Succeed()) 249 return backup 250 } 251 252 getBackupRepo := func(g Gomega, key types.NamespacedName) *dpv1alpha1.BackupRepo { 253 repo := &dpv1alpha1.BackupRepo{} 254 err := testCtx.Cli.Get(testCtx.Ctx, key, repo) 255 g.Expect(err).ShouldNot(HaveOccurred()) 256 return repo 257 } 258 259 deleteBackup := func(g Gomega, key types.NamespacedName) { 260 backupObj := &dpv1alpha1.Backup{} 261 err := testCtx.Cli.Get(testCtx.Ctx, key, backupObj) 262 if apierrors.IsNotFound(err) { 263 return 264 } 265 g.Expect(err).ShouldNot(HaveOccurred()) 266 // remove finalizers 267 backupObj.Finalizers = nil 268 err = testCtx.Cli.Update(testCtx.Ctx, backupObj) 269 g.Expect(err).ShouldNot(HaveOccurred()) 270 // delete the backup 271 err = testCtx.Cli.Delete(testCtx.Ctx, backupObj) 272 g.Expect(err).ShouldNot(HaveOccurred()) 273 } 274 275 preCheckResouceName := func(repo *dpv1alpha1.BackupRepo) string { 276 reconCtx := reconcileContext{repo: repo} 277 return reconCtx.preCheckResourceName() 278 } 279 280 completePreCheckJob := func(repo *dpv1alpha1.BackupRepo) { 281 jobName := preCheckResouceName(repo) 282 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 283 Eventually(testapps.GetAndChangeObjStatus(&testCtx, types.NamespacedName{Name: jobName, Namespace: namespace}, func(job *batchv1.Job) { 284 job.Status.Conditions = append(job.Status.Conditions, batchv1.JobCondition{ 285 Type: batchv1.JobComplete, 286 Status: corev1.ConditionTrue, 287 }) 288 })).Should(Succeed()) 289 } 290 291 completePreCheckJobWithError := func(repo *dpv1alpha1.BackupRepo, message string) { 292 jobName := preCheckResouceName(repo) 293 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 294 Eventually(testapps.GetAndChangeObjStatus(&testCtx, types.NamespacedName{Name: jobName, Namespace: namespace}, func(job *batchv1.Job) { 295 job.Status.Conditions = append(job.Status.Conditions, batchv1.JobCondition{ 296 Type: batchv1.JobFailed, 297 Status: corev1.ConditionTrue, 298 Reason: "Failed", 299 Message: message, 300 }) 301 })).Should(Succeed()) 302 } 303 304 removePVCProtectionFinalizer := func(pvcKey types.NamespacedName) { 305 Eventually(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { 306 controllerutil.RemoveFinalizer(pvc, pvcProtectionFinalizer) 307 })).Should(Succeed()) 308 } 309 310 BeforeEach(func() { 311 cleanEnv() 312 Expect(client.IgnoreAlreadyExists(testCtx.CreateNamespace())).To(Not(HaveOccurred())) 313 createCredentialSecretSpec() 314 createCSIDriverObjectSpec(defaultCSIDriverName) 315 createStorageProviderSpec(nil) 316 createBackupRepoSpec(nil) 317 completePreCheckJob(repo) 318 }) 319 320 AfterEach(func() { 321 cleanEnv() 322 }) 323 324 It("should monitor the status of the storage provider", func() { 325 By("creating a BackupRepo which is referencing a non-existent storage provider") 326 createBackupRepoSpec(func(repo *dpv1alpha1.BackupRepo) { 327 repo.Spec.StorageProviderRef = "myprovider" // not exist for now 328 }) 329 By("checking the status of the BackupRepo, should be not ready") 330 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 331 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageProviderReady) 332 g.Expect(cond).ToNot(BeNil()) 333 g.Expect(cond.Status).Should(BeEquivalentTo(metav1.ConditionFalse)) 334 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonStorageProviderNotFound)) 335 g.Expect(repo.Status.Phase).To(Equal(dpv1alpha1.BackupRepoFailed)) 336 })).Should(Succeed()) 337 338 By("creating the required storage provider") 339 createStorageProviderSpec(func(provider *storagev1alpha1.StorageProvider) { 340 provider.GenerateName = "" 341 provider.Name = "myprovider" 342 }) 343 344 By("checking the status of the BackupRepo") 345 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 346 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageProviderReady) 347 g.Expect(cond).ToNot(BeNil()) 348 g.Expect(cond.Status).Should(BeEquivalentTo(metav1.ConditionTrue)) 349 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonStorageProviderReady)) 350 g.Expect(repo.Status.Phase).To(Equal(dpv1alpha1.BackupRepoPreChecking)) 351 })).Should(Succeed()) 352 353 By("updating the status of the storage provider to not ready") 354 Eventually(testapps.GetAndChangeObjStatus(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 355 provider.Status.Phase = storagev1alpha1.StorageProviderNotReady 356 meta.SetStatusCondition(&provider.Status.Conditions, metav1.Condition{ 357 Type: storagev1alpha1.ConditionTypeCSIDriverInstalled, 358 Status: metav1.ConditionFalse, 359 Reason: "CSINotInstalled", 360 }) 361 })).Should(Succeed()) 362 By("checking the status of the BackupRepo, should become failed") 363 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 364 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageProviderReady) 365 g.Expect(cond).ToNot(BeNil()) 366 g.Expect(cond.Status).Should(BeEquivalentTo(metav1.ConditionFalse)) 367 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonStorageProviderNotReady)) 368 g.Expect(repo.Status.Phase).To(Equal(dpv1alpha1.BackupRepoFailed)) 369 })).Should(Succeed()) 370 371 By("deleting the storage provider") 372 testapps.DeleteObject(&testCtx, providerKey, &storagev1alpha1.StorageProvider{}) 373 By("checking the status of the BackupRepo, condition should become NotFound") 374 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 375 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageProviderReady) 376 g.Expect(cond).ToNot(BeNil()) 377 g.Expect(cond.Status).Should(BeEquivalentTo(metav1.ConditionFalse)) 378 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonStorageProviderNotFound)) 379 g.Expect(repo.Status.Phase).To(Equal(dpv1alpha1.BackupRepoFailed)) 380 })).Should(Succeed()) 381 }) 382 383 It("should create StorageClass and Secret for the CSI driver", func() { 384 var secretRef corev1.SecretReference 385 var storageClassName string 386 By("checking the BackupRepo, should be ready") 387 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 388 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady)) 389 g.Expect(repo.Status.GeneratedCSIDriverSecret).NotTo(BeNil()) 390 g.Expect(repo.Status.GeneratedStorageClassName).NotTo(BeEmpty()) 391 g.Expect(repo.Status.BackupPVCName).NotTo(BeEmpty()) 392 secretRef = *repo.Status.GeneratedCSIDriverSecret 393 storageClassName = repo.Status.GeneratedStorageClassName 394 })).Should(Succeed()) 395 396 By("checking the Secret") 397 secretKey := types.NamespacedName{Name: secretRef.Name, Namespace: secretRef.Namespace} 398 Eventually(testapps.CheckObj(&testCtx, secretKey, func(g Gomega, secret *corev1.Secret) { 399 g.Expect(secret.Data).To(Equal(map[string][]byte{ 400 "value-of-key1": []byte("val1"), 401 "value-of-key2": []byte("val2"), 402 "value-of-cred-key1": []byte("cred-val1"), 403 "value-of-cred-key2": []byte("cred-val2"), 404 })) 405 g.Expect(isOwned(repo, secret)).To(BeTrue()) 406 g.Expect(secret.Labels[dataProtectionBackupRepoKey]).To(Equal(repoKey.Name)) 407 })).Should(Succeed()) 408 409 By("checking the StorageClass") 410 storageClassNameKey := types.NamespacedName{Name: storageClassName} 411 Eventually(testapps.CheckObj(&testCtx, storageClassNameKey, func(g Gomega, storageClass *storagev1.StorageClass) { 412 g.Expect(storageClass.Parameters).To(Equal(map[string]string{ 413 "value-of-key1": "val1", 414 "value-of-key2": "val2", 415 "value-of-cred-key1": "cred-val1", 416 "value-of-cred-key2": "cred-val2", 417 "secret-name": secretKey.Name, 418 "secret-namespace": secretKey.Namespace, 419 })) 420 g.Expect(isOwned(repo, storageClass)).To(BeTrue()) 421 g.Expect(storageClass.Labels[dataProtectionBackupRepoKey]).To(Equal(repoKey.Name)) 422 g.Expect(storageClass.Provisioner).To(Equal(defaultCSIDriverName)) 423 g.Expect(*storageClass.ReclaimPolicy).To(Equal(corev1.PersistentVolumeReclaimRetain)) 424 g.Expect(*storageClass.VolumeBindingMode).To(Equal(storagev1.VolumeBindingImmediate)) 425 })).Should(Succeed()) 426 }) 427 428 It("should update the Secret object if the template or values got changed", func() { 429 By("checking the Secret") 430 var secretKey types.NamespacedName 431 var reversion string 432 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 433 g.Expect(repo.Status.GeneratedCSIDriverSecret).NotTo(BeNil()) 434 secretKey = types.NamespacedName{ 435 Name: repo.Status.GeneratedCSIDriverSecret.Name, 436 Namespace: repo.Status.GeneratedCSIDriverSecret.Namespace, 437 } 438 })).Should(Succeed()) 439 Eventually(testapps.CheckObj(&testCtx, secretKey, func(g Gomega, secret *corev1.Secret) { 440 reversion = secret.ResourceVersion 441 })).Should(Succeed()) 442 443 By("updating the template") 444 Eventually(testapps.GetAndChangeObj(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 445 provider.Spec.CSIDriverSecretTemplate += "\nnew-item: new-value" 446 })).Should(Succeed()) 447 By("checking the Secret again, should have new generation and new content") 448 Eventually(testapps.CheckObj(&testCtx, secretKey, func(g Gomega, secret *corev1.Secret) { 449 g.Expect(secret.Data).To(Equal(map[string][]byte{ 450 "value-of-key1": []byte("val1"), 451 "value-of-key2": []byte("val2"), 452 "value-of-cred-key1": []byte("cred-val1"), 453 "value-of-cred-key2": []byte("cred-val2"), 454 "new-item": []byte("new-value"), 455 })) 456 g.Expect(secret.ResourceVersion).ToNot(Equal(reversion)) 457 reversion = secret.ResourceVersion 458 })).Should(Succeed()) 459 460 By("updating the config") 461 Eventually(testapps.GetAndChangeObj(&testCtx, repoKey, func(repo *dpv1alpha1.BackupRepo) { 462 repo.Spec.Config["key1"] = "changed-val1" 463 })).Should(Succeed()) 464 By("checking the Secret again, should have new generation and new content") 465 Eventually(testapps.CheckObj(&testCtx, secretKey, func(g Gomega, secret *corev1.Secret) { 466 g.Expect(secret.Data).To(Equal(map[string][]byte{ 467 "value-of-key1": []byte("changed-val1"), 468 "value-of-key2": []byte("val2"), 469 "value-of-cred-key1": []byte("cred-val1"), 470 "value-of-cred-key2": []byte("cred-val2"), 471 "new-item": []byte("new-value"), 472 })) 473 g.Expect(secret.ResourceVersion).ToNot(Equal(reversion)) 474 reversion = secret.ResourceVersion 475 })).Should(Succeed()) 476 477 By("updating the credential") 478 Eventually(testapps.GetAndChangeObj(&testCtx, credentialSecretKey, func(secret *corev1.Secret) { 479 secret.Data["cred-key1"] = []byte("changed-cred-val1") 480 })).Should(Succeed()) 481 By("checking the Secret again, should have new generation and new content") 482 Eventually(testapps.CheckObj(&testCtx, secretKey, func(g Gomega, secret *corev1.Secret) { 483 g.Expect(secret.Data).To(Equal(map[string][]byte{ 484 "value-of-key1": []byte("changed-val1"), 485 "value-of-key2": []byte("val2"), 486 "value-of-cred-key1": []byte("changed-cred-val1"), 487 "value-of-cred-key2": []byte("cred-val2"), 488 "new-item": []byte("new-value"), 489 })) 490 g.Expect(secret.ResourceVersion).ToNot(Equal(reversion)) 491 reversion = secret.ResourceVersion 492 })).Should(Succeed()) 493 }) 494 495 It("should fail if the secret referenced by the credential secret not found", func() { 496 By("checking the repo object to make sure it's ready") 497 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 498 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady)) 499 })).Should(Succeed()) 500 By("updating to a non-existing credential") 501 Eventually(testapps.GetAndChangeObj(&testCtx, repoKey, func(repo *dpv1alpha1.BackupRepo) { 502 repo.Spec.Credential.Name += "non-existing" 503 })).Should(Succeed()) 504 By("checking the repo object again, it should be failed") 505 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 506 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 507 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeParametersChecked) 508 g.Expect(cond).NotTo(BeNil()) 509 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 510 g.Expect(cond.Reason).Should(Equal(ReasonCredentialSecretNotFound)) 511 })).Should(Succeed()) 512 }) 513 514 It("should fail if the secret template is invalid", func() { 515 By("setting a invalid template") 516 Eventually(testapps.GetAndChangeObj(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 517 provider.Spec.CSIDriverSecretTemplate = "{{ bad template }" 518 })).Should(Succeed()) 519 By("checking the repo status") 520 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 521 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 522 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageClassCreated) 523 g.Expect(cond).NotTo(BeNil()) 524 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 525 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonPrepareCSISecretFailed)) 526 g.Expect(cond.Message).Should(ContainSubstring(`function "bad" not defined`)) 527 })).Should(Succeed()) 528 }) 529 530 It("should fail if the render result of the secret template is not a yaml", func() { 531 By("setting a invalid template") 532 Eventually(testapps.GetAndChangeObj(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 533 provider.Spec.CSIDriverSecretTemplate = "bad yaml" 534 })).Should(Succeed()) 535 By("checking the repo status") 536 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 537 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 538 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageClassCreated) 539 g.Expect(cond).NotTo(BeNil()) 540 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 541 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonPrepareCSISecretFailed)) 542 g.Expect(cond.Message).Should(ContainSubstring(`cannot unmarshal string into Go value of type map[string]string`)) 543 })).Should(Succeed()) 544 }) 545 546 It("should fail if the storage class template is invalid", func() { 547 By("setting a invalid template") 548 Eventually(testapps.GetAndChangeObj(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 549 provider.Spec.StorageClassTemplate = "{{ bad template }" 550 })).Should(Succeed()) 551 By("creating a new repo to reference the provider") 552 createBackupRepoSpec(nil) 553 By("checking the repo status") 554 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 555 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 556 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageClassCreated) 557 g.Expect(cond).NotTo(BeNil()) 558 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 559 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonPrepareStorageClassFailed)) 560 g.Expect(cond.Message).Should(ContainSubstring(`function "bad" not defined`)) 561 })).Should(Succeed()) 562 }) 563 564 It("should fail if the render result of the storage class template is not a yaml", func() { 565 By("setting a invalid template") 566 Eventually(testapps.GetAndChangeObj(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 567 provider.Spec.StorageClassTemplate = "bad yaml" 568 })).Should(Succeed()) 569 By("creating a new repo to reference the provider") 570 createBackupRepoSpec(nil) 571 By("checking the repo status") 572 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 573 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 574 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageClassCreated) 575 g.Expect(cond).NotTo(BeNil()) 576 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 577 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonPrepareStorageClassFailed)) 578 g.Expect(cond.Message).Should(ContainSubstring(`cannot unmarshal string into Go value of type v1.StorageClass`)) 579 })).Should(Succeed()) 580 }) 581 582 It("should run a pre-check job", func() { 583 By("creating a backup repo") 584 createBackupRepoSpec(nil) 585 586 By("checking the pre-check job resources") 587 pvcName := preCheckResouceName(repo) 588 jobName := preCheckResouceName(repo) 589 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 590 checkResources := func(exists bool) { 591 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: jobName, Namespace: namespace}, 592 &batchv1.Job{}, exists)).WithOffset(1).Should(Succeed()) 593 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: pvcName, Namespace: namespace}, 594 &corev1.PersistentVolumeClaim{}, exists)).WithOffset(1).Should(Succeed()) 595 } 596 checkResources(true) 597 598 By("checking repo's status, it should fail if the pre-check job has failed") 599 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 600 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoPreChecking)) 601 })).Should(Succeed()) 602 completePreCheckJobWithError(repo, "connect to endpoint failed") 603 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 604 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 605 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypePreCheckPassed) 606 g.Expect(cond).NotTo(BeNil()) 607 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 608 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonPreCheckFailed)) 609 g.Expect(cond.Message).Should(ContainSubstring(`connect to endpoint failed`)) 610 })).Should(Succeed()) 611 612 By("checking the resources, they should be deleted") 613 removePVCProtectionFinalizer(types.NamespacedName{Name: pvcName, Namespace: namespace}) 614 checkResources(false) 615 616 By("updating the repo, it should run the pre-check job again") 617 Eventually(testapps.GetAndChangeObj(&testCtx, credentialSecretKey, func(cred *corev1.Secret) { 618 cred.Data["new-key"] = []byte("new-value") 619 })).Should(Succeed()) 620 checkResources(true) 621 completePreCheckJob(repo) 622 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 623 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady)) 624 })).Should(Succeed()) 625 }) 626 627 It("should remove the stale pre-check job if the repo spec has changed too quickly", func() { 628 resourceName := preCheckResouceName(repo) 629 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 630 updateRepoAndCheckResources := func(content string, lastJobUID types.UID) (string, types.UID) { 631 Eventually(testapps.GetAndChangeObj(&testCtx, credentialSecretKey, func(cred *corev1.Secret) { 632 cred.Data["new-key"] = []byte(content) 633 })).WithOffset(1).Should(Succeed()) 634 var digest string 635 var uid types.UID 636 Eventually(func(g Gomega) { 637 pvc := &corev1.PersistentVolumeClaim{} 638 err := testCtx.Cli.Get(testCtx.Ctx, client.ObjectKey{Name: resourceName, Namespace: namespace}, pvc) 639 g.Expect(err).ToNot(HaveOccurred()) 640 job := &batchv1.Job{} 641 err = testCtx.Cli.Get(testCtx.Ctx, client.ObjectKey{Name: resourceName, Namespace: namespace}, job) 642 g.Expect(err).ToNot(HaveOccurred()) 643 g.Expect(pvc.Annotations[dataProtectionBackupRepoDigestAnnotationKey]).To( 644 BeEquivalentTo(job.Annotations[dataProtectionBackupRepoDigestAnnotationKey])) 645 digest = job.Annotations[dataProtectionBackupRepoDigestAnnotationKey] 646 uid = job.UID 647 g.Expect(digest).ToNot(BeEmpty()) 648 g.Expect(uid).ToNot(Equal(lastJobUID)) 649 }).WithOffset(1).Should(Succeed()) 650 return digest, uid 651 } 652 653 By("updating the repo, and then get the digest from the pre-check resources") 654 digest1, uid1 := updateRepoAndCheckResources("value1", "") 655 656 By("updating the repo again, and then get the digest") 657 removePVCProtectionFinalizer(types.NamespacedName{Name: resourceName, Namespace: namespace}) 658 digest2, uid2 := updateRepoAndCheckResources("value2", uid1) 659 660 By("checking the digests are different") 661 Expect(digest1).ToNot(Equal(digest2)) 662 Expect(uid1).ToNot(Equal(uid2)) 663 completePreCheckJob(repo) 664 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 665 g.Expect(repo.Annotations[dataProtectionBackupRepoDigestAnnotationKey]).To(Equal(digest2)) 666 })).Should(Succeed()) 667 }) 668 669 It("should timeout if the pre-check job runs too long", func() { 670 fakeClock := testing.NewFakeClock(time.Now()) 671 original := wallClock 672 wallClock = fakeClock 673 defer func() { 674 wallClock = original 675 }() 676 // create a new repo 677 createBackupRepoSpec(nil) 678 // make the job timed out 679 fakeClock.Step(defaultPreCheckTimeout * 2) 680 // trigger reconciliation 681 Eventually(testapps.GetAndChangeObj(&testCtx, repoKey, func(repo *dpv1alpha1.BackupRepo) { 682 if repo.Annotations == nil { 683 repo.Annotations = make(map[string]string) 684 } 685 repo.Annotations["touch"] = "whatever" 686 })).Should(Succeed()) 687 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 688 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 689 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypePreCheckPassed) 690 g.Expect(cond).ToNot(BeNil()) 691 g.Expect(cond.Status).Should(BeEquivalentTo(metav1.ConditionFalse)) 692 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonPreCheckFailed)) 693 g.Expect(cond.Message).Should(ContainSubstring("timeout")) 694 })).Should(Succeed()) 695 }) 696 697 createBackupAndCheckPVC := func(namespace string) (backup *dpv1alpha1.Backup, pvcName string) { 698 By("making sure the repo is ready") 699 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 700 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady), "%+v", repo) 701 g.Expect(repo.Status.BackupPVCName).ShouldNot(BeEmpty()) 702 pvcName = repo.Status.BackupPVCName 703 })).Should(Succeed()) 704 By("creating a Backup object in the namespace") 705 backup = createBackupSpec(func(backup *dpv1alpha1.Backup) { 706 backup.Namespace = namespace 707 }) 708 By("checking the PVC has been created in the namespace") 709 pvcKey := types.NamespacedName{ 710 Name: pvcName, 711 Namespace: namespace, 712 } 713 Eventually(testapps.CheckObjExists(&testCtx, pvcKey, &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) 714 return backup, pvcName 715 } 716 717 It("should create a PVC in Backup's namespace (in default namespace)", func() { 718 createBackupAndCheckPVC(testCtx.DefaultNamespace) 719 }) 720 721 It("should create a PVC in Backup's namespace (in namespace2)", func() { 722 createBackupAndCheckPVC(namespace2) 723 }) 724 725 Context("storage provider with PersistentVolumeClaimTemplate", func() { 726 It("should create a PVC in Backup's namespace (in default namespace)", func() { 727 By("setting the PersistentVolumeClaimTemplate") 728 createStorageProviderSpec(func(provider *storagev1alpha1.StorageProvider) { 729 provider.Spec.PersistentVolumeClaimTemplate = ` 730 apiVersion: v1 731 kind: PersistentVolumeClaim 732 metadata: 733 labels: 734 byPVCTemplate: "true" 735 spec: 736 storageClassName: {{ .GeneratedStorageClassName }} 737 accessModes: 738 - ReadWriteOnce 739 resources: 740 volumeMode: Filesystem 741 ` 742 }) 743 createBackupRepoSpec(nil) 744 completePreCheckJob(repo) 745 _, pvcName := createBackupAndCheckPVC(testCtx.DefaultNamespace) 746 747 Eventually(testapps.CheckObj(&testCtx, types.NamespacedName{Name: pvcName, Namespace: testCtx.DefaultNamespace}, 748 func(g Gomega, pvc *corev1.PersistentVolumeClaim) { 749 repo := getBackupRepo(g, repoKey) 750 g.Expect(pvc.Spec.StorageClassName).ShouldNot(BeNil()) 751 g.Expect(*pvc.Spec.StorageClassName).Should(Equal(repo.Status.GeneratedStorageClassName)) 752 g.Expect(pvc.Spec.Resources.Requests.Storage()).ShouldNot(BeNil()) 753 g.Expect(pvc.Spec.Resources.Requests.Storage().String()).Should(Equal(repo.Spec.VolumeCapacity.String())) 754 g.Expect(pvc.Spec.AccessModes).Should(Equal([]corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce})) 755 g.Expect(pvc.Spec.VolumeMode).ShouldNot(BeNil()) 756 g.Expect(*pvc.Spec.VolumeMode).Should(BeEquivalentTo(corev1.PersistentVolumeFilesystem)) 757 g.Expect(pvc.Labels["byPVCTemplate"]).Should(Equal("true")) 758 })).Should(Succeed()) 759 }) 760 761 It("should fail if the PVC template is invalid", func() { 762 By("setting a invalid PersistentVolumeClaimTemplate") 763 Eventually(testapps.GetAndChangeObj(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 764 provider.Spec.PersistentVolumeClaimTemplate = `bad spec` 765 })).Should(Succeed()) 766 767 By("checking repo's status") 768 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 769 g.Expect(repo.Status.Phase, dpv1alpha1.BackupRepoFailed) 770 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypePVCTemplateChecked) 771 g.Expect(cond).NotTo(BeNil()) 772 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 773 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonBadPVCTemplate)) 774 })).Should(Succeed()) 775 }) 776 }) 777 778 Context("storage provider contains only PersistentVolumeClaimTemplate", func() { 779 BeforeEach(func() { 780 createStorageProviderSpec(func(provider *storagev1alpha1.StorageProvider) { 781 provider.Spec.CSIDriverName = "" 782 provider.Spec.CSIDriverSecretTemplate = "" 783 provider.Spec.StorageClassTemplate = "" 784 provider.Spec.PersistentVolumeClaimTemplate = ` 785 spec: 786 storageClassName: some.storage.class 787 accessModes: 788 - ReadWriteOnce 789 ` 790 }) 791 createBackupRepoSpec(nil) 792 completePreCheckJob(repo) 793 }) 794 It("should create the PVC based on the PersistentVolumeClaimTemplate", func() { 795 _, pvcName := createBackupAndCheckPVC(namespace2) 796 Eventually(testapps.CheckObj(&testCtx, types.NamespacedName{Name: pvcName, Namespace: namespace2}, 797 func(g Gomega, pvc *corev1.PersistentVolumeClaim) { 798 g.Expect(pvc.Spec.StorageClassName).ShouldNot(BeNil()) 799 g.Expect(*pvc.Spec.StorageClassName).Should(Equal("some.storage.class")) 800 g.Expect(pvc.Spec.AccessModes).Should(Equal([]corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce})) 801 g.Expect(pvc.Spec.VolumeMode).ShouldNot(BeNil()) 802 g.Expect(*pvc.Spec.VolumeMode).Should(BeEquivalentTo(corev1.PersistentVolumeFilesystem)) 803 g.Expect(pvc.Spec.Resources.Requests.Storage()).ShouldNot(BeNil()) 804 g.Expect(pvc.Spec.Resources.Requests.Storage().String()).Should(Equal(repo.Spec.VolumeCapacity.String())) 805 })).Should(Succeed()) 806 }) 807 }) 808 809 It("should fail if both StorageClassTemplate and PersistentVolumeClaimTemplate are empty", func() { 810 By("creating a storage provider with empty PersistentVolumeClaimTemplate and StorageClassTemplate") 811 createStorageProviderSpec(func(provider *storagev1alpha1.StorageProvider) { 812 provider.Spec.CSIDriverName = "" 813 provider.Spec.CSIDriverSecretTemplate = "" 814 provider.Spec.StorageClassTemplate = "" 815 provider.Spec.PersistentVolumeClaimTemplate = "" 816 }) 817 By("creating a backup repo with the storage provider") 818 createBackupRepoSpec(nil) 819 By("checking repo's status") 820 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 821 g.Expect(repo.Status.Phase).Should(BeEquivalentTo(dpv1alpha1.BackupRepoFailed)) 822 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageProviderReady) 823 g.Expect(cond).NotTo(BeNil()) 824 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 825 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonInvalidStorageProvider)) 826 })).Should(Succeed()) 827 }) 828 829 Context("with AccessMethodTool", func() { 830 var backup *dpv1alpha1.Backup 831 var toolConfigSecretKey types.NamespacedName 832 833 createStorageProviderSpecForToolAccessMethod := func(mutateFunc func(provider *storagev1alpha1.StorageProvider)) { 834 createStorageProviderSpec(func(provider *storagev1alpha1.StorageProvider) { 835 provider.Spec.DatasafedConfigTemplate = ` 836 [storage] 837 type=local 838 key1={{ index .Parameters "key1" }} 839 key2={{ index .Parameters "key2" }} 840 cred-key1={{ index .Parameters "cred-key1" }} 841 cred-key2={{ index .Parameters "cred-key2" }} 842 ` 843 if mutateFunc != nil { 844 mutateFunc(provider) 845 } 846 }) 847 } 848 849 BeforeEach(func() { 850 By("preparing") 851 createStorageProviderSpecForToolAccessMethod(nil) 852 createBackupRepoSpec(func(repo *dpv1alpha1.BackupRepo) { 853 repo.Spec.AccessMethod = dpv1alpha1.AccessMethodTool 854 }) 855 completePreCheckJob(repo) 856 857 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, obj *dpv1alpha1.BackupRepo) { 858 g.Expect(obj.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady)) 859 repo = obj 860 })).Should(Succeed()) 861 862 backup = createBackupSpec(nil) 863 toolConfigSecretKey = types.NamespacedName{ 864 Name: repo.Status.ToolConfigSecretName, 865 Namespace: backup.Namespace, 866 } 867 Eventually(testapps.CheckObjExists(&testCtx, toolConfigSecretKey, &corev1.Secret{}, true)).Should(Succeed()) 868 }) 869 870 It("should check that the storage provider has a non-empty datasafedConfigTemplate", func() { 871 By("preparing") 872 createStorageProviderSpecForToolAccessMethod(func(provider *storagev1alpha1.StorageProvider) { 873 provider.Spec.DatasafedConfigTemplate = "" 874 }) 875 createBackupRepoSpec(func(repo *dpv1alpha1.BackupRepo) { 876 repo.Spec.AccessMethod = dpv1alpha1.AccessMethodTool 877 }) 878 By("checking") 879 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 880 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 881 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeStorageProviderReady) 882 g.Expect(cond).NotTo(BeNil()) 883 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 884 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonInvalidStorageProvider)) 885 g.Expect(cond.Message).Should(ContainSubstring("DatasafedConfigTemplate is empty")) 886 })).Should(Succeed()) 887 }) 888 889 It("should fail if the datasafedConfigTemplate is invalid", func() { 890 By("preparing") 891 createStorageProviderSpecForToolAccessMethod(func(provider *storagev1alpha1.StorageProvider) { 892 provider.Spec.DatasafedConfigTemplate = "bad template {{" 893 }) 894 createBackupRepoSpec(func(repo *dpv1alpha1.BackupRepo) { 895 repo.Spec.AccessMethod = dpv1alpha1.AccessMethodTool 896 }) 897 By("checking") 898 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 899 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoPreChecking)) 900 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypePreCheckPassed) 901 g.Expect(cond).NotTo(BeNil()) 902 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionUnknown)) 903 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonUnknownError)) 904 g.Expect(cond.Message).Should(ContainSubstring("failed to render tool config template")) 905 })).Should(Succeed()) 906 }) 907 908 It("should work even if the CSI driver required by the storage provider is not installed", func() { 909 By("preparing") 910 createStorageProviderSpecForToolAccessMethod(func(provider *storagev1alpha1.StorageProvider) { 911 provider.Status.Phase = storagev1alpha1.StorageProviderNotReady 912 meta.SetStatusCondition(&provider.Status.Conditions, metav1.Condition{ 913 Type: storagev1alpha1.ConditionTypeCSIDriverInstalled, 914 Status: metav1.ConditionFalse, 915 Reason: "NotInstalled", 916 }) 917 }) 918 createBackupRepoSpec(func(repo *dpv1alpha1.BackupRepo) { 919 repo.Spec.AccessMethod = dpv1alpha1.AccessMethodTool 920 }) 921 completePreCheckJob(repo) 922 By("checking") 923 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 924 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady)) 925 })).Should(Succeed()) 926 }) 927 928 It("should create the secret containing the tool config", func() { 929 Eventually(testapps.CheckObj(&testCtx, toolConfigSecretKey, func(g Gomega, secret *corev1.Secret) { 930 g.Expect(secret.Data).Should(HaveKeyWithValue("datasafed.conf", []byte(` 931 [storage] 932 type=local 933 key1=val1 934 key2=val2 935 cred-key1=cred-val1 936 cred-key2=cred-val2 937 `))) 938 })).Should(Succeed()) 939 940 By("creating a backup in namespace2") 941 createBackupSpec(func(backup *dpv1alpha1.Backup) { 942 backup.Namespace = namespace2 943 }) 944 secretKey := types.NamespacedName{ 945 Name: repo.Status.ToolConfigSecretName, 946 Namespace: namespace2, 947 } 948 Eventually(testapps.CheckObjExists(&testCtx, secretKey, &corev1.Secret{}, true)).Should(Succeed()) 949 }) 950 951 It("should update the content of the secret when the template or the value changes", func() { 952 By("changing the template") 953 Eventually(testapps.GetAndChangeObj(&testCtx, providerKey, func(provider *storagev1alpha1.StorageProvider) { 954 provider.Spec.DatasafedConfigTemplate += "new-item=new-value\n" 955 })).Should(Succeed()) 956 completePreCheckJob(repo) 957 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 958 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady)) 959 })).Should(Succeed()) 960 Eventually(testapps.CheckObj(&testCtx, toolConfigSecretKey, func(g Gomega, secret *corev1.Secret) { 961 g.Expect(secret.Data).Should(HaveKeyWithValue("datasafed.conf", []byte(` 962 [storage] 963 type=local 964 key1=val1 965 key2=val2 966 cred-key1=cred-val1 967 cred-key2=cred-val2 968 new-item=new-value 969 `))) 970 })).Should(Succeed()) 971 972 By("changing the value") 973 Eventually(testapps.GetAndChangeObj(&testCtx, repoKey, func(repo *dpv1alpha1.BackupRepo) { 974 repo.Spec.Config["key1"] = "changed-val1" 975 })).Should(Succeed()) 976 completePreCheckJob(repo) 977 Eventually(testapps.CheckObj(&testCtx, toolConfigSecretKey, func(g Gomega, secret *corev1.Secret) { 978 g.Expect(secret.Data).Should(HaveKeyWithValue("datasafed.conf", []byte(` 979 [storage] 980 type=local 981 key1=changed-val1 982 key2=val2 983 cred-key1=cred-val1 984 cred-key2=cred-val2 985 new-item=new-value 986 `))) 987 })).Should(Succeed()) 988 }) 989 990 It("should run a pre-check job", func() { 991 By("creating a backup repo") 992 createBackupRepoSpec(func(repo *dpv1alpha1.BackupRepo) { 993 repo.Spec.AccessMethod = dpv1alpha1.AccessMethodTool 994 }) 995 996 By("checking the pre-check job resources") 997 secretName := preCheckResouceName(repo) 998 jobName := preCheckResouceName(repo) 999 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 1000 checkResources := func(exists bool) { 1001 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: jobName, Namespace: namespace}, 1002 &batchv1.Job{}, exists)).WithOffset(1).Should(Succeed()) 1003 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: secretName, Namespace: namespace}, 1004 &corev1.Secret{}, exists)).WithOffset(1).Should(Succeed()) 1005 } 1006 checkResources(true) 1007 1008 By("checking repo's status, it should fail if the pre-check job has failed") 1009 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 1010 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoPreChecking)) 1011 })).Should(Succeed()) 1012 completePreCheckJobWithError(repo, "connect to endpoint failed") 1013 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 1014 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoFailed)) 1015 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypePreCheckPassed) 1016 g.Expect(cond).NotTo(BeNil()) 1017 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 1018 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonPreCheckFailed)) 1019 g.Expect(cond.Message).Should(ContainSubstring(`connect to endpoint failed`)) 1020 })).Should(Succeed()) 1021 1022 By("checking the resources, they should be deleted") 1023 checkResources(false) 1024 1025 By("updating the repo, it should run the pre-check job again") 1026 Eventually(testapps.GetAndChangeObj(&testCtx, credentialSecretKey, func(cred *corev1.Secret) { 1027 cred.Data["new-key"] = []byte("new-value") 1028 })).Should(Succeed()) 1029 checkResources(true) 1030 completePreCheckJob(repo) 1031 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 1032 g.Expect(repo.Status.Phase).Should(Equal(dpv1alpha1.BackupRepoReady)) 1033 })).Should(Succeed()) 1034 }) 1035 1036 It("should remove the stale pre-check job if the repo spec has changed too quickly", func() { 1037 resourceName := preCheckResouceName(repo) 1038 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 1039 updateRepoAndCheckResources := func(content string, lastJobUID types.UID) (string, types.UID) { 1040 Eventually(testapps.GetAndChangeObj(&testCtx, credentialSecretKey, func(cred *corev1.Secret) { 1041 cred.Data["new-key"] = []byte(content) 1042 })).WithOffset(1).Should(Succeed()) 1043 var digest string 1044 var uid types.UID 1045 Eventually(func(g Gomega) { 1046 secret := &corev1.Secret{} 1047 err := testCtx.Cli.Get(testCtx.Ctx, client.ObjectKey{Name: resourceName, Namespace: namespace}, secret) 1048 g.Expect(err).ToNot(HaveOccurred()) 1049 job := &batchv1.Job{} 1050 err = testCtx.Cli.Get(testCtx.Ctx, client.ObjectKey{Name: resourceName, Namespace: namespace}, job) 1051 g.Expect(err).ToNot(HaveOccurred()) 1052 g.Expect(secret.Annotations[dataProtectionBackupRepoDigestAnnotationKey]).To( 1053 BeEquivalentTo(job.Annotations[dataProtectionBackupRepoDigestAnnotationKey])) 1054 digest = job.Annotations[dataProtectionBackupRepoDigestAnnotationKey] 1055 uid = job.UID 1056 g.Expect(digest).ToNot(BeEmpty()) 1057 g.Expect(uid).ToNot(Equal(lastJobUID)) 1058 }).WithOffset(1).Should(Succeed()) 1059 return digest, uid 1060 } 1061 1062 By("updating the repo, and then get the digest from the pre-check resources") 1063 digest1, uid1 := updateRepoAndCheckResources("value1", "") 1064 1065 By("updating the repo again, and then get the digest") 1066 digest2, uid2 := updateRepoAndCheckResources("value2", uid1) 1067 1068 By("checking the digests are different") 1069 Expect(digest1).ToNot(Equal(digest2)) 1070 Expect(uid1).ToNot(Equal(uid2)) 1071 completePreCheckJob(repo) 1072 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 1073 g.Expect(repo.Annotations[dataProtectionBackupRepoDigestAnnotationKey]).To(Equal(digest2)) 1074 })).Should(Succeed()) 1075 }) 1076 1077 It("should delete resources for pre-checking when deleting the repo", func() { 1078 By("preparing") 1079 createBackupRepoSpec(func(repo *dpv1alpha1.BackupRepo) { 1080 repo.Spec.AccessMethod = dpv1alpha1.AccessMethodTool 1081 }) 1082 resourceName := preCheckResouceName(repo) 1083 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 1084 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1085 &batchv1.Job{}, true)).Should(Succeed()) 1086 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1087 &corev1.Secret{}, true)).Should(Succeed()) 1088 1089 By("deleting the repo") 1090 testapps.DeleteObject(&testCtx, repoKey, &dpv1alpha1.BackupRepo{}) 1091 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1092 &batchv1.Job{}, false)).Should(Succeed()) 1093 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1094 &corev1.Secret{}, false)).Should(Succeed()) 1095 }) 1096 1097 It("should delete the secret when the repo is deleted", func() { 1098 By("deleting the Backup and BackupRepo") 1099 testapps.DeleteObject(&testCtx, client.ObjectKeyFromObject(backup), &dpv1alpha1.Backup{}) 1100 testapps.DeleteObject(&testCtx, repoKey, &dpv1alpha1.BackupRepo{}) 1101 By("checking the secret is deleted") 1102 Eventually(testapps.CheckObjExists(&testCtx, toolConfigSecretKey, &corev1.Secret{}, false)).Should(Succeed()) 1103 }) 1104 }) 1105 1106 It("should block the deletion of the BackupRepo if derived objects are not deleted", func() { 1107 backup, pvcName := createBackupAndCheckPVC(namespace2) 1108 1109 By("deleting the BackupRepo") 1110 testapps.DeleteObject(&testCtx, repoKey, &dpv1alpha1.BackupRepo{}) 1111 1112 By("checking the BackupRepo, the deletion should be blocked because there are associated backups") 1113 Eventually(func(g Gomega) { 1114 repo := &dpv1alpha1.BackupRepo{} 1115 err := testCtx.Cli.Get(testCtx.Ctx, repoKey, repo) 1116 g.Expect(err).ShouldNot(HaveOccurred()) 1117 g.Expect(repo.DeletionTimestamp).ShouldNot(BeNil()) 1118 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeDerivedObjectsDeleted) 1119 g.Expect(cond).NotTo(BeNil()) 1120 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 1121 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonHaveAssociatedBackups)) 1122 }).Should(Succeed()) 1123 1124 By("deleting the Backup") 1125 Eventually(func(g Gomega) { 1126 deleteBackup(g, client.ObjectKeyFromObject(backup)) 1127 }).Should(Succeed()) 1128 1129 By("checking the BackupRepo, the deletion should be blocked because the PVC is still present") 1130 Eventually(func(g Gomega) { 1131 repo := &dpv1alpha1.BackupRepo{} 1132 err := testCtx.Cli.Get(testCtx.Ctx, repoKey, repo) 1133 g.Expect(err).ShouldNot(HaveOccurred()) 1134 g.Expect(repo.DeletionTimestamp).ShouldNot(BeNil()) 1135 cond := meta.FindStatusCondition(repo.Status.Conditions, ConditionTypeDerivedObjectsDeleted) 1136 g.Expect(cond).NotTo(BeNil()) 1137 g.Expect(cond.Status).Should(BeEquivalentTo(corev1.ConditionFalse)) 1138 g.Expect(cond.Reason).Should(BeEquivalentTo(ReasonHaveResidualPVCs)) 1139 }).Should(Succeed()) 1140 1141 By("releasing the PVC for pre-checking") 1142 pvcKey := types.NamespacedName{ 1143 Name: (&reconcileContext{repo: repo}).preCheckResourceName(), 1144 Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), 1145 } 1146 removePVCProtectionFinalizer(pvcKey) 1147 1148 By("releasing the PVC") 1149 pvcKey = types.NamespacedName{ 1150 Namespace: namespace2, 1151 Name: pvcName, 1152 } 1153 removePVCProtectionFinalizer(pvcKey) 1154 1155 By("checking the BackupRepo, it should have been deleted") 1156 Eventually(func(g Gomega) { 1157 repo := &dpv1alpha1.BackupRepo{} 1158 err := testCtx.Cli.Get(testCtx.Ctx, repoKey, repo) 1159 g.Expect(apierrors.IsNotFound(err)).Should(BeTrue()) 1160 }).Should(Succeed()) 1161 1162 By("checking derived objects should be all deleted") 1163 Eventually(func(g Gomega) { 1164 // get the newest repo object 1165 repo := &dpv1alpha1.BackupRepo{} 1166 err := testCtx.Cli.Get(testCtx.Ctx, repoKey, repo) 1167 if apierrors.IsNotFound(err) { 1168 return 1169 } 1170 g.Expect(err).ShouldNot(HaveOccurred()) 1171 1172 // check the secret for the CSI driver 1173 err = testCtx.Cli.Get(testCtx.Ctx, types.NamespacedName{ 1174 Name: repo.Status.GeneratedCSIDriverSecret.Name, 1175 Namespace: repo.Status.GeneratedCSIDriverSecret.Namespace, 1176 }, &corev1.Secret{}) 1177 g.Expect(apierrors.IsNotFound(err)).Should(BeTrue()) 1178 1179 // check the storage class 1180 err = testCtx.Cli.Get(testCtx.Ctx, types.NamespacedName{ 1181 Name: repo.Status.GeneratedStorageClassName, 1182 }, &storagev1.StorageClass{}) 1183 g.Expect(apierrors.IsNotFound(err)).Should(BeTrue()) 1184 1185 // check the PVC 1186 pvc := &corev1.PersistentVolumeClaim{} 1187 err = testCtx.Cli.Get(testCtx.Ctx, pvcKey, pvc) 1188 g.Expect(apierrors.IsNotFound(err)).Should(BeTrue()) 1189 }).Should(Succeed()) 1190 }) 1191 1192 It("should delete resources for pre-checking when deleting the repo", func() { 1193 By("preparing") 1194 createBackupRepoSpec(nil) 1195 resourceName := preCheckResouceName(repo) 1196 namespace := viper.GetString(constant.CfgKeyCtrlrMgrNS) 1197 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1198 &batchv1.Job{}, true)).Should(Succeed()) 1199 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1200 &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) 1201 removePVCProtectionFinalizer(types.NamespacedName{Name: resourceName, Namespace: namespace}) 1202 1203 By("deleting the repo") 1204 testapps.DeleteObject(&testCtx, repoKey, &dpv1alpha1.BackupRepo{}) 1205 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1206 &batchv1.Job{}, false)).Should(Succeed()) 1207 Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{Name: resourceName, Namespace: namespace}, 1208 &corev1.PersistentVolumeClaim{}, false)).Should(Succeed()) 1209 }) 1210 1211 It("should update backupRepo.status.isDefault", func() { 1212 By("making the repo default") 1213 Eventually(testapps.GetAndChangeObj(&testCtx, repoKey, func(repo *dpv1alpha1.BackupRepo) { 1214 repo.Annotations = map[string]string{ 1215 dptypes.DefaultBackupRepoAnnotationKey: trueVal, 1216 } 1217 })).Should(Succeed()) 1218 By("checking the repo is default") 1219 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 1220 g.Expect(repo.Status.IsDefault).Should(BeTrue()) 1221 })).Should(Succeed()) 1222 1223 By("making the repo non default") 1224 Eventually(testapps.GetAndChangeObj(&testCtx, repoKey, func(repo *dpv1alpha1.BackupRepo) { 1225 repo.Annotations = nil 1226 })).Should(Succeed()) 1227 By("checking the repo is not default") 1228 Eventually(testapps.CheckObj(&testCtx, repoKey, func(g Gomega, repo *dpv1alpha1.BackupRepo) { 1229 g.Expect(repo.Status.IsDefault).Should(BeFalse()) 1230 })).Should(Succeed()) 1231 }) 1232 }) 1233 })