From 47e57d6406ac22a19aebe66ec3ebd8bd4e483533 Mon Sep 17 00:00:00 2001 From: Wei Cao Date: Tue, 7 Jul 2026 02:23:37 +0800 Subject: [PATCH 1/4] fix(operations): use resolved component for switchover runtime --- pkg/operations/ops_runtime.go | 16 ++-------------- pkg/operations/switchover.go | 2 +- pkg/operations/switchover_test.go | 5 +++++ pkg/operations/type.go | 3 ++- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/pkg/operations/ops_runtime.go b/pkg/operations/ops_runtime.go index 75200304564..ee7683dbf6e 100644 --- a/pkg/operations/ops_runtime.go +++ b/pkg/operations/ops_runtime.go @@ -191,27 +191,15 @@ func (r *opsRuntime) GenerateTemplateInstanceNames(clusterName, compName, templa return instanceset.GenerateInstanceNamesFromTemplate(workloadName, templateName, replicas, offlineInstances, ordinalList) } -func (r *opsRuntime) Switchover(ctx context.Context, namespace, clusterName, compName, instanceName, candidateName string) error { - synthesizedComp, err := r.buildSynthesizedCompByCompName(ctx, r.cli, namespace, clusterName, compName) - if err != nil { - return err - } +func (r *opsRuntime) Switchover(ctx context.Context, synthesizedComp *component.SynthesizedComponent, instanceName, candidateName string) error { switchover := &opsv1alpha1.Switchover{ - ComponentName: compName, + ComponentName: synthesizedComp.Name, InstanceName: instanceName, CandidateName: candidateName, } return r.doSwitchover(ctx, r.cli, synthesizedComp, switchover) } -func (r *opsRuntime) buildSynthesizedCompByCompName(ctx context.Context, cli client.Client, namespace, clusterName, compName string) (*component.SynthesizedComponent, error) { - compObj, compDefObj, err := component.GetCompNCompDefByName(ctx, cli, namespace, constant.GenerateClusterComponentName(clusterName, compName)) - if err != nil { - return nil, err - } - return component.BuildSynthesizedComponent(ctx, cli, compDefObj, compObj) -} - // We consider a switchover action succeeds if the action returns without error. // We don't need to know if a switchover is actually executed. func (r *opsRuntime) doSwitchover(ctx context.Context, cli client.Reader, synthesizedComp *component.SynthesizedComponent, diff --git a/pkg/operations/switchover.go b/pkg/operations/switchover.go index 1584b4cae36..a5947c01efb 100644 --- a/pkg/operations/switchover.go +++ b/pkg/operations/switchover.go @@ -229,7 +229,7 @@ func handleSwitchover(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes * switch progressDetail.Status { case opsv1alpha1.PendingProgressStatus: - if err = runtime.Switchover(reqCtx.Ctx, synthesizedComp.Namespace, synthesizedComp.ClusterName, synthesizedComp.Name, switchover.InstanceName, switchover.CandidateName); err != nil { + if err = runtime.Switchover(reqCtx.Ctx, synthesizedComp, switchover.InstanceName, switchover.CandidateName); err != nil { progressDetail.Status = opsv1alpha1.FailedProgressStatus progressDetail.Message = fmt.Sprintf("component %s %s", compName, err.Error()) } else { diff --git a/pkg/operations/switchover_test.go b/pkg/operations/switchover_test.go index 6accd930265..7dd17aaec60 100644 --- a/pkg/operations/switchover_test.go +++ b/pkg/operations/switchover_test.go @@ -660,10 +660,12 @@ var _ = Describe("", func() { ops := testops.NewOpsRequestObj("ops-switchover-"+testCtx.GetRandomStr(), testCtx.DefaultNamespace, shardingCluster.Name, opsv1alpha1.SwitchoverType) instanceName := fmt.Sprintf("%s-%d", its.Name, 1) + candidateName := fmt.Sprintf("%s-%d", its.Name, 0) ops.Spec.SwitchoverList = []opsv1alpha1.Switchover{ { ComponentName: shardingName, InstanceName: instanceName, + CandidateName: candidateName, }, } opsRes.OpsRequest = testops.CreateOpsRequest(ctx, testCtx, ops) @@ -685,6 +687,9 @@ var _ = Describe("", func() { recorder.Action(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(ctx context.Context, req kbagentproto.ActionRequest) (kbagentproto.ActionResponse, error) { GinkgoWriter.Printf("ActionRequest: %#v\n", req) Expect(req.Parameters["KB_SWITCHOVER_CURRENT_NAME"]).Should(Equal(instanceName)) + Expect(req.Parameters["KB_SWITCHOVER_CANDIDATE_NAME"]).Should(Equal(candidateName)) + Expect(req.Parameters["KB_SWITCHOVER_CURRENT_FQDN"]).Should(ContainSubstring(shardingCluster.Name + "-" + shardComponentName + "-headless")) + Expect(req.Parameters["KB_SWITCHOVER_CANDIDATE_FQDN"]).Should(ContainSubstring(shardingCluster.Name + "-" + shardComponentName + "-headless")) rsp := kbagentproto.ActionResponse{Message: "mock success"} return rsp, nil }) diff --git a/pkg/operations/type.go b/pkg/operations/type.go index 6ad37987028..86f6a98e793 100644 --- a/pkg/operations/type.go +++ b/pkg/operations/type.go @@ -33,6 +33,7 @@ import ( appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" + "github.com/apecloud/kubeblocks/pkg/controller/component" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) @@ -129,7 +130,7 @@ type OpsRuntime interface { ListInstances(namespace, clusterName, compName string) ([]Instance, error) GenerateInstanceNameSet(clusterName, compName string, compReplicas int32, instances []appsv1.InstanceTemplate, offlineInstances []string) (map[string]string, error) GenerateTemplateInstanceNames(clusterName, compName, templateName string, replicas int32, offlineInstances []string, ordinals appsv1.Ordinals) ([]string, error) - Switchover(ctx context.Context, namespace, clusterName, compName, instanceName, candidateName string) error + Switchover(ctx context.Context, synthesizedComp *component.SynthesizedComponent, instanceName, candidateName string) error } type Workload interface { From 20ffba806f4e7250dba9ff45443c0ee035ea419c Mon Sep 17 00:00:00 2001 From: Wei Cao Date: Tue, 14 Jul 2026 15:55:51 +0800 Subject: [PATCH 2/4] fix(operations): narrow switchover runtime context --- pkg/operations/ops_runtime.go | 24 +++++++++--------------- pkg/operations/switchover.go | 9 ++++++++- pkg/operations/type.go | 14 ++++++++++++-- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/pkg/operations/ops_runtime.go b/pkg/operations/ops_runtime.go index ee7683dbf6e..ef573415c9f 100644 --- a/pkg/operations/ops_runtime.go +++ b/pkg/operations/ops_runtime.go @@ -33,7 +33,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" - opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" "github.com/apecloud/kubeblocks/pkg/constant" "github.com/apecloud/kubeblocks/pkg/controller/component" @@ -191,27 +190,22 @@ func (r *opsRuntime) GenerateTemplateInstanceNames(clusterName, compName, templa return instanceset.GenerateInstanceNamesFromTemplate(workloadName, templateName, replicas, offlineInstances, ordinalList) } -func (r *opsRuntime) Switchover(ctx context.Context, synthesizedComp *component.SynthesizedComponent, instanceName, candidateName string) error { - switchover := &opsv1alpha1.Switchover{ - ComponentName: synthesizedComp.Name, - InstanceName: instanceName, - CandidateName: candidateName, - } - return r.doSwitchover(ctx, r.cli, synthesizedComp, switchover) +func (r *opsRuntime) Switchover(ctx context.Context, actionCtx SwitchoverActionContext, instanceName, candidateName string) error { + return r.doSwitchover(ctx, r.cli, actionCtx, instanceName, candidateName) } // We consider a switchover action succeeds if the action returns without error. // We don't need to know if a switchover is actually executed. -func (r *opsRuntime) doSwitchover(ctx context.Context, cli client.Reader, synthesizedComp *component.SynthesizedComponent, - switchover *opsv1alpha1.Switchover) error { - pods, err := component.ListOwnedPods(r.dataContext(), cli, synthesizedComp.Namespace, synthesizedComp.ClusterName, synthesizedComp.Name, r.dataListOpts...) +func (r *opsRuntime) doSwitchover(ctx context.Context, cli client.Reader, actionCtx SwitchoverActionContext, + instanceName, candidateName string) error { + pods, err := component.ListOwnedPods(r.dataContext(), cli, actionCtx.Namespace, actionCtx.ClusterName, actionCtx.ComponentName, r.dataListOpts...) if err != nil { return err } pod := &corev1.Pod{} for _, p := range pods { - if p.Name == switchover.InstanceName { + if p.Name == instanceName { pod = p break } @@ -232,8 +226,8 @@ func (r *opsRuntime) doSwitchover(ctx context.Context, cli client.Reader, synthe } } - lfa, err := lifecycle.New(synthesizedComp.Namespace, synthesizedComp.ClusterName, synthesizedComp.Name, - synthesizedComp.LifecycleActions.ComponentLifecycleActions, synthesizedComp.TemplateVars, pod, pods) + lfa, err := lifecycle.New(actionCtx.Namespace, actionCtx.ClusterName, actionCtx.ComponentName, + actionCtx.LifecycleActions, actionCtx.TemplateVars, pod, pods) if err != nil { return err } @@ -241,7 +235,7 @@ func (r *opsRuntime) doSwitchover(ctx context.Context, cli client.Reader, synthe // NOTE: switchover is a blocking action currently. May change to non-blocking for better performance. // Lifecycle preconditions still use the lifecycle reader contract as-is. If a multi-cluster // action needs data-plane runtime readiness checks, model that explicitly in the lifecycle API. - return lfa.Switchover(ctx, cli, nil, switchover.CandidateName) + return lfa.Switchover(ctx, cli, nil, candidateName) } func (r *opsRuntime) buildInstances(namespace, clusterName, compName string, pods []*corev1.Pod) ([]Instance, error) { diff --git a/pkg/operations/switchover.go b/pkg/operations/switchover.go index a5947c01efb..57ea29f39b9 100644 --- a/pkg/operations/switchover.go +++ b/pkg/operations/switchover.go @@ -229,7 +229,14 @@ func handleSwitchover(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes * switch progressDetail.Status { case opsv1alpha1.PendingProgressStatus: - if err = runtime.Switchover(reqCtx.Ctx, synthesizedComp, switchover.InstanceName, switchover.CandidateName); err != nil { + actionCtx := SwitchoverActionContext{ + Namespace: synthesizedComp.Namespace, + ClusterName: synthesizedComp.ClusterName, + ComponentName: synthesizedComp.Name, + LifecycleActions: synthesizedComp.LifecycleActions.ComponentLifecycleActions, + TemplateVars: synthesizedComp.TemplateVars, + } + if err = runtime.Switchover(reqCtx.Ctx, actionCtx, switchover.InstanceName, switchover.CandidateName); err != nil { progressDetail.Status = opsv1alpha1.FailedProgressStatus progressDetail.Message = fmt.Sprintf("component %s %s", compName, err.Error()) } else { diff --git a/pkg/operations/type.go b/pkg/operations/type.go index 86f6a98e793..3397a8b746e 100644 --- a/pkg/operations/type.go +++ b/pkg/operations/type.go @@ -33,7 +33,6 @@ import ( appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" opsv1alpha1 "github.com/apecloud/kubeblocks/apis/operations/v1alpha1" - "github.com/apecloud/kubeblocks/pkg/controller/component" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) @@ -130,7 +129,18 @@ type OpsRuntime interface { ListInstances(namespace, clusterName, compName string) ([]Instance, error) GenerateInstanceNameSet(clusterName, compName string, compReplicas int32, instances []appsv1.InstanceTemplate, offlineInstances []string) (map[string]string, error) GenerateTemplateInstanceNames(clusterName, compName, templateName string, replicas int32, offlineInstances []string, ordinals appsv1.Ordinals) ([]string, error) - Switchover(ctx context.Context, synthesizedComp *component.SynthesizedComponent, instanceName, candidateName string) error + Switchover(ctx context.Context, actionCtx SwitchoverActionContext, instanceName, candidateName string) error +} + +// SwitchoverActionContext is the resolved, stable input needed to dispatch a +// switchover lifecycle action. It deliberately excludes the controller's +// synthesized component implementation details. +type SwitchoverActionContext struct { + Namespace string + ClusterName string + ComponentName string + LifecycleActions *appsv1.ComponentLifecycleActions + TemplateVars map[string]string } type Workload interface { From 95b49721ff231d327f588b6ce7b9843972b96da2 Mon Sep 17 00:00:00 2001 From: Wei Cao Date: Mon, 20 Jul 2026 17:53:41 +0800 Subject: [PATCH 3/4] fix switchover runtime parameter precedence --- pkg/controller/lifecycle/kbagent.go | 18 ++++++-- .../lifecycle/kbagent_parameters_test.go | 44 ++++++++++++++++++- pkg/controller/lifecycle/lfa_member.go | 2 + pkg/operations/ops_runtime.go | 10 ++--- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/pkg/controller/lifecycle/kbagent.go b/pkg/controller/lifecycle/kbagent.go index c2a55a29f59..251f3b7f546 100644 --- a/pkg/controller/lifecycle/kbagent.go +++ b/pkg/controller/lifecycle/kbagent.go @@ -47,6 +47,12 @@ type lifecycleAction interface { parameters(ctx context.Context, cli client.Reader) (map[string]string, error) } +// authoritativeActionParameters marks action parameters that are defined by +// the runtime contract and therefore cannot be overridden by template vars. +type authoritativeActionParameters interface { + authoritativeActionParameters() +} + type kbagent struct { namespace string clusterName string @@ -302,11 +308,17 @@ func (a *kbagent) parameters(ctx context.Context, cli client.Reader, lfa lifecyc return nil, err } - for k, v := range sys { - // template vars take precedence - if _, ok := m[k]; !ok { + if _, authoritative := lfa.(authoritativeActionParameters); authoritative { + for k, v := range sys { m[k] = v } + } else { + for k, v := range sys { + // template vars take precedence for existing lifecycle actions. + if _, ok := m[k]; !ok { + m[k] = v + } + } } addShellSafeParameterAliases(m) return m, nil diff --git a/pkg/controller/lifecycle/kbagent_parameters_test.go b/pkg/controller/lifecycle/kbagent_parameters_test.go index 201d470767b..977df29ed9e 100644 --- a/pkg/controller/lifecycle/kbagent_parameters_test.go +++ b/pkg/controller/lifecycle/kbagent_parameters_test.go @@ -19,7 +19,10 @@ along with this program. If not, see . package lifecycle -import "testing" +import ( + "context" + "testing" +) func TestAddShellSafeParameterAliasesPreservesRawParameter(t *testing.T) { parameters := map[string]string{ @@ -75,3 +78,42 @@ func TestShellSafeParameterAlias(t *testing.T) { } } } + +func TestSwitchoverRuntimeParametersOverrideTemplateVars(t *testing.T) { + action := &kbagent{ + templateVars: map[string]string{ + switchoverCurrentName: "spoofed-current", + switchoverCurrentFQDN: "spoofed-current-fqdn", + switchoverCandidateName: "spoofed-candidate", + switchoverCandidateFQDN: "spoofed-candidate-fqdn", + switchoverRole: "spoofed-role", + "USER_DEFINED": "preserved", + }, + } + runtime := &switchover{ + namespace: "default", + clusterName: "demo", + compName: "redis", + role: "primary", + currentPod: "demo-redis-0", + candidatePod: "demo-redis-1", + } + + parameters, err := action.parameters(context.Background(), nil, runtime) + if err != nil { + t.Fatalf("parameters returned error: %v", err) + } + want := map[string]string{ + switchoverCurrentName: "demo-redis-0", + switchoverCurrentFQDN: "demo-redis-0.demo-redis-headless.default.svc.cluster.local", + switchoverCandidateName: "demo-redis-1", + switchoverCandidateFQDN: "demo-redis-1.demo-redis-headless.default.svc.cluster.local", + switchoverRole: "primary", + "USER_DEFINED": "preserved", + } + for key, expected := range want { + if got := parameters[key]; got != expected { + t.Fatalf("parameter %s=%q, want %q", key, got, expected) + } + } +} diff --git a/pkg/controller/lifecycle/lfa_member.go b/pkg/controller/lifecycle/lfa_member.go index 273ebb12e8a..038b66c39dc 100644 --- a/pkg/controller/lifecycle/lfa_member.go +++ b/pkg/controller/lifecycle/lfa_member.go @@ -68,6 +68,8 @@ func (a *switchover) name() string { return "switchover" } +func (a *switchover) authoritativeActionParameters() {} + func (a *switchover) parameters(ctx context.Context, cli client.Reader) (map[string]string, error) { // refer to ComponentLifecycleActions.Switchover's documentation for explanation of each variable. m := make(map[string]string) diff --git a/pkg/operations/ops_runtime.go b/pkg/operations/ops_runtime.go index ef573415c9f..db5fd15642c 100644 --- a/pkg/operations/ops_runtime.go +++ b/pkg/operations/ops_runtime.go @@ -211,18 +211,18 @@ func (r *opsRuntime) doSwitchover(ctx context.Context, cli client.Reader, action } } if pod.Name == "" { - return intctrlutil.NewFatalError(fmt.Sprintf(`instance "%s" not found`, switchover.InstanceName)) + return intctrlutil.NewFatalError(fmt.Sprintf(`instance "%s" not found`, instanceName)) } - if switchover.CandidateName != "" { - candidate, err := r.GetInstance(synthesizedComp.Namespace, synthesizedComp.ClusterName, synthesizedComp.Name, switchover.CandidateName) + if candidateName != "" { + candidate, err := r.GetInstance(actionCtx.Namespace, actionCtx.ClusterName, actionCtx.ComponentName, candidateName) if err != nil { if apierrors.IsNotFound(err) { - return intctrlutil.NewFatalError(fmt.Sprintf(`candidate instance "%s" not found`, switchover.CandidateName)) + return intctrlutil.NewFatalError(fmt.Sprintf(`candidate instance "%s" not found`, candidateName)) } return err } if !candidate.HasPod() { - return intctrlutil.NewFatalError(fmt.Sprintf(`candidate instance "%s" not found`, switchover.CandidateName)) + return intctrlutil.NewFatalError(fmt.Sprintf(`candidate instance "%s" not found`, candidateName)) } } From d46081b83dced6a0b3a677eb3bbe7010b112f1b9 Mon Sep 17 00:00:00 2001 From: weicao Date: Mon, 20 Jul 2026 18:11:22 +0800 Subject: [PATCH 4/4] fix: preserve empty switchover candidate parameters --- .../lifecycle/kbagent_parameters_test.go | 30 +++++++++++++++++++ pkg/controller/lifecycle/lfa_member.go | 5 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/pkg/controller/lifecycle/kbagent_parameters_test.go b/pkg/controller/lifecycle/kbagent_parameters_test.go index 977df29ed9e..123f4d6730f 100644 --- a/pkg/controller/lifecycle/kbagent_parameters_test.go +++ b/pkg/controller/lifecycle/kbagent_parameters_test.go @@ -117,3 +117,33 @@ func TestSwitchoverRuntimeParametersOverrideTemplateVars(t *testing.T) { } } } + +func TestSwitchoverEmptyCandidateOverridesTemplateVars(t *testing.T) { + action := &kbagent{ + templateVars: map[string]string{ + switchoverCandidateName: "spoofed-candidate", + switchoverCandidateFQDN: "spoofed-candidate-fqdn", + }, + } + runtime := &switchover{ + namespace: "default", + clusterName: "demo", + compName: "redis", + role: "primary", + currentPod: "demo-redis-0", + } + + parameters, err := action.parameters(context.Background(), nil, runtime) + if err != nil { + t.Fatalf("parameters returned error: %v", err) + } + for _, key := range []string{switchoverCandidateName, switchoverCandidateFQDN} { + value, ok := parameters[key] + if !ok { + t.Fatalf("authoritative empty parameter %s is missing", key) + } + if value != "" { + t.Fatalf("parameter %s=%q, want authoritative empty value", key, value) + } + } +} diff --git a/pkg/controller/lifecycle/lfa_member.go b/pkg/controller/lifecycle/lfa_member.go index 038b66c39dc..9ce8bcf53b6 100644 --- a/pkg/controller/lifecycle/lfa_member.go +++ b/pkg/controller/lifecycle/lfa_member.go @@ -72,7 +72,10 @@ func (a *switchover) authoritativeActionParameters() {} func (a *switchover) parameters(ctx context.Context, cli client.Reader) (map[string]string, error) { // refer to ComponentLifecycleActions.Switchover's documentation for explanation of each variable. - m := make(map[string]string) + m := map[string]string{ + switchoverCandidateName: "", + switchoverCandidateFQDN: "", + } compName := constant.GenerateClusterComponentName(a.clusterName, a.compName) if len(a.candidatePod) > 0 { m[switchoverCandidateName] = a.candidatePod