diff --git a/api/v1beta1/aerospikebackup_types.go b/api/v1beta1/aerospikebackup_types.go index 93efbf90..1bc37439 100644 --- a/api/v1beta1/aerospikebackup_types.go +++ b/api/v1beta1/aerospikebackup_types.go @@ -34,10 +34,10 @@ type AerospikeBackupSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Backup Config" Config runtime.RawExtension `json:"config"` - // OnDemand is the configuration on demand backups. + // OnDemandBackups is the configuration for on-demand backups. // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="On Demand Backups" // +kubebuilder:validation:MaxItems:=1 - OnDemand []OnDemandSpec `json:"onDemand,omitempty"` + OnDemandBackups []OnDemandBackupSpec `json:"OnDemandBackups,omitempty"` } type BackupService struct { @@ -48,7 +48,7 @@ type BackupService struct { Namespace string `json:"namespace"` } -type OnDemandSpec struct { +type OnDemandBackupSpec struct { // ID is the unique identifier for the on-demand backup. // +kubebuilder:validation:MinLength=1 ID string `json:"id"` @@ -69,8 +69,9 @@ type AerospikeBackupStatus struct { // This config is used to trigger backups. It includes: aerospike-cluster, backup-routines. Config runtime.RawExtension `json:"config"` - // OnDemand is the configuration for demand backups. - OnDemand []OnDemandSpec `json:"onDemand,omitempty"` + // OnDemandBackups is the configuration for on-demand backups. + OnDemandBackups []OnDemandBackupSpec `json:"OnDemandBackups,omitempty"` + // TODO: finalize the status and phase } diff --git a/api/v1beta1/aerospikebackup_webhook.go b/api/v1beta1/aerospikebackup_webhook.go index 1673b9cf..fc61b144 100644 --- a/api/v1beta1/aerospikebackup_webhook.go +++ b/api/v1beta1/aerospikebackup_webhook.go @@ -62,7 +62,7 @@ var _ webhook.Validator = &AerospikeBackup{} func (r *AerospikeBackup) ValidateCreate() (admission.Warnings, error) { aerospikeBackupLog.Info("validate create", "name", r.Name) - if len(r.Spec.OnDemand) != 0 && r.Spec.Config.Raw != nil { + if len(r.Spec.OnDemandBackups) != 0 && r.Spec.Config.Raw != nil { return nil, fmt.Errorf("onDemand and backup config cannot be specified together while creating backup") } @@ -87,16 +87,16 @@ func (r *AerospikeBackup) ValidateUpdate(old runtime.Object) (admission.Warnings return nil, err } - if len(r.Spec.OnDemand) > 0 && len(oldObj.Spec.OnDemand) > 0 { + if len(r.Spec.OnDemandBackups) > 0 && len(oldObj.Spec.OnDemandBackups) > 0 { // Check if onDemand backup spec is updated - if r.Spec.OnDemand[0].ID == oldObj.Spec.OnDemand[0].ID && - !reflect.DeepEqual(r.Spec.OnDemand[0], oldObj.Spec.OnDemand[0]) { + if r.Spec.OnDemandBackups[0].ID == oldObj.Spec.OnDemandBackups[0].ID && + !reflect.DeepEqual(r.Spec.OnDemandBackups[0], oldObj.Spec.OnDemandBackups[0]) { return nil, fmt.Errorf("onDemand backup cannot be updated") } // Check if previous onDemand backup is completed before allowing new onDemand backup - if r.Spec.OnDemand[0].ID != oldObj.Spec.OnDemand[0].ID && (len(r.Status.OnDemand) == 0 || - r.Status.OnDemand[0].ID != oldObj.Spec.OnDemand[0].ID) { + if r.Spec.OnDemandBackups[0].ID != oldObj.Spec.OnDemandBackups[0].ID && (len(r.Status.OnDemandBackups) == 0 || + r.Status.OnDemandBackups[0].ID != oldObj.Spec.OnDemandBackups[0].ID) { return nil, fmt.Errorf("can not add new onDemand backup when previous onDemand backup is not completed") } @@ -133,27 +133,36 @@ func (r *AerospikeBackup) validateBackupConfig() error { return err } - backupSvcConfigMap := make(map[string]interface{}) + backupConfigMap := make(map[string]interface{}) - if err := yaml.Unmarshal(r.Spec.Config.Raw, &backupSvcConfigMap); err != nil { + if err := yaml.Unmarshal(r.Spec.Config.Raw, &backupConfigMap); err != nil { return err } - aeroClusters, err := validateAerospikeCluster(&backupSvcConfig, backupSvcConfigMap) - if err != nil { - return err + if _, ok := backupConfigMap[common.ServiceKey]; ok { + return fmt.Errorf("service field cannot be specified in backup config") + } + + if _, ok := backupConfigMap[common.BackupPoliciesKey]; ok { + return fmt.Errorf("backup-policies field cannot be specified in backup config") } - backupRoutines, err := validateBackupRoutines(&backupSvcConfig, backupSvcConfigMap) + if _, ok := backupConfigMap[common.StorageKey]; ok { + return fmt.Errorf("storage field cannot be specified in backup config") + } + + if _, ok := backupConfigMap[common.SecretAgentsKey]; ok { + return fmt.Errorf("secret-agent field cannot be specified in backup config") + } + + aeroClusters, err := validateAerospikeCluster(&backupSvcConfig, backupConfigMap) if err != nil { return err } - // validate if only correct aerospike cluster (the one referred in Backup CR) is used in backup routines - for _, routine := range backupRoutines { - if _, ok := aeroClusters[routine.SourceCluster]; !ok { - return fmt.Errorf("cluster %s not found in backup backupSvcConfig", routine.SourceCluster) - } + err = validateBackupRoutines(&backupSvcConfig, backupConfigMap, aeroClusters) + if err != nil { + return err } // Add empty placeholders for missing backupSvcConfig sections. This is required for validation to work. @@ -174,10 +183,10 @@ func (r *AerospikeBackup) validateBackupConfig() error { } // Validate on-demand backup - if len(r.Spec.OnDemand) > 0 { - if _, ok := backupSvcConfig.BackupRoutines[r.Spec.OnDemand[0].RoutineName]; !ok { + if len(r.Spec.OnDemandBackups) > 0 { + if _, ok := backupSvcConfig.BackupRoutines[r.Spec.OnDemandBackups[0].RoutineName]; !ok { return fmt.Errorf("invalid onDemand config, backup routine %s not found", - r.Spec.OnDemand[0].RoutineName) + r.Spec.OnDemandBackups[0].RoutineName) } } @@ -241,25 +250,36 @@ func validateAerospikeCluster(backupSvcConfig *model.Config, backupSvcConfigMap } func validateBackupRoutines(backupSvcConfig *model.Config, backupSvcConfigMap map[string]interface{}, -) (map[string]*model.BackupRoutine, error) { + aeroClusters map[string]*model.AerospikeCluster) error { if _, ok := backupSvcConfigMap[common.BackupRoutinesKey]; !ok { - return nil, fmt.Errorf("backup-routines field is required in backupSvcConfig") + return fmt.Errorf("backup-routines field is required in backup onfig") } routines, ok := backupSvcConfigMap[common.BackupRoutinesKey].(map[string]interface{}) if !ok { - return nil, fmt.Errorf("backup-routines field is not in the right format") + return fmt.Errorf("backup-routines field is not in the right format") } routineBytes, rErr := yaml.Marshal(routines) if rErr != nil { - return nil, rErr + return rErr } backupRoutines := make(map[string]*model.BackupRoutine) if err := yaml.Unmarshal(routineBytes, &backupRoutines); err != nil { - return nil, err + return err + } + + if len(backupRoutines) == 0 { + return fmt.Errorf("backup-routines field is empty") + } + + // validate if only correct aerospike cluster (the one referred in Backup CR) is used in backup routines + for _, routine := range backupRoutines { + if _, ok := aeroClusters[routine.SourceCluster]; !ok { + return fmt.Errorf("cluster %s not found in backup aerospike-cluster config", routine.SourceCluster) + } } if len(backupSvcConfig.BackupRoutines) == 0 { @@ -270,5 +290,5 @@ func validateBackupRoutines(backupSvcConfig *model.Config, backupSvcConfigMap ma backupSvcConfig.BackupRoutines[name] = routine } - return backupRoutines, nil + return nil } diff --git a/api/v1beta1/aerospikebackupservice_webhook.go b/api/v1beta1/aerospikebackupservice_webhook.go index 311c6b26..34f13d8e 100644 --- a/api/v1beta1/aerospikebackupservice_webhook.go +++ b/api/v1beta1/aerospikebackupservice_webhook.go @@ -17,6 +17,8 @@ limitations under the License. package v1beta1 import ( + "fmt" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -85,6 +87,14 @@ func (r *AerospikeBackupService) validateBackupServiceConfig() error { return err } + if len(config.BackupRoutines) != 0 { + return fmt.Errorf("backup-routines field cannot be specified in backup service config") + } + + if len(config.AerospikeClusters) != 0 { + return fmt.Errorf("aerospike-clusters field cannot be specified in backup service config") + } + // Add empty placeholders for missing config sections. This is required for validation to work. if config.ServiceConfig == nil { config.ServiceConfig = &model.BackupServiceConfig{} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index d7fce012..1930b2fc 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -251,9 +251,9 @@ func (in *AerospikeBackupSpec) DeepCopyInto(out *AerospikeBackupSpec) { *out = *in out.BackupService = in.BackupService in.Config.DeepCopyInto(&out.Config) - if in.OnDemand != nil { - in, out := &in.OnDemand, &out.OnDemand - *out = make([]OnDemandSpec, len(*in)) + if in.OnDemandBackups != nil { + in, out := &in.OnDemandBackups, &out.OnDemandBackups + *out = make([]OnDemandBackupSpec, len(*in)) copy(*out, *in) } } @@ -273,9 +273,9 @@ func (in *AerospikeBackupStatus) DeepCopyInto(out *AerospikeBackupStatus) { *out = *in out.BackupService = in.BackupService in.Config.DeepCopyInto(&out.Config) - if in.OnDemand != nil { - in, out := &in.OnDemand, &out.OnDemand - *out = make([]OnDemandSpec, len(*in)) + if in.OnDemandBackups != nil { + in, out := &in.OnDemandBackups, &out.OnDemandBackups + *out = make([]OnDemandBackupSpec, len(*in)) copy(*out, *in) } } @@ -1116,17 +1116,17 @@ func (in *MountOptions) DeepCopy() *MountOptions { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *OnDemandSpec) DeepCopyInto(out *OnDemandSpec) { +func (in *OnDemandBackupSpec) DeepCopyInto(out *OnDemandBackupSpec) { *out = *in out.Delay = in.Delay } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnDemandSpec. -func (in *OnDemandSpec) DeepCopy() *OnDemandSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnDemandBackupSpec. +func (in *OnDemandBackupSpec) DeepCopy() *OnDemandBackupSpec { if in == nil { return nil } - out := new(OnDemandSpec) + out := new(OnDemandBackupSpec) in.DeepCopyInto(out) return out } diff --git a/config/crd/bases/asdb.aerospike.com_aerospikebackups.yaml b/config/crd/bases/asdb.aerospike.com_aerospikebackups.yaml index f4b99d5f..098a0f72 100644 --- a/config/crd/bases/asdb.aerospike.com_aerospikebackups.yaml +++ b/config/crd/bases/asdb.aerospike.com_aerospikebackups.yaml @@ -45,6 +45,28 @@ spec: description: AerospikeBackupSpec defines the desired state of AerospikeBackup for a given AerospikeCluster properties: + OnDemandBackups: + description: OnDemandBackups is the configuration for on-demand backups. + items: + properties: + delay: + description: Delay is the interval before starting the on-demand + backup. + type: string + id: + description: ID is the unique identifier for the on-demand backup. + minLength: 1 + type: string + routineName: + description: RoutineName is the routine name used to trigger + on-demand backup. + type: string + required: + - id + - routineName + type: object + maxItems: 1 + type: array backupService: description: BackupService is the backup service reference i.e. name and namespace. It is used to communicate to the backup service to @@ -66,8 +88,15 @@ spec: aerospike-cluster, backup-routines.' type: object x-kubernetes-preserve-unknown-fields: true - onDemand: - description: OnDemand is the configuration on demand backups. + required: + - backupService + - config + type: object + status: + description: AerospikeBackupStatus defines the observed state of AerospikeBackup + properties: + OnDemandBackups: + description: OnDemandBackups is the configuration for on-demand backups. items: properties: delay: @@ -86,15 +115,7 @@ spec: - id - routineName type: object - maxItems: 1 type: array - required: - - backupService - - config - type: object - status: - description: AerospikeBackupStatus defines the observed state of AerospikeBackup - properties: backupService: description: BackupService is the backup service reference i.e. name and namespace. @@ -115,27 +136,6 @@ spec: backup-routines.' type: object x-kubernetes-preserve-unknown-fields: true - onDemand: - description: OnDemand is the configuration for demand backups. - items: - properties: - delay: - description: Delay is the interval before starting the on-demand - backup. - type: string - id: - description: ID is the unique identifier for the on-demand backup. - minLength: 1 - type: string - routineName: - description: RoutineName is the routine name used to trigger - on-demand backup. - type: string - required: - - id - - routineName - type: object - type: array required: - backupService - config diff --git a/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml b/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml index eebb337e..0f42acb2 100644 --- a/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml @@ -25,6 +25,9 @@ spec: kind: AerospikeBackup name: aerospikebackups.asdb.aerospike.com specDescriptors: + - description: OnDemandBackups is the configuration for on-demand backups. + displayName: On Demand Backups + path: OnDemandBackups - description: BackupService is the backup service reference i.e. name and namespace. It is used to communicate to the backup service to trigger backups. This field is immutable @@ -35,9 +38,6 @@ spec: backup-routines.' displayName: Backup Config path: config - - description: OnDemand is the configuration on demand backups. - displayName: On Demand Backups - path: onDemand version: v1beta1 - description: AerospikeBackupService is the Schema for the aerospikebackupservices API diff --git a/config/samples/aerospikebackup.yaml b/config/samples/aerospikebackup.yaml index 4ea3d64c..e6fad646 100644 --- a/config/samples/aerospikebackup.yaml +++ b/config/samples/aerospikebackup.yaml @@ -12,7 +12,7 @@ spec: backupService: name: aerospikebackupservice-sample namespace: aerospike -# onDemand: +# onDemandBackups: # - id: first-ad-hoc-backup # routineName: test-routine config: @@ -30,7 +30,7 @@ spec: interval-cron: "@daily" incr-interval-cron: "@hourly" namespaces: ["test"] - source-cluster: test-cluster1 + source-cluster: test-cluster storage: local test-routine1: backup-policy: test-policy diff --git a/controllers/backup-service/aerospikebackupservice_controller.go b/controllers/backup-service/aerospikebackupservice_controller.go index cb400780..c4bfd020 100644 --- a/controllers/backup-service/aerospikebackupservice_controller.go +++ b/controllers/backup-service/aerospikebackupservice_controller.go @@ -47,14 +47,12 @@ type AerospikeBackupServiceReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile func (r *AerospikeBackupServiceReconciler) Reconcile(_ context.Context, request ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("aerospikebackupservice", request.NamespacedName) log.Info("Reconciling AerospikeBackupService") - // Fetch the AerospikeBackup instance + // Fetch the AerospikeBackupService instance aeroBackupService := &asdbv1beta1.AerospikeBackupService{} if err := r.Client.Get(context.TODO(), request.NamespacedName, aeroBackupService); err != nil { if errors.IsNotFound(err) { diff --git a/controllers/backup-service/reconciler.go b/controllers/backup-service/reconciler.go index eb3f3cd0..c9e95209 100644 --- a/controllers/backup-service/reconciler.go +++ b/controllers/backup-service/reconciler.go @@ -27,7 +27,8 @@ import ( "github.com/aerospike/aerospike-kubernetes-operator/pkg/utils" ) -const BackupConfigYAML = "aerospike-backup-service.yml" +// BackupServiceConfigYAML is the backup service configuration yaml file +const BackupServiceConfigYAML = "aerospike-backup-service.yml" type serviceConfig struct { portInfo map[string]int32 @@ -156,23 +157,23 @@ func (r *SingleBackupServiceReconciler) reconcileConfigMap() error { return err } - data := cm.Data[BackupConfigYAML] + data := cm.Data[BackupServiceConfigYAML] if err := yaml.Unmarshal([]byte(data), ¤tDataMap); err != nil { return err } - currentDataMap["service"] = desiredDataMap["service"] + currentDataMap[common.ServiceKey] = desiredDataMap[common.ServiceKey] currentDataMap[common.BackupPoliciesKey] = desiredDataMap[common.BackupPoliciesKey] currentDataMap[common.StorageKey] = desiredDataMap[common.StorageKey] - currentDataMap["secret-agent"] = desiredDataMap["secret-agent"] + currentDataMap[common.SecretAgentsKey] = desiredDataMap[common.SecretAgentsKey] updatedConfig, err := yaml.Marshal(currentDataMap) if err != nil { return err } - cm.Data[BackupConfigYAML] = string(updatedConfig) + cm.Data[BackupServiceConfigYAML] = string(updatedConfig) if err = r.Client.Update( context.TODO(), cm, common.UpdateOption, @@ -191,7 +192,7 @@ func (r *SingleBackupServiceReconciler) reconcileConfigMap() error { func (r *SingleBackupServiceReconciler) getConfigMapData() map[string]string { data := make(map[string]string) - data[BackupConfigYAML] = string(r.aeroBackupService.Spec.Config.Raw) + data[BackupServiceConfigYAML] = string(r.aeroBackupService.Spec.Config.Raw) return data } @@ -418,8 +419,8 @@ func (r *SingleBackupServiceReconciler) getVolumeAndMounts() ([]corev1.VolumeMou // Backup service configMap mountPath volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: "backup-service-config", - MountPath: fmt.Sprintf("/etc/aerospike-backup-service/%s", BackupConfigYAML), - SubPath: BackupConfigYAML, + MountPath: fmt.Sprintf("/etc/aerospike-backup-service/%s", BackupServiceConfigYAML), + SubPath: BackupServiceConfigYAML, }) // Backup service configMap diff --git a/controllers/backup/aerospikebackup_controller.go b/controllers/backup/aerospikebackup_controller.go index c626da6f..cdec3598 100644 --- a/controllers/backup/aerospikebackup_controller.go +++ b/controllers/backup/aerospikebackup_controller.go @@ -47,8 +47,6 @@ type AerospikeBackupReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile func (r *AerospikeBackupReconciler) Reconcile(_ context.Context, request ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("aerospikebackup", request.NamespacedName) diff --git a/controllers/backup/reconciler.go b/controllers/backup/reconciler.go index 5e26e55f..f0326bf3 100644 --- a/controllers/backup/reconciler.go +++ b/controllers/backup/reconciler.go @@ -24,7 +24,7 @@ import ( const BackupConfigYAML = "aerospike-backup-service.yml" -// SingleClusterReconciler reconciles a single AerospikeCluster +// SingleBackupReconciler reconciles a single AerospikeBackup object type SingleBackupReconciler struct { client.Client Recorder record.EventRecorder @@ -42,11 +42,11 @@ func (r *SingleBackupReconciler) Reconcile() (result ctrl.Result, recErr error) return reconcile.Result{}, err } - // Stop reconciliation as the cluster is being deleted + // Stop reconciliation as the backup is being deleted return reconcile.Result{}, nil } - // The cluster is not being deleted, add finalizer if not added already + // The backup is not being deleted, add finalizer if not added already if err := r.addFinalizer(finalizerName); err != nil { r.Log.Error(err, "Failed to add finalizer") return reconcile.Result{}, err @@ -284,29 +284,29 @@ func (r *SingleBackupReconciler) removeBackupInfoFromConfigMap() error { func (r *SingleBackupReconciler) scheduleOnDemandBackup() error { // There can be only one on-demand backup allowed right now. - if len(r.aeroBackup.Status.OnDemand) > 0 && - r.aeroBackup.Spec.OnDemand[0].ID == r.aeroBackup.Status.OnDemand[0].ID { + if len(r.aeroBackup.Status.OnDemandBackups) > 0 && + r.aeroBackup.Spec.OnDemandBackups[0].ID == r.aeroBackup.Status.OnDemandBackups[0].ID { r.Log.Info("On-demand backup already scheduled for the same ID", - "ID", r.aeroBackup.Status.OnDemand[0].ID) + "ID", r.aeroBackup.Status.OnDemandBackups[0].ID) return nil } r.Log.Info("Schedule on-demand backup", - "ID", r.aeroBackup.Spec.OnDemand[0].ID, "routine", r.aeroBackup.Spec.OnDemand[0].RoutineName) + "ID", r.aeroBackup.Spec.OnDemandBackups[0].ID, "routine", r.aeroBackup.Spec.OnDemandBackups[0].RoutineName) backupServiceClient, err := backup_service.GetBackupServiceClient(r.Client, &r.aeroBackup.Spec.BackupService) if err != nil { return err } - if err = backupServiceClient.ScheduleBackup(r.aeroBackup.Spec.OnDemand[0].RoutineName, - r.aeroBackup.Spec.OnDemand[0].Delay); err != nil { + if err = backupServiceClient.ScheduleBackup(r.aeroBackup.Spec.OnDemandBackups[0].RoutineName, + r.aeroBackup.Spec.OnDemandBackups[0].Delay); err != nil { r.Log.Error(err, "Failed to schedule on-demand backup") return err } - r.Log.Info("Scheduled on-demand backup", "ID", r.aeroBackup.Spec.OnDemand[0].ID, - "routine", r.aeroBackup.Spec.OnDemand[0].RoutineName) + r.Log.Info("Scheduled on-demand backup", "ID", r.aeroBackup.Spec.OnDemandBackups[0].ID, + "routine", r.aeroBackup.Spec.OnDemandBackups[0].RoutineName) return nil } @@ -364,6 +364,7 @@ func (r *SingleBackupReconciler) reconcileScheduledBackup() error { return gErr } + // TODO: Remove these API calls when hot reload is implemented for name, clusterConfig := range cluster { if _, ok := currentClusters[name]; ok { err = serviceClient.PutCluster(name, clusterConfig) @@ -387,6 +388,7 @@ func (r *SingleBackupReconciler) reconcileScheduledBackup() error { return gErr } + // TODO: Remove these API calls when hot reload is implemented for name, routine := range routines { if _, ok := currentRoutines[name]; ok { err = serviceClient.PutBackupRoutine(name, routine) @@ -415,7 +417,7 @@ func (r *SingleBackupReconciler) reconcileScheduledBackup() error { func (r *SingleBackupReconciler) reconcileOnDemandBackup() error { // Schedule on-demand backup if given - if len(r.aeroBackup.Spec.OnDemand) > 0 { + if len(r.aeroBackup.Spec.OnDemandBackups) > 0 { if err := r.scheduleOnDemandBackup(); err != nil { r.Log.Error(err, "Failed to schedule backup") return err @@ -491,7 +493,7 @@ func (r *SingleBackupReconciler) unregisterBackup() error { func (r *SingleBackupReconciler) updateStatus() error { r.aeroBackup.Status.BackupService = r.aeroBackup.Spec.BackupService r.aeroBackup.Status.Config = r.aeroBackup.Spec.Config - r.aeroBackup.Status.OnDemand = r.aeroBackup.Spec.OnDemand + r.aeroBackup.Status.OnDemandBackups = r.aeroBackup.Spec.OnDemandBackups return r.Client.Status().Update(context.Background(), r.aeroBackup) } diff --git a/controllers/common/constant.go b/controllers/common/constant.go index dbf89506..01c42e32 100644 --- a/controllers/common/constant.go +++ b/controllers/common/constant.go @@ -8,7 +8,7 @@ const ( StorageKey = "storage" BackupRoutinesKey = "backup-routines" BackupPoliciesKey = "backup-policies" - SecretAgentsKey = "secret-agents" + SecretAgentsKey = "secret-agent" ) const ( diff --git a/controllers/restore/aerospikerestore_controller.go b/controllers/restore/aerospikerestore_controller.go index ac9aba88..f9d36126 100644 --- a/controllers/restore/aerospikerestore_controller.go +++ b/controllers/restore/aerospikerestore_controller.go @@ -46,8 +46,6 @@ type AerospikeRestoreReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile func (r *AerospikeRestoreReconciler) Reconcile(_ context.Context, request ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("aerospikerestore", request.NamespacedName) diff --git a/helm-charts/aerospike-kubernetes-operator/crds/customresourcedefinition_aerospikebackups.asdb.aerospike.com.yaml b/helm-charts/aerospike-kubernetes-operator/crds/customresourcedefinition_aerospikebackups.asdb.aerospike.com.yaml index f4b99d5f..098a0f72 100644 --- a/helm-charts/aerospike-kubernetes-operator/crds/customresourcedefinition_aerospikebackups.asdb.aerospike.com.yaml +++ b/helm-charts/aerospike-kubernetes-operator/crds/customresourcedefinition_aerospikebackups.asdb.aerospike.com.yaml @@ -45,6 +45,28 @@ spec: description: AerospikeBackupSpec defines the desired state of AerospikeBackup for a given AerospikeCluster properties: + OnDemandBackups: + description: OnDemandBackups is the configuration for on-demand backups. + items: + properties: + delay: + description: Delay is the interval before starting the on-demand + backup. + type: string + id: + description: ID is the unique identifier for the on-demand backup. + minLength: 1 + type: string + routineName: + description: RoutineName is the routine name used to trigger + on-demand backup. + type: string + required: + - id + - routineName + type: object + maxItems: 1 + type: array backupService: description: BackupService is the backup service reference i.e. name and namespace. It is used to communicate to the backup service to @@ -66,8 +88,15 @@ spec: aerospike-cluster, backup-routines.' type: object x-kubernetes-preserve-unknown-fields: true - onDemand: - description: OnDemand is the configuration on demand backups. + required: + - backupService + - config + type: object + status: + description: AerospikeBackupStatus defines the observed state of AerospikeBackup + properties: + OnDemandBackups: + description: OnDemandBackups is the configuration for on-demand backups. items: properties: delay: @@ -86,15 +115,7 @@ spec: - id - routineName type: object - maxItems: 1 type: array - required: - - backupService - - config - type: object - status: - description: AerospikeBackupStatus defines the observed state of AerospikeBackup - properties: backupService: description: BackupService is the backup service reference i.e. name and namespace. @@ -115,27 +136,6 @@ spec: backup-routines.' type: object x-kubernetes-preserve-unknown-fields: true - onDemand: - description: OnDemand is the configuration for demand backups. - items: - properties: - delay: - description: Delay is the interval before starting the on-demand - backup. - type: string - id: - description: ID is the unique identifier for the on-demand backup. - minLength: 1 - type: string - routineName: - description: RoutineName is the routine name used to trigger - on-demand backup. - type: string - required: - - id - - routineName - type: object - type: array required: - backupService - config diff --git a/test/backup/backup_test.go b/test/backup/backup_test.go index 28bfa64f..3db0ab24 100644 --- a/test/backup/backup_test.go +++ b/test/backup/backup_test.go @@ -54,7 +54,7 @@ var _ = Describe( backup, err = newBackup() Expect(err).ToNot(HaveOccurred()) - backup.Spec.OnDemand = []asdbv1beta1.OnDemandSpec{ + backup.Spec.OnDemandBackups = []asdbv1beta1.OnDemandBackupSpec{ { ID: "on-demand", RoutineName: "test-routine", @@ -75,7 +75,7 @@ var _ = Describe( backup, err = getBackupObj(k8sClient, backup.Name, backup.Namespace) Expect(err).ToNot(HaveOccurred()) - backup.Spec.OnDemand = []asdbv1beta1.OnDemandSpec{ + backup.Spec.OnDemandBackups = []asdbv1beta1.OnDemandBackupSpec{ { ID: "on-demand", RoutineName: "non-existing-routine", @@ -168,6 +168,82 @@ var _ = Describe( Expect(err).To(HaveOccurred()) }) + It("Should fail when service field is given in backup config", func() { + config := getBackupConfigInMap() + config[common.ServiceKey] = map[string]interface{}{ + "http": map[string]interface{}{ + "port": 8081, + }, + } + + configBytes, mErr := json.Marshal(config) + Expect(mErr).ToNot(HaveOccurred()) + + backup = newBackupWithConfig(configBytes) + err = deployBackup(k8sClient, backup) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To( + ContainSubstring("service field cannot be specified in backup config")) + }) + + It("Should fail when backup-policies field is given in backup config", func() { + config := getBackupConfigInMap() + config[common.BackupPoliciesKey] = map[string]interface{}{ + "test-policy": map[string]interface{}{ + "parallel": 3, + "remove-files": "KeepAll", + "type": 1, + }, + } + + configBytes, mErr := json.Marshal(config) + Expect(mErr).ToNot(HaveOccurred()) + + backup = newBackupWithConfig(configBytes) + err = deployBackup(k8sClient, backup) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To( + ContainSubstring("backup-policies field cannot be specified in backup config")) + }) + + It("Should fail when storage field is given in backup config", func() { + config := getBackupConfigInMap() + config[common.StorageKey] = map[string]interface{}{ + "local": map[string]interface{}{ + "path": "/localStorage", + "type": "local", + }, + } + + configBytes, mErr := json.Marshal(config) + Expect(mErr).ToNot(HaveOccurred()) + + backup = newBackupWithConfig(configBytes) + err = deployBackup(k8sClient, backup) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To( + ContainSubstring("storage field cannot be specified in backup config")) + }) + + It("Should fail when secret-agent is given in backup config", func() { + config := getBackupConfigInMap() + config[common.SecretAgentsKey] = map[string]interface{}{ + "test-agent": map[string]interface{}{ + "address": "localhost", + "port": 4000, + }, + } + + configBytes, mErr := json.Marshal(config) + Expect(mErr).ToNot(HaveOccurred()) + + backup = newBackupWithConfig(configBytes) + err = deployBackup(k8sClient, backup) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To( + ContainSubstring("secret-agent field cannot be specified in backup config")) + }) + }, ) @@ -192,7 +268,7 @@ var _ = Describe( backup, err = getBackupObj(k8sClient, backup.Name, backup.Namespace) Expect(err).ToNot(HaveOccurred()) - backup.Spec.OnDemand = []asdbv1beta1.OnDemandSpec{ + backup.Spec.OnDemandBackups = []asdbv1beta1.OnDemandBackupSpec{ { ID: "on-demand", RoutineName: "test-routine", diff --git a/test/backup/test_utils.go b/test/backup/test_utils.go index 7f6cd348..4456a7c2 100644 --- a/test/backup/test_utils.go +++ b/test/backup/test_utils.go @@ -125,7 +125,7 @@ func waitForBackup(cl client.Client, backup *asdbv1beta1.AerospikeBackup, status := asdbv1beta1.AerospikeBackupStatus{} status.BackupService = backup.Spec.BackupService status.Config = backup.Spec.Config - status.OnDemand = backup.Spec.OnDemand + status.OnDemandBackups = backup.Spec.OnDemandBackups if !reflect.DeepEqual(status, backup.Status) { pkgLog.Info("Backup status not updated yet") diff --git a/test/backup_service/backup_service_test.go b/test/backup_service/backup_service_test.go index 377d0673..01c7e3d8 100644 --- a/test/backup_service/backup_service_test.go +++ b/test/backup_service/backup_service_test.go @@ -1,9 +1,12 @@ package backupservice import ( + "encoding/json" "net/http" "time" + "github.com/aerospike/aerospike-kubernetes-operator/controllers/common" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" @@ -26,17 +29,13 @@ var _ = Describe( Context( "When doing Invalid operations", func() { It("Should fail when wrong format backup config is given", func() { - backupService, err = NewBackupService() - Expect(err).ToNot(HaveOccurred()) - badConfig, gErr := getWrongBackupServiceConfBytes() Expect(gErr).ToNot(HaveOccurred()) - backupService.Spec.Config.Raw = badConfig + backupService = newBackupServiceWithConfig(badConfig) err = DeployBackupService(k8sClient, backupService) Expect(err).To(HaveOccurred()) - }, - ) + }) It("Should fail when wrong image is given", func() { backupService, err = NewBackupService() @@ -46,8 +45,59 @@ var _ = Describe( err = deployBackupServiceWithTO(k8sClient, backupService, 1*time.Minute) Expect(err).To(HaveOccurred()) - }, - ) + }) + + It("Should fail when aerospike-clusters field is given", func() { + configMap := getBackupServiceConfMap() + configMap[common.AerospikeClustersKey] = map[string]interface{}{ + "test-cluster": map[string]interface{}{ + "credentials": map[string]interface{}{ + "password": "admin123", + "user": "admin", + }, + "seed-nodes": []map[string]interface{}{ + { + "host-name": "aerocluster.aerospike.svc.cluster.local", + "port": 3000, + }, + }, + }, + } + + configBytes, mErr := json.Marshal(configMap) + Expect(mErr).ToNot(HaveOccurred()) + + backupService = newBackupServiceWithConfig(configBytes) + + err = deployBackupServiceWithTO(k8sClient, backupService, 1*time.Minute) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring( + "aerospike-clusters field cannot be specified in backup service config")) + }) + + It("Should fail when backup-routines field is given", func() { + configMap := getBackupServiceConfMap() + configMap[common.BackupRoutinesKey] = map[string]interface{}{ + "test-routine": map[string]interface{}{ + "backup-policy": "test-policy", + "interval-cron": "@daily", + "incr-interval-cron": "@hourly", + "namespaces": []string{"test"}, + "source-cluster": "test-cluster", + "storage": "local", + }, + } + + configBytes, mErr := json.Marshal(configMap) + Expect(mErr).ToNot(HaveOccurred()) + + backupService = newBackupServiceWithConfig(configBytes) + + err = deployBackupServiceWithTO(k8sClient, backupService, 1*time.Minute) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To( + ContainSubstring("backup-routines field cannot be specified in backup service config")) + }) }, ) diff --git a/test/backup_service/test_utils.go b/test/backup_service/test_utils.go index 22c6a515..0181a324 100644 --- a/test/backup_service/test_utils.go +++ b/test/backup_service/test_utils.go @@ -6,6 +6,10 @@ import ( "fmt" "time" + "github.com/aerospike/aerospike-kubernetes-operator/controllers/common" + + asdbv1beta1 "github.com/aerospike/aerospike-kubernetes-operator/api/v1beta1" + "github.com/aerospike/aerospike-kubernetes-operator/pkg/utils" app "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -16,10 +20,6 @@ import ( "k8s.io/apimachinery/pkg/util/wait" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" - - asdbv1beta1 "github.com/aerospike/aerospike-kubernetes-operator/api/v1beta1" - "github.com/aerospike/aerospike-kubernetes-operator/pkg/utils" ) const BackupServiceImage = "aerospike.jfrog.io/ecosystem-container-prod-local/aerospike-backup-service:1.0.0" @@ -55,6 +55,21 @@ func NewBackupService() (*asdbv1beta1.AerospikeBackupService, error) { }, nil } +func newBackupServiceWithConfig(config []byte) *asdbv1beta1.AerospikeBackupService { + return &asdbv1beta1.AerospikeBackupService{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: asdbv1beta1.AerospikeBackupServiceSpec{ + Image: BackupServiceImage, + Config: runtime.RawExtension{ + Raw: config, + }, + }, + } +} + func getBackupServiceObj(cl client.Client, name, namespace string) (*asdbv1beta1.AerospikeBackupService, error) { var backupService asdbv1beta1.AerospikeBackupService @@ -160,30 +175,31 @@ func waitForBackupService(cl client.Client, backupService *asdbv1beta1.Aerospike } func getBackupServiceConfBytes() ([]byte, error) { - config := `service: - http: - port: 8081 -backup-policies: - test-policy: - parallel: 3 - remove-files: KeepAll - type: 1 - test-policy1: - parallel: 3 - remove-files: KeepAll - type: 1 -storage: - local: - path: /localStorage - type: local` - - configMap := make(map[string]interface{}) - - if err := yaml.Unmarshal([]byte(config), &configMap); err != nil { + config := getBackupServiceConfMap() + + configBytes, err := json.Marshal(config) + if err != nil { return nil, err } - configBytes, err := json.Marshal(configMap) + pkgLog.Info(string(configBytes)) + + return configBytes, nil +} + +func getWrongBackupServiceConfBytes() ([]byte, error) { + config := getBackupServiceConfMap() + + tempList := make([]interface{}, 0, len(config[common.BackupPoliciesKey].(map[string]interface{}))) + + for _, policy := range config[common.BackupPoliciesKey].(map[string]interface{}) { + tempList = append(tempList, policy) + } + + // change the format from map to list + config[common.BackupPoliciesKey] = tempList + + configBytes, err := json.Marshal(config) if err != nil { return nil, err } @@ -193,6 +209,34 @@ storage: return configBytes, nil } +func getBackupServiceConfMap() map[string]interface{} { + return map[string]interface{}{ + common.ServiceKey: map[string]interface{}{ + "http": map[string]interface{}{ + "port": 8081, + }, + }, + common.BackupPoliciesKey: map[string]interface{}{ + "test-policy": map[string]interface{}{ + "parallel": 3, + "remove-files": "KeepAll", + "type": 1, + }, + "test-policy1": map[string]interface{}{ + "parallel": 3, + "remove-files": "KeepAll", + "type": 1, + }, + }, + common.StorageKey: map[string]interface{}{ + "local": map[string]interface{}{ + "path": "/localStorage", + "type": "local", + }, + }, + } +} + func getBackupServicePodList(cl client.Client, backupService *asdbv1beta1.AerospikeBackupService) (*corev1.PodList, error) { var podList corev1.PodList @@ -209,40 +253,6 @@ func getBackupServicePodList(cl client.Client, backupService *asdbv1beta1.Aerosp return &podList, nil } -func getWrongBackupServiceConfBytes() ([]byte, error) { - config := `service: - http: - port: 8081 -backup-policies: - - test-policy: - parallel: 3 - remove-files: KeepAll - type: 1 - - test-policy1: - parallel: 3 - remove-files: KeepAll - type: 1 -storage: - local: - path: /localStorage - type: local` - - configMap := make(map[string]interface{}) - - if err := yaml.Unmarshal([]byte(config), &configMap); err != nil { - return nil, err - } - - configBytes, err := json.Marshal(configMap) - if err != nil { - return nil, err - } - - pkgLog.Info(string(configBytes)) - - return configBytes, nil -} - func DeleteBackupService( k8sClient client.Client, backService *asdbv1beta1.AerospikeBackupService,