Skip to content

Commit 5cb23b4

Browse files
fix(deploymentConfig): handle empty affinity objects to match OLMv0 erasure behavior
- Fixed isNodeAffinityEmpty() to treat non-nil RequiredDuringSchedulingIgnoredDuringExecution with empty NodeSelectorTerms as empty (handles YAML unmarshaling edge case) - Added unit test for empty nodeSelectorTerms scenario Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent df1b502 commit 5cb23b4

2 files changed

Lines changed: 584 additions & 8 deletions

File tree

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

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -772,29 +772,104 @@ 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+
requiredEmpty := na.RequiredDuringSchedulingIgnoredDuringExecution == nil ||
792+
len(na.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms) == 0
793+
return requiredEmpty && len(na.PreferredDuringSchedulingIgnoredDuringExecution) == 0
794+
}
795+
796+
// isPodAffinityEmpty checks if a PodAffinity object is semantically empty.
797+
func isPodAffinityEmpty(pa *corev1.PodAffinity) bool {
798+
if pa == nil {
799+
return true
800+
}
801+
return len(pa.RequiredDuringSchedulingIgnoredDuringExecution) == 0 &&
802+
len(pa.PreferredDuringSchedulingIgnoredDuringExecution) == 0
803+
}
804+
805+
// isPodAntiAffinityEmpty checks if a PodAntiAffinity object is semantically empty.
806+
func isPodAntiAffinityEmpty(paa *corev1.PodAntiAffinity) bool {
807+
if paa == nil {
808+
return true
809+
}
810+
return len(paa.RequiredDuringSchedulingIgnoredDuringExecution) == 0 &&
811+
len(paa.PreferredDuringSchedulingIgnoredDuringExecution) == 0
812+
}
813+
775814
// 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
815+
// This follows OLMv0 behavior where:
816+
// - nil affinity means "don't touch" the deployment's existing affinity
817+
// - empty affinity ({}) means "erase" the deployment's existing affinity
818+
// - non-nil sub-attributes override the corresponding deployment sub-attributes
819+
// - nil sub-attributes within a non-empty affinity are left unchanged
820+
// - empty sub-attributes ({}) erase the corresponding deployment sub-attributes
821+
//
822+
// See: https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L273-L341
779823
func applyAffinityConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
780824
if config.Affinity == nil {
781825
return
782826
}
783827

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

788847
if config.Affinity.NodeAffinity != nil {
789-
deployment.Spec.Template.Spec.Affinity.NodeAffinity = config.Affinity.NodeAffinity
848+
if isNodeAffinityEmpty(config.Affinity.NodeAffinity) {
849+
podSpec.Affinity.NodeAffinity = nil
850+
} else {
851+
podSpec.Affinity.NodeAffinity = config.Affinity.NodeAffinity
852+
}
790853
}
791854

792855
if config.Affinity.PodAffinity != nil {
793-
deployment.Spec.Template.Spec.Affinity.PodAffinity = config.Affinity.PodAffinity
856+
if isPodAffinityEmpty(config.Affinity.PodAffinity) {
857+
podSpec.Affinity.PodAffinity = nil
858+
} else {
859+
podSpec.Affinity.PodAffinity = config.Affinity.PodAffinity
860+
}
794861
}
795862

796863
if config.Affinity.PodAntiAffinity != nil {
797-
deployment.Spec.Template.Spec.Affinity.PodAntiAffinity = config.Affinity.PodAntiAffinity
864+
if isPodAntiAffinityEmpty(config.Affinity.PodAntiAffinity) {
865+
podSpec.Affinity.PodAntiAffinity = nil
866+
} else {
867+
podSpec.Affinity.PodAntiAffinity = config.Affinity.PodAntiAffinity
868+
}
869+
}
870+
871+
if isAffinityEmpty(podSpec.Affinity) {
872+
podSpec.Affinity = nil
798873
}
799874
}
800875

0 commit comments

Comments
 (0)