Skip to content

Commit d381248

Browse files
authored
chore: improve pkg dataprotection coverage (#10383)
1 parent f5c9e44 commit d381248

8 files changed

Lines changed: 1222 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
Copyright (C) 2022-2026 ApeCloud Co., Ltd
3+
4+
This file is part of KubeBlocks project
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public License
17+
along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package action
21+
22+
import (
23+
"context"
24+
"errors"
25+
"testing"
26+
"time"
27+
28+
vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
29+
"github.com/stretchr/testify/assert"
30+
appsv1 "k8s.io/api/apps/v1"
31+
corev1 "k8s.io/api/core/v1"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/runtime"
34+
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/utils/pointer"
36+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
37+
38+
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
39+
dptypes "github.com/apecloud/kubeblocks/pkg/dataprotection/types"
40+
)
41+
42+
func TestStatefulSetActionHelpers(t *testing.T) {
43+
action := &StatefulSetAction{Name: "continuous"}
44+
45+
assert.Equal(t, "continuous", action.GetName())
46+
assert.Equal(t, dpv1alpha1.ActionTypeStatefulSet, action.Type())
47+
assert.Equal(t, "60s", action.getIntervalSeconds("@hourly"))
48+
assert.Equal(t, "300s", action.getIntervalSeconds("*/5 * * * *"))
49+
assert.Equal(t, "7200s", action.getIntervalSeconds("CRON_TZ=UTC 0 */2 * * *"))
50+
}
51+
52+
func TestCreateVolumeSnapshotSmallHelpers(t *testing.T) {
53+
pvc := corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: "data-pvc"}}
54+
wrapper := NewPersistentVolumeClaimWrapper(pvc, "data")
55+
assert.Equal(t, "data", wrapper.VolumeName)
56+
assert.Equal(t, "data-pvc", wrapper.PersistentVolumeClaim.Name)
57+
58+
message := "Failed to get snapshot class with error: missing default"
59+
assert.True(t, isVolumeSnapshotConfigError(&vsv1.VolumeSnapshot{Status: &vsv1.VolumeSnapshotStatus{Error: &vsv1.VolumeSnapshotError{Message: &message}}}))
60+
other := "transient api error"
61+
assert.False(t, isVolumeSnapshotConfigError(&vsv1.VolumeSnapshot{Status: &vsv1.VolumeSnapshotStatus{Error: &vsv1.VolumeSnapshotError{Message: &other}}}))
62+
assert.False(t, isVolumeSnapshotConfigError(&vsv1.VolumeSnapshot{}))
63+
}
64+
65+
func TestStatusBuilderFluentFields(t *testing.T) {
66+
start := metav1.NewTime(time.Now())
67+
end := metav1.NewTime(start.Add(time.Hour))
68+
status := newStatusBuilder(&StatefulSetAction{Name: "continuous"}).
69+
phase(dpv1alpha1.ActionPhaseCompleted).
70+
reason("done").
71+
startTimestamp(&start).
72+
completionTimestamp(&end).
73+
totalSize("10Gi").
74+
timeRange(&start, &end).
75+
volumeSnapshots([]dpv1alpha1.VolumeSnapshotStatus{{Name: "snap", VolumeName: "data"}}).
76+
objectRef(&corev1.ObjectReference{Name: "sts"}).
77+
build()
78+
79+
assert.Equal(t, dpv1alpha1.ActionPhaseCompleted, status.Phase)
80+
assert.Equal(t, "done", status.FailureReason)
81+
assert.Equal(t, "10Gi", status.TotalSize)
82+
assert.Equal(t, "snap", status.VolumeSnapshots[0].Name)
83+
assert.Equal(t, "sts", status.ObjectRef.Name)
84+
assert.Equal(t, start, *status.TimeRange.Start)
85+
86+
status = newStatusBuilder(&StatefulSetAction{Name: "continuous"}).withErr(errors.New("failed")).build()
87+
assert.Equal(t, dpv1alpha1.ActionPhaseFailed, status.Phase)
88+
assert.Equal(t, "failed", status.FailureReason)
89+
}
90+
91+
func TestStatefulSetActionExecuteCreatesAndUpdates(t *testing.T) {
92+
scheme := runtime.NewScheme()
93+
assert.NoError(t, corev1.AddToScheme(scheme))
94+
assert.NoError(t, appsv1.AddToScheme(scheme))
95+
assert.NoError(t, dpv1alpha1.AddToScheme(scheme))
96+
97+
backup := &dpv1alpha1.Backup{
98+
ObjectMeta: metav1.ObjectMeta{
99+
Name: "backup",
100+
Namespace: "ns",
101+
Labels: map[string]string{
102+
dptypes.BackupScheduleLabelKey: "schedule",
103+
},
104+
},
105+
Spec: dpv1alpha1.BackupSpec{BackupMethod: "continuous", RetentionPeriod: "2h"},
106+
}
107+
schedule := &dpv1alpha1.BackupSchedule{
108+
ObjectMeta: metav1.ObjectMeta{Name: "schedule", Namespace: "ns"},
109+
Spec: dpv1alpha1.BackupScheduleSpec{Schedules: []dpv1alpha1.SchedulePolicy{{BackupMethod: "continuous", CronExpression: "*/10 * * * *"}}},
110+
}
111+
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(backup, schedule).Build()
112+
action := &StatefulSetAction{
113+
Name: "continuous",
114+
Backup: backup,
115+
ObjectMeta: metav1.ObjectMeta{Name: "backup-sts", Namespace: "ns", Labels: map[string]string{"app": "backup"}},
116+
Replicas: pointer.Int32(1),
117+
PodSpec: &corev1.PodSpec{Containers: []corev1.Container{{Name: "manager", Image: "busybox"}}},
118+
}
119+
120+
status, err := action.Execute(ActionContext{Ctx: context.Background(), Client: cli, Scheme: scheme})
121+
assert.NoError(t, err)
122+
assert.Equal(t, dpv1alpha1.ActionPhaseRunning, status.Phase)
123+
124+
sts := &appsv1.StatefulSet{}
125+
assert.NoError(t, cli.Get(context.Background(), types.NamespacedName{Name: "backup-sts", Namespace: "ns"}, sts))
126+
assert.Equal(t, corev1.RestartPolicyAlways, sts.Spec.Template.Spec.RestartPolicy)
127+
env := sts.Spec.Template.Spec.Containers[0].Env
128+
assert.Contains(t, env, corev1.EnvVar{Name: dptypes.DPArchiveInterval, Value: "600s"})
129+
assert.Contains(t, env, corev1.EnvVar{Name: dptypes.DPContinuousTTLSeconds, Value: "7200"})
130+
131+
sts.Status.AvailableReplicas = 1
132+
assert.NoError(t, cli.Status().Update(context.Background(), sts))
133+
status, err = action.Execute(ActionContext{Ctx: context.Background(), Client: cli, Scheme: scheme})
134+
assert.NoError(t, err)
135+
assert.Equal(t, dpv1alpha1.ActionPhaseRunning, status.Phase)
136+
assert.Equal(t, int32(1), *status.AvailableReplicas)
137+
assert.Equal(t, dpv1alpha1.BackupPhaseRunning, backup.Status.Phase)
138+
}

