diff --git a/apis/apps/v1alpha1/cluster_types.go b/apis/apps/v1alpha1/cluster_types.go index 9eab7918c01..62af838c03c 100644 --- a/apis/apps/v1alpha1/cluster_types.go +++ b/apis/apps/v1alpha1/cluster_types.go @@ -298,6 +298,11 @@ type ClusterBackup struct { // +optional PITREnabled *bool `json:"pitrEnabled,omitempty"` + // Specifies the backup method to use, if not set, use the first continuous method. + // + // +optional + ContinuousMethod string `json:"continuousMethod,omitempty"` + // Specifies whether to enable incremental backup. // // +kubebuilder:default=false diff --git a/config/crd/bases/apps.kubeblocks.io_clusters.yaml b/config/crd/bases/apps.kubeblocks.io_clusters.yaml index 7ee4c7e8aee..e54380038ef 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusters.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusters.yaml @@ -200,6 +200,10 @@ spec: backup: description: Specifies the backup configuration of the Cluster. properties: + continuousMethod: + description: Specifies the backup method to use, if not set, use + the first continuous method. + type: string cronExpression: description: The cron expression for the schedule. The timezone is in UTC. See https://en.wikipedia.org/wiki/Cron. diff --git a/controllers/apps/cluster_controller_test.go b/controllers/apps/cluster_controller_test.go index faa20ae54af..2a9a5b1e81c 100644 --- a/controllers/apps/cluster_controller_test.go +++ b/controllers/apps/cluster_controller_test.go @@ -40,6 +40,7 @@ import ( "github.com/apecloud/kubeblocks/pkg/controller/builder" "github.com/apecloud/kubeblocks/pkg/controller/component" "github.com/apecloud/kubeblocks/pkg/controller/scheduling" + "github.com/apecloud/kubeblocks/pkg/dataprotection/utils/boolptr" "github.com/apecloud/kubeblocks/pkg/generics" testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps" testdp "github.com/apecloud/kubeblocks/pkg/testutil/dataprotection" @@ -161,7 +162,7 @@ var _ = Describe("Cluster Controller", func() { GetObject() By("Create a bpt obj") - createBackupPolicyTpl(clusterDefObj, compDefObj.Name, clusterVersionName) + createBackupPolicyTpl(clusterDefObj, compDefObj.Name, false, clusterVersionName) By("Create a componentVersion obj") compVersionObj = testapps.NewComponentVersionFactory(compVersionName). @@ -1368,6 +1369,19 @@ var _ = Describe("Cluster Controller", func() { RepoName: backupRepoName, }, }, + { + desc: "backup with snapshot method and specified continuous method", + backup: &appsv1alpha1.ClusterBackup{ + Enabled: &boolTrue, + RetentionPeriod: retention("1d"), + Method: vsBackupMethodName, + CronExpression: "*/1 * * * *", + StartingDeadlineMinutes: int64Ptr(int64(10)), + ContinuousMethod: continuousMethodName1, + PITREnabled: &boolTrue, + RepoName: backupRepoName, + }, + }, { desc: "disable backup", backup: &appsv1alpha1.ClusterBackup{ @@ -1405,19 +1419,36 @@ var _ = Describe("Cluster Controller", func() { checkSchedule := func(g Gomega, schedule *dpv1alpha1.BackupSchedule) { var policy *dpv1alpha1.SchedulePolicy - enableOtherFullMethod := false - for i, s := range schedule.Spec.Schedules { + hasCheckPITRMethod := false + for i := range schedule.Spec.Schedules { + s := &schedule.Spec.Schedules[i] if s.BackupMethod == backup.Method { Expect(*s.Enabled).Should(BeEquivalentTo(*backup.Enabled)) - policy = &schedule.Spec.Schedules[i] - if *backup.Enabled { - enableOtherFullMethod = true + policy = s + continue + } + if !slices.Contains([]string{continuousMethodName, continuousMethodName1}, s.BackupMethod) { + if boolptr.IsSetToTrue(backup.Enabled) { + // another full backup method should be disabled. + Expect(*s.Enabled).Should(BeFalse()) } continue } - if enableOtherFullMethod { - // another full backup method should be disabled. - Expect(*s.Enabled).Should(BeFalse()) + if len(backup.ContinuousMethod) == 0 { + // first continuous backup method should be equal to "PITREnabled", another is disabled. + if !hasCheckPITRMethod { + Expect(*s.Enabled).Should(BeEquivalentTo(*backup.PITREnabled)) + hasCheckPITRMethod = true + } else { + Expect(*s.Enabled).Should(BeFalse()) + } + } else { + // specified continuous backup method should be equal to "PITREnabled", another is disabled. + if backup.ContinuousMethod == s.BackupMethod { + Expect(*s.Enabled).Should(BeEquivalentTo(*backup.PITREnabled)) + } else { + Expect(*s.Enabled).Should(BeFalse()) + } } } if backup.Enabled != nil && *backup.Enabled { @@ -1593,9 +1624,10 @@ var _ = Describe("Cluster Controller", func() { }) }) -func createBackupPolicyTpl(clusterDefObj *appsv1alpha1.ClusterDefinition, compDef string, mappingClusterVersions ...string) { +func createBackupPolicyTpl(clusterDefObj *appsv1alpha1.ClusterDefinition, compDef string, forHScale bool, mappingClusterVersions ...string) { By("create actionSet") - fakeActionSet(clusterDefObj.Name) + fakeActionSet(actionSetName, clusterDefObj.Name, dpv1alpha1.BackupTypeFull) + fakeActionSet(continuousActionSetName, clusterDefObj.Name, dpv1alpha1.BackupTypeContinuous) By("Creating a BackupPolicyTemplate") bpt := testapps.NewBackupPolicyTemplateFactory(backupPolicyTPLName). @@ -1618,6 +1650,16 @@ func createBackupPolicyTpl(clusterDefObj *appsv1alpha1.ClusterDefinition, compDe case appsv1alpha1.Replication: bpt.SetTargetRole("primary") } + if !forHScale { + bpt.AddBackupMethod(continuousMethodName, false, continuousActionSetName). + SetComponentDef(compDef). + SetBackupMethodVolumeMounts("data", "/data"). + AddBackupMethod(continuousMethodName1, false, continuousActionSetName). + SetComponentDef(compDef). + SetBackupMethodVolumeMounts("data", "/data"). + AddSchedule(continuousMethodName, "0 0 * * *", ttl, false). + AddSchedule(continuousMethodName1, "0 0 * * *", ttl, false) + } } bpt.Create(&testCtx) } diff --git a/controllers/apps/component_controller_test.go b/controllers/apps/component_controller_test.go index a227b7d68ee..07725439de3 100644 --- a/controllers/apps/component_controller_test.go +++ b/controllers/apps/component_controller_test.go @@ -66,10 +66,13 @@ import ( ) const ( - backupPolicyTPLName = "test-backup-policy-template-mysql" - backupMethodName = "test-backup-method" - vsBackupMethodName = "test-vs-backup-method" - actionSetName = "test-action-set" + backupPolicyTPLName = "test-backup-policy-template-mysql" + backupMethodName = "test-backup-method" + continuousMethodName = "continuous-backup-method" + continuousMethodName1 = "continuous-backup-method1" + vsBackupMethodName = "test-vs-backup-method" + actionSetName = "test-action-set" + continuousActionSetName = "test-continuous-action-set" ) var ( @@ -700,7 +703,7 @@ var _ = Describe("Component Controller", func() { if policyType == appsv1alpha1.HScaleDataClonePolicyCloneVolume { By("creating actionSet if backup policy is backup") - fakeActionSet(clusterDef.Name) + fakeActionSet(actionSetName, clusterDef.Name, dpv1alpha1.BackupTypeFull) } } })()).ShouldNot(HaveOccurred()) @@ -2052,7 +2055,7 @@ var _ = Describe("Component Controller", func() { Context("provisioning", func() { BeforeEach(func() { createAllWorkloadTypesClusterDef() - createBackupPolicyTpl(clusterDefObj, compDefName) + createBackupPolicyTpl(clusterDefObj, compDefName, true) }) AfterEach(func() { @@ -2123,7 +2126,7 @@ var _ = Describe("Component Controller", func() { BeforeEach(func() { createAllWorkloadTypesClusterDef() - createBackupPolicyTpl(clusterDefObj, compDefName) + createBackupPolicyTpl(clusterDefObj, compDefName, true) }) AfterEach(func() { @@ -2143,7 +2146,7 @@ var _ = Describe("Component Controller", func() { BeforeEach(func() { cleanEnv() createAllWorkloadTypesClusterDef() - createBackupPolicyTpl(clusterDefObj, compDefName) + createBackupPolicyTpl(clusterDefObj, compDefName, true) }) createNWaitClusterObj := func(components map[string]string, @@ -2220,7 +2223,7 @@ var _ = Describe("Component Controller", func() { BeforeEach(func() { createAllWorkloadTypesClusterDef() - createBackupPolicyTpl(clusterDefObj, compDefName) + createBackupPolicyTpl(clusterDefObj, compDefName, true) mockStorageClass = testk8s.CreateMockStorageClass(&testCtx, testk8s.DefaultStorageClassName) }) @@ -2289,7 +2292,7 @@ var _ = Describe("Component Controller", func() { When("creating cluster with workloadType=consensus component", func() { BeforeEach(func() { createAllWorkloadTypesClusterDef() - createBackupPolicyTpl(clusterDefObj, compDefName) + createBackupPolicyTpl(clusterDefObj, compDefName, true) }) AfterEach(func() { @@ -2544,7 +2547,7 @@ func checkRestoreAndSetCompleted(clusterKey types.NamespacedName, compName strin mockRestoreCompleted(ml) } -func fakeActionSet(clusterDefName string) *dpv1alpha1.ActionSet { +func fakeActionSet(actionSetName, clusterDefName string, backupType dpv1alpha1.BackupType) *dpv1alpha1.ActionSet { actionSet := &dpv1alpha1.ActionSet{ ObjectMeta: metav1.ObjectMeta{ Name: actionSetName, @@ -2559,7 +2562,7 @@ func fakeActionSet(clusterDefName string) *dpv1alpha1.ActionSet { Value: "test-value", }, }, - BackupType: dpv1alpha1.BackupTypeFull, + BackupType: backupType, Backup: &dpv1alpha1.BackupActionSpec{ BackupData: &dpv1alpha1.BackupDataActionSpec{ JobActionSpec: dpv1alpha1.JobActionSpec{ diff --git a/controllers/apps/opsrequest_controller_test.go b/controllers/apps/opsrequest_controller_test.go index 1f15988251a..1919e618be7 100644 --- a/controllers/apps/opsrequest_controller_test.go +++ b/controllers/apps/opsrequest_controller_test.go @@ -319,7 +319,7 @@ var _ = Describe("OpsRequest Controller", func() { } createMysqlCluster := func(replicas int32) { - createBackupPolicyTpl(clusterDefObj, mysqlCompDefName) + createBackupPolicyTpl(clusterDefObj, mysqlCompDefName, true) By("set component to horizontal with snapshot policy and create a cluster") testk8s.MockEnableVolumeSnapshot(&testCtx, testk8s.DefaultStorageClassName) diff --git a/controllers/apps/transformer_cluster_backup_policy.go b/controllers/apps/transformer_cluster_backup_policy.go index 875b72513fb..a59f11ed177 100644 --- a/controllers/apps/transformer_cluster_backup_policy.go +++ b/controllers/apps/transformer_cluster_backup_policy.go @@ -647,7 +647,8 @@ func (r *clusterBackupPolicyTransformer) mergeClusterBackup( hasSyncPITRMethod := false hasSyncIncMethod := false enableAutoBackup := boolptr.IsSetToTrue(backup.Enabled) - for i, s := range backupSchedule.Spec.Schedules { + for i := range backupSchedule.Spec.Schedules { + s := &backupSchedule.Spec.Schedules[i] if s.BackupMethod == backup.Method { mergeSchedulePolicy(sp, &backupSchedule.Spec.Schedules[i]) exist = true @@ -671,15 +672,23 @@ func (r *clusterBackupPolicyTransformer) mergeClusterBackup( r.Error(err, "failed to get ActionSet for backup.", "ActionSet", as.Name) continue } - if as.Spec.BackupType == dpv1alpha1.BackupTypeContinuous && backup.PITREnabled != nil && !hasSyncPITRMethod { - // auto-sync the first continuous backup for the 'pirtEnable' option. - backupSchedule.Spec.Schedules[i].Enabled = backup.PITREnabled + switch as.Spec.BackupType { + case dpv1alpha1.BackupTypeContinuous: + if backup.PITREnabled == nil { + continue + } + if boolptr.IsSetToFalse(backup.PITREnabled) || hasSyncPITRMethod || + (len(backup.ContinuousMethod) > 0 && backup.ContinuousMethod != s.BackupMethod) { + s.Enabled = boolptr.False() + continue + } + // auto-sync the first or specified continuous backup for the 'pirtEnable' option. + s.Enabled = backup.PITREnabled if backup.RetentionPeriod.String() != "" { - backupSchedule.Spec.Schedules[i].RetentionPeriod = backup.RetentionPeriod + s.RetentionPeriod = backup.RetentionPeriod } hasSyncPITRMethod = true - } - if as.Spec.BackupType == dpv1alpha1.BackupTypeIncremental { + case dpv1alpha1.BackupTypeIncremental: if len(backup.Method) == 0 || m.CompatibleMethod != backup.Method { // disable other incremental backup schedules backupSchedule.Spec.Schedules[i].Enabled = boolptr.False() @@ -692,10 +701,11 @@ func (r *clusterBackupPolicyTransformer) mergeClusterBackup( }, &backupSchedule.Spec.Schedules[i]) hasSyncIncMethod = true } - } - if as.Spec.BackupType == dpv1alpha1.BackupTypeFull && enableAutoBackup { - // disable the automatic backup for other full backup method - backupSchedule.Spec.Schedules[i].Enabled = boolptr.False() + case dpv1alpha1.BackupTypeFull: + if enableAutoBackup { + // disable the automatic backup for other full backup method + s.Enabled = boolptr.False() + } } } if !exist { diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml index 7ee4c7e8aee..e54380038ef 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml @@ -200,6 +200,10 @@ spec: backup: description: Specifies the backup configuration of the Cluster. properties: + continuousMethod: + description: Specifies the backup method to use, if not set, use + the first continuous method. + type: string cronExpression: description: The cron expression for the schedule. The timezone is in UTC. See https://en.wikipedia.org/wiki/Cron. diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index a2427b52e03..1017c0750e0 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -4548,6 +4548,18 @@ bool
continuousMethod
Specifies the backup method to use, if not set, use the first continuous method.
+incrementalBackupEnabled