diff --git a/api/v1alpha1/dataprotectionapplication_types.go b/api/v1alpha1/dataprotectionapplication_types.go index ea603aa39e1..f77400a1024 100644 --- a/api/v1alpha1/dataprotectionapplication_types.go +++ b/api/v1alpha1/dataprotectionapplication_types.go @@ -433,8 +433,33 @@ type NodeAgentConfig struct { // Embedding NodeAgentConfigMapSettings // +optional NodeAgentConfigMapSettings `json:",inline"` + // Embedding KopiaRepoOptions + // +optional + KopiaRepoOptions `json:",inline"` +} + +type KopiaRepoOptions struct { + // CacheLimitMB specifies the size limit(in MB) for the local data cache + // +kubebuilder:validation:Minimum=0 + // +optional + CacheLimitMB *int64 `json:"cacheLimitMB,omitempty"` + // fullMaintenanceInterval determines the time between kopia full maintenance operations. + // normalGC: 24 hours + // fastGC: 12 hours + // eagerGC: 6 hours + // +kubebuilder:validation:Enum=normalGC;fastGC;eagerGC + // +optional + FullMaintenanceInterval FullMaintenanceInterval `json:"fullMaintenanceInterval,omitempty"` } +type FullMaintenanceInterval string + +const ( + FullMaintenanceIntervalNormalGC FullMaintenanceInterval = "normalGC" + FullMaintenanceIntervalFastGC FullMaintenanceInterval = "fastGC" + FullMaintenanceIntervalEagerGC FullMaintenanceInterval = "eagerGC" +) + // ResticConfig is the configuration for restic server type ResticConfig struct { // Embedding NodeAgentCommonFields diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2f7be3a0e89..a8e458439e1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -525,6 +525,26 @@ func (in *GlobalFlags) DeepCopy() *GlobalFlags { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KopiaRepoOptions) DeepCopyInto(out *KopiaRepoOptions) { + *out = *in + if in.CacheLimitMB != nil { + in, out := &in.CacheLimitMB, &out.CacheLimitMB + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KopiaRepoOptions. +func (in *KopiaRepoOptions) DeepCopy() *KopiaRepoOptions { + if in == nil { + return nil + } + out := new(KopiaRepoOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LoadConcurrency) DeepCopyInto(out *LoadConcurrency) { *out = *in @@ -652,6 +672,7 @@ func (in *NodeAgentConfig) DeepCopyInto(out *NodeAgentConfig) { **out = **in } in.NodeAgentConfigMapSettings.DeepCopyInto(&out.NodeAgentConfigMapSettings) + in.KopiaRepoOptions.DeepCopyInto(&out.KopiaRepoOptions) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeAgentConfig. diff --git a/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml b/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml index 4872408131e..427d78448af 100644 --- a/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml +++ b/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml @@ -221,6 +221,11 @@ spec: type: object description: BackupPVCConfig is the config for backupPVC (intermediate PVC) of snapshot data movement type: object + cacheLimitMB: + description: CacheLimitMB specifies the size limit(in MB) for the local data cache + format: int64 + minimum: 0 + type: integer dataMoverPrepareTimeout: description: How long to wait for preparing a DataUpload/DataDownload. Default is 30 minutes. type: string @@ -229,6 +234,17 @@ spec: enable defines a boolean pointer whether we want the daemonset to exist or not type: boolean + fullMaintenanceInterval: + description: |- + fullMaintenanceInterval determines the time between kopia full maintenance operations. + normalGC: 24 hours + fastGC: 12 hours + eagerGC: 6 hours + enum: + - normalGC + - fastGC + - eagerGC + type: string loadConcurrency: description: LoadConcurrency is the config for data path load concurrency per node. properties: diff --git a/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml b/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml index 8b678a141d9..252c3cebad7 100644 --- a/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml +++ b/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml @@ -221,6 +221,11 @@ spec: type: object description: BackupPVCConfig is the config for backupPVC (intermediate PVC) of snapshot data movement type: object + cacheLimitMB: + description: CacheLimitMB specifies the size limit(in MB) for the local data cache + format: int64 + minimum: 0 + type: integer dataMoverPrepareTimeout: description: How long to wait for preparing a DataUpload/DataDownload. Default is 30 minutes. type: string @@ -229,6 +234,17 @@ spec: enable defines a boolean pointer whether we want the daemonset to exist or not type: boolean + fullMaintenanceInterval: + description: |- + fullMaintenanceInterval determines the time between kopia full maintenance operations. + normalGC: 24 hours + fastGC: 12 hours + eagerGC: 6 hours + enum: + - normalGC + - fastGC + - eagerGC + type: string loadConcurrency: description: LoadConcurrency is the config for data path load concurrency per node. properties: diff --git a/go.mod b/go.mod index 5bd1fa7f589..ab7518d60c0 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 github.com/deckarep/golang-set/v2 v2.3.0 github.com/google/go-cmp v0.6.0 + github.com/stretchr/testify v1.10.0 github.com/vmware-tanzu/velero v1.16.0-rc.1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 k8s.io/klog/v2 v2.130.1 @@ -97,6 +98,7 @@ require ( github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oklog/run v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect diff --git a/internal/controller/backup_repository.go b/internal/controller/backup_repository.go new file mode 100644 index 00000000000..f2f3a1fe7b3 --- /dev/null +++ b/internal/controller/backup_repository.go @@ -0,0 +1,106 @@ +package controller + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" + "github.com/openshift/oadp-operator/pkg/common" +) + +func isBackupRepositoryCmRequired(config *oadpv1alpha1.NodeAgentConfig) bool { + return config != nil && (config.KopiaRepoOptions.CacheLimitMB != nil || len(config.KopiaRepoOptions.FullMaintenanceInterval) > 0) +} + +// updateBackupRepositoryCM handles the creation or update of the BackupRepository ConfigMap with all required data. +func (r *DataProtectionApplicationReconciler) updateBackupRepositoryCM(cm *corev1.ConfigMap) error { + // Set the owner reference to ensure the ConfigMap is managed by the DPA + if err := controllerutil.SetControllerReference(r.dpa, cm, r.Scheme); err != nil { + return fmt.Errorf("failed to set controller reference: %w", err) + } + + // Convert KopiaRepoOptions to a generic map + configBackupRepositoryJSON, err := json.Marshal(r.dpa.Spec.Configuration.NodeAgent.KopiaRepoOptions) + if err != nil { + return fmt.Errorf("failed to serialize backup repository config: %w", err) + } + + cm.Name = common.BackupRepoConfigMapPrefix + r.dpa.Name + cm.Namespace = r.NamespacedName.Namespace + cm.Labels = map[string]string{ + "app.kubernetes.io/instance": r.dpa.Name, + "app.kubernetes.io/managed-by": common.OADPOperator, + "app.kubernetes.io/component": "backup-repository-config", + oadpv1alpha1.OadpOperatorLabel: "True", + } + + if cm.Data == nil { + cm.Data = make(map[string]string) + } + cm.Data["kopia"] = string(configBackupRepositoryJSON) + + return nil +} + +// GetBackupRepositoryConfigMapName returns the NamespacedName of the BackupRepository ConfigMap +func (r *DataProtectionApplicationReconciler) GetBackupRepositoryConfigMapName() types.NamespacedName { + return types.NamespacedName{ + Name: common.BackupRepoConfigMapPrefix + r.dpa.Name, + Namespace: r.NamespacedName.Namespace, + } +} + +// ReconcileBackupRepositoryConfigMap handles creation, update, and deletion of the BackupRepository ConfigMap. +func (r *DataProtectionApplicationReconciler) ReconcileBackupRepositoryConfigMap(log logr.Logger) (bool, error) { + dpa := r.dpa + cmName := r.GetBackupRepositoryConfigMapName() + configMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: cmName.Name, + Namespace: cmName.Namespace, + }, + } + + // Delete CM if it is not required + if !isBackupRepositoryCmRequired(dpa.Spec.Configuration.NodeAgent) { + err := r.Get(r.Context, cmName, &configMap) + if err != nil && !errors.IsNotFound(err) { + return false, err + } + if errors.IsNotFound(err) { + return true, nil + } + deleteContext := context.Background() + if err := r.Delete(deleteContext, &configMap); err != nil { + if errors.IsNotFound(err) { + return true, nil + } + return false, err + } + r.EventRecorder.Event(&configMap, corev1.EventTypeNormal, "DeletedBackupRepositoryConfigMap", "BackupRepository config map deleted") + return true, nil + } + + op, err := controllerutil.CreateOrPatch(r.Context, r.Client, &configMap, func() error { + return r.updateBackupRepositoryCM(&configMap) + }) + if err != nil { + return false, fmt.Errorf("failed to create or patch config map: %w", err) + } + + if op == controllerutil.OperationResultCreated { + r.EventRecorder.Event(&configMap, corev1.EventTypeNormal, "CreatedBackupRepositoryConfigMap", "BackupRepository config map created") + } else if op == controllerutil.OperationResultUpdated { + r.EventRecorder.Event(&configMap, corev1.EventTypeNormal, "UpdatedBackupRepositoryConfigMap", "BackupRepository config map updated") + } + + return true, nil +} diff --git a/internal/controller/backup_repository_test.go b/internal/controller/backup_repository_test.go new file mode 100644 index 00000000000..22a031c2042 --- /dev/null +++ b/internal/controller/backup_repository_test.go @@ -0,0 +1,109 @@ +package controller + +import ( + "context" + "encoding/json" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" +) + +func TestDataProtectionApplicationReconciler_updateBackupRepositoryCM(t *testing.T) { + tests := []struct { + name string + cm *corev1.ConfigMap + dpa *oadpv1alpha1.DataProtectionApplication + wantCM *corev1.ConfigMap + }{ + { + name: "backup repository cm is updated successfully with full config", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backup-repository-test-dpa", + Namespace: "test-ns", + }, + }, + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-ns", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + BackupImages: ptr.To(false), + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + NoDefaultBackupLocation: true, + }, + NodeAgent: &oadpv1alpha1.NodeAgentConfig{ + KopiaRepoOptions: oadpv1alpha1.KopiaRepoOptions{ + CacheLimitMB: ptr.To(int64(4096)), + FullMaintenanceInterval: "fastGC", + }, + }, + }, + }, + }, + wantCM: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backup-repository-test-dpa", + Namespace: "test-ns", + Labels: map[string]string{ + "app.kubernetes.io/instance": "test-dpa", + "app.kubernetes.io/managed-by": "oadp-operator", + "app.kubernetes.io/component": "backup-repository-config", + "openshift.io/oadp": "True", + }, + }, + Data: map[string]string{ + "kopia": `{"cacheLimitMB":4096,"fullMaintenanceInterval":"fastGC"}`, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient, err := getFakeClientFromObjects(tt.cm, tt.dpa) + if err != nil { + t.Errorf("error in creating fake client, likely programmer error") + } + r := &DataProtectionApplicationReconciler{ + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: logr.Discard(), + Context: context.Background(), + NamespacedName: types.NamespacedName{ + Namespace: tt.cm.Namespace, + Name: tt.cm.Name, + }, + EventRecorder: record.NewFakeRecorder(10), + dpa: tt.dpa, + } + + err = r.updateBackupRepositoryCM(tt.cm) + require.NoError(t, err) + require.Equal(t, tt.wantCM.ObjectMeta.Name, tt.cm.ObjectMeta.Name, "ConfigMap Name does not match") + require.Equal(t, tt.wantCM.ObjectMeta.Namespace, tt.cm.ObjectMeta.Namespace, "ConfigMap Namespace does not match") + require.Equal(t, tt.wantCM.ObjectMeta.Labels, tt.cm.ObjectMeta.Labels, "ConfigMap Labels do not match") + + // Compare Data fields, we need to unmarshal the JSON to ignore key order + expectedData := tt.wantCM.Data["kopia"] + actualData := tt.cm.Data["kopia"] + + var expectedMap map[string]interface{} + var actualMap map[string]interface{} + + require.NoError(t, json.Unmarshal([]byte(expectedData), &expectedMap), "Failed to unmarshal expected Data") + require.NoError(t, json.Unmarshal([]byte(actualData), &actualMap), "Failed to unmarshal actual Data") + require.Equal(t, expectedMap, actualMap, "ConfigMap Data does not match") + }) + } +} diff --git a/internal/controller/dataprotectionapplication_controller.go b/internal/controller/dataprotectionapplication_controller.go index 00b74fe8032..6b6f74d9182 100644 --- a/internal/controller/dataprotectionapplication_controller.go +++ b/internal/controller/dataprotectionapplication_controller.go @@ -105,6 +105,7 @@ func (r *DataProtectionApplicationReconciler) Reconcile(ctx context.Context, req r.ReconcileVolumeSnapshotLocations, r.ReconcileVeleroDeployment, r.ReconcileNodeAgentConfigMap, + r.ReconcileBackupRepositoryConfigMap, r.ReconcileNodeAgentDaemonset, r.ReconcileVeleroMetricsSVC, r.ReconcileNonAdminController, diff --git a/internal/controller/velero.go b/internal/controller/velero.go index 63ddcd376c4..be41b7889c8 100644 --- a/internal/controller/velero.go +++ b/internal/controller/velero.go @@ -374,6 +374,16 @@ func (r *DataProtectionApplicationReconciler) customizeVeleroDeployment(veleroDe veleroContainer.Args = append(veleroContainer.Args, fmt.Sprintf("--item-block-worker-count=%v", dpa.Spec.Configuration.Velero.ItemBlockWorkerCount)) } + // check for backup-repository-configmap parameter + if isBackupRepositoryCmRequired(dpa.Spec.Configuration.NodeAgent) { + // Add the --backup-repository-configmap parameter with the name + // of the backup-repository ConfigMap + cmName := r.GetBackupRepositoryConfigMapName() + if cmName.Name != "" { + veleroContainer.Args = append(veleroContainer.Args, fmt.Sprintf("--backup-repository-configmap=%s", cmName.Name)) + } + } + // Set defaults to avoid update events if veleroDeployment.Spec.Strategy.Type == "" { veleroDeployment.Spec.Strategy.Type = appsv1.RollingUpdateDeploymentStrategyType diff --git a/internal/controller/velero_test.go b/internal/controller/velero_test.go index 0971d3b175b..5e662e7aa1a 100644 --- a/internal/controller/velero_test.go +++ b/internal/controller/velero_test.go @@ -1072,6 +1072,32 @@ func TestDPAReconciler_buildVeleroDeployment(t *testing.T) { veleroDeployment: testVeleroDeployment.DeepCopy(), errorMessage: "invalid log level infotypo, use: trace, debug, info, warning, error, fatal, or panic", }, + { + name: "valid DPA CR with BackupRepository config, Velero Deployment is built with BackupRepository configmap arg", + dpa: createTestDpaWith( + nil, + oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{}, + NodeAgent: &oadpv1alpha1.NodeAgentConfig{ + KopiaRepoOptions: oadpv1alpha1.KopiaRepoOptions{ + CacheLimitMB: ptr.To(int64(4096)), + FullMaintenanceInterval: "fastGC", + }, + }, + }, + }, + ), + veleroDeployment: testVeleroDeployment.DeepCopy(), + wantVeleroDeployment: createTestBuiltVeleroDeployment(TestBuiltVeleroDeploymentOptions{ + args: []string{ + defaultFileSystemBackupTimeout, + defaultRestoreResourcePriorities, + defaultDisableInformerCache, + "--backup-repository-configmap=backup-repository-test-DPA-CR", + }, + }), + }, { name: "valid DPA CR with ResourceTimeout, Velero Deployment is built with ResourceTimeout arg", dpa: createTestDpaWith( diff --git a/pkg/common/common.go b/pkg/common/common.go index 8666d72c6df..05171386841 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -25,6 +25,7 @@ const ( OADPOperatorServiceAccount = OADPOperatorPrefix + "controller-manager" RegistryDeploymentLabel = "openshift.io/oadp-registry" NodeAgentConfigMapPrefix = "node-agent-" + BackupRepoConfigMapPrefix = "backup-repository-" ) var DefaultRestoreResourcePriorities = types.Priorities{