diff --git a/apis/dataprotection/v1alpha1/actionset_types.go b/apis/dataprotection/v1alpha1/actionset_types.go index f693f38c715..42cd81a6bf5 100644 --- a/apis/dataprotection/v1alpha1/actionset_types.go +++ b/apis/dataprotection/v1alpha1/actionset_types.go @@ -37,6 +37,11 @@ type ActionSetSpec struct { // +kubebuilder:validation:Required BackupType BackupType `json:"backupType"` + // Specifies the schema of parameters in backups and restores before their usage. + // + // +optional + ParametersSchema *ActionSetParametersSchema `json:"parametersSchema,omitempty"` + // Specifies a list of environment variables to be set in the container. // // +kubebuilder:pruning:PreserveUnknownFields @@ -117,6 +122,11 @@ type BackupActionSpec struct { // // +optional PreDeleteBackup *BaseJobActionSpec `json:"preDelete,omitempty"` + + // Specifies the parameters used by the backup action + // + // +optional + WithParameters []string `json:"withParameters,omitempty"` } // BackupDataActionSpec defines how to back up data. @@ -162,6 +172,11 @@ type RestoreActionSpec struct { // +optional // +kubebuilder:default=true BaseBackupRequired *bool `json:"baseBackupRequired,omitempty"` + + // Specifies the parameters used by the restore action + // + // +optional + WithParameters []string `json:"withParameters,omitempty"` } // ActionSpec defines an action that should be executed. Only one of the fields may be set. diff --git a/apis/dataprotection/v1alpha1/backup_types.go b/apis/dataprotection/v1alpha1/backup_types.go index 584ca1f8f04..25f93afe762 100644 --- a/apis/dataprotection/v1alpha1/backup_types.go +++ b/apis/dataprotection/v1alpha1/backup_types.go @@ -22,6 +22,7 @@ import ( ) // BackupSpec defines the desired state of Backup. +// +kubebuilder:validation:XValidation:rule="has(oldSelf.parameters) == has(self.parameters)",message="forbidden to update spec.parameters" type BackupSpec struct { // Specifies the backup policy to be applied for this backup. // @@ -74,6 +75,16 @@ type BackupSpec struct { // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.parentBackupName" ParentBackupName string `json:"parentBackupName,omitempty"` + + // Specifies a list of name-value pairs representing parameters and their corresponding values. + // Parameters match the schema specified in the `actionset.spec.parametersSchema` + // + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=128 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.parameters" + // +optional + Parameters []ParameterPair `json:"parameters,omitempty"` } // BackupStatus defines the observed state of Backup. diff --git a/apis/dataprotection/v1alpha1/backupschedule_types.go b/apis/dataprotection/v1alpha1/backupschedule_types.go index 37ff4c1c215..ba0476cc4a6 100644 --- a/apis/dataprotection/v1alpha1/backupschedule_types.go +++ b/apis/dataprotection/v1alpha1/backupschedule_types.go @@ -49,6 +49,12 @@ type SchedulePolicy struct { // +optional Enabled *bool `json:"enabled,omitempty"` + // Specifies the name of the schedule. Names cannot be duplicated. + // If the name is empty, it will be considered the same as the value of the backupMethod below. + // + // +optional + Name string `json:"name,omitempty"` + // Specifies the backup method name that is defined in backupPolicy. // // +kubebuilder:validation:Required @@ -76,6 +82,17 @@ type SchedulePolicy struct { // +optional // +kubebuilder:default="7d" RetentionPeriod RetentionPeriod `json:"retentionPeriod,omitempty"` + + // Specifies a list of name-value pairs representing parameters and their corresponding values. + // Parameters match the schema specified in the `actionset.spec.parametersSchema` + // + // +patchMergeKey=name + // +patchStrategy=merge,retainKeys + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=128 + // +optional + Parameters []ParameterPair `json:"parameters,omitempty"` } // BackupScheduleStatus defines the observed state of BackupSchedule. @@ -174,3 +191,12 @@ type BackupScheduleList struct { func init() { SchemeBuilder.Register(&BackupSchedule{}, &BackupScheduleList{}) } + +// GetScheduleName gets the name of schedulePolicy. +// If name is empty, return backupMethod. +func (s *SchedulePolicy) GetScheduleName() string { + if len(s.Name) > 0 { + return s.Name + } + return s.BackupMethod +} diff --git a/apis/dataprotection/v1alpha1/restore_types.go b/apis/dataprotection/v1alpha1/restore_types.go index 7794ccca108..0c72d7b354c 100644 --- a/apis/dataprotection/v1alpha1/restore_types.go +++ b/apis/dataprotection/v1alpha1/restore_types.go @@ -22,6 +22,7 @@ import ( ) // RestoreSpec defines the desired state of Restore +// +kubebuilder:validation:XValidation:rule="has(oldSelf.parameters) == has(self.parameters)",message="forbidden to update spec.parameters" type RestoreSpec struct { // Specifies the backup to be restored. The restore behavior is based on the backup type: // @@ -84,6 +85,16 @@ type RestoreSpec struct { // +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=10 BackoffLimit *int32 `json:"backoffLimit,omitempty"` + + // Specifies a list of name-value pairs representing parameters and their corresponding values. + // Parameters match the schema specified in the `actionset.spec.parametersSchema` + // + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=128 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.parameters" + // +optional + Parameters []ParameterPair `json:"parameters,omitempty"` } // BackupRef describes the backup info. diff --git a/apis/dataprotection/v1alpha1/types.go b/apis/dataprotection/v1alpha1/types.go index 26c4b8f42bb..5b4512e5434 100644 --- a/apis/dataprotection/v1alpha1/types.go +++ b/apis/dataprotection/v1alpha1/types.go @@ -24,6 +24,7 @@ import ( "unicode" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) // Phase defines the BackupPolicy and ActionSet CR .status.phase @@ -261,3 +262,28 @@ type EncryptionConfig struct { // +kubebuilder:validation:Required PassPhraseSecretKeyRef *corev1.SecretKeySelector `json:"passPhraseSecretKeyRef"` } + +type ActionSetParametersSchema struct { + // Defines the schema for parameters using the OpenAPI v3. + // The supported property types include: + // - string + // - number + // - integer + // - array: Note that only items of string type are supported. + // +kubebuilder:validation:Schemaless + // +kubebuilder:validation:Type=object + // +kubebuilder:pruning:PreserveUnknownFields + // +k8s:conversion-gen=false + // +optional + OpenAPIV3Schema *apiextensionsv1.JSONSchemaProps `json:"openAPIV3Schema,omitempty"` +} + +type ParameterPair struct { + // Represents the name of the parameter. + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Represents the parameter values. + // +kubebuilder:validation:Required + Value string `json:"value"` +} diff --git a/apis/dataprotection/v1alpha1/zz_generated.deepcopy.go b/apis/dataprotection/v1alpha1/zz_generated.deepcopy.go index e17f4620e1e..d9ec61e6d6c 100644 --- a/apis/dataprotection/v1alpha1/zz_generated.deepcopy.go +++ b/apis/dataprotection/v1alpha1/zz_generated.deepcopy.go @@ -88,9 +88,33 @@ func (in *ActionSetList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionSetParametersSchema) DeepCopyInto(out *ActionSetParametersSchema) { + *out = *in + if in.OpenAPIV3Schema != nil { + in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionSetParametersSchema. +func (in *ActionSetParametersSchema) DeepCopy() *ActionSetParametersSchema { + if in == nil { + return nil + } + out := new(ActionSetParametersSchema) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ActionSetSpec) DeepCopyInto(out *ActionSetSpec) { *out = *in + if in.ParametersSchema != nil { + in, out := &in.ParametersSchema, &out.ParametersSchema + *out = new(ActionSetParametersSchema) + (*in).DeepCopyInto(*out) + } if in.Env != nil { in, out := &in.Env, &out.Env *out = make([]v1.EnvVar, len(*in)) @@ -215,7 +239,7 @@ func (in *Backup) DeepCopyInto(out *Backup) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -264,6 +288,11 @@ func (in *BackupActionSpec) DeepCopyInto(out *BackupActionSpec) { *out = new(BaseJobActionSpec) (*in).DeepCopyInto(*out) } + if in.WithParameters != nil { + in, out := &in.WithParameters, &out.WithParameters + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupActionSpec. @@ -897,6 +926,11 @@ func (in *BackupScheduleStatus) DeepCopy() *BackupScheduleStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackupSpec) DeepCopyInto(out *BackupSpec) { *out = *in + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]ParameterPair, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec. @@ -1331,6 +1365,21 @@ func (in *KubeResources) DeepCopy() *KubeResources { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ParameterPair) DeepCopyInto(out *ParameterPair) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParameterPair. +func (in *ParameterPair) DeepCopy() *ParameterPair { + if in == nil { + return nil + } + out := new(ParameterPair) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ParametersSchema) DeepCopyInto(out *ParametersSchema) { *out = *in @@ -1556,6 +1605,11 @@ func (in *RestoreActionSpec) DeepCopyInto(out *RestoreActionSpec) { *out = new(bool) **out = **in } + if in.WithParameters != nil { + in, out := &in.WithParameters, &out.WithParameters + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreActionSpec. @@ -1654,6 +1708,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) { *out = new(int32) **out = **in } + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]ParameterPair, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSpec. @@ -1812,6 +1871,11 @@ func (in *SchedulePolicy) DeepCopyInto(out *SchedulePolicy) { *out = new(bool) **out = **in } + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]ParameterPair, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchedulePolicy. diff --git a/apis/operations/v1alpha1/opsrequest_types.go b/apis/operations/v1alpha1/opsrequest_types.go index 5957c167424..8ed5417ed09 100644 --- a/apis/operations/v1alpha1/opsrequest_types.go +++ b/apis/operations/v1alpha1/opsrequest_types.go @@ -24,6 +24,7 @@ import ( appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" ) // OpsRequestSpec defines the desired state of OpsRequest @@ -205,12 +206,15 @@ type SpecificOpsRequest struct { ExposeList []Expose `json:"expose,omitempty"` // Specifies the parameters to back up a Cluster. + // + // +kubebuilder:validation:XValidation:rule="has(oldSelf.parameters) == has(self.parameters)",message="forbidden to update backup.parameters" // +optional Backup *Backup `json:"backup,omitempty"` // Specifies the parameters to restore a Cluster. // Note that this restore operation will roll back cluster services. // + // +kubebuilder:validation:XValidation:rule="has(oldSelf.parameters) == has(self.parameters)",message="forbidden to update restore.parameters" // +optional Restore *Restore `json:"restore,omitempty"` @@ -906,6 +910,16 @@ type Backup struct { // // +optional ParentBackupName string `json:"parentBackupName,omitempty"` + + // Specifies a list of name-value pairs representing parameters and their corresponding values. + // Parameters match the schema specified in the `actionset.spec.parametersSchema` + // + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=128 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update parameters" + // +optional + Parameters []dpv1alpha1.ParameterPair `json:"parameters,omitempty"` } type Restore struct { @@ -947,6 +961,16 @@ type Restore struct { // // This setting is useful for coordinating PostReady operations across the Cluster for optimal cluster conditions. DeferPostReadyUntilClusterRunning bool `json:"deferPostReadyUntilClusterRunning,omitempty"` + + // Specifies a list of name-value pairs representing parameters and their corresponding values. + // Parameters match the schema specified in the `actionset.spec.parametersSchema` + // + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=128 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update parameters" + // +optional + Parameters []dpv1alpha1.ParameterPair `json:"parameters,omitempty"` } // OpsRequestStatus represents the observed state of an OpsRequest. diff --git a/apis/operations/v1alpha1/zz_generated.deepcopy.go b/apis/operations/v1alpha1/zz_generated.deepcopy.go index cbcd16b9798..cc2f8b6c02d 100644 --- a/apis/operations/v1alpha1/zz_generated.deepcopy.go +++ b/apis/operations/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,7 @@ package v1alpha1 import ( appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -49,6 +50,11 @@ func (in *ActionTask) DeepCopy() *ActionTask { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Backup) DeepCopyInto(out *Backup) { *out = *in + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]dataprotectionv1alpha1.ParameterPair, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backup. @@ -1342,6 +1348,11 @@ func (in *Restore) DeepCopyInto(out *Restore) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]dataprotectionv1alpha1.ParameterPair, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Restore. @@ -1484,7 +1495,7 @@ func (in *SpecificOpsRequest) DeepCopyInto(out *SpecificOpsRequest) { if in.Backup != nil { in, out := &in.Backup, &out.Backup *out = new(Backup) - **out = **in + (*in).DeepCopyInto(*out) } if in.Restore != nil { in, out := &in.Restore, &out.Restore diff --git a/config/crd/bases/dataprotection.kubeblocks.io_actionsets.yaml b/config/crd/bases/dataprotection.kubeblocks.io_actionsets.yaml index 40a85d26a67..71b5f38182c 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_actionsets.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_actionsets.yaml @@ -269,6 +269,11 @@ spec: - command - image type: object + withParameters: + description: Specifies the parameters used by the backup action + items: + type: string + type: array type: object backupType: allOf: @@ -457,6 +462,21 @@ spec: type: object type: array x-kubernetes-preserve-unknown-fields: true + parametersSchema: + description: Specifies the schema of parameters in backups and restores + before their usage. + properties: + openAPIV3Schema: + description: |- + Defines the schema for parameters using the OpenAPI v3. + The supported property types include: + - string + - number + - integer + - array: Note that only items of string type are supported. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object restore: description: Specifies the restore action. properties: @@ -568,6 +588,11 @@ spec: - command - image type: object + withParameters: + description: Specifies the parameters used by the restore action + items: + type: string + type: array type: object required: - backupType diff --git a/config/crd/bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml b/config/crd/bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml index 7cd65a409c2..ba430dc6ac7 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml @@ -359,6 +359,32 @@ spec: description: Specifies whether the backup schedule is enabled or not. type: boolean + name: + description: |- + Specifies the name of the schedule. Names cannot be duplicated. + If the name is empty, it will be considered the same as the value of the backupMethod below. + type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map retentionPeriod: default: 7d description: "Determines the duration for which the backup should diff --git a/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml b/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml index a2d196267d1..23060c13a49 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml @@ -110,6 +110,30 @@ spec: the backup CR but retaining the backup contents in backup repository. The current implementation only prevent accidental deletion of backup data. type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: self == oldSelf parentBackupName: description: Determines the parent backup name for incremental or differential backup. @@ -130,6 +154,9 @@ spec: - backupMethod - backupPolicyName type: object + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: has(oldSelf.parameters) == has(self.parameters) status: description: BackupStatus defines the observed state of Backup. properties: diff --git a/config/crd/bases/dataprotection.kubeblocks.io_backupschedules.yaml b/config/crd/bases/dataprotection.kubeblocks.io_backupschedules.yaml index f702df13a54..bd81a15826b 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_backupschedules.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_backupschedules.yaml @@ -72,6 +72,32 @@ spec: description: Specifies whether the backup schedule is enabled or not. type: boolean + name: + description: |- + Specifies the name of the schedule. Names cannot be duplicated. + If the name is empty, it will be considered the same as the value of the backupMethod below. + type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map retentionPeriod: default: 7d description: "Determines the duration for which the backup should diff --git a/config/crd/bases/dataprotection.kubeblocks.io_restores.yaml b/config/crd/bases/dataprotection.kubeblocks.io_restores.yaml index a4d6823c12a..199ea02288d 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_restores.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_restores.yaml @@ -270,6 +270,30 @@ spec: type: object type: array x-kubernetes-preserve-unknown-fields: true + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: self == oldSelf prepareDataConfig: description: |- Configuration for the action of "prepareData" phase, including the persistent volume claims @@ -2414,6 +2438,9 @@ spec: required: - backup type: object + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: has(oldSelf.parameters) == has(self.parameters) status: description: RestoreStatus defines the observed state of Restore properties: diff --git a/config/crd/bases/operations.kubeblocks.io_opsrequests.yaml b/config/crd/bases/operations.kubeblocks.io_opsrequests.yaml index 6dd951e6267..100c4528de8 100644 --- a/config/crd/bases/operations.kubeblocks.io_opsrequests.yaml +++ b/config/crd/bases/operations.kubeblocks.io_opsrequests.yaml @@ -91,6 +91,30 @@ spec: - Delete - Retain type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update parameters + rule: self == oldSelf parentBackupName: description: If the specified BackupMethod is incremental, `parentBackupName` is required. @@ -121,6 +145,9 @@ spec: Otherwise, only the Backup custom resource will be deleted. type: string type: object + x-kubernetes-validations: + - message: forbidden to update backup.parameters + rule: has(oldSelf.parameters) == has(self.parameters) cancel: description: |- Indicates whether the current operation should be canceled and terminated gracefully if it's in the @@ -4396,6 +4423,30 @@ spec: type: object type: array x-kubernetes-preserve-unknown-fields: true + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update parameters + rule: self == oldSelf restorePointInTime: description: |- Specifies the point in time to which the restore should be performed. @@ -4422,6 +4473,9 @@ spec: required: - backupName type: object + x-kubernetes-validations: + - message: forbidden to update restore.parameters + rule: has(oldSelf.parameters) == has(self.parameters) start: description: Lists Components to be started. If empty, all components will be started. diff --git a/controllers/apps/transformer_cluster_backup_policy.go b/controllers/apps/transformer_cluster_backup_policy.go index 2ce9c3090ed..f6867accd71 100644 --- a/controllers/apps/transformer_cluster_backup_policy.go +++ b/controllers/apps/transformer_cluster_backup_policy.go @@ -283,11 +283,14 @@ func (r *backupPolicyBuilder) buildBackupSchedule( var schedules []dpv1alpha1.SchedulePolicy for _, s := range r.backupPolicyTPL.Spec.Schedules { + name := s.GetScheduleName() schedules = append(schedules, dpv1alpha1.SchedulePolicy{ BackupMethod: s.BackupMethod, CronExpression: s.CronExpression, Enabled: s.Enabled, RetentionPeriod: s.RetentionPeriod, + Name: name, + Parameters: s.Parameters, }) } backupSchedule.Spec.Schedules = schedules @@ -295,16 +298,22 @@ func (r *backupPolicyBuilder) buildBackupSchedule( } func (r *backupPolicyBuilder) syncBackupSchedule(backupSchedule *dpv1alpha1.BackupSchedule) { - scheduleMethodMap := map[string]struct{}{} - for _, s := range backupSchedule.Spec.Schedules { - scheduleMethodMap[s.BackupMethod] = struct{}{} + scheduleNameMap := map[string]struct{}{} + for i := range backupSchedule.Spec.Schedules { + s := &backupSchedule.Spec.Schedules[i] + if len(s.Name) == 0 { + // assign to backupMethod if name is empty. + s.Name = s.BackupMethod + } + scheduleNameMap[s.Name] = struct{}{} } mergeMap(backupSchedule.Annotations, r.buildAnnotations()) // update backupSchedule annotation to reconcile it. backupSchedule.Annotations[constant.ReconcileAnnotationKey] = r.Cluster.ResourceVersion // sync the newly added schedule policies. for _, s := range r.backupPolicyTPL.Spec.Schedules { - if _, ok := scheduleMethodMap[s.BackupMethod]; ok { + name := s.GetScheduleName() + if _, ok := scheduleNameMap[name]; ok { continue } backupSchedule.Spec.Schedules = append(backupSchedule.Spec.Schedules, dpv1alpha1.SchedulePolicy{ @@ -312,6 +321,8 @@ func (r *backupPolicyBuilder) syncBackupSchedule(backupSchedule *dpv1alpha1.Back CronExpression: s.CronExpression, Enabled: s.Enabled, RetentionPeriod: s.RetentionPeriod, + Name: name, + Parameters: s.Parameters, }) } } diff --git a/controllers/dataprotection/actionset_controller.go b/controllers/dataprotection/actionset_controller.go index 787977e47c0..43f963fe967 100644 --- a/controllers/dataprotection/actionset_controller.go +++ b/controllers/dataprotection/actionset_controller.go @@ -21,6 +21,7 @@ package dataprotection import ( "context" + "fmt" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" @@ -68,8 +69,7 @@ func (r *ActionSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return *res, err } - if actionSet.Status.ObservedGeneration == actionSet.Generation && - actionSet.Status.Phase.IsAvailable() { + if actionSet.Status.ObservedGeneration == actionSet.Generation && actionSet.Status.Phase.IsAvailable() { return ctrl.Result{}, nil } @@ -81,9 +81,14 @@ func (r *ActionSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return r.Client.Status().Patch(reqCtx.Ctx, actionSet, patch) } - // TODO(ldm): validate actionSet + phase := dpv1alpha1.AvailablePhase + var message string + if err = r.validate(actionSet); err != nil { + phase = dpv1alpha1.UnavailablePhase + message = err.Error() + } - if err = patchStatus(dpv1alpha1.AvailablePhase, ""); err != nil { + if err = patchStatus(phase, message); err != nil { return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") } intctrlutil.RecordCreatedEvent(r.Recorder, actionSet) @@ -102,3 +107,35 @@ func (r *ActionSetReconciler) deleteExternalResources( _ *dpv1alpha1.ActionSet) error { return nil } + +func (r *ActionSetReconciler) validate(actionset *dpv1alpha1.ActionSet) error { + validateWithParameters := func(withParameters []string) error { + if len(withParameters) == 0 { + return nil + } + schema := actionset.Spec.ParametersSchema + if schema == nil || schema.OpenAPIV3Schema == nil || len(schema.OpenAPIV3Schema.Properties) == 0 { + return fmt.Errorf("the parametersSchema is invalid") + } + properties := schema.OpenAPIV3Schema.Properties + for _, parameter := range withParameters { + if _, ok := properties[parameter]; !ok { + return fmt.Errorf("parameter %s is not defined in the parametersSchema", parameter) + } + } + return nil + } + + // validate withParameters + if actionset.Spec.Backup != nil { + if err := validateWithParameters(actionset.Spec.Backup.WithParameters); err != nil { + return fmt.Errorf("fails to validate backup withParameters: %v", err) + } + } + if actionset.Spec.Restore != nil { + if err := validateWithParameters(actionset.Spec.Restore.WithParameters); err != nil { + return fmt.Errorf("fails to validate restore withParameters: %v", err) + } + } + return nil +} diff --git a/controllers/dataprotection/actionset_controller_test.go b/controllers/dataprotection/actionset_controller_test.go index e53f9df1f5b..e3495edaf52 100644 --- a/controllers/dataprotection/actionset_controller_test.go +++ b/controllers/dataprotection/actionset_controller_test.go @@ -22,9 +22,10 @@ package dataprotection import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "sigs.k8s.io/controller-runtime/pkg/client" + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" intctrlutil "github.com/apecloud/kubeblocks/pkg/generics" testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" testdp "github.com/apecloud/kubeblocks/pkg/testutil/dataprotection" @@ -58,4 +59,50 @@ var _ = Describe("ActionSet Controller test", func() { Expect(as).ShouldNot(BeNil()) }) }) + + Context("validate a actionSet", func() { + It("validate withParameters", func() { + as := testdp.NewFakeActionSet(&testCtx) + Expect(as).ShouldNot(BeNil()) + By("set invalid withParameters and schema") + Expect(testapps.ChangeObj(&testCtx, as, func(action *dpv1alpha1.ActionSet) { + as.Spec.ParametersSchema = &dpv1alpha1.ActionSetParametersSchema{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + testdp.ParameterString: { + Type: testdp.ParameterStringType, + }, + testdp.ParameterArray: { + Type: testdp.ParameterArrayType, + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: testdp.ParameterStringType, + }, + }, + }, + }, + }, + } + as.Spec.Backup.WithParameters = []string{testdp.InvalidParameter} + })).Should(Succeed()) + By("should be unavailable with invalid withParameters") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(as), + func(g Gomega, as *dpv1alpha1.ActionSet) { + g.Expect(as.Status.ObservedGeneration).Should(Equal(as.Generation)) + g.Expect(as.Status.Phase).Should(BeEquivalentTo(dpv1alpha1.UnavailablePhase)) + g.Expect(as.Status.Message).ShouldNot(BeEmpty()) + })).Should(Succeed()) + By("set valid parameters") + Expect(testapps.ChangeObj(&testCtx, as, func(action *dpv1alpha1.ActionSet) { + as.Spec.Backup.WithParameters = []string{testdp.ParameterString, testdp.ParameterArray} + })).Should(Succeed()) + By("should be available") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(as), + func(g Gomega, as *dpv1alpha1.ActionSet) { + g.Expect(as.Status.ObservedGeneration).Should(Equal(as.Generation)) + g.Expect(as.Status.Phase).Should(BeEquivalentTo(dpv1alpha1.AvailablePhase)) + g.Expect(as.Status.Message).Should(BeEmpty()) + })).Should(Succeed()) + }) + }) }) diff --git a/controllers/dataprotection/backup_controller.go b/controllers/dataprotection/backup_controller.go index 3595876b8e0..cabe2f844ab 100644 --- a/controllers/dataprotection/backup_controller.go +++ b/controllers/dataprotection/backup_controller.go @@ -358,7 +358,6 @@ func (r *BackupReconciler) prepareBackupRequest( RequestCtx: reqCtx, Client: r.Client, } - if request.Annotations == nil { request.Annotations = make(map[string]string) } @@ -413,6 +412,11 @@ func (r *BackupReconciler) prepareBackupRequest( backupSchedule.Namespace, backupSchedule.Name) } } + + // validate parameters + if err := dputils.ValidateParameters(actionSet, backup.Spec.Parameters, true); err != nil { + return nil, fmt.Errorf("fails to validate parameters with actionset %s: %v", actionSet.Name, err) + } } // check encryption config diff --git a/controllers/dataprotection/backup_controller_test.go b/controllers/dataprotection/backup_controller_test.go index e138d6609ab..f9ceb194bb9 100644 --- a/controllers/dataprotection/backup_controller_test.go +++ b/controllers/dataprotection/backup_controller_test.go @@ -26,11 +26,10 @@ import ( "strconv" "time" + vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" - - vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -100,6 +99,7 @@ var _ = Describe("Backup Controller test", func() { When("with default settings", func() { var ( + actionSet *dpv1alpha1.ActionSet backupPolicy *dpv1alpha1.BackupPolicy repoPVCName string cluster *kbappsv1.Cluster @@ -109,7 +109,7 @@ var _ = Describe("Backup Controller test", func() { BeforeEach(func() { By("creating an actionSet") - actionSet := testdp.NewFakeActionSet(&testCtx) + actionSet = testdp.NewFakeActionSet(&testCtx) By("creating storage provider") _ = testdp.NewFakeStorageProvider(&testCtx, nil) @@ -483,6 +483,55 @@ var _ = Describe("Backup Controller test", func() { g.Expect(getDPDBPortEnv(&fetched.Spec.Template.Spec.Containers[0]).Value).Should(Equal(strconv.Itoa(testdp.PortNum))) })).Should(Succeed()) }) + Context("creates backups with parameters", func() { + BeforeEach(func() { + By("set backup parameters and schema in acitionSet") + testdp.MockActionSetWithSchema(&testCtx, actionSet) + }) + It("should fail if parameters are invalid", func() { + By("create a backup with invalid parameters") + backup := testdp.NewFakeBackup(&testCtx, func(bp *dpv1alpha1.Backup) { + bp.Spec.Parameters = testdp.InvalidParameters + }) + By("check the backup") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(backup), func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseFailed)) + })).Should(Succeed()) + + }) + It("should succeed if parameters are valid", func() { + By("create a backup with parameters") + backup := testdp.NewFakeBackup(&testCtx, func(bp *dpv1alpha1.Backup) { + bp.Spec.Parameters = testdp.TestParameters + }) + By("check the backup") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(backup), func(g Gomega, fetched *dpv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupPhaseRunning)) + })).Should(Succeed()) + + By("check the backup job and env") + getJobKey := func(index int) client.ObjectKey { + return client.ObjectKey{ + Name: dpbackup.GenerateBackupJobName(backup, fmt.Sprintf("%s-%d", dpbackup.BackupDataJobNamePrefix, index)), + Namespace: backup.Namespace, + } + } + Eventually(testapps.CheckObj(&testCtx, getJobKey(0), func(g Gomega, job *batchv1.Job) { + g.Expect(len(job.Spec.Template.Spec.Containers)).ShouldNot(BeZero()) + for _, c := range job.Spec.Template.Spec.Containers { + count := 0 + for _, env := range c.Env { + for _, param := range testdp.TestParameters { + if param.Name == env.Name && param.Value == env.Value { + count++ + } + } + } + g.Expect(count).To(Equal(len(testdp.TestParameters))) + } + })).Should(Succeed()) + }) + }) Context("creates a backup with encryption", func() { const ( encryptionKeySecretName = "backup-encryption" diff --git a/controllers/dataprotection/backuppolicytemplate_controller.go b/controllers/dataprotection/backuppolicytemplate_controller.go index 40d46cedcc4..fafcde67158 100644 --- a/controllers/dataprotection/backuppolicytemplate_controller.go +++ b/controllers/dataprotection/backuppolicytemplate_controller.go @@ -24,7 +24,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -36,6 +35,7 @@ import ( dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/pkg/controller/component" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" + dputils "github.com/apecloud/kubeblocks/pkg/dataprotection/utils" "github.com/apecloud/kubeblocks/pkg/dataprotection/utils/boolptr" ) @@ -95,11 +95,12 @@ func (r *BackupPolicyTemplateReconciler) setComponentDefLabels(reqCtx intctrluti func (r *BackupPolicyTemplateReconciler) validateAvailable(reqCtx intctrlutil.RequestCtx, oldBPT, bpt *dpv1alpha1.BackupPolicyTemplate) error { message := "" - backupMethodMap := map[string]sets.Empty{} + backupMethodMap := map[string]*dpv1alpha1.ActionSet{} actionSetNotFound := false // validate the referred actionSetName of the backupMethod for _, v := range bpt.Spec.BackupMethods { - backupMethodMap[v.Name] = sets.Empty{} + // confirm the method exists + backupMethodMap[v.Name] = nil if boolptr.IsSetToFalse(v.SnapshotVolumes) && v.ActionSetName == "" { message += fmt.Sprintf(`backupMethod "%s" is missing an ActionSet name;`, v.Name) continue @@ -116,13 +117,28 @@ func (r *BackupPolicyTemplateReconciler) validateAvailable(reqCtx intctrlutil.Re } return err } + // record found actionSets + backupMethodMap[v.Name] = actionSet + } + // validate the schedule names + if err := dputils.ValidateScheduleNames(bpt.Spec.Schedules); err != nil { + message += fmt.Sprintf(`fails to validate schedule name: %v;`, err) } // validate the schedules for _, v := range bpt.Spec.Schedules { - if _, ok := backupMethodMap[v.BackupMethod]; !ok { + actionSet, ok := backupMethodMap[v.BackupMethod] + if !ok { message += fmt.Sprintf(`backupMethod "%s" not found in the spec.backupMethods;`, v.BackupMethod) + continue + } + // validate schedule parameters + if actionSet != nil { + if err := dputils.ValidateParameters(actionSet, v.Parameters, true); err != nil { + message += fmt.Sprintf(`fails to validate parameters of backupMethod "%s": %v;`, v.BackupMethod, err) + } } } + bpt.Status.ObservedGeneration = bpt.Generation bpt.Status.Message = message if len(message) > 0 { diff --git a/controllers/dataprotection/backuppolicytemplate_controller_test.go b/controllers/dataprotection/backuppolicytemplate_controller_test.go index 7f958e55e29..8a1f419127c 100644 --- a/controllers/dataprotection/backuppolicytemplate_controller_test.go +++ b/controllers/dataprotection/backuppolicytemplate_controller_test.go @@ -21,7 +21,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "sigs.k8s.io/controller-runtime/pkg/client" dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" @@ -47,6 +46,7 @@ var _ = Describe("", func() { ml := client.HasLabels{testCtx.TestObjLabelKey} testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.BackupPolicyTemplateSignature, true, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.ActionSetSignature, true, ml) } BeforeEach(func() { @@ -77,8 +77,8 @@ var _ = Describe("", func() { SetBackupMethodVolumeMounts("data", "/data"). AddBackupMethod(VsBackupMethodName, true, ""). SetBackupMethodVolumeMounts("data", "/data"). - AddSchedule(BackupMethod, "0 0 * * *", ttl, true). - AddSchedule(VsBackupMethodName, "0 0 * * *", ttl, true). + AddSchedule(BackupMethod, "0 0 * * *", ttl, true, "", nil). + AddSchedule(VsBackupMethodName, "0 0 * * *", ttl, true, "", nil). Create(&testCtx).GetObject() key := client.ObjectKeyFromObject(bpt) @@ -103,6 +103,38 @@ var _ = Describe("", func() { g.Expect(pobj.Status.Message).To(BeEmpty()) })).Should(Succeed()) }) + It("test BackupPolicyTemplate schedule parameters", func() { + const ( + scheduleName1 = "test1" + scheduleName2 = "test2" + ) + By("set backup parameters and schema in acitionSet") + actionSet := testdp.NewFakeActionSet(&testCtx) + testdp.MockActionSetWithSchema(&testCtx, actionSet) + bpt := testdp.NewBackupPolicyTemplateFactory(BackupPolicyTemplateName). + AddBackupMethod(BackupMethod, false, testdp.ActionSetName). + SetBackupMethodVolumeMounts("data", "/data"). + AddSchedule(BackupMethod, "0 0 * * *", ttl, true, scheduleName1, testdp.InvalidParameters). + AddSchedule(BackupMethod, "0 0 * * *", ttl, true, scheduleName2, testdp.TestParameters). + AddSchedule(BackupMethod, "0 0 * * *", ttl, true, "", nil). + Create(&testCtx).GetObject() + key := client.ObjectKeyFromObject(bpt) + By("should be unavailable") + Eventually(testapps.CheckObj(&testCtx, key, func(g Gomega, pobj *dpv1alpha1.BackupPolicyTemplate) { + g.Expect(pobj.Status.ObservedGeneration).To(Equal(bpt.Generation)) + g.Expect(pobj.Status.Phase).To(Equal(dpv1alpha1.UnavailablePhase)) + g.Expect(pobj.Status.Message).To(ContainSubstring(fmt.Sprintf(`fails to validate parameters of backupMethod "%s"`, BackupMethod))) + })).Should(Succeed()) + By("should be available") + Expect(testapps.ChangeObj(&testCtx, bpt, func(pobj *dpv1alpha1.BackupPolicyTemplate) { + bpt.Spec.Schedules[0].Parameters = testdp.TestParameters + })).Should(Succeed()) + Eventually(testapps.CheckObj(&testCtx, key, func(g Gomega, pobj *dpv1alpha1.BackupPolicyTemplate) { + g.Expect(pobj.Status.ObservedGeneration).To(Equal(bpt.Generation)) + g.Expect(pobj.Status.Phase).To(Equal(dpv1alpha1.AvailablePhase)) + g.Expect(pobj.Status.Message).To(BeEmpty()) + })).Should(Succeed()) + }) }) }) diff --git a/controllers/dataprotection/backupschedule_controller_test.go b/controllers/dataprotection/backupschedule_controller_test.go index 3557f62cf31..25c4a82ab04 100644 --- a/controllers/dataprotection/backupschedule_controller_test.go +++ b/controllers/dataprotection/backupschedule_controller_test.go @@ -24,7 +24,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - batchv1 "k8s.io/api/batch/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -85,19 +84,20 @@ var _ = Describe("Backup Schedule Controller", func() { When("creating backup schedule with default settings", func() { var ( backupPolicy *dpv1alpha1.BackupPolicy + actionSet *dpv1alpha1.ActionSet ) getCronjobKey := func(backupSchedule *dpv1alpha1.BackupSchedule, - method string) client.ObjectKey { + method string, name string) client.ObjectKey { return client.ObjectKey{ - Name: dpbackup.GenerateCRNameByBackupSchedule(backupSchedule, method), + Name: dpbackup.GenerateCRNameByScheduleNameAndMethod(backupSchedule, method, name), Namespace: backupPolicy.Namespace, } } BeforeEach(func() { By("creating an actionSet") - actionSet := testdp.NewFakeActionSet(&testCtx) + actionSet = testdp.NewFakeActionSet(&testCtx) By("creating storage provider") _ = testdp.NewFakeStorageProvider(&testCtx, nil) @@ -130,16 +130,16 @@ var _ = Describe("Backup Schedule Controller", func() { })).Should(Succeed()) By("checking cronjob, should not exist because all schedule policies of methods are disabled") - Eventually(testapps.CheckObjExists(&testCtx, getCronjobKey(backupSchedule, testdp.BackupMethodName), + Eventually(testapps.CheckObjExists(&testCtx, getCronjobKey(backupSchedule, testdp.BackupMethodName, ""), &batchv1.CronJob{}, false)).Should(Succeed()) - Eventually(testapps.CheckObjExists(&testCtx, getCronjobKey(backupSchedule, testdp.VSBackupMethodName), + Eventually(testapps.CheckObjExists(&testCtx, getCronjobKey(backupSchedule, testdp.VSBackupMethodName, ""), &batchv1.CronJob{}, false)).Should(Succeed()) By(fmt.Sprintf("enabling %s method schedule", testdp.BackupMethodName)) testdp.EnableBackupSchedule(&testCtx, backupSchedule, testdp.BackupMethodName) By("checking cronjob, should exist one cronjob to create backup") - Eventually(testapps.CheckObj(&testCtx, getCronjobKey(backupSchedule, testdp.BackupMethodName), func(g Gomega, fetched *batchv1.CronJob) { + Eventually(testapps.CheckObj(&testCtx, getCronjobKey(backupSchedule, testdp.BackupMethodName, ""), func(g Gomega, fetched *batchv1.CronJob) { schedulePolicy := dpbackup.GetSchedulePolicyByMethod(backupSchedule, testdp.BackupMethodName) timeZone, cronExpr := dpbackup.BuildCronJobSchedule(schedulePolicy.CronExpression) g.Expect(fetched.Labels[constant.AppManagedByLabelKey]).Should(Equal(dptypes.AppName)) @@ -192,6 +192,57 @@ var _ = Describe("Backup Schedule Controller", func() { })).Should(Succeed()) }) }) + + Context("create a backup schedule with parameters", func() { + const ( + scheduleName = "test" + ) + BeforeEach(func() { + By("set backup parameters and schema in acitionSet") + testdp.MockActionSetWithSchema(&testCtx, actionSet) + }) + It("with parameters", func() { + By("creating a backupSchedule with invalid parameters") + backupSchedule := testdp.NewFakeBackupSchedule(&testCtx, func(schedule *dpv1alpha1.BackupSchedule) { + schedule.Spec.Schedules[1].BackupMethod = testdp.BackupMethodName + schedule.Spec.Schedules[1].Name = scheduleName + schedule.Spec.Schedules[1].Parameters = testdp.InvalidParameters + }) + backupScheduleKey := client.ObjectKeyFromObject(backupSchedule) + By("the backupSchedule should fail ") + Eventually(testapps.CheckObj(&testCtx, backupScheduleKey, func(g Gomega, fetched *dpv1alpha1.BackupSchedule) { + g.Expect(fetched.Status.Phase).NotTo(Equal(dpv1alpha1.BackupSchedulePhaseAvailable)) + })).Should(Succeed()) + By("set valid parameters") + Expect(testapps.ChangeObj(&testCtx, backupSchedule, func(bs *dpv1alpha1.BackupSchedule) { + backupSchedule.Spec.Schedules[1].Parameters = testdp.TestParameters + })).Should(Succeed()) + By("checking backupSchedule status, should be available") + Eventually(testapps.CheckObj(&testCtx, backupScheduleKey, func(g Gomega, fetched *dpv1alpha1.BackupSchedule) { + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupSchedulePhaseAvailable)) + })).Should(Succeed()) + + By("checking cronjob, should not exist because all schedule policies of methods are disabled") + Eventually(testapps.CheckObjExists(&testCtx, getCronjobKey(backupSchedule, testdp.BackupMethodName, ""), + &batchv1.CronJob{}, false)).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, getCronjobKey(backupSchedule, testdp.BackupMethodName, scheduleName), + &batchv1.CronJob{}, false)).Should(Succeed()) + By(fmt.Sprintf("enabling %s method schedule", testdp.BackupMethodName)) + testdp.EnableBackupSchedule(&testCtx, backupSchedule, testdp.BackupMethodName) + + By("checking cronjob, should exist one cronjob to create backup") + Eventually(testapps.CheckObj(&testCtx, getCronjobKey(backupSchedule, testdp.BackupMethodName, scheduleName), func(g Gomega, fetched *batchv1.CronJob) { + g.Expect(fetched.Spec.StartingDeadlineSeconds).ShouldNot(BeNil()) + g.Expect(*fetched.Spec.StartingDeadlineSeconds).To(Equal(getStartingDeadlineSeconds(backupSchedule))) + g.Expect(fetched.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName).To(Equal(viper.GetString(dptypes.CfgKeyWorkerServiceAccountName))) + By("check parameters manifest") + g.Expect(fetched.Spec.JobTemplate.Spec.Template.Spec.Containers).Should(HaveLen(1)) + args := fetched.Spec.JobTemplate.Spec.Template.Spec.Containers[0].Args + g.Expect(args).Should(HaveLen(1)) + g.Expect(args[0]).Should(ContainSubstring(` parameters: [{"name":"testString","value":"stringValue"},{"name":"testArray","value":"v1,v2"}]`)) + })).Should(Succeed()) + }) + }) }) }) diff --git a/controllers/dataprotection/restore_controller_test.go b/controllers/dataprotection/restore_controller_test.go index b0359897ff7..e5cd5c9e944 100644 --- a/controllers/dataprotection/restore_controller_test.go +++ b/controllers/dataprotection/restore_controller_test.go @@ -26,7 +26,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -235,7 +234,28 @@ var _ = Describe("Restore Controller test", func() { By("mock jobs are completed and wait for restore is completed") mockAndCheckRestoreCompleted(restore) } + checkJobParametersEnv := func(restore *dpv1alpha1.Restore) { + By("check parameters env in restore jobs") + jobList := &batchv1.JobList{} + Expect(k8sClient.List(ctx, jobList, + client.MatchingLabels{dprestore.DataProtectionRestoreLabelKey: restore.Name}, + client.InNamespace(testCtx.DefaultNamespace))).Should(Succeed()) + for _, job := range jobList.Items { + Expect(len(job.Spec.Template.Spec.Containers)).ShouldNot(BeZero()) + for _, c := range job.Spec.Template.Spec.Containers { + count := 0 + for _, env := range c.Env { + for _, param := range testdp.TestParameters { + if param.Name == env.Name && param.Value == env.Value { + count++ + } + } + } + Expect(count).To(Equal(len(testdp.TestParameters))) + } + } + } Context("with restore fails", func() { It("test restore is Failed when backup is not completed", func() { By("expect for restore is Failed ") @@ -307,7 +327,25 @@ var _ = Describe("Restore Controller test", func() { It("test volumeClaimsTemplate when startingIndex is 1", func() { testRestoreWithVolumeClaimsTemplate(2, 1) }) + It("test restore parameters", func() { + By("set schema and parameters in actionSet") + testdp.MockActionSetWithSchema(&testCtx, actionSet) + replicas := 3 + startingIndex := 0 + restore := initResourcesAndWaitRestore(true, false, false, dpv1alpha1.RestorePhaseRunning, + func(f *testdp.MockRestoreFactory) { + f.SetVolumeClaimsTemplate(testdp.MysqlTemplateName, testdp.DataVolumeName, + testdp.DataVolumeMountPath, "", int32(replicas), int32(startingIndex), nil) + // Note: should ignore this policy when podSelectionStrategy is Any of the source target. + f.SetPrepareDataRequiredPolicy(dpv1alpha1.OneToOneRestorePolicy, "") + f.SetParameters(testdp.TestParameters) + }, nil) + By("expect restore jobs and pvcs are created") + checkJobAndPVCSCount(restore, replicas, replicas, startingIndex) + By("expect parameters env in restore jobs") + checkJobParametersEnv(restore) + }) It("test volumeClaimsTemplate when volumeClaimRestorePolicy is Serial", func() { replicas := 2 startingIndex := 1 @@ -509,7 +547,6 @@ var _ = Describe("Restore Controller test", func() { } return corev1.EnvVar{} } - By("wait for creating two exec jobs with the matchLabels") a := testapps.List(&testCtx, generics.JobSignature, client.MatchingLabels{dprestore.DataProtectionRestoreLabelKey: restore.Name}, @@ -532,6 +569,28 @@ var _ = Describe("Restore Controller test", func() { })).Should(Succeed()) }) + It("test parameters env", func() { + By("set schema and parameters in actionSet") + testdp.MockActionSetWithSchema(&testCtx, actionSet) + By("remove the prepareData stage for testing post ready actions") + Expect(testapps.ChangeObj(&testCtx, actionSet, func(set *dpv1alpha1.ActionSet) { + set.Spec.Restore.PrepareData = nil + })).Should(Succeed()) + + matchLabels := map[string]string{ + constant.AppInstanceLabelKey: testdp.ClusterName, + } + + restore := initResourcesAndWaitRestore(true, false, false, dpv1alpha1.RestorePhaseRunning, + func(f *testdp.MockRestoreFactory) { + f.SetJobActionConfig(matchLabels).SetExecActionConfig(matchLabels) + f.SetParameters(testdp.TestParameters) + }, func(b *dpv1alpha1.Backup) { + b.Status.Target.ConnectionCredential = nil + }) + By("expect parameters env in restore jobs") + checkJobParametersEnv(restore) + }) }) Context("test cross namespace", func() { diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_actionsets.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_actionsets.yaml index 40a85d26a67..71b5f38182c 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_actionsets.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_actionsets.yaml @@ -269,6 +269,11 @@ spec: - command - image type: object + withParameters: + description: Specifies the parameters used by the backup action + items: + type: string + type: array type: object backupType: allOf: @@ -457,6 +462,21 @@ spec: type: object type: array x-kubernetes-preserve-unknown-fields: true + parametersSchema: + description: Specifies the schema of parameters in backups and restores + before their usage. + properties: + openAPIV3Schema: + description: |- + Defines the schema for parameters using the OpenAPI v3. + The supported property types include: + - string + - number + - integer + - array: Note that only items of string type are supported. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object restore: description: Specifies the restore action. properties: @@ -568,6 +588,11 @@ spec: - command - image type: object + withParameters: + description: Specifies the parameters used by the restore action + items: + type: string + type: array type: object required: - backupType diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicytemplates.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicytemplates.yaml index 7cd65a409c2..ba430dc6ac7 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicytemplates.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicytemplates.yaml @@ -359,6 +359,32 @@ spec: description: Specifies whether the backup schedule is enabled or not. type: boolean + name: + description: |- + Specifies the name of the schedule. Names cannot be duplicated. + If the name is empty, it will be considered the same as the value of the backupMethod below. + type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map retentionPeriod: default: 7d description: "Determines the duration for which the backup should diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml index a2d196267d1..23060c13a49 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml @@ -110,6 +110,30 @@ spec: the backup CR but retaining the backup contents in backup repository. The current implementation only prevent accidental deletion of backup data. type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: self == oldSelf parentBackupName: description: Determines the parent backup name for incremental or differential backup. @@ -130,6 +154,9 @@ spec: - backupMethod - backupPolicyName type: object + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: has(oldSelf.parameters) == has(self.parameters) status: description: BackupStatus defines the observed state of Backup. properties: diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_backupschedules.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_backupschedules.yaml index f702df13a54..bd81a15826b 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_backupschedules.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_backupschedules.yaml @@ -72,6 +72,32 @@ spec: description: Specifies whether the backup schedule is enabled or not. type: boolean + name: + description: |- + Specifies the name of the schedule. Names cannot be duplicated. + If the name is empty, it will be considered the same as the value of the backupMethod below. + type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map retentionPeriod: default: 7d description: "Determines the duration for which the backup should diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_restores.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_restores.yaml index a4d6823c12a..199ea02288d 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_restores.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_restores.yaml @@ -270,6 +270,30 @@ spec: type: object type: array x-kubernetes-preserve-unknown-fields: true + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: self == oldSelf prepareDataConfig: description: |- Configuration for the action of "prepareData" phase, including the persistent volume claims @@ -2414,6 +2438,9 @@ spec: required: - backup type: object + x-kubernetes-validations: + - message: forbidden to update spec.parameters + rule: has(oldSelf.parameters) == has(self.parameters) status: description: RestoreStatus defines the observed state of Restore properties: diff --git a/deploy/helm/crds/operations.kubeblocks.io_opsrequests.yaml b/deploy/helm/crds/operations.kubeblocks.io_opsrequests.yaml index 6dd951e6267..100c4528de8 100755 --- a/deploy/helm/crds/operations.kubeblocks.io_opsrequests.yaml +++ b/deploy/helm/crds/operations.kubeblocks.io_opsrequests.yaml @@ -91,6 +91,30 @@ spec: - Delete - Retain type: string + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update parameters + rule: self == oldSelf parentBackupName: description: If the specified BackupMethod is incremental, `parentBackupName` is required. @@ -121,6 +145,9 @@ spec: Otherwise, only the Backup custom resource will be deleted. type: string type: object + x-kubernetes-validations: + - message: forbidden to update backup.parameters + rule: has(oldSelf.parameters) == has(self.parameters) cancel: description: |- Indicates whether the current operation should be canceled and terminated gracefully if it's in the @@ -4396,6 +4423,30 @@ spec: type: object type: array x-kubernetes-preserve-unknown-fields: true + parameters: + description: |- + Specifies a list of name-value pairs representing parameters and their corresponding values. + Parameters match the schema specified in the `actionset.spec.parametersSchema` + items: + properties: + name: + description: Represents the name of the parameter. + type: string + value: + description: Represents the parameter values. + type: string + required: + - name + - value + type: object + maxItems: 128 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update parameters + rule: self == oldSelf restorePointInTime: description: |- Specifies the point in time to which the restore should be performed. @@ -4422,6 +4473,9 @@ spec: required: - backupName type: object + x-kubernetes-validations: + - message: forbidden to update restore.parameters + rule: has(oldSelf.parameters) == has(self.parameters) start: description: Lists Components to be started. If empty, all components will be started. diff --git a/docs/developer_docs/api-reference/backup.md b/docs/developer_docs/api-reference/backup.md index 908897949d3..e4a3d1c1773 100644 --- a/docs/developer_docs/api-reference/backup.md +++ b/docs/developer_docs/api-reference/backup.md @@ -108,6 +108,20 @@ BackupType
parametersSchema
Specifies the schema of parameters in backups and restores before their usage.
+env
Determines the parent backup name for incremental or differential backup.
parameters
Specifies a list of name-value pairs representing parameters and their corresponding values.
+Parameters match the schema specified in the actionset.spec.parametersSchema
Specifies the number of retries before marking the restore failed.
+parameters
Specifies a list of name-value pairs representing parameters and their corresponding values.
+Parameters match the schema specified in the actionset.spec.parametersSchema
+(Appears on:ActionSetSpec) +
+Field | +Description | +
---|---|
+openAPIV3Schema + + +Kubernetes api extensions v1.JSONSchemaProps + + + |
+
+(Optional)
+ Defines the schema for parameters using the OpenAPI v3. +The supported property types include: +- string +- number +- integer +- array: Note that only items of string type are supported. + |
+
@@ -1274,6 +1354,20 @@ BackupType
parametersSchema
Specifies the schema of parameters in backups and restores before their usage.
+env
withParameters
Specifies the parameters used by the backup action
+Determines the parent backup name for incremental or differential backup.
+parameters
Specifies a list of name-value pairs representing parameters and their corresponding values.
+Parameters match the schema specified in the actionset.spec.parametersSchema
+(Appears on:BackupSpec, RestoreSpec, SchedulePolicy) +
+Field | +Description | +
---|---|
+name + +string + + |
+
+ Represents the name of the parameter. + |
+
+value + +string + + |
+
+ Represents the parameter values. + |
+
@@ -4871,6 +5031,18 @@ bool
Determines if a base backup is required during restoration.
+withParameters
Specifies the parameters used by the restore action
+Specifies the number of retries before marking the restore failed.
+parameters
Specifies a list of name-value pairs representing parameters and their corresponding values.
+Parameters match the schema specified in the actionset.spec.parametersSchema
name
Specifies the name of the schedule. Names cannot be duplicated. +If the name is empty, it will be considered the same as the value of the backupMethod below.
+backupMethod
You can also combine the above durations. For example: 30d12h30m
parameters
Specifies a list of name-value pairs representing parameters and their corresponding values.
+Parameters match the schema specified in the actionset.spec.parametersSchema