diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 192214c1fe8..b77c0666a35 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -4753,7 +4753,14 @@ func ValidatePodUpdate(newPod, oldPod *core.Pod, opts PodValidationOptions) fiel // already effectively nil, no change needed case mungedPodSpec.Affinity == nil && oldNodeAffinity != nil: mungedPodSpec.Affinity = &core.Affinity{NodeAffinity: oldNodeAffinity} // +k8s:verify-mutation:reason=clone + case mungedPodSpec.Affinity != nil && oldPod.Spec.Affinity == nil && + mungedPodSpec.Affinity.PodAntiAffinity == nil && mungedPodSpec.Affinity.PodAffinity == nil: + // We ensure no other fields are being changed, but the NodeAffinity. If that's the case, and the + // old pod's affinity is nil, we set the mungedPodSpec's affinity to nil. + mungedPodSpec.Affinity = nil // +k8s:verify-mutation:reason=clone default: + // The node affinity is being updated and the old pod Affinity is not nil. + // We set the mungedPodSpec's node affinity to the old pod's node affinity. mungedPodSpec.Affinity.NodeAffinity = oldNodeAffinity // +k8s:verify-mutation:reason=clone } } diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 80f2be937c1..f62cfb4d8a4 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -12836,6 +12836,117 @@ func TestValidatePodUpdate(t *testing.T) { }, err: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0]: Invalid value:", test: "empty NodeSelectorTerm (selects nothing) cannot become populated (selects something)", + }, { + old: core.Pod{ + Spec: core.PodSpec{ + Affinity: nil, + SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, + }, + }, + new: core.Pod{ + Spec: core.PodSpec{ + Affinity: &core.Affinity{ + NodeAffinity: &core.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ + NodeSelectorTerms: []core.NodeSelectorTerm{{ + MatchExpressions: []core.NodeSelectorRequirement{{ + Key: "expr", + Operator: core.NodeSelectorOpIn, + Values: []string{"foo"}, + }}, + }}, + }, + }, + }, + SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, + }, + }, + opts: PodValidationOptions{ + AllowMutableNodeSelectorAndNodeAffinity: true, + }, + test: "nil affinity can be mutated for gated pods", + }, + { + old: core.Pod{ + Spec: core.PodSpec{ + Affinity: nil, + SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, + }, + }, + new: core.Pod{ + Spec: core.PodSpec{ + Affinity: &core.Affinity{ + NodeAffinity: &core.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ + NodeSelectorTerms: []core.NodeSelectorTerm{{ + MatchExpressions: []core.NodeSelectorRequirement{{ + Key: "expr", + Operator: core.NodeSelectorOpIn, + Values: []string{"foo"}, + }}, + }}, + }, + }, + PodAffinity: &core.PodAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ + { + TopologyKey: "foo", + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + }, + }, + }, + }, + SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, + }, + }, + opts: PodValidationOptions{ + AllowMutableNodeSelectorAndNodeAffinity: true, + }, + err: "pod updates may not change fields other than", + test: "the podAffinity cannot be updated on gated pods", + }, + { + old: core.Pod{ + Spec: core.PodSpec{ + Affinity: nil, + SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, + }, + }, + new: core.Pod{ + Spec: core.PodSpec{ + Affinity: &core.Affinity{ + NodeAffinity: &core.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ + NodeSelectorTerms: []core.NodeSelectorTerm{{ + MatchExpressions: []core.NodeSelectorRequirement{{ + Key: "expr", + Operator: core.NodeSelectorOpIn, + Values: []string{"foo"}, + }}, + }}, + }, + }, + PodAntiAffinity: &core.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{ + { + TopologyKey: "foo", + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + }, + }, + }, + }, + SchedulingGates: []core.PodSchedulingGate{{Name: "baz"}}, + }, + }, + opts: PodValidationOptions{ + AllowMutableNodeSelectorAndNodeAffinity: true, + }, + err: "pod updates may not change fields other than", + test: "the podAntiAffinity cannot be updated on gated pods", }, } for _, test := range tests { diff --git a/test/integration/pods/pods_test.go b/test/integration/pods/pods_test.go index 3f463e7e9f1..52ee551c3cd 100644 --- a/test/integration/pods/pods_test.go +++ b/test/integration/pods/pods_test.go @@ -844,6 +844,63 @@ func TestMutablePodSchedulingDirectives(t *testing.T) { }, enableSchedulingGates: true, }, + { + name: "addition to nodeAffinity is allowed for gated pods with nil affinity", + create: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "fake-name", + Image: "fakeimage", + }, + }, + SchedulingGates: []v1.PodSchedulingGate{{Name: "baz"}}, + }, + }, + update: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "fake-name", + Image: "fakeimage", + }, + }, + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + // Add 1 MatchExpression and 1 MatchField. + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "expr", + Operator: v1.NodeSelectorOpIn, + Values: []string{"foo"}, + }, + }, + MatchFields: []v1.NodeSelectorRequirement{ + { + Key: "metadata.name", + Operator: v1.NodeSelectorOpIn, + Values: []string{"foo"}, + }, + }, + }, + }, + }, + }, + }, + SchedulingGates: []v1.PodSchedulingGate{{Name: "baz"}}, + }, + }, + enableSchedulingGates: true, + }, } for _, tc := range cases { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodSchedulingReadiness, tc.enableSchedulingGates)()