Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apis/operations/v1alpha1/opsrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,13 @@ type FromBackup struct {
// +optional
Namespace string `json:"namespace,omitempty"`

// Specifies the backup source target name to restore from.
// This field is required when the referenced Backup has multiple source targets.
// It is propagated to Restore.spec.backup.sourceTargetName.
//
// +optional
SourceTargetName string `json:"sourceTargetName,omitempty"`

// Defines container environment variables for the restore process.
// merged with the ones specified in the Backup and ActionSet resources.
//
Expand Down
6 changes: 6 additions & 0 deletions config/crd/bases/operations.kubeblocks.io_opsrequests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,12 @@ spec:
- RFC3339 format, e.g. "2023-11-25T18:52:53Z"
- A human-readable date-time format, e.g. "Jul 25,2023 18:52:53 UTC+0800"
type: string
sourceTargetName:
description: |-
Specifies the backup source target name to restore from.
This field is required when the referenced Backup has multiple source targets.
It is propagated to Restore.spec.backup.sourceTargetName.
type: string
required:
- name
type: object
Expand Down
6 changes: 6 additions & 0 deletions deploy/helm/crds/operations.kubeblocks.io_opsrequests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,12 @@ spec:
- RFC3339 format, e.g. "2023-11-25T18:52:53Z"
- A human-readable date-time format, e.g. "Jul 25,2023 18:52:53 UTC+0800"
type: string
sourceTargetName:
description: |-
Specifies the backup source target name to restore from.
This field is required when the referenced Backup has multiple source targets.
It is propagated to Restore.spec.backup.sourceTargetName.
type: string
required:
- name
type: object
Expand Down
14 changes: 14 additions & 0 deletions docs/developer_docs/api-reference/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,20 @@ If not specified, the namespace of the OpsRequest will be used.</p>
</tr>
<tr>
<td>
<code>sourceTargetName</code><br/>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Specifies the backup source target name to restore from.
This field is required when the referenced Backup has multiple source targets.
It is propagated to Restore.spec.backup.sourceTargetName.</p>
</td>
</tr>
<tr>
<td>
<code>restoreEnv</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#envvar-v1-core">
Expand Down
11 changes: 10 additions & 1 deletion pkg/controller/plan/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type RestoreManager struct {
replicas int32
restoreLabels map[string]string
RestoreNamePrefix string
SourceTargetName string
}

func NewRestoreManager(ctx context.Context,
Expand Down Expand Up @@ -184,6 +185,14 @@ func (r *RestoreManager) BuildPrepareDataRestore(comp *component.SynthesizedComp
return nil, nil
}
sourceTargetName, sourceTarget := backupSourceTargetForRestore(backupObj)
requiredPolicy := r.buildRequiredPolicy(sourceTarget)
if r.SourceTargetName != "" {
// DataProtection owns resolving and validating an explicit source target.
sourceTargetName = r.SourceTargetName
requiredPolicy = &dpv1alpha1.RequiredPolicyForAllPodSelection{
DataRestorePolicy: dpv1alpha1.OneToOneRestorePolicy,
}
}
restore := &dpv1alpha1.Restore{
ObjectMeta: r.GetRestoreObjectMeta(comp, dpv1alpha1.PrepareData, templateName),
Spec: dpv1alpha1.RestoreSpec{
Expand All @@ -196,7 +205,7 @@ func (r *RestoreManager) BuildPrepareDataRestore(comp *component.SynthesizedComp
Env: r.env,
Parameters: r.parameters,
PrepareDataConfig: &dpv1alpha1.PrepareDataConfig{
RequiredPolicyForAllPodSelection: r.buildRequiredPolicy(sourceTarget),
RequiredPolicyForAllPodSelection: requiredPolicy,
SchedulingSpec: r.buildSchedulingSpec(comp, template),
VolumeClaimRestorePolicy: r.volumeRestorePolicy,
RestoreVolumeClaimsTemplate: &dpv1alpha1.RestoreVolumeClaimsTemplate{
Expand Down
91 changes: 90 additions & 1 deletion pkg/controller/plan/restore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func TestRestoreManagerBuildPrepareDataRestore(t *testing.T) {
SchedulingPolicy: &appsv1.SchedulingPolicy{NodeName: "node-a"},
}
backup := &dpv1alpha1.Backup{
ObjectMeta: metav1.ObjectMeta{Name: "backup"},
ObjectMeta: metav1.ObjectMeta{Name: "backup", Namespace: "default"},
Status: dpv1alpha1.BackupStatus{
Targets: []dpv1alpha1.BackupStatusTarget{{
BackupTarget: dpv1alpha1.BackupTarget{
Expand Down Expand Up @@ -298,6 +298,95 @@ func TestRestoreManagerBuildPrepareDataRestore(t *testing.T) {
}
}

func TestRestoreManagerBuildPrepareDataRestoreWithExplicitSourceTarget(t *testing.T) {
manager := newRestoreManagerForTest()
manager.SourceTargetName = "target-b"
comp := &component.SynthesizedComponent{
Name: "mysql",
Replicas: 1,
VolumeClaimTemplates: []corev1.PersistentVolumeClaimTemplate{{
ObjectMeta: metav1.ObjectMeta{Name: "data"},
}},
}
backup := &dpv1alpha1.Backup{
ObjectMeta: metav1.ObjectMeta{Name: "backup"},
Status: dpv1alpha1.BackupStatus{
Targets: []dpv1alpha1.BackupStatusTarget{
{BackupTarget: dpv1alpha1.BackupTarget{
Name: "target-a",
PodSelector: &dpv1alpha1.PodSelector{},
}},
{BackupTarget: dpv1alpha1.BackupTarget{
Name: "target-b",
PodSelector: &dpv1alpha1.PodSelector{Strategy: dpv1alpha1.PodSelectionStrategyAll},
}},
},
BackupMethod: &dpv1alpha1.BackupMethod{
Name: "snapshot",
TargetVolumes: &dpv1alpha1.TargetVolumeInfo{
Volumes: []string{"data"},
},
},
},
}

restore, err := manager.BuildPrepareDataRestore(comp, backup, nil)
if err != nil {
t.Fatalf("BuildPrepareDataRestore() error = %v", err)
}
if restore == nil {
t.Fatal("restore is nil")
}
if restore.Spec.Backup.SourceTargetName != "target-b" {
t.Fatalf("source target name = %q, want target-b", restore.Spec.Backup.SourceTargetName)
}
policy := restore.Spec.PrepareDataConfig.RequiredPolicyForAllPodSelection
if policy == nil || policy.DataRestorePolicy != dpv1alpha1.OneToOneRestorePolicy {
t.Fatalf("required policy = %#v, want one-to-one policy for target-b", policy)
}
}

func TestRestoreManagerBuildPrepareDataRestorePropagatesUnknownSourceTarget(t *testing.T) {
manager := newRestoreManagerForTest()
manager.SourceTargetName = "missing-target"
comp := &component.SynthesizedComponent{
Name: "mysql",
Replicas: 1,
VolumeClaimTemplates: []corev1.PersistentVolumeClaimTemplate{{
ObjectMeta: metav1.ObjectMeta{Name: "data"},
}},
}
backup := &dpv1alpha1.Backup{
ObjectMeta: metav1.ObjectMeta{Name: "backup"},
Status: dpv1alpha1.BackupStatus{
Targets: []dpv1alpha1.BackupStatusTarget{{
BackupTarget: dpv1alpha1.BackupTarget{
Name: "target-a",
PodSelector: &dpv1alpha1.PodSelector{},
},
}},
BackupMethod: &dpv1alpha1.BackupMethod{
Name: "snapshot",
TargetVolumes: &dpv1alpha1.TargetVolumeInfo{
Volumes: []string{"data"},
},
},
},
}

restore, err := manager.BuildPrepareDataRestore(comp, backup, nil)
if err != nil {
t.Fatalf("BuildPrepareDataRestore() error = %v, want DataProtection to validate source target", err)
}
if restore.Spec.Backup.SourceTargetName != "missing-target" {
t.Fatalf("source target name = %q, want missing-target propagated", restore.Spec.Backup.SourceTargetName)
}
policy := restore.Spec.PrepareDataConfig.RequiredPolicyForAllPodSelection
if policy == nil || policy.DataRestorePolicy != dpv1alpha1.OneToOneRestorePolicy {
t.Fatalf("required policy = %#v, want one-to-one for explicit source target", policy)
}
}

func TestRestoreManagerBuildPrepareDataRestoreWithoutVolumes(t *testing.T) {
manager := newRestoreManagerForTest()
comp := &component.SynthesizedComponent{
Expand Down
5 changes: 1 addition & 4 deletions pkg/operations/horizontal_scaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,6 @@ func (hs horizontalScalingOpsHandler) createRestore(reqCtx intctrlutil.RequestCt
}
return nil
}
if len(backupObj.Status.Targets) > 1 {
// TODO: support explicit source target selection for scale-out restore from multi-target backups.
return intctrlutil.NewFatalError(fmt.Sprintf("scale-out from backup %s/%s is not supported because it has multiple source targets", backupObj.Namespace, backupObj.Name))
}
// create restore
restore, err := restoreMGR.BuildPrepareDataRestore(synthesizedComponent, backupObj, getTemplate(templateName))
if err != nil {
Expand Down Expand Up @@ -303,6 +299,7 @@ func (hs horizontalScalingOpsHandler) restoreDataFromBackup(reqCtx intctrlutil.R
restoreMGR.RestoreTime = fromBackup.RestorePointInTime
restoreMGR.SetRestoreEnv(fromBackup.RestoreEnv)
restoreMGR.RestoreNamePrefix = string(opsRes.OpsRequest.UID[:8])
restoreMGR.SourceTargetName = fromBackup.SourceTargetName
Comment thread
weicao marked this conversation as resolved.
// check restore status
restoreMeta := restoreMGR.GetRestoreObjectMeta(synthesizedComponent, dpv1alpha1.PrepareData, templateName)
restore := &dpv1alpha1.Restore{}
Expand Down
29 changes: 26 additions & 3 deletions pkg/operations/horizontal_scaling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,25 +278,48 @@ var _ = Describe("HorizontalScaling OpsRequest", func() {
Expect(testapps.ChangeObjStatus(&testCtx, backup, func() {
backup.Status.Phase = dpv1alpha1.BackupPhaseCompleted
backup.Status.BackupMethod = &dpv1alpha1.BackupMethod{
Name: testdp.VSBackupMethodName,
Name: testdp.VSBackupMethodName,
SnapshotVolumes: pointer.Bool(true),
TargetVolumes: &dpv1alpha1.TargetVolumeInfo{
Volumes: []string{"data"},
},
}
backup.Status.Targets = []dpv1alpha1.BackupStatusTarget{
{BackupTarget: dpv1alpha1.BackupTarget{
Name: "target-a",
PodSelector: &dpv1alpha1.PodSelector{},
}},
{BackupTarget: dpv1alpha1.BackupTarget{
Name: "target-b",
PodSelector: &dpv1alpha1.PodSelector{Strategy: dpv1alpha1.PodSelectionStrategyAll},
}},
}
})).Should(Succeed())

By("scale out replicas from a full backup")
restoreEnv := []corev1.EnvVar{{Name: "RESTORE_ENV", Value: "true"}}
horizontalScaling := opsv1alpha1.HorizontalScaling{ScaleOut: &opsv1alpha1.ScaleOut{
FromBackup: &opsv1alpha1.FromBackup{
Name: backupName,
RestoreEnv: restoreEnv,
Name: backupName,
SourceTargetName: "target-b",
RestoreEnv: restoreEnv,
},
}}

horizontalScaling.ScaleOut.ReplicaChanges = pointer.Int32(2)
reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx, Recorder: eventRecorder}
opsRes, _ := commonHScaleConsensusCompTest(reqCtx, nil, horizontalScaling, false, true)
restoreList := &dpv1alpha1.RestoreList{}
Expect(k8sClient.List(ctx, restoreList, client.MatchingLabels{
constant.OpsRequestNameLabelKey: opsRes.OpsRequest.Name,
}, client.InNamespace(opsRes.OpsRequest.Namespace))).Should(Succeed())
Expect(restoreList.Items).Should(HaveLen(2))
for i := range restoreList.Items {
Expect(restoreList.Items[i].Spec.Backup.SourceTargetName).Should(Equal("target-b"))
Expect(restoreList.Items[i].Spec.PrepareDataConfig.RequiredPolicyForAllPodSelection).NotTo(BeNil())
Expect(restoreList.Items[i].Spec.PrepareDataConfig.RequiredPolicyForAllPodSelection.DataRestorePolicy).
Should(Equal(dpv1alpha1.OneToOneRestorePolicy))
}

By("mock restore phase to completed")
comp, compDef, err := component.GetCompNCompDefByName(reqCtx.Ctx, k8sClient, opsRes.Cluster.Namespace, constant.GenerateClusterComponentName(opsRes.Cluster.Name, defaultCompName))
Expand Down
Loading