@@ -38,6 +38,7 @@ import (
3838 "github.com/apecloud/kubeblocks/pkg/controller/graph"
3939 "github.com/apecloud/kubeblocks/pkg/controller/model"
4040 intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil"
41+ viper "github.com/apecloud/kubeblocks/pkg/viperx"
4142)
4243
4344// componentWorkloadTransformer handles component workload generation
@@ -167,7 +168,7 @@ func (t *componentWorkloadTransformer) handleUpdate(transCtx *componentTransform
167168 }
168169 }
169170
170- objCopy := copyAndMergeITS (runningITS , protoITS )
171+ objCopy := copyAndMergeITS (runningITS , protoITS , legacyConfigManagerRequired ( comp ) )
171172 if objCopy != nil {
172173 cli .Update (dag , nil , objCopy , & model.ReplaceIfExistingOption {})
173174 // make sure the workload is updated after the env CM
@@ -220,7 +221,7 @@ func (t *componentWorkloadTransformer) handleWorkloadUpdate(transCtx *componentT
220221// copyAndMergeITS merges two ITS objects for updating:
221222// 1. new an object targetObj by copying from oldObj
222223// 2. merge all fields can be updated from newObj into targetObj
223- func copyAndMergeITS (oldITS , newITS * workloads.InstanceSet ) * workloads.InstanceSet {
224+ func copyAndMergeITS (oldITS , newITS * workloads.InstanceSet , legacyConfigManagerPolicy legacyConfigManagerPolicy ) * workloads.InstanceSet {
224225 itsObjCopy := oldITS .DeepCopy ()
225226 itsProto := newITS
226227
@@ -241,6 +242,9 @@ func copyAndMergeITS(oldITS, newITS *workloads.InstanceSet) *workloads.InstanceS
241242 podTemplateCopy .Annotations = itsObjCopy .Spec .Template .Annotations
242243
243244 itsObjCopy .Spec .Template = podTemplateCopy
245+ // Preserve the legacy config-manager only for existing workloads that still have it in their live template.
246+ // This avoids an upgrade-only template diff from forcing all old Pods to restart after config-manager moved to kbagent.
247+ preserveLegacyConfigManagerPodSpec (oldITS , itsProto , itsObjCopy , legacyConfigManagerPolicy )
244248 itsObjCopy .Spec .Replicas = itsProto .Spec .Replicas
245249 itsObjCopy .Spec .Roles = itsProto .Spec .Roles
246250 itsObjCopy .Spec .LifecycleActions = itsProto .Spec .LifecycleActions
@@ -289,6 +293,346 @@ func copyAndMergeITS(oldITS, newITS *workloads.InstanceSet) *workloads.InstanceS
289293 return itsObjCopy
290294}
291295
296+ const (
297+ legacyConfigManagerContainerName = "config-manager"
298+ legacyConfigManagerToolsInitName = "install-config-manager-tool"
299+ legacyConfigManagerToolsVolumeName = "kb-tools"
300+ )
301+
302+ type legacyConfigManagerPolicy string
303+
304+ const (
305+ legacyConfigManagerPolicyKeep legacyConfigManagerPolicy = "keep"
306+ legacyConfigManagerPolicyCleanup legacyConfigManagerPolicy = "cleanup"
307+ )
308+
309+ func preserveLegacyConfigManagerPodSpec (oldITS , desiredITS , mergedITS * workloads.InstanceSet , legacyPolicy legacyConfigManagerPolicy ) {
310+ if oldITS == nil || desiredITS == nil || mergedITS == nil {
311+ return
312+ }
313+ oldSpec := & oldITS .Spec .Template .Spec
314+ newSpec := & mergedITS .Spec .Template .Spec
315+ _ , oldCfg := intctrlutil .GetContainerByName (oldSpec .Containers , legacyConfigManagerContainerName )
316+ if oldCfg == nil {
317+ return
318+ }
319+ // The compatibility marker is owned by upstream parameters logic. Existing workloads should keep the
320+ // legacy config-manager unless upstream has explicitly marked cleanup as safe.
321+ if legacyPolicy == legacyConfigManagerPolicyCleanup && shouldCleanupLegacyConfigManager (oldITS , desiredITS ) {
322+ return
323+ }
324+ newSpec .Containers = preserveLegacyContainerOrder (oldSpec .Containers , newSpec .Containers , legacyConfigManagerContainerName , oldCfg )
325+
326+ var oldInit * corev1.Container
327+ if _ , initContainer := intctrlutil .GetContainerByName (oldSpec .InitContainers , legacyConfigManagerToolsInitName ); initContainer != nil {
328+ oldInit = initContainer
329+ newSpec .InitContainers = preserveLegacyContainerOrder (oldSpec .InitContainers , newSpec .InitContainers , legacyConfigManagerToolsInitName , initContainer )
330+ }
331+
332+ volumeNames := collectVolumeNamesFromContainers (oldCfg , oldInit )
333+ newSpec .Volumes = preserveLegacyVolumeOrder (oldSpec .Volumes , newSpec .Volumes , volumeNames )
334+ if _ , ok := volumeNames [legacyConfigManagerToolsVolumeName ]; ok {
335+ mergeVolumeMountsByVolumeName (oldSpec .Containers , & newSpec .Containers , legacyConfigManagerToolsVolumeName )
336+ mergeVolumeMountsByVolumeName (oldSpec .InitContainers , & newSpec .InitContainers , legacyConfigManagerToolsVolumeName )
337+ }
338+ }
339+
340+ func legacyConfigManagerRequired (comp * appsv1.Component ) legacyConfigManagerPolicy {
341+ if comp == nil || comp .Annotations == nil {
342+ return legacyConfigManagerPolicyKeep
343+ }
344+ value , ok := comp .Annotations [constant .LegacyConfigManagerRequiredAnnotationKey ]
345+ if ! ok {
346+ return legacyConfigManagerPolicyKeep
347+ }
348+ if value == "false" {
349+ return legacyConfigManagerPolicyCleanup
350+ }
351+ return legacyConfigManagerPolicyKeep
352+ }
353+
354+ func shouldCleanupLegacyConfigManager (oldITS , desiredITS * workloads.InstanceSet ) bool {
355+ oldTemplate := stripLegacyConfigManagerPodTemplate (oldITS .Spec .Template )
356+ newTemplate := stripLegacyConfigManagerPodTemplate (desiredITS .Spec .Template )
357+ if reflect .DeepEqual (oldTemplate , newTemplate ) {
358+ return false
359+ }
360+ // Container list or init container changes are evaluated by InstanceSet as pod upgrade changes.
361+ // Clean up only when the configured policy will recreate Pods. Otherwise we keep the live template aligned
362+ // with old Pods that still carry the legacy sidecar and avoid creating an extra rollout only for cleanup.
363+ if hasPodUpgradeTemplateChanges (oldTemplate , newTemplate ) {
364+ return desiredITS .Spec .PodUpgradePolicy == appsv1 .ReCreatePodUpdatePolicyType
365+ }
366+ // Resource-only updates may still be in-place when vertical scaling is enabled; otherwise they recreate Pods.
367+ if hasPodResourceChanges (oldTemplate .Spec , newTemplate .Spec ) {
368+ return ! viper .GetBool (constant .FeatureGateInPlacePodVerticalScaling )
369+ }
370+ // Annotation/label/toleration style changes follow the regular pod update policy.
371+ if hasPodUpdateTemplateChanges (oldTemplate , newTemplate ) {
372+ return desiredITS .Spec .PodUpdatePolicy == appsv1 .ReCreatePodUpdatePolicyType
373+ }
374+ return false
375+ }
376+
377+ func stripLegacyConfigManagerPodTemplate (template corev1.PodTemplateSpec ) corev1.PodTemplateSpec {
378+ template = * template .DeepCopy ()
379+ volumeNames := map [string ]struct {}{}
380+ template .Spec .Containers = filterLegacyContainers (template .Spec .Containers , volumeNames , legacyConfigManagerContainerName )
381+ template .Spec .InitContainers = filterLegacyContainers (template .Spec .InitContainers , volumeNames , legacyConfigManagerToolsInitName )
382+ template .Spec .Containers = filterLegacyVolumeMounts (template .Spec .Containers , volumeNames )
383+ template .Spec .InitContainers = filterLegacyVolumeMounts (template .Spec .InitContainers , volumeNames )
384+ template .Spec .Volumes = filterLegacyVolumes (template .Spec .Volumes , volumeNames )
385+ return template
386+ }
387+
388+ func hasPodUpgradeTemplateChanges (oldTemplate , newTemplate corev1.PodTemplateSpec ) bool {
389+ oldTemplate = * oldTemplate .DeepCopy ()
390+ newTemplate = * newTemplate .DeepCopy ()
391+ oldTemplate .Annotations = nil
392+ oldTemplate .Labels = nil
393+ newTemplate .Annotations = nil
394+ newTemplate .Labels = nil
395+ oldTemplate .Spec .ActiveDeadlineSeconds = nil
396+ newTemplate .Spec .ActiveDeadlineSeconds = nil
397+ oldTemplate .Spec .Tolerations = nil
398+ newTemplate .Spec .Tolerations = nil
399+ clearContainerResources (oldTemplate .Spec .Containers )
400+ clearContainerResources (oldTemplate .Spec .InitContainers )
401+ clearContainerResources (newTemplate .Spec .Containers )
402+ clearContainerResources (newTemplate .Spec .InitContainers )
403+ return ! reflect .DeepEqual (oldTemplate .Spec , newTemplate .Spec )
404+ }
405+
406+ func hasPodResourceChanges (oldSpec , newSpec corev1.PodSpec ) bool {
407+ return ! reflect .DeepEqual (getContainerResources (oldSpec .Containers ), getContainerResources (newSpec .Containers )) ||
408+ ! reflect .DeepEqual (getContainerResources (oldSpec .InitContainers ), getContainerResources (newSpec .InitContainers ))
409+ }
410+
411+ func hasPodUpdateTemplateChanges (oldTemplate , newTemplate corev1.PodTemplateSpec ) bool {
412+ if ! reflect .DeepEqual (oldTemplate .Annotations , newTemplate .Annotations ) {
413+ return true
414+ }
415+ if ! reflect .DeepEqual (oldTemplate .Labels , newTemplate .Labels ) {
416+ return true
417+ }
418+ if ! reflect .DeepEqual (oldTemplate .Spec .ActiveDeadlineSeconds , newTemplate .Spec .ActiveDeadlineSeconds ) {
419+ return true
420+ }
421+ return ! reflect .DeepEqual (oldTemplate .Spec .Tolerations , newTemplate .Spec .Tolerations )
422+ }
423+
424+ func filterLegacyContainers (containers []corev1.Container , volumeNames map [string ]struct {}, legacyContainerName string ) []corev1.Container {
425+ filtered := make ([]corev1.Container , 0 , len (containers ))
426+ for _ , container := range containers {
427+ if container .Name == legacyContainerName {
428+ for _ , mount := range container .VolumeMounts {
429+ volumeNames [mount .Name ] = struct {}{}
430+ }
431+ continue
432+ }
433+ filtered = append (filtered , container )
434+ }
435+ return filtered
436+ }
437+
438+ func filterLegacyVolumeMounts (containers []corev1.Container , volumeNames map [string ]struct {}) []corev1.Container {
439+ for i := range containers {
440+ mounts := containers [i ].VolumeMounts [:0 ]
441+ for _ , mount := range containers [i ].VolumeMounts {
442+ if _ , ok := volumeNames [mount .Name ]; ok {
443+ continue
444+ }
445+ mounts = append (mounts , mount )
446+ }
447+ containers [i ].VolumeMounts = mounts
448+ }
449+ return containers
450+ }
451+
452+ func filterLegacyVolumes (volumes []corev1.Volume , volumeNames map [string ]struct {}) []corev1.Volume {
453+ filtered := make ([]corev1.Volume , 0 , len (volumes ))
454+ for _ , volume := range volumes {
455+ if _ , ok := volumeNames [volume .Name ]; ok {
456+ continue
457+ }
458+ filtered = append (filtered , volume )
459+ }
460+ return filtered
461+ }
462+
463+ func clearContainerResources (containers []corev1.Container ) {
464+ for i := range containers {
465+ containers [i ].Resources = corev1.ResourceRequirements {}
466+ }
467+ }
468+
469+ func getContainerResources (containers []corev1.Container ) map [string ]corev1.ResourceRequirements {
470+ resources := make (map [string ]corev1.ResourceRequirements , len (containers ))
471+ for _ , container := range containers {
472+ resources [container .Name ] = container .Resources
473+ }
474+ return resources
475+ }
476+
477+ func collectVolumeNamesFromContainers (containers ... * corev1.Container ) map [string ]struct {} {
478+ volumeNames := map [string ]struct {}{}
479+ for _ , container := range containers {
480+ if container == nil {
481+ continue
482+ }
483+ for _ , mount := range container .VolumeMounts {
484+ volumeNames [mount .Name ] = struct {}{}
485+ }
486+ }
487+ return volumeNames
488+ }
489+
490+ func preserveLegacyContainerOrder (oldContainers , newContainers []corev1.Container , legacyName string , legacyContainer * corev1.Container ) []corev1.Container {
491+ if legacyContainer == nil {
492+ return newContainers
493+ }
494+ if _ , container := intctrlutil .GetContainerByName (newContainers , legacyName ); container != nil {
495+ return newContainers
496+ }
497+
498+ newContainerMap := make (map [string ]corev1.Container , len (newContainers ))
499+ used := make (map [string ]struct {}, len (newContainers ))
500+ for _ , container := range newContainers {
501+ newContainerMap [container .Name ] = container
502+ }
503+
504+ merged := make ([]corev1.Container , 0 , len (newContainers )+ 1 )
505+ for _ , oldContainer := range oldContainers {
506+ switch {
507+ case oldContainer .Name == legacyName :
508+ // Reinsert the legacy container at its historical position to avoid order-only PodSpec diffs.
509+ merged = append (merged , * legacyContainer .DeepCopy ())
510+ case hasContainerByName (newContainerMap , oldContainer .Name ):
511+ merged = append (merged , newContainerMap [oldContainer .Name ])
512+ used [oldContainer .Name ] = struct {}{}
513+ }
514+ }
515+ for _ , container := range newContainers {
516+ if _ , ok := used [container .Name ]; ok {
517+ continue
518+ }
519+ merged = append (merged , container )
520+ }
521+ return merged
522+ }
523+
524+ func preserveLegacyVolumeOrder (oldVolumes , newVolumes []corev1.Volume , legacyVolumeNames map [string ]struct {}) []corev1.Volume {
525+ if len (legacyVolumeNames ) == 0 {
526+ return newVolumes
527+ }
528+ newVolumeMap := make (map [string ]corev1.Volume , len (newVolumes ))
529+ used := make (map [string ]struct {}, len (newVolumes ))
530+ for _ , volume := range newVolumes {
531+ newVolumeMap [volume .Name ] = volume
532+ }
533+
534+ merged := make ([]corev1.Volume , 0 , len (newVolumes )+ len (legacyVolumeNames ))
535+ for _ , oldVolume := range oldVolumes {
536+ switch {
537+ case hasVolumeByName (legacyVolumeNames , oldVolume .Name ):
538+ // Keep the original volume order for the same reason as containers: avoid restart-causing order diffs.
539+ merged = append (merged , oldVolume )
540+ case hasVolumeByNameMap (newVolumeMap , oldVolume .Name ):
541+ merged = append (merged , newVolumeMap [oldVolume .Name ])
542+ used [oldVolume .Name ] = struct {}{}
543+ }
544+ }
545+ for _ , volume := range newVolumes {
546+ if _ , ok := used [volume .Name ]; ok {
547+ continue
548+ }
549+ if hasVolumeByName (legacyVolumeNames , volume .Name ) {
550+ continue
551+ }
552+ merged = append (merged , volume )
553+ }
554+ return merged
555+ }
556+
557+ func mergeVolumeMountsByVolumeName (oldContainers []corev1.Container , newContainers * []corev1.Container , volumeName string ) {
558+ for i := range * newContainers {
559+ container := & (* newContainers )[i ]
560+ _ , oldContainer := intctrlutil .GetContainerByName (oldContainers , container .Name )
561+ if oldContainer == nil {
562+ continue
563+ }
564+ container .VolumeMounts = preserveLegacyVolumeMountOrder (oldContainer .VolumeMounts , container .VolumeMounts , volumeName )
565+ }
566+ }
567+
568+ func preserveLegacyVolumeMountOrder (oldMounts , newMounts []corev1.VolumeMount , volumeName string ) []corev1.VolumeMount {
569+ hasLegacyMount := false
570+ for _ , mount := range oldMounts {
571+ if mount .Name == volumeName {
572+ hasLegacyMount = true
573+ break
574+ }
575+ }
576+ if ! hasLegacyMount || hasVolumeMount (newMounts , volumeName ) {
577+ return newMounts
578+ }
579+
580+ newMountMap := make (map [string ]corev1.VolumeMount , len (newMounts ))
581+ used := make (map [string ]struct {}, len (newMounts ))
582+ for _ , mount := range newMounts {
583+ newMountMap [volumeMountKey (mount )] = mount
584+ }
585+
586+ merged := make ([]corev1.VolumeMount , 0 , len (newMounts )+ 1 )
587+ for _ , oldMount := range oldMounts {
588+ if oldMount .Name == volumeName {
589+ merged = append (merged , oldMount )
590+ } else {
591+ key := volumeMountKey (oldMount )
592+ if mount , ok := newMountMap [key ]; ok {
593+ merged = append (merged , mount )
594+ used [key ] = struct {}{}
595+ }
596+ }
597+ }
598+ for _ , mount := range newMounts {
599+ key := volumeMountKey (mount )
600+ if _ , ok := used [key ]; ok {
601+ continue
602+ }
603+ merged = append (merged , mount )
604+ }
605+ return merged
606+ }
607+
608+ func hasContainerByName (containers map [string ]corev1.Container , name string ) bool {
609+ _ , ok := containers [name ]
610+ return ok
611+ }
612+
613+ func hasVolumeByName (names map [string ]struct {}, name string ) bool {
614+ _ , ok := names [name ]
615+ return ok
616+ }
617+
618+ func hasVolumeByNameMap (volumes map [string ]corev1.Volume , name string ) bool {
619+ _ , ok := volumes [name ]
620+ return ok
621+ }
622+
623+ func hasVolumeMount (mounts []corev1.VolumeMount , name string ) bool {
624+ for _ , mount := range mounts {
625+ if mount .Name == name {
626+ return true
627+ }
628+ }
629+ return false
630+ }
631+
632+ func volumeMountKey (mount corev1.VolumeMount ) string {
633+ return mount .Name + "|" + mount .MountPath + "|" + mount .SubPath
634+ }
635+
292636func checkNRollbackProtoImages (itsObj , itsProto * workloads.InstanceSet ) {
293637 if itsObj .Annotations == nil || itsProto .Annotations == nil {
294638 return
0 commit comments