Skip to content

Commit 0400191

Browse files
fix(deploymentConfig): handle empty affinity objects to match OLMv0 erasure behavior
Assisted-by: Cursor/Claude
1 parent df1b502 commit 0400191

32 files changed

Lines changed: 45325 additions & 8 deletions

File tree

internal/operator-controller/rukpak/render/registryv1/generators/generators.go

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -772,29 +772,103 @@ func applyNodeSelectorConfig(deployment *appsv1.Deployment, config *config.Deplo
772772
deployment.Spec.Template.Spec.NodeSelector = config.NodeSelector
773773
}
774774

775+
// isAffinityEmpty checks if an Affinity object is semantically empty.
776+
// This accounts for YAML unmarshaling which creates empty slices instead of nil.
777+
func isAffinityEmpty(a *corev1.Affinity) bool {
778+
if a == nil {
779+
return true
780+
}
781+
return isNodeAffinityEmpty(a.NodeAffinity) &&
782+
isPodAffinityEmpty(a.PodAffinity) &&
783+
isPodAntiAffinityEmpty(a.PodAntiAffinity)
784+
}
785+
786+
// isNodeAffinityEmpty checks if a NodeAffinity object is semantically empty.
787+
func isNodeAffinityEmpty(na *corev1.NodeAffinity) bool {
788+
if na == nil {
789+
return true
790+
}
791+
return na.RequiredDuringSchedulingIgnoredDuringExecution == nil &&
792+
len(na.PreferredDuringSchedulingIgnoredDuringExecution) == 0
793+
}
794+
795+
// isPodAffinityEmpty checks if a PodAffinity object is semantically empty.
796+
func isPodAffinityEmpty(pa *corev1.PodAffinity) bool {
797+
if pa == nil {
798+
return true
799+
}
800+
return len(pa.RequiredDuringSchedulingIgnoredDuringExecution) == 0 &&
801+
len(pa.PreferredDuringSchedulingIgnoredDuringExecution) == 0
802+
}
803+
804+
// isPodAntiAffinityEmpty checks if a PodAntiAffinity object is semantically empty.
805+
func isPodAntiAffinityEmpty(paa *corev1.PodAntiAffinity) bool {
806+
if paa == nil {
807+
return true
808+
}
809+
return len(paa.RequiredDuringSchedulingIgnoredDuringExecution) == 0 &&
810+
len(paa.PreferredDuringSchedulingIgnoredDuringExecution) == 0
811+
}
812+
775813
// applyAffinityConfig applies affinity configuration to the deployment's pod spec.
776-
// This selectively overrides non-nil affinity sub-attributes.
777-
// This follows OLMv0 behavior:
778-
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L273-L341
814+
// This follows OLMv0 behavior where:
815+
// - nil affinity means "don't touch" the deployment's existing affinity
816+
// - empty affinity ({}) means "erase" the deployment's existing affinity
817+
// - non-nil sub-attributes override the corresponding deployment sub-attributes
818+
// - nil sub-attributes within a non-empty affinity are left unchanged
819+
// - empty sub-attributes ({}) erase the corresponding deployment sub-attributes
820+
//
821+
// See: https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L273-L341
779822
func applyAffinityConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
780823
if config.Affinity == nil {
781824
return
782825
}
783826

784-
if deployment.Spec.Template.Spec.Affinity == nil {
785-
deployment.Spec.Template.Spec.Affinity = &corev1.Affinity{}
827+
podSpec := &deployment.Spec.Template.Spec
828+
829+
// Check if the config specifies an empty affinity object with all fields unset.
830+
// This is different from having empty sub-fields - an empty affinity {} with no fields
831+
// means erase everything, while affinity with empty sub-fields means selectively erase.
832+
configHasNoFields := config.Affinity.NodeAffinity == nil &&
833+
config.Affinity.PodAffinity == nil &&
834+
config.Affinity.PodAntiAffinity == nil
835+
836+
if configHasNoFields {
837+
// Config is affinity: {} with no fields - erase entire affinity
838+
podSpec.Affinity = nil
839+
return
840+
}
841+
842+
if podSpec.Affinity == nil {
843+
podSpec.Affinity = &corev1.Affinity{}
786844
}
787845

788846
if config.Affinity.NodeAffinity != nil {
789-
deployment.Spec.Template.Spec.Affinity.NodeAffinity = config.Affinity.NodeAffinity
847+
if isNodeAffinityEmpty(config.Affinity.NodeAffinity) {
848+
podSpec.Affinity.NodeAffinity = nil
849+
} else {
850+
podSpec.Affinity.NodeAffinity = config.Affinity.NodeAffinity
851+
}
790852
}
791853

792854
if config.Affinity.PodAffinity != nil {
793-
deployment.Spec.Template.Spec.Affinity.PodAffinity = config.Affinity.PodAffinity
855+
if isPodAffinityEmpty(config.Affinity.PodAffinity) {
856+
podSpec.Affinity.PodAffinity = nil
857+
} else {
858+
podSpec.Affinity.PodAffinity = config.Affinity.PodAffinity
859+
}
794860
}
795861

796862
if config.Affinity.PodAntiAffinity != nil {
797-
deployment.Spec.Template.Spec.Affinity.PodAntiAffinity = config.Affinity.PodAntiAffinity
863+
if isPodAntiAffinityEmpty(config.Affinity.PodAntiAffinity) {
864+
podSpec.Affinity.PodAntiAffinity = nil
865+
} else {
866+
podSpec.Affinity.PodAntiAffinity = config.Affinity.PodAntiAffinity
867+
}
868+
}
869+
870+
if isAffinityEmpty(podSpec.Affinity) {
871+
podSpec.Affinity = nil
798872
}
799873
}
800874

0 commit comments

Comments
 (0)