Skip to content

Commit 6c6da58

Browse files
authored
chore: preserve legacy config-manager during controller upgrade (#10081)
1 parent e03180c commit 6c6da58

5 files changed

Lines changed: 1190 additions & 3 deletions

File tree

controllers/apps/component/transformer_component_workload.go

Lines changed: 346 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
292636
func checkNRollbackProtoImages(itsObj, itsProto *workloads.InstanceSet) {
293637
if itsObj.Annotations == nil || itsProto.Annotations == nil {
294638
return

0 commit comments

Comments
 (0)