diff --git a/cmd/main.go b/cmd/main.go index dba27ef..c568bd0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -176,6 +176,17 @@ func main() { os.Exit(1) } // +kubebuilder:scaffold:builder + if dpaConfiguration.BackupSyncPeriod.Duration > 0 { + if err = (&controller.NonAdminBackupSynchronizerReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + OADPNamespace: oadpNamespace, + SyncPeriod: dpaConfiguration.BackupSyncPeriod.Duration, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to setup NonAdminBackupSynchronizer controller with manager") + os.Exit(1) + } + } if dpaConfiguration.GarbageCollectionPeriod.Duration > 0 { if err = (&controller.GarbageCollectorReconciler{ Client: mgr.GetClient(), @@ -209,6 +220,9 @@ func getDPAConfiguration(restConfig *rest.Config, oadpNamespace string) (v1alpha GarbageCollectionPeriod: &metav1.Duration{ Duration: 24 * time.Hour, //nolint:revive // 1 day }, + BackupSyncPeriod: &metav1.Duration{ + Duration: 2 * time.Minute, //nolint:revive // velero default is 1 minute + }, EnforceBackupSpec: &velerov1.BackupSpec{}, EnforceRestoreSpec: &velerov1.RestoreSpec{}, } @@ -238,6 +252,9 @@ func getDPAConfiguration(restConfig *rest.Config, oadpNamespace string) (v1alpha if nonAdmin.GarbageCollectionPeriod != nil { dpaConfiguration.GarbageCollectionPeriod.Duration = nonAdmin.GarbageCollectionPeriod.Duration } + if nonAdmin.BackupSyncPeriod != nil { + dpaConfiguration.BackupSyncPeriod.Duration = nonAdmin.BackupSyncPeriod.Duration + } break } } diff --git a/go.mod b/go.mod index d201f80..0d2998f 100644 --- a/go.mod +++ b/go.mod @@ -82,3 +82,5 @@ require ( ) replace github.com/vmware-tanzu/velero => github.com/openshift/velero v0.10.2-0.20241211163542-fa8f2486175b + +replace github.com/openshift/oadp-operator => github.com/mateusoliveira43/oadp-operator v0.0.0-20250205190228-fcc5ea4a6c32 diff --git a/go.sum b/go.sum index b7e17cf..721dcde 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mateusoliveira43/oadp-operator v0.0.0-20250205190228-fcc5ea4a6c32 h1:yQYiudGVEOJDv7GlXKL4h1vOlaVUzLrOK/l7JwzA9hg= +github.com/mateusoliveira43/oadp-operator v0.0.0-20250205190228-fcc5ea4a6c32/go.mod h1:gNUsBZgNcoS90Z68mNSUgx7pwtOc3THddWJx6uGV14A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -81,8 +83,6 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/openshift/oadp-operator v1.0.2-0.20250205172928-bb0c25a9c6af h1:qYsD1Hu2yt0S5lUcol603NeeSnEtDIEt7mLRzuFTdOo= -github.com/openshift/oadp-operator v1.0.2-0.20250205172928-bb0c25a9c6af/go.mod h1:gNUsBZgNcoS90Z68mNSUgx7pwtOc3THddWJx6uGV14A= github.com/openshift/velero v0.10.2-0.20241211163542-fa8f2486175b h1:ykGzFFul6lhtphKH4V6lWzdIW1oGEtiFfOdS1WyOuNw= github.com/openshift/velero v0.10.2-0.20241211163542-fa8f2486175b/go.mod h1:bbcPBz7mGYVY7ORWbQhD2Yx09e2ZKH2gqS+MQj+zJCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/internal/common/constant/constant.go b/internal/common/constant/constant.go index 6d870be..5adadc5 100644 --- a/internal/common/constant/constant.go +++ b/internal/common/constant/constant.go @@ -34,6 +34,7 @@ const ( NabOriginNACUUIDLabel = v1alpha1.OadpOperatorLabel + "-nab-origin-nacuuid" NarOriginNACUUIDLabel = v1alpha1.OadpOperatorLabel + "-nar-origin-nacuuid" NabslOriginNACUUIDLabel = v1alpha1.OadpOperatorLabel + "-nabsl-origin-nacuuid" + NabSyncLabel = v1alpha1.OadpOperatorLabel + "-nab-synced-from-nacuuid" NabOriginNameAnnotation = v1alpha1.OadpOperatorLabel + "-nab-origin-name" NabOriginNamespaceAnnotation = v1alpha1.OadpOperatorLabel + "-nab-origin-namespace" diff --git a/internal/controller/nonadminbackup_controller.go b/internal/controller/nonadminbackup_controller.go index 717cb27..0e7843c 100644 --- a/internal/controller/nonadminbackup_controller.go +++ b/internal/controller/nonadminbackup_controller.go @@ -117,6 +117,14 @@ func (r *NonAdminBackupReconciler) Reconcile(ctx context.Context, req ctrl.Reque r.removeNabFinalizerUponVeleroBackupDeletion, } + case function.CheckLabelAnnotationValueIsValid(nab.Labels, constant.NabSyncLabel): + logger.V(1).Info("Executing nab sync path") + reconcileSteps = []nonAdminBackupReconcileStepFunction{ + r.setBackupUUIDInStatus, + r.setFinalizerOnNonAdminBackup, + r.createVeleroBackupAndSyncWithNonAdminBackup, + } + default: // Standard creation/update path logger.V(1).Info("Executing nab creation/update path") @@ -555,7 +563,12 @@ func (r *NonAdminBackupReconciler) setBackupUUIDInStatus(ctx context.Context, lo } if nab.Status.VeleroBackup == nil || nab.Status.VeleroBackup.NACUUID == constant.EmptyString { - veleroBackupNACUUID := function.GenerateNacObjectUUID(nab.Namespace, nab.Name) + var veleroBackupNACUUID string + if value, ok := nab.Labels[constant.NabSyncLabel]; ok { + veleroBackupNACUUID = value + } else { + veleroBackupNACUUID = function.GenerateNacObjectUUID(nab.Namespace, nab.Name) + } nab.Status.VeleroBackup = &nacv1alpha1.VeleroBackup{ NACUUID: veleroBackupNACUUID, Namespace: r.OADPNamespace, diff --git a/internal/controller/nonadminbackupstoragelocation_controller.go b/internal/controller/nonadminbackupstoragelocation_controller.go index 930d50d..f80879f 100644 --- a/internal/controller/nonadminbackupstoragelocation_controller.go +++ b/internal/controller/nonadminbackupstoragelocation_controller.go @@ -72,7 +72,7 @@ type naBSLReconcileStepFunction func(ctx context.Context, logger logr.Logger, na // move the current state of the cluster closer to the desired state. func (r *NonAdminBackupStorageLocationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) - logger.V(1).Info("NonAdminBackup Reconcile start") + logger.V(1).Info("NonAdminBackupStorageLocation Reconcile start") // Get the NonAdminBackupStorageLocation object nabsl := &nacv1alpha1.NonAdminBackupStorageLocation{} @@ -124,7 +124,7 @@ func (r *NonAdminBackupStorageLocationReconciler) Reconcile(ctx context.Context, } } - logger.V(1).Info("NonAdminBackup Reconcile exit") + logger.V(1).Info("NonAdminBackupStorageLocation Reconcile exit") return ctrl.Result{}, nil } @@ -292,12 +292,6 @@ func (r *NonAdminBackupStorageLocationReconciler) initNaBSLCreate(ctx context.Co // validateNaBSLSpec validates the NonAdminBackupStorageLocation spec func (r *NonAdminBackupStorageLocationReconciler) validateNaBSLSpec(ctx context.Context, logger logr.Logger, nabsl *nacv1alpha1.NonAdminBackupStorageLocation) (bool, error) { - // Skip validation if not in New phase - if nabsl.Status.Phase != nacv1alpha1.NonAdminPhaseNew { - logger.V(1).Info("Skipping validation, not in New phase", constant.CurrentPhaseString, nabsl.Status.Phase) - return false, nil - } - err := function.ValidateBslSpec(ctx, r.Client, nabsl) if err != nil { updatedPhase := updateNonAdminPhase(&nabsl.Status.Phase, nacv1alpha1.NonAdminPhaseBackingOff) diff --git a/internal/controller/nonadminbackupsynchronizer_controller.go b/internal/controller/nonadminbackupsynchronizer_controller.go new file mode 100644 index 0000000..736fa85 --- /dev/null +++ b/internal/controller/nonadminbackupsynchronizer_controller.go @@ -0,0 +1,172 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/go-logr/logr" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/migtools/oadp-non-admin/internal/common/constant" + "github.com/migtools/oadp-non-admin/internal/common/function" + "github.com/migtools/oadp-non-admin/internal/source" +) + +// NonAdminBackupSynchronizerReconciler reconciles BackupStorageLocation objects +type NonAdminBackupSynchronizerReconciler struct { + client.Client + Scheme *runtime.Scheme + OADPNamespace string + SyncPeriod time.Duration +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *NonAdminBackupSynchronizerReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + labelSelector := client.MatchingLabels(function.GetNonAdminLabels()) + + logger.V(1).Info("NonAdminBackup Synchronization start") + + veleroBackupStorageLocationList := &velerov1.BackupStorageLocationList{} + if err := r.List(ctx, veleroBackupStorageLocationList, client.InNamespace(r.OADPNamespace)); err != nil { + return ctrl.Result{}, err + } + + var watchedBackupStorageLocations []string + relatedNonAdminBackupStorageLocations := map[string]string{} + for _, backupStorageLocation := range veleroBackupStorageLocationList.Items { + if backupStorageLocation.Spec.Default { + watchedBackupStorageLocations = append(watchedBackupStorageLocations, backupStorageLocation.Name) + } + if function.CheckVeleroBackupStorageLocationMetadata(&backupStorageLocation) { + err := r.Get(ctx, types.NamespacedName{ + Name: backupStorageLocation.Annotations[constant.NabslOriginNameAnnotation], + Namespace: backupStorageLocation.Annotations[constant.NabslOriginNamespaceAnnotation], + }, &nacv1alpha1.NonAdminBackupStorageLocation{}) + if err != nil { + if apierrors.IsNotFound(err) { + continue + } + logger.Error(err, "Unable to fetch NonAdminBackupStorageLocation") + return ctrl.Result{}, err + } + watchedBackupStorageLocations = append(watchedBackupStorageLocations, backupStorageLocation.Name) + relatedNonAdminBackupStorageLocations[backupStorageLocation.Name] = backupStorageLocation.Annotations[constant.NabslOriginNameAnnotation] + } + } + + veleroBackupList := &velerov1.BackupList{} + if err := r.List(ctx, veleroBackupList, client.InNamespace(r.OADPNamespace), labelSelector); err != nil { + return ctrl.Result{}, err + } + + var backupsToSync []velerov1.Backup + var possibleBackupsToSync []velerov1.Backup + for _, backup := range veleroBackupList.Items { + if function.CheckVeleroBackupAnnotations(&backup) && + function.CheckLabelAnnotationValueIsValid(backup.GetLabels(), constant.NabOriginNACUUIDLabel) && + backup.Status.CompletionTimestamp != nil && + slices.Contains(watchedBackupStorageLocations, backup.Spec.StorageLocation) { + possibleBackupsToSync = append(possibleBackupsToSync, backup) + } + } + + logger.V(1).Info(fmt.Sprintf("%v possible Backup(s) to be synced to NonAdmin namespaces", len(possibleBackupsToSync))) + for _, backup := range possibleBackupsToSync { + err := r.Get(ctx, types.NamespacedName{ + Name: backup.Annotations[constant.NabOriginNamespaceAnnotation], + }, &corev1.Namespace{}) + if err != nil { + if apierrors.IsNotFound(err) { + continue + } + logger.Error(err, "Unable to fetch Namespace") + return ctrl.Result{}, err + } + + err = r.Get(ctx, types.NamespacedName{ + Namespace: backup.Annotations[constant.NabOriginNamespaceAnnotation], + Name: backup.Annotations[constant.NabOriginNameAnnotation], + }, &nacv1alpha1.NonAdminBackup{}) + if err != nil { + if apierrors.IsNotFound(err) { + backupsToSync = append(backupsToSync, backup) + continue + } + logger.Error(err, "Unable to fetch NonAdminBackup") + return ctrl.Result{}, err + } + } + + logger.V(1).Info(fmt.Sprintf("%v Backup(s) to sync to NonAdmin namespaces", len(backupsToSync))) + for _, backup := range backupsToSync { + nab := &nacv1alpha1.NonAdminBackup{ + ObjectMeta: metav1.ObjectMeta{ + Name: backup.Annotations[constant.NabOriginNameAnnotation], + Namespace: backup.Annotations[constant.NabOriginNamespaceAnnotation], + // TODO sync operation does not preserve labels + Labels: map[string]string{ + constant.NabSyncLabel: backup.Labels[constant.NabOriginNACUUIDLabel], + }, + }, + Spec: nacv1alpha1.NonAdminBackupSpec{ + BackupSpec: &backup.Spec, + }, + } + value, exist := relatedNonAdminBackupStorageLocations[nab.Spec.BackupSpec.StorageLocation] + if exist { + nab.Spec.BackupSpec.StorageLocation = value + } else { + nab.Spec.BackupSpec.StorageLocation = constant.EmptyString + } + err := r.Create(ctx, nab) + if err != nil { + logger.Error(err, "Failed to create NonAdminBackup") + return ctrl.Result{}, err + } + } + + logger.V(1).Info("NonAdminBackup Synchronization exit") + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *NonAdminBackupSynchronizerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + Named("nonadminbackupsynchronizer"). + WithLogConstructor(func(_ *reconcile.Request) logr.Logger { + return logr.New(ctrl.Log.GetSink().WithValues("controller", "nonadminbackupsynchronizer")) + }). + WatchesRawSource(&source.PeriodicalSource{Frequency: r.SyncPeriod}). + Complete(r) +} diff --git a/internal/controller/nonadminbackupsynchronizer_controller_test.go b/internal/controller/nonadminbackupsynchronizer_controller_test.go new file mode 100644 index 0000000..b29782f --- /dev/null +++ b/internal/controller/nonadminbackupsynchronizer_controller_test.go @@ -0,0 +1,256 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/migtools/oadp-non-admin/internal/common/constant" + "github.com/migtools/oadp-non-admin/internal/common/function" +) + +type nonAdminBackupSynchronizerFullReconcileScenario struct { + backupsToCreate []backupToCreate + errorLogs int +} + +type backupToCreate struct { + nonAdminBSL bool + namespaceExist bool + withNonAdminLabelsAnnotations bool + finished bool +} + +var _ = ginkgo.Describe("Test full reconcile loop of NonAdminBackup Synchronizer Controller", func() { + var ( + ctx context.Context + cancel context.CancelFunc + nonAdminNamespace string + oadpNamespace string + counter int + ) + + ginkgo.BeforeEach(func() { + counter++ + nonAdminNamespace = fmt.Sprintf("test-non-admin-backup-synchronizer-reconcile-full-%v", counter) + oadpNamespace = nonAdminNamespace + "-oadp" + }) + + ginkgo.AfterEach(func() { + gomega.Expect(deleteTestNamespaces(ctx, nonAdminNamespace, oadpNamespace)).To(gomega.Succeed()) + + cancel() + + // wait manager shutdown + gomega.Eventually(func() (bool, error) { + logOutput := ginkgo.CurrentSpecReport().CapturedGinkgoWriterOutput + shutdownlog := "INFO Wait completed, proceeding to shutdown the manager" + return strings.Contains(logOutput, shutdownlog) && strings.Count(logOutput, shutdownlog) == 1, nil + }, 5*time.Second, 1*time.Second).Should(gomega.BeTrue()) + }) + + ginkgo.DescribeTable("Reconcile triggered by NAC Pod start up", + func(scenario nonAdminBackupSynchronizerFullReconcileScenario) { + ctx, cancel = context.WithCancel(context.Background()) + + gomega.Expect(createTestNamespaces(ctx, nonAdminNamespace, oadpNamespace)).To(gomega.Succeed()) + + defaultBSL := &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-default-bsl", + Namespace: oadpNamespace, + }, + Spec: velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "example-bucket", + }, + }, + Default: true, + }, + } + gomega.Expect(k8sClient.Create(ctx, defaultBSL)).To(gomega.Succeed()) + + nonAdminBSL := buildTestNonAdminBackupStorageLocation(nonAdminNamespace, "test-non-admin-bsl", nacv1alpha1.NonAdminBackupStorageLocationSpec{ + BackupStorageLocationSpec: &velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "another-bucket", + }, + }, + }, + }) + gomega.Expect(k8sClient.Create(ctx, nonAdminBSL)).To(gomega.Succeed()) + + bslForNonAdminBSL := &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{ + Name: fakeUUID, + Namespace: oadpNamespace, + Labels: function.GetNonAdminLabels(), + Annotations: function.GetNonAdminBackupStorageLocationAnnotations(nonAdminBSL.ObjectMeta), + }, + Spec: velerov1.BackupStorageLocationSpec{ + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "another-bucket", + }, + }, + }, + } + bslForNonAdminBSL.Labels[constant.NabslOriginNACUUIDLabel] = fakeUUID + gomega.Expect(k8sClient.Create(ctx, bslForNonAdminBSL)).To(gomega.Succeed()) + + for index, create := range scenario.backupsToCreate { + backup := buildTestBackup(oadpNamespace, fmt.Sprintf("test-backup-%v", index), nonAdminNamespace) + backup.Annotations[constant.NabOriginNameAnnotation] = fmt.Sprintf("test-non-admin-backup-%v", index) + if create.nonAdminBSL { + backup.Spec.StorageLocation = bslForNonAdminBSL.Name + } else { + backup.Spec.StorageLocation = defaultBSL.Name + } + if !create.namespaceExist { + backup.Annotations[constant.NabOriginNamespaceAnnotation] = "non-existent" + } + if !create.withNonAdminLabelsAnnotations { + backup.Labels = map[string]string{} + backup.Annotations = map[string]string{} + } + gomega.Expect(k8sClient.Create(ctx, backup)).To(gomega.Succeed()) + + if create.finished { + backup.Status = velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + CompletionTimestamp: &metav1.Time{Time: time.Now()}, + } + // can not call .Status().Update() for veleroBackup object https://github.com/vmware-tanzu/velero/issues/8285 + gomega.Expect(k8sClient.Update(ctx, backup)).To(gomega.Succeed()) + } + } + + nonAdminBackupsInNonAminNamespace := &nacv1alpha1.NonAdminBackupList{} + gomega.Expect(k8sClient.List(ctx, nonAdminBackupsInNonAminNamespace, client.InNamespace(nonAdminNamespace))).To(gomega.Succeed()) + gomega.Expect(nonAdminBackupsInNonAminNamespace.Items).To(gomega.BeEmpty()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: k8sClient.Scheme(), + Cache: cache.Options{ + DefaultNamespaces: map[string]cache.Config{ + nonAdminNamespace: {}, + oadpNamespace: {}, + "non-existent": {}, + }, + }, + }) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + err = (&NonAdminBackupSynchronizerReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + OADPNamespace: oadpNamespace, + SyncPeriod: 2 * time.Second, + }).SetupWithManager(k8sManager) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + go func() { + defer ginkgo.GinkgoRecover() + err = k8sManager.Start(ctx) + gomega.Expect(err).ToNot(gomega.HaveOccurred(), "failed to run manager") + }() + // wait manager start + gomega.Eventually(func() (bool, error) { + logOutput := ginkgo.CurrentSpecReport().CapturedGinkgoWriterOutput + startUpLog := `INFO Starting workers {"controller": "nonadminbackupsynchronizer", "worker count": 1}` + return strings.Contains(logOutput, startUpLog) && + strings.Count(logOutput, startUpLog) == 1, nil + }, 5*time.Second, 1*time.Second).Should(gomega.BeTrue()) + + time.Sleep(8 * time.Second) + gomega.Expect(strings.Count(ginkgo.CurrentSpecReport().CapturedGinkgoWriterOutput, "NonAdminBackup Synchronization start")).Should(gomega.Equal(5)) + gomega.Expect(strings.Count(ginkgo.CurrentSpecReport().CapturedGinkgoWriterOutput, "4 possible Backup(s) to be synced to NonAdmin namespaces")).Should(gomega.Equal(5)) + gomega.Expect(strings.Count(ginkgo.CurrentSpecReport().CapturedGinkgoWriterOutput, "2 Backup(s) to sync to NonAdmin namespaces")).Should(gomega.Equal(1)) + gomega.Expect(strings.Count(ginkgo.CurrentSpecReport().CapturedGinkgoWriterOutput, "0 Backup(s) to sync to NonAdmin namespaces")).Should(gomega.Equal(4)) + gomega.Expect(strings.Count(ginkgo.CurrentSpecReport().CapturedGinkgoWriterOutput, "ERROR")).Should(gomega.Equal(scenario.errorLogs)) + + gomega.Expect(k8sClient.List(ctx, nonAdminBackupsInNonAminNamespace, client.InNamespace(nonAdminNamespace))).To(gomega.Succeed()) + gomega.Expect(nonAdminBackupsInNonAminNamespace.Items).To(gomega.HaveLen(2)) + }, + ginkgo.Entry("Should sync NonAdminBackups to non admin namespace", nonAdminBackupSynchronizerFullReconcileScenario{ + backupsToCreate: []backupToCreate{ + { + nonAdminBSL: false, + namespaceExist: true, + withNonAdminLabelsAnnotations: true, + finished: true, + }, + { + nonAdminBSL: false, + namespaceExist: false, + withNonAdminLabelsAnnotations: true, + finished: true, + }, + { + nonAdminBSL: false, + namespaceExist: true, + withNonAdminLabelsAnnotations: false, + finished: true, + }, + { + nonAdminBSL: false, + namespaceExist: true, + withNonAdminLabelsAnnotations: true, + finished: false, + }, + { + nonAdminBSL: true, + namespaceExist: true, + withNonAdminLabelsAnnotations: true, + finished: true, + }, + { + nonAdminBSL: true, + namespaceExist: false, + withNonAdminLabelsAnnotations: true, + finished: true, + }, + { + nonAdminBSL: true, + namespaceExist: true, + withNonAdminLabelsAnnotations: false, + finished: true, + }, + { + nonAdminBSL: true, + namespaceExist: true, + withNonAdminLabelsAnnotations: true, + finished: false, + }, + }, + }), + ) +})