pkg/dataprotection/backup/deleter_test.go

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

2222
import (
23+
"context"
24+
"testing"
25+
2326
. "github.com/onsi/ginkgo/v2"
2427
. "github.com/onsi/gomega"
2528

29+
"github.com/stretchr/testify/assert"
2630
batchv1 "k8s.io/api/batch/v1"
31+
corev1 "k8s.io/api/core/v1"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/runtime"
34+
"k8s.io/apimachinery/pkg/types"
2735
"k8s.io/utils/pointer"
2836
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2938

3039
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
3140
"github.com/apecloud/kubeblocks/pkg/constant"
3241
ctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil"
42+
dptypes "github.com/apecloud/kubeblocks/pkg/dataprotection/types"
3343
"github.com/apecloud/kubeblocks/pkg/generics"
3444
testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps"
3545
testdp "github.com/apecloud/kubeblocks/pkg/testutil/dataprotection"
3646
viper "github.com/apecloud/kubeblocks/pkg/viperx"
3747
)
3848

49+
func TestDeleterDoPreDeleteActionCreatesAndReusesJob(t *testing.T) {
50+
scheme := runtime.NewScheme()
51+
assert.NoError(t, corev1.AddToScheme(scheme))
52+
assert.NoError(t, batchv1.AddToScheme(scheme))
53+
assert.NoError(t, dpv1alpha1.AddToScheme(scheme))
54+
55+
cli := fake.NewClientBuilder().WithScheme(scheme).Build()
56+
backup := &dpv1alpha1.Backup{
57+
ObjectMeta: metav1.ObjectMeta{Name: "backup", Namespace: "ns", UID: types.UID("backup-uid")},
58+
Status: dpv1alpha1.BackupStatus{BackupMethod: &dpv1alpha1.BackupMethod{Env: []corev1.EnvVar{{Name: "IMAGE_TAG", Value: "1.0"}}}},
59+
}
60+
repo := &dpv1alpha1.BackupRepo{Spec: dpv1alpha1.BackupRepoSpec{}, Status: dpv1alpha1.BackupRepoStatus{BackupPVCName: "repo-pvc"}}
61+
deleter := &Deleter{
62+
RequestCtx: ctrlutil.RequestCtx{Ctx: context.Background()},
63+
Client: cli,
64+
Scheme: scheme,
65+
WorkerServiceAccount: "worker",
66+
actionSet: &dpv1alpha1.ActionSet{Spec: dpv1alpha1.ActionSetSpec{Env: []corev1.EnvVar{{Name: "ACTION_ENV", Value: "set"}}}},
67+
}
68+
69+
job, err := deleter.doPreDeleteAction(backup, repo, &dpv1alpha1.BaseJobActionSpec{Image: "deleter:$(IMAGE_TAG)", Command: []string{"delete"}}, "", "/backup/path")
70+
assert.NoError(t, err)
71+
assert.Empty(t, job.Name)
72+
73+
got := &batchv1.Job{}
74+
assert.NoError(t, cli.Get(context.Background(), BuildDeleteBackupFilesJobKey(backup, true), got))
75+
assert.Contains(t, got.Spec.Template.Spec.Containers[0].Image, "deleter:1.0")
76+
assert.Equal(t, "worker", got.Spec.Template.Spec.ServiceAccountName)
77+
envMap := map[string]string{}
78+
for _, env := range got.Spec.Template.Spec.Containers[0].Env {
79+
envMap[env.Name] = env.Value
80+
}
81+
assert.Equal(t, "/backup/path", envMap[dptypes.DPBackupBasePath])
82+
assert.Equal(t, "set", envMap["ACTION_ENV"])
83+
84+
job, err = deleter.doPreDeleteAction(backup, repo, &dpv1alpha1.BaseJobActionSpec{Image: "deleter:$(IMAGE_TAG)", Command: []string{"delete"}}, "", "/backup/path")
85+
assert.NoError(t, err)
86+
assert.Equal(t, got.Name, job.Name)
87+
}
88+
3989
var _ = Describe("Backup Deleter Test", func() {
4090
const (
4191
backupRepoPVCName = "backup-repo-pvc"

pkg/dataprotection/backup/request_test.go

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

2222
import (
23+
"context"
24+
"testing"
25+
2326
. "github.com/onsi/ginkgo/v2"
2427
. "github.com/onsi/gomega"
2528

29+
"github.com/stretchr/testify/assert"
2630
corev1 "k8s.io/api/core/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/apimachinery/pkg/types"
34+
ctrl "sigs.k8s.io/controller-runtime"
2735
"sigs.k8s.io/controller-runtime/pkg/client"
36+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2837

2938
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
3039
"github.com/apecloud/kubeblocks/pkg/constant"
3140
ctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil"
41+
"github.com/apecloud/kubeblocks/pkg/dataprotection/action"
42+
dptypes "github.com/apecloud/kubeblocks/pkg/dataprotection/types"
3243
"github.com/apecloud/kubeblocks/pkg/dataprotection/utils/boolptr"
3344
"github.com/apecloud/kubeblocks/pkg/generics"
3445
testapps "github.com/apecloud/kubeblocks/pkg/testutil/apps"
3546
testdp "github.com/apecloud/kubeblocks/pkg/testutil/dataprotection"
47+
viper "github.com/apecloud/kubeblocks/pkg/viperx"
3648
)
3749

50+
func newRequestTestFixture(t *testing.T) (*Request, *corev1.Pod) {
51+
scheme := runtime.NewScheme()
52+
assert.NoError(t, corev1.AddToScheme(scheme))
53+
assert.NoError(t, dpv1alpha1.AddToScheme(scheme))
54+
pod := &corev1.Pod{
55+
ObjectMeta: metav1.ObjectMeta{Name: "pod-0", Namespace: "ns", Labels: map[string]string{constant.RoleLabelKey: "leader"}},
56+
Spec: corev1.PodSpec{
57+
NodeName: "node-0",
58+
Containers: []corev1.Container{{
59+
Name: "db",
60+
Env: []corev1.EnvVar{{Name: "EXISTING", Value: "old"}},
61+
Ports: []corev1.ContainerPort{{Name: "mysql", ContainerPort: 3306}},
62+
}},
63+
Volumes: []corev1.Volume{{Name: "data", VolumeSource: corev1.VolumeSource{PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: "data-pvc"}}}},
64+
},
65+
}
66+
req := &Request{
67+
RequestCtx: ctrlutil.RequestCtx{Ctx: context.Background(), Req: ctrl.Request{}},
68+
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
69+
Backup: &dpv1alpha1.Backup{
70+
ObjectMeta: metav1.ObjectMeta{
71+
Name: "backup", Namespace: "ns", UID: types.UID("1234567890abcdef"),
72+
Labels: map[string]string{dptypes.ClusterUIDLabelKey: "uid", constant.AppInstanceLabelKey: "cluster", constant.KBAppComponentLabelKey: "mysql"},
73+
},
74+
Spec: dpv1alpha1.BackupSpec{RetentionPeriod: "7d"},
75+
},
76+
BackupPolicy: &dpv1alpha1.BackupPolicy{Spec: dpv1alpha1.BackupPolicySpec{BackoffLimit: func() *int32 { v := int32(1); return &v }(), PathPrefix: "policy-path"}},
77+
BackupRepo: &dpv1alpha1.BackupRepo{Spec: dpv1alpha1.BackupRepoSpec{PathPrefix: "repo-path"}, Status: dpv1alpha1.BackupRepoStatus{BackupPVCName: "repo-pvc"}},
78+
BackupMethod: &dpv1alpha1.BackupMethod{Env: []corev1.EnvVar{{Name: "EXISTING", Value: "method"}}},
79+
Target: &dpv1alpha1.BackupTarget{
80+
Name: "target",
81+
PodSelector: &dpv1alpha1.PodSelector{Strategy: dpv1alpha1.PodSelectionStrategyAll},
82+
},
83+
TargetPods: []*corev1.Pod{pod},
84+
WorkerServiceAccount: "worker",
85+
}
86+
return req, pod
87+
}
88+
89+
func TestRequestGetBackupType(t *testing.T) {
90+
req, _ := newRequestTestFixture(t)
91+
assert.Empty(t, req.GetBackupType())
92+
93+
req.BackupMethod.SnapshotVolumes = func() *bool { v := true; return &v }()
94+
assert.Equal(t, string(dpv1alpha1.BackupTypeFull), req.GetBackupType())
95+
96+
req.ActionSet = &dpv1alpha1.ActionSet{Spec: dpv1alpha1.ActionSetSpec{BackupType: dpv1alpha1.BackupTypeIncremental}}
97+
assert.Equal(t, string(dpv1alpha1.BackupTypeIncremental), req.GetBackupType())
98+
}
99+
100+
func TestRequestBuildActionBranches(t *testing.T) {
101+
oldNamespace := viper.GetString(constant.CfgKeyCtrlrMgrNS)
102+
oldServiceAccount := viper.GetString(dptypes.CfgKeyExecWorkerServiceAccountName)
103+
defer func() {
104+
viper.Set(constant.CfgKeyCtrlrMgrNS, oldNamespace)
105+
viper.Set(dptypes.CfgKeyExecWorkerServiceAccountName, oldServiceAccount)
106+
}()
107+
viper.Set(constant.CfgKeyCtrlrMgrNS, "kb-system")
108+
viper.Set(dptypes.CfgKeyExecWorkerServiceAccountName, "exec-worker")
109+
110+
req, pod := newRequestTestFixture(t)
111+
_, err := req.buildAction(pod, "invalid", &dpv1alpha1.ActionSpec{})
112+
assert.Error(t, err)
113+
_, err = req.buildAction(pod, "invalid", &dpv1alpha1.ActionSpec{Exec: &dpv1alpha1.ExecActionSpec{}, Job: &dpv1alpha1.JobActionSpec{}})
114+
assert.Error(t, err)
115+
116+
execAction, err := req.buildAction(pod, "exec", &dpv1alpha1.ActionSpec{Exec: &dpv1alpha1.ExecActionSpec{Command: []string{"echo", "ok"}}})
117+
assert.NoError(t, err)
118+
assert.IsType(t, &action.ExecAction{}, execAction)
119+
assert.Equal(t, "exec", execAction.GetName())
120+
121+
jobAction, err := req.buildAction(pod, "job", &dpv1alpha1.ActionSpec{Job: &dpv1alpha1.JobActionSpec{BaseJobActionSpec: dpv1alpha1.BaseJobActionSpec{Image: "busybox:$(EXISTING)", Command: []string{"backup"}}}})
122+
assert.NoError(t, err)
123+
assert.IsType(t, &action.JobAction{}, jobAction)
124+
assert.Equal(t, dpv1alpha1.ActionTypeJob, jobAction.Type())
125+
}
126+
127+
func TestRequestBuildBackupDataActions(t *testing.T) {
128+
req, pod := newRequestTestFixture(t)
129+
req.ActionSet = &dpv1alpha1.ActionSet{Spec: dpv1alpha1.ActionSetSpec{BackupType: dpv1alpha1.BackupTypeFull, Backup: &dpv1alpha1.BackupActionSpec{BackupData: &dpv1alpha1.BackupDataActionSpec{JobActionSpec: dpv1alpha1.JobActionSpec{BaseJobActionSpec: dpv1alpha1.BaseJobActionSpec{Image: "busybox"}}}}}}
130+
backupDataAction, err := req.buildBackupDataAction(pod, "backup-data")
131+
assert.NoError(t, err)
132+
assert.IsType(t, &action.JobAction{}, backupDataAction)
133+
134+
req.ActionSet.Spec.BackupType = dpv1alpha1.BackupTypeContinuous
135+
backupDataAction, err = req.buildBackupDataAction(pod, "continuous")
136+
assert.NoError(t, err)
137+
assert.IsType(t, &action.StatefulSetAction{}, backupDataAction)
138+
assert.Contains(t, req.buildContinuousSyncProgressCommand(), "retryTimes")
139+
140+
req.ActionSet.Spec.BackupType = dpv1alpha1.BackupType("Unknown")
141+
_, err = req.buildBackupDataAction(pod, "unsupported")
142+
assert.Error(t, err)
143+
}
144+
145+
func TestRequestBuildActionsIncludesPreAndPostHooks(t *testing.T) {
146+
req, pod := newRequestTestFixture(t)
147+
req.ActionSet = &dpv1alpha1.ActionSet{Spec: dpv1alpha1.ActionSetSpec{
148+
BackupType: dpv1alpha1.BackupTypeFull,
149+
Backup: &dpv1alpha1.BackupActionSpec{
150+
PreBackup: []dpv1alpha1.ActionSpec{{Exec: &dpv1alpha1.ExecActionSpec{Command: []string{"pre"}}}},
151+
BackupData: &dpv1alpha1.BackupDataActionSpec{JobActionSpec: dpv1alpha1.JobActionSpec{BaseJobActionSpec: dpv1alpha1.BaseJobActionSpec{Image: "busybox", Command: []string{"backup"}}}},
152+
PostBackup: []dpv1alpha1.ActionSpec{{Job: &dpv1alpha1.JobActionSpec{BaseJobActionSpec: dpv1alpha1.BaseJobActionSpec{Image: "busybox", Command: []string{"post"}}}}},
153+
},
154+
}}
155+
156+
actions, err := req.BuildActions()
157+
assert.NoError(t, err)
158+
assert.Len(t, actions[pod.Name], 3)
159+
assert.Equal(t, dpv1alpha1.ActionTypeJob, actions[pod.Name][0].Type())
160+
assert.Equal(t, dpv1alpha1.ActionTypeJob, actions[pod.Name][1].Type())
161+
assert.Equal(t, dpv1alpha1.ActionTypeJob, actions[pod.Name][2].Type())
162+
}
163+
38164
var _ = Describe("Request Test", func() {
39165
buildRequest := func() *Request {
40166
return &Request{

0 commit comments

Comments
 (0)