Skip to content

Commit 060add8

Browse files
authored
fix(ops): guard scale-out restore without target volumes (#10475)
1 parent 100e747 commit 060add8

3 files changed

Lines changed: 111 additions & 0 deletions

File tree

pkg/controller/plan/restore.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ func (r *RestoreManager) DoPrepareData(comp *component.SynthesizedComponent,
118118
return r.createRestoreAndWait(compObj, restores...)
119119
}
120120

121+
// BuildPrepareDataRestore returns (nil, nil) when the backup method has no
122+
// target volumes matching the component's volume claim templates, i.e. there
123+
// is nothing to prepareData-restore for this component. Callers must handle
124+
// the nil Restore instead of using it.
121125
func (r *RestoreManager) BuildPrepareDataRestore(comp *component.SynthesizedComponent, backupObj *dpv1alpha1.Backup, template *appsv1.InstanceTemplate) (*dpv1alpha1.Restore, error) {
122126
templateName := ""
123127
startingIndex := r.startingIndex

pkg/operations/horizontal_scaling.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ func (hs horizontalScalingOpsHandler) createRestore(reqCtx intctrlutil.RequestCt
233233
if err != nil {
234234
return err
235235
}
236+
if restore == nil {
237+
return intctrlutil.NewFatalError(fmt.Sprintf("scale-out from backup %s/%s is not supported because backup method %q has no target volumes matching component %q",
238+
backupObj.Namespace, backupObj.Name, backupObj.Status.BackupMethod.Name, synthesizedComponent.Name))
239+
}
236240
scheme, _ := opsv1alpha1.SchemeBuilder.Build()
237241
if err := intctrlutil.SetOwnership(opsRes.OpsRequest, restore, scheme, ""); err != nil {
238242
return err

pkg/operations/horizontal_scaling_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
package operations
2121

2222
import (
23+
"context"
2324
"fmt"
25+
"strings"
26+
"testing"
2427
"time"
2528

2629
. "github.com/onsi/ginkgo/v2"
2730
. "github.com/onsi/gomega"
2831

2932
corev1 "k8s.io/api/core/v1"
3033
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/runtime"
3135
"k8s.io/apimachinery/pkg/types"
3236
"k8s.io/apimachinery/pkg/util/sets"
3337
"k8s.io/utils/pointer"
3438
"sigs.k8s.io/controller-runtime/pkg/client"
39+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3540

3641
appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1"
3742
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
@@ -998,3 +1003,101 @@ func mockConsensusCompToRunning(opsRes *OpsResource) {
9981003
compStatus.Phase = appsv1.RunningComponentPhase
9991004
opsRes.Cluster.Status.Components[defaultCompName] = compStatus
10001005
}
1006+
1007+
func TestHorizontalScalingCreateRestoreReturnsFatalWhenNoRestoreBuilt(t *testing.T) {
1008+
testCases := []struct {
1009+
name string
1010+
backupMethod *dpv1alpha1.BackupMethod
1011+
}{{
1012+
// logical backups (e.g. TiDB BR) have no targetVolumes at all
1013+
name: "backup method without target volumes",
1014+
backupMethod: &dpv1alpha1.BackupMethod{Name: "br"},
1015+
}, {
1016+
// targetVolumes exist but none match the component's volume claim templates
1017+
name: "backup method target volumes match no component volume",
1018+
backupMethod: &dpv1alpha1.BackupMethod{
1019+
Name: "br",
1020+
TargetVolumes: &dpv1alpha1.TargetVolumeInfo{
1021+
Volumes: []string{"other-data"},
1022+
},
1023+
},
1024+
}}
1025+
for _, tc := range testCases {
1026+
t.Run(tc.name, func(t *testing.T) {
1027+
ctx := context.Background()
1028+
scheme := runtime.NewScheme()
1029+
for _, addToScheme := range []func(*runtime.Scheme) error{
1030+
corev1.AddToScheme,
1031+
appsv1.AddToScheme,
1032+
dpv1alpha1.AddToScheme,
1033+
opsv1alpha1.AddToScheme,
1034+
} {
1035+
if err := addToScheme(scheme); err != nil {
1036+
t.Fatalf("add scheme: %v", err)
1037+
}
1038+
}
1039+
1040+
cluster := &appsv1.Cluster{
1041+
ObjectMeta: metav1.ObjectMeta{
1042+
Name: "tidb",
1043+
Namespace: "default",
1044+
UID: types.UID("cluster1"),
1045+
},
1046+
}
1047+
opsRequest := &opsv1alpha1.OpsRequest{
1048+
ObjectMeta: metav1.ObjectMeta{
1049+
Name: "scale-out-from-backup",
1050+
Namespace: "default",
1051+
UID: types.UID("opsreq1"),
1052+
},
1053+
}
1054+
backup := &dpv1alpha1.Backup{
1055+
ObjectMeta: metav1.ObjectMeta{
1056+
Name: "br-full",
1057+
Namespace: "default",
1058+
},
1059+
Status: dpv1alpha1.BackupStatus{
1060+
Phase: dpv1alpha1.BackupPhaseCompleted,
1061+
BackupMethod: tc.backupMethod,
1062+
},
1063+
}
1064+
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster, opsRequest, backup).Build()
1065+
opsRes := &OpsResource{
1066+
Cluster: cluster,
1067+
OpsRequest: opsRequest,
1068+
}
1069+
synthesizedComponent := &component.SynthesizedComponent{
1070+
Name: "tikv",
1071+
VolumeClaimTemplates: []corev1.PersistentVolumeClaimTemplate{{
1072+
ObjectMeta: metav1.ObjectMeta{Name: "data"},
1073+
Spec: corev1.PersistentVolumeClaimSpec{
1074+
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
1075+
},
1076+
}},
1077+
}
1078+
restoreMGR := plan.NewRestoreManager(ctx, cli, cluster, scheme, map[string]string{
1079+
constant.OpsRequestNameLabelKey: opsRequest.Name,
1080+
}, 1, 3)
1081+
1082+
err := horizontalScalingOpsHandler{}.createRestore(intctrlutil.RequestCtx{Ctx: ctx}, cli, opsRes,
1083+
synthesizedComponent, restoreMGR, &appsv1.ClusterComponentSpec{Name: "tikv"}, backup, "")
1084+
if err == nil {
1085+
t.Fatal("expected fatal error when backup method cannot build prepareData restore")
1086+
}
1087+
if !intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeFatal) {
1088+
t.Fatalf("expected fatal error, got %T: %v", err, err)
1089+
}
1090+
if !strings.Contains(err.Error(), "has no target volumes matching component") {
1091+
t.Fatalf("unexpected error: %v", err)
1092+
}
1093+
1094+
restoreList := &dpv1alpha1.RestoreList{}
1095+
if err := cli.List(ctx, restoreList, client.InNamespace("default")); err != nil {
1096+
t.Fatalf("list restores: %v", err)
1097+
}
1098+
if len(restoreList.Items) != 0 {
1099+
t.Fatalf("expected no restore to be created, got %d", len(restoreList.Items))
1100+
}
1101+
})
1102+
}
1103+
}

0 commit comments

Comments
 (0)