Skip to content

Commit 305e2ae

Browse files
authored
fix(dataprotection): pass target metadata to post-ready jobs (#10838)
1 parent cdc2d7d commit 305e2ae

6 files changed

Lines changed: 238 additions & 4 deletions

File tree

controllers/dataprotection/restore_controller_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ var _ = Describe("Restore Controller test", func() {
6464

6565
// namespaced
6666
testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.ClusterSignature, true, inNS, ml)
67+
testapps.ClearResources(&testCtx, generics.ComponentSignature, inNS, ml)
6768
testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml)
6869
testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupSignature, true, inNS)
6970

@@ -487,6 +488,10 @@ var _ = Describe("Restore Controller test", func() {
487488
BeforeEach(func() {
488489
By("fake a new cluster")
489490
_ = testdp.NewFakeCluster(&testCtx)
491+
testapps.NewComponentFactory(testCtx.DefaultNamespace,
492+
constant.GenerateClusterComponentName(testdp.ClusterName, testdp.ComponentName), "test-cmpd").
493+
SetServiceVersion("8.0.30").
494+
Create(&testCtx)
490495
})
491496

492497
It("test post ready actions", func() {

pkg/dataprotection/restore/builder.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,23 @@ func (r *restoreJobBuilder) addTargetPodAndCredentialEnv(pod *corev1.Pod,
310310
return r
311311
}
312312

313+
func (r *restoreJobBuilder) overridePostReadyTargetEnv(targetEnv []corev1.EnvVar) *restoreJobBuilder {
314+
if len(targetEnv) == 0 {
315+
return r
316+
}
317+
env := make([]corev1.EnvVar, 0, len(r.env)+len(targetEnv))
318+
for i := range r.env {
319+
if r.env[i].Name == dptypes.DPTargetClusterTopology ||
320+
r.env[i].Name == dptypes.DPTargetServiceVersion {
321+
continue
322+
}
323+
env = append(env, r.env[i])
324+
}
325+
env = append(env, targetEnv...)
326+
r.env = env
327+
return r
328+
}
329+
313330
// builderRestoreJobName builds restore job name.
314331
func (r *restoreJobBuilder) builderRestoreJobName(jobIndex int) string {
315332
jobName := fmt.Sprintf("restore-%s-%s-%s-%d", strings.ToLower(string(r.stage)), r.restore.UID[:8], r.backupSet.Backup.Name, jobIndex)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 restore
21+
22+
import (
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
corev1 "k8s.io/api/core/v1"
27+
28+
dptypes "github.com/apecloud/kubeblocks/pkg/dataprotection/types"
29+
)
30+
31+
func TestRestoreJobBuilderOverridePostReadyTargetEnv(t *testing.T) {
32+
builder := &restoreJobBuilder{env: []corev1.EnvVar{
33+
{Name: "KEEP", Value: "kept"},
34+
{Name: dptypes.DPTargetClusterTopology, Value: "restore-value"},
35+
{Name: dptypes.DPTargetClusterTopology, Value: "pod-value"},
36+
{Name: dptypes.DPTargetServiceVersion, Value: "spoofed-version"},
37+
}}
38+
builder.overridePostReadyTargetEnv([]corev1.EnvVar{
39+
{Name: dptypes.DPTargetClusterTopology, Value: "shared-nothing"},
40+
{Name: dptypes.DPTargetServiceVersion, Value: "3.4.0"},
41+
})
42+
43+
values := func(name string) []string {
44+
var result []string
45+
for i := range builder.env {
46+
if builder.env[i].Name == name {
47+
result = append(result, builder.env[i].Value)
48+
}
49+
}
50+
return result
51+
}
52+
require.Equal(t, []string{"kept"}, values("KEEP"))
53+
require.Equal(t, []string{"shared-nothing"}, values(dptypes.DPTargetClusterTopology))
54+
require.Equal(t, []string{"3.4.0"}, values(dptypes.DPTargetServiceVersion))
55+
}

pkg/dataprotection/restore/manager.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"sigs.k8s.io/controller-runtime/pkg/client"
3737
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3838

39+
appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1"
3940
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
4041
"github.com/apecloud/kubeblocks/pkg/constant"
4142
"github.com/apecloud/kubeblocks/pkg/controller/instanceset"
@@ -661,6 +662,50 @@ func (r *RestoreManager) isJobForRestoreAction(job *batchv1.Job) bool {
661662
return restoreNamespace == "" || restoreNamespace == r.Restore.Namespace
662663
}
663664

665+
func (r *RestoreManager) postReadyTargetEnv(reqCtx intctrlutil.RequestCtx, cli client.Client, pod *corev1.Pod) ([]corev1.EnvVar, error) {
666+
clusterName := pod.Labels[constant.AppInstanceLabelKey]
667+
componentName := pod.Labels[constant.KBAppComponentLabelKey]
668+
if clusterName == "" || componentName == "" {
669+
return nil, nil
670+
}
671+
cluster := &appsv1.Cluster{}
672+
if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Namespace: pod.Namespace, Name: clusterName}, cluster); err != nil {
673+
return nil, err
674+
}
675+
component := &appsv1.Component{}
676+
componentKey := types.NamespacedName{
677+
Namespace: pod.Namespace,
678+
Name: constant.GenerateClusterComponentName(clusterName, componentName),
679+
}
680+
if err := cli.Get(reqCtx.Ctx, componentKey, component); err != nil {
681+
return nil, err
682+
}
683+
684+
serviceVersion := component.Spec.ServiceVersion
685+
if templateName := pod.Labels[constant.KBAppInstanceTemplateLabelKey]; templateName != "" {
686+
found := false
687+
for i := range component.Spec.Instances {
688+
instance := component.Spec.Instances[i]
689+
if instance.Name != templateName {
690+
continue
691+
}
692+
found = true
693+
if instance.ServiceVersion != "" {
694+
serviceVersion = instance.ServiceVersion
695+
}
696+
break
697+
}
698+
if !found {
699+
return nil, intctrlutil.NewFatalError(fmt.Sprintf(
700+
"target Pod %s/%s references unknown instance template %q", pod.Namespace, pod.Name, templateName))
701+
}
702+
}
703+
return []corev1.EnvVar{
704+
{Name: dptypes.DPTargetClusterTopology, Value: cluster.Spec.Topology},
705+
{Name: dptypes.DPTargetServiceVersion, Value: serviceVersion},
706+
}, nil
707+
}
708+
664709
// BuildPostReadyActionJobs builds the post ready jobs.
665710
func (r *RestoreManager) BuildPostReadyActionJobs(reqCtx intctrlutil.RequestCtx, cli client.Client, backupSet BackupActionSet, target *dpv1alpha1.BackupStatusTarget, step int) ([]*batchv1.Job, error) {
666711
readyConfig := r.Restore.Spec.ReadyConfig
@@ -705,7 +750,7 @@ func (r *RestoreManager) BuildPostReadyActionJobs(reqCtx intctrlutil.RequestCtx,
705750
return nil, err
706751
}
707752
sort.Sort(intctrlutil.ByPodName(targetPodList.Items))
708-
buildJob := func(targetPod *corev1.Pod, sourceTargetPodName string, index int) *batchv1.Job {
753+
buildJob := func(targetPod *corev1.Pod, sourceTargetPodName string, index int) (*batchv1.Job, error) {
709754
if boolptr.IsSetToTrue(actionSpec.Job.RunOnTargetPodNode) {
710755
jobBuilder.resetSpecificVolumesAndMounts()
711756
jobBuilder.setNodeNameToNodeSelector(targetPod.Spec.NodeName)
@@ -719,15 +764,20 @@ func (r *RestoreManager) BuildPostReadyActionJobs(reqCtx intctrlutil.RequestCtx,
719764
}
720765
}
721766
}
767+
targetEnv, err := r.postReadyTargetEnv(reqCtx, cli, targetPod)
768+
if err != nil {
769+
return nil, err
770+
}
722771
return jobBuilder.setImage(actionSpec.Job.Image).
723772
setJobName(buildJobName(index)).
724773
addCommonEnv(sourceTargetPodName).
725774
attachBackupRepo().
726775
setCommand(actionSpec.Job.Command).
727776
setToleration(targetPod.Spec.Tolerations).
728777
addTargetPodAndCredentialEnv(targetPod, readyConfig.ConnectionCredential, &target.BackupTarget).
778+
overridePostReadyTargetEnv(targetEnv).
729779
setServiceAccount(r.WorkerServiceAccount).
730-
build()
780+
build(), nil
731781
}
732782

733783
if podSelector.Strategy == dpv1alpha1.PodSelectionStrategyAny {
@@ -747,7 +797,11 @@ func (r *RestoreManager) BuildPostReadyActionJobs(reqCtx intctrlutil.RequestCtx,
747797
// no need to recover the volume when the pod selection policy is 'All' and sourceTargetPodName is not found.
748798
continue
749799
}
750-
jobs = append(jobs, buildJob(&targetPodList.Items[i], sourceTargetPodName, i))
800+
job, err := buildJob(&targetPodList.Items[i], sourceTargetPodName, i)
801+
if err != nil {
802+
return nil, err
803+
}
804+
jobs = append(jobs, job)
751805
}
752806
return jobs, nil
753807
}

pkg/dataprotection/restore/manager_test.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,27 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
package restore
2121

2222
import (
23+
"context"
2324
"fmt"
2425
"strconv"
2526
"strings"
27+
"testing"
2628
"time"
2729

2830
. "github.com/onsi/ginkgo/v2"
2931
. "github.com/onsi/gomega"
32+
"github.com/stretchr/testify/require"
3033
batchv1 "k8s.io/api/batch/v1"
3134
corev1 "k8s.io/api/core/v1"
3235
apierrors "k8s.io/apimachinery/pkg/api/errors"
3336
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
"k8s.io/apimachinery/pkg/runtime"
3438
"k8s.io/apimachinery/pkg/types"
3539
ctrl "sigs.k8s.io/controller-runtime"
3640
"sigs.k8s.io/controller-runtime/pkg/client"
41+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3742

43+
appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1"
3844
dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
3945
"github.com/apecloud/kubeblocks/pkg/constant"
4046
intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil"
@@ -47,6 +53,82 @@ import (
4753
viper "github.com/apecloud/kubeblocks/pkg/viperx"
4854
)
4955

56+
func postReadyJobEnvValues(job *batchv1.Job, name string) []string {
57+
var values []string
58+
for i := range job.Spec.Template.Spec.Containers[0].Env {
59+
env := job.Spec.Template.Spec.Containers[0].Env[i]
60+
if env.Name == name {
61+
values = append(values, env.Value)
62+
}
63+
}
64+
return values
65+
}
66+
67+
func TestRestoreManagerPostReadyTargetEnv(t *testing.T) {
68+
const (
69+
namespace = "default"
70+
clusterName = "target"
71+
componentName = "mysql"
72+
)
73+
scheme := runtime.NewScheme()
74+
require.NoError(t, appsv1.AddToScheme(scheme))
75+
cluster := &appsv1.Cluster{
76+
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: clusterName},
77+
Spec: appsv1.ClusterSpec{Topology: "shared-nothing"},
78+
}
79+
component := &appsv1.Component{
80+
ObjectMeta: metav1.ObjectMeta{
81+
Namespace: namespace,
82+
Name: constant.GenerateClusterComponentName(clusterName, componentName),
83+
},
84+
Spec: appsv1.ComponentSpec{
85+
ServiceVersion: "3.3.2",
86+
Instances: []appsv1.InstanceTemplate{
87+
{Name: "canary", ServiceVersion: "3.4.0"},
88+
{Name: "inherited"},
89+
},
90+
},
91+
}
92+
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster, component).Build()
93+
manager := &RestoreManager{Restore: &dpv1alpha1.Restore{}}
94+
reqCtx := intctrlutil.RequestCtx{Ctx: context.Background()}
95+
96+
tests := []struct {
97+
name string
98+
instanceTemplate string
99+
wantVersion string
100+
}{
101+
{name: "component default", wantVersion: "3.3.2"},
102+
{name: "instance override", instanceTemplate: "canary", wantVersion: "3.4.0"},
103+
{name: "instance inherits component", instanceTemplate: "inherited", wantVersion: "3.3.2"},
104+
}
105+
for _, tt := range tests {
106+
t.Run(tt.name, func(t *testing.T) {
107+
pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{
108+
Namespace: namespace,
109+
Name: "target-mysql-0",
110+
Labels: map[string]string{
111+
constant.AppInstanceLabelKey: clusterName,
112+
constant.KBAppComponentLabelKey: componentName,
113+
constant.KBAppInstanceTemplateLabelKey: tt.instanceTemplate,
114+
},
115+
}}
116+
117+
env, err := manager.postReadyTargetEnv(reqCtx, cli, pod)
118+
119+
require.NoError(t, err)
120+
require.Equal(t, []corev1.EnvVar{
121+
{Name: dptypes.DPTargetClusterTopology, Value: "shared-nothing"},
122+
{Name: dptypes.DPTargetServiceVersion, Value: tt.wantVersion},
123+
}, env)
124+
})
125+
}
126+
127+
env, err := manager.postReadyTargetEnv(reqCtx, cli, &corev1.Pod{})
128+
require.NoError(t, err)
129+
require.Empty(t, env, "non-KubeBlocks target Pods must keep their existing behavior")
130+
}
131+
50132
var _ = Describe("RestoreManager Test", func() {
51133

52134
cleanEnv := func() {
@@ -58,6 +140,7 @@ var _ = Describe("RestoreManager Test", func() {
58140
// namespaced
59141
testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml)
60142
testapps.ClearResources(&testCtx, generics.ClusterSignature, inNS, ml)
143+
testapps.ClearResources(&testCtx, generics.ComponentSignature, inNS, ml)
61144
testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.BackupSignature, true, inNS)
62145

63146
// wait all backup to be deleted, otherwise the controller maybe create
@@ -538,9 +621,23 @@ var _ = Describe("RestoreManager Test", func() {
538621
restoreMGR, backupSet := initResources(reqCtx, 0, false, func(f *testdp.MockRestoreFactory) {
539622
f.SetConnectCredential(testdp.ClusterName).SetJobActionConfig(matchLabels).SetExecActionConfig(matchLabels)
540623
})
624+
restoreMGR.Restore.Spec.Env = append(restoreMGR.Restore.Spec.Env,
625+
corev1.EnvVar{Name: dptypes.DPTargetClusterTopology, Value: "spoofed-topology"},
626+
corev1.EnvVar{Name: dptypes.DPTargetServiceVersion, Value: "spoofed-version"})
541627

542628
By("create cluster to restore")
543-
testdp.NewFakeCluster(&testCtx)
629+
clusterInfo := testdp.NewFakeCluster(&testCtx)
630+
Expect(testapps.ChangeObj(&testCtx, clusterInfo.Cluster, func(cluster *appsv1.Cluster) {
631+
cluster.Spec.Topology = "shared-nothing"
632+
})).Should(Succeed())
633+
testapps.NewComponentFactory(testCtx.DefaultNamespace,
634+
constant.GenerateClusterComponentName(testdp.ClusterName, testdp.ComponentName), "test-cmpd").
635+
SetServiceVersion("3.3.2").
636+
AddInstances(appsv1.InstanceTemplate{Name: "canary", ServiceVersion: "3.4.0"}).
637+
Create(&testCtx)
638+
Expect(testapps.ChangeObj(&testCtx, clusterInfo.TargetPod, func(pod *corev1.Pod) {
639+
pod.Labels[constant.KBAppInstanceTemplateLabelKey] = "canary"
640+
})).Should(Succeed())
544641

545642
By("test with execAction and expect for creating 2 exec job")
546643
target := utils.GetBackupStatusTarget(backupSet.Backup, restoreMGR.Restore.Spec.Backup.SourceTargetName)
@@ -558,6 +655,8 @@ var _ = Describe("RestoreManager Test", func() {
558655
Expect(err).ShouldNot(HaveOccurred())
559656
// count of job should equal to 1
560657
Expect(len(jobs)).Should(Equal(1))
658+
Expect(postReadyJobEnvValues(jobs[0], dptypes.DPTargetClusterTopology)).Should(Equal([]string{"shared-nothing"}))
659+
Expect(postReadyJobEnvValues(jobs[0], dptypes.DPTargetServiceVersion)).Should(Equal([]string{"3.4.0"}))
561660
// test timeZone transform
562661
var backupStopTimeEnv string
563662
for _, v := range jobs[0].Spec.Template.Spec.Containers[0].Env {

pkg/dataprotection/types/constant.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ const (
120120
DPTargetPodName = "DP_TARGET_POD_NAME"
121121
// DPTargetPodRole the target pod role
122122
DPTargetPodRole = "DP_TARGET_POD_ROLE"
123+
// DPTargetClusterTopology is the topology of the restore target Cluster.
124+
DPTargetClusterTopology = "DP_TARGET_CLUSTER_TOPOLOGY"
125+
// DPTargetServiceVersion is the expected serviceVersion of the restore target instance.
126+
DPTargetServiceVersion = "DP_TARGET_SERVICE_VERSION"
123127
// DPBackupBasePath the base path for backup data in the storage
124128
// In a backup action pod, it equals ${DP_BACKUP_ROOT_PATH}/${DP_BACKUP_NAME}/${DP_TARGET_RELATIVE_PATH}
125129
DPBackupBasePath = "DP_BACKUP_BASE_PATH"

0 commit comments

Comments
 (0)