Repository: kubernetes/kubernetes (language: go)

Issue:
[Bug] Missing constraint and validation for label selector's values
### What happened?

Hello,
There are some [constraints](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set) that label selector's `values` need to conform:
>* must be 63 characters or less (can be empty),
>* unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]),
>* could contain dashes (-), underscores (_), dots (.), and alphanumerics between.

Every label selctor's `values` will be checked [here](https://github.com/kubernetes/kubernetes/blob/e5ba5cd2b0d3bddd4a0393b98fde14960f31a33c/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L166C1-L175C2), and when values is not conforming these constraints, an error will be returned.

`nodeAfffinty.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions.values` also needs to conform these constraints (because `matchExpressions` will be parsed as a label selector), but cccording to the [concept documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#operators) and the [API documentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#nodeselectorrequirement-v1-core), 
Kubernetes only requires `V1NodeSelectorRequirement.values` to be strings. 
>`values` (string array): An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.

So when creating a pod with invalid `V1NodeSelectorRequirement.values`, no warning or error message appears and the pod is created successfully.
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-test-pod
  namespace: default
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: int-cmkzofgjsu
            operator: NotIn
            values:
            - '-1' # <- invalid value
  containers:
  - image: nginx
    name: test-container
# pod/node-affinity-test-pod created <- no warning or error here
```
By contrast, when you set an invalid value in matchFields, it will be rejected.
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-test-pod
  namespace: default
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchFields:
          - key: metadata.name
            operator: NotIn
            values:
            - '-1'
  containers:
  - image: nginx
    name: test-container
# The Pod "node-affinity-test-pod" is invalid: spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchFields[0].values[0]: Invalid value: "-1": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
```
---
And after creating the first pod successfully, it couldn’t be scheduled even though one node satisfied its `nodeAffinity`. By adding some log codes, we found that when the scheduler tries to parse a `nodeSelectorTerm`, it receives a non-empty `parseErr`(because the value is not conforming label value constraints as stated above), causing it to consider the entire `nodeSelectorTerm` unmatched. [code](https://github.com/kubernetes/kubernetes/blob/d88b4e3b6e34a85f58778b7ef96e64edffff6823/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity.go#L191C2-L193C3) here.

This behavior may cause the scheduler to make unexpected decisions without the user's awareness. More critically, an invalid value will prevent the entire nodeSelectorTerm from matching.

```yaml
nodeSelectorTerms:
 - matchExpressions:
    - matchExpressions1{key, operator, values} -> has an invalid value, but user is not aware of it
    - matchExpressions2{key, operator, values}
    - ...
   matchFields:
    - matchFields1{key, operator, values}
    - matchFields2{key, operator, values}
    - ...
# matchExpressions1 cannot be parsed, so the whole nodeSelectorTerm is considered as not matched in nodeAffinity
```

### What did you expect to happen?

* Implicit constraints on the [label selector](https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/labels/selector.go)'s `values` field should be specified in the API documentation and also validated before creating the pod. (We think not only `nodeAfffinty.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms` will be parsed as label selector, other fields in V1Pod also need to be checked for the same reason.)
* Those implicit constraints should also be added to the concept documentation. (will create an issue in kuberentes/website later)


### How can we reproduce it (as minimally and precisely as possible)?

Use the yaml file above.

### Anything else we need to know?

/sig bugs scheduling

### Kubernetes version

Tested on 1.30.2

### Cloud provider

<details>

</details>


### OS version

<details>

```console
# On Linux:
$ cat /etc/os-release
# paste output here
$ uname -a
# paste output here

# On Windows:
C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
# paste output here
```

</details>


### Install tools

<details>

</details>


### Container runtime (CRI) and version (if applicable)

<details>

</details>


### Related plugins (CNI, CSI, ...) and versions (if applicable)

<details>

</details>



Discussion / hints:
@scheduler-tester: The label(s) `sig/bugs` cannot be applied, because the repository doesn't have them.

<details>

In response to [this](https://github.com/kubernetes/kubernetes/issues/128156):

>### What happened?
>
>Hello,
>There are some [constraints](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set) that label selector's `values` need to conform:
>>* must be 63 characters or less (can be empty),
>>* unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]),
>>* could contain dashes (-), underscores (_), dots (.), and alphanumerics between.
>
>Every label selctor's `values` will be checked [here](https://github.com/kubernetes/kubernetes/blob/e5ba5cd2b0d3bddd4a0393b98fde14960f31a33c/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L166C1-L175C2), and when values is not conforming these constraints, an error will be returned.
>
>`nodeAfffinty.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions.values` also needs to conform these constraints (because `matchExpressions` will be parsed as a label selector), but cccording to the [concept documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#operators) and the [API documentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#nodeselectorrequirement-v1-core), 
>Kubernetes only requires `V1NodeSelectorRequirement.values` to be strings. 
>>`values` (string array): An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.
>
>So when creating a pod with invalid `V1NodeSelectorRequirement.values`, no warning or error message appears and the pod is created successfully.
>```yaml
>apiVersion: v1
>kind: Pod
>metadata:
>  name: node-affinity-test-pod
>  namespace: default
>spec:
>  affinity:
>    nodeAffinity:
>      requiredDuringSchedulingIgnoredDuringExecution:
>        nodeSelectorTerms:
>        - matchExpressions:
>          - key: int-cmkzofgjsu
>            operator: NotIn
>            values:
>            - '-1' # <- invalid value
>  containers:
>  - image: nginx
>    name: test-container
># pod/node-affinity-test-pod created <- no warning or error here
>```
>By contrast, when you set an invalid value in matchFields, it will be rejected.
>```yaml
>apiVersion: v1
>kind: Pod
>metadata:
>  name: node-affinity-test-pod
>  namespace: default
>spec:
>  affinity:
>    nodeAffinity:
>      requiredDuringSchedulingIgnoredDuringExecution:
>        nodeSelectorTerms:
>        - matchFields:
>          - key: metadata.name
>            operator: NotIn
>            values:
>            - '-1'
>  containers:
>  - image: nginx
>    name: test-container
># The Pod "node-affinity-test-pod" is invalid: spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchFields[0].values[0]: Invalid value: "-1": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
>```
>---
>And after creating the first pod successfully, it couldn’t be scheduled even though one node satisfied its `nodeAffinity`. By adding some log codes, we found that when the scheduler tries to parse a `nodeSelectorTerm`, it receives a non-empty `parseErr`(because the value is not conforming label value constraints as stated above), causing it to consider the entire `nodeSelectorTerm` unmatched. [code](https://github.com/kubernetes/kubernetes/blob/d88b4e3b6e34a85f58778b7ef96e64edffff6823/staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity/nodeaffinity.go#L191C2-L193C3) here.
>
>This behavior may cause the scheduler to make unexpected decisions without the user's awareness. More critically, an invalid value will prevent the entire nodeSelectorTerm from matching.
>
>```yaml
>nodeSelectorTerms:
> - matchExpressions:
>    - matchExpressions1{key, operator, values} -> has an invalid value, but user is not aware of it
>    - matchExpressions2{key, operator, values}
>    - ...
>   matchFields:
>    - matchFields1{key, operator, values}
>    - matchFields2{key, operator, values}
>    - ...
># matchExpressions1 cannot be parsed, so the whole nodeSelectorTerm is considered as not matched in nodeAffinity
>```
>
>### What did you expect to happen?
>
>* Implicit constraints on the [label selector](https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/labels/selector.go)'s `values` field should be specified in the API documentation and also validated before creating the pod. (We think not only `nodeAfffinty.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms` will be parsed as label selector, other fields in V1Pod also need to be checked for the same reason.)
>* Those implicit constraints should also be added to the concept documentation. (will create an issue in kuberentes/website later)
>
>
>### How can we reproduce it (as minimally and precisely as possible)?
>
>Use the yaml file above.
>
>### Anything else we need to know?
>
>/sig bugs scheduling
>
>### Kubernetes version
>
>Tested on 1.30.2
>
>### Cloud provider
>
><details>
>
></details>
>
>
>### OS version
>
><details>
>
>```console
># On Linux:
>$ cat /etc/os-release
># paste output here
>$ uname -a
># paste output here
>
># On Windows:
>C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
># paste output here
>```
>
></details>
>
>
>### Install tools
>
><details>
>
></details>
>
>
>### Container runtime (CRI) and version (if applicable)
>
><details>
>
></details>
>
>
>### Related plugins (CNI, CSI, ...) and versions (if applicable)
>
><details>
>
></details>
>


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
/cc
/triage accepted
/assign

Fix patch:
diff --git a/pkg/api/node/util.go b/pkg/api/node/util.go
index daf5ee0cf6222..a221642f6249f 100644
--- a/pkg/api/node/util.go
+++ b/pkg/api/node/util.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/validation"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	api "k8s.io/kubernetes/pkg/apis/core"
 	"k8s.io/kubernetes/pkg/apis/node"
@@ -91,7 +92,7 @@ func GetWarningsForNodeSelector(nodeSelector *metav1.LabelSelector, fieldPath *f
 }
 
 // GetWarningsForNodeSelectorTerm checks match expressions of node selector term
-func GetWarningsForNodeSelectorTerm(nodeSelectorTerm api.NodeSelectorTerm, fieldPath *field.Path) []string {
+func GetWarningsForNodeSelectorTerm(nodeSelectorTerm api.NodeSelectorTerm, checkLabelValue bool, fieldPath *field.Path) []string {
 	var warnings []string
 	// use of deprecated node labels in matchLabelExpressions
 	for i, expression := range nodeSelectorTerm.MatchExpressions {
@@ -106,6 +107,19 @@ func GetWarningsForNodeSelectorTerm(nodeSelectorTerm api.NodeSelectorTerm, field
 				),
 			)
 		}
+		if checkLabelValue {
+			for index, value := range expression.Values {
+				for _, msg := range validation.IsValidLabelValue(value) {
+					warnings = append(warnings,
+						fmt.Sprintf(
+							"%s: %s is invalid, %s",
+							fieldPath.Child("matchExpressions").Index(i).Child("values").Index(index),
+							value,
+							msg,
+						))
+				}
+			}
+		}
 	}
 	return warnings
 }
diff --git a/pkg/api/persistentvolume/util.go b/pkg/api/persistentvolume/util.go
index 0b4dfbfb21b8f..b0a98e57b49d8 100644
--- a/pkg/api/persistentvolume/util.go
+++ b/pkg/api/persistentvolume/util.go
@@ -76,7 +76,7 @@ func warningsForPersistentVolumeSpecAndMeta(fieldPath *field.Path, pvSpec *api.P
 		termFldPath := fieldPath.Child("spec", "nodeAffinity", "required", "nodeSelectorTerms")
 		// use of deprecated node labels in node affinity
 		for i, term := range pvSpec.NodeAffinity.Required.NodeSelectorTerms {
-			warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term, termFldPath.Index(i))...)
+			warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term, false, termFldPath.Index(i))...)
 		}
 	}
 	// If we are on deprecated volume plugin
diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go
index 621438091b2fd..7cae53daad260 100644
--- a/pkg/api/pod/util.go
+++ b/pkg/api/pod/util.go
@@ -385,6 +385,7 @@ func GetValidationOptionsFromPodSpecAndMeta(podSpec, oldPodSpec *api.PodSpec, po
 		AllowNonLocalProjectedTokenPath:                   false,
 		AllowPodLifecycleSleepActionZeroValue:             utilfeature.DefaultFeatureGate.Enabled(features.PodLifecycleSleepActionAllowZero),
 		PodLevelResourcesEnabled:                          utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResources),
+		AllowInvalidLabelValueInRequiredNodeAffinity:      false,
 	}
 
 	// If old spec uses relaxed validation or enabled the RelaxedEnvironmentVariableValidation feature gate,
@@ -399,6 +400,7 @@ func GetValidationOptionsFromPodSpecAndMeta(podSpec, oldPodSpec *api.PodSpec, po
 		opts.AllowIndivisibleHugePagesValues = usesIndivisibleHugePagesValues(oldPodSpec)
 
 		opts.AllowInvalidLabelValueInSelector = hasInvalidLabelValueInAffinitySelector(oldPodSpec)
+		opts.AllowInvalidLabelValueInRequiredNodeAffinity = hasInvalidLabelValueInRequiredNodeAffinity(oldPodSpec)
 		// if old spec has invalid labelSelector in topologySpreadConstraint, we must allow it
 		opts.AllowInvalidTopologySpreadConstraintLabelSelector = hasInvalidTopologySpreadConstraintLabelSelector(oldPodSpec)
 		// if old spec has an invalid projected token volume path, we must allow it
@@ -1271,6 +1273,16 @@ func IsRestartableInitContainer(initContainer *api.Container) bool {
 	return *initContainer.RestartPolicy == api.ContainerRestartPolicyAlways
 }
 
+func hasInvalidLabelValueInRequiredNodeAffinity(spec *api.PodSpec) bool {
+	if spec == nil ||
+		spec.Affinity == nil ||
+		spec.Affinity.NodeAffinity == nil ||
+		spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil {
+		return false
+	}
+	return helper.HasInvalidLabelValueInNodeSelectorTerms(spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms)
+}
+
 func MarkPodProposedForResize(oldPod, newPod *api.Pod) {
 	if len(newPod.Spec.Containers) != len(oldPod.Spec.Containers) {
 		// Update is invalid: ignore changes and let validation handle it
diff --git a/pkg/api/pod/warnings.go b/pkg/api/pod/warnings.go
index 84fb25a64d02e..7ef6e99bdd779 100644
--- a/pkg/api/pod/warnings.go
+++ b/pkg/api/pod/warnings.go
@@ -96,12 +96,12 @@ func warningsForPodSpecAndMeta(fieldPath *field.Path, podSpec *api.PodSpec, meta
 		if n.RequiredDuringSchedulingIgnoredDuringExecution != nil {
 			termFldPath := fieldPath.Child("spec", "affinity", "nodeAffinity", "requiredDuringSchedulingIgnoredDuringExecution", "nodeSelectorTerms")
 			for i, term := range n.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms {
-				warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term, termFldPath.Index(i))...)
+				warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term, false, termFldPath.Index(i))...)
 			}
 		}
 		preferredFldPath := fieldPath.Child("spec", "affinity", "nodeAffinity", "preferredDuringSchedulingIgnoredDuringExecution")
 		for i, term := range n.PreferredDuringSchedulingIgnoredDuringExecution {
-			warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term.Preference, preferredFldPath.Index(i).Child("preference"))...)
+			warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term.Preference, true, preferredFldPath.Index(i).Child("preference"))...)
 		}
 	}
 	for i, t := range podSpec.TopologySpreadConstraints {
diff --git a/pkg/apis/core/helper/helpers.go b/pkg/apis/core/helper/helpers.go
index 0724e6378bf94..01913653083a8 100644
--- a/pkg/apis/core/helper/helpers.go
+++ b/pkg/apis/core/helper/helpers.go
@@ -500,3 +500,18 @@ func validFirstDigit(str string) bool {
 	}
 	return str[0] == '-' || (str[0] == '0' && str == "0") || (str[0] >= '1' && str[0] <= '9')
 }
+
+// HasInvalidLabelValueInNodeSelectorTerms checks if there's an invalid label value
+// in one NodeSelectorTerm's MatchExpression values
+func HasInvalidLabelValueInNodeSelectorTerms(terms []core.NodeSelectorTerm) bool {
+	for _, term := range terms {
+		for _, expression := range term.MatchExpressions {
+			for _, value := range expression.Values {
+				if len(validation.IsValidLabelValue(value)) > 0 {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go
index 694896ee750c6..89baa3d592e79 100644
--- a/pkg/apis/core/validation/validation.go
+++ b/pkg/apis/core/validation/validation.go
@@ -1777,6 +1777,8 @@ var allowedTemplateObjectMetaFields = map[string]bool{
 type PersistentVolumeSpecValidationOptions struct {
 	// Allow users to modify the class of volume attributes
 	EnableVolumeAttributesClass bool
+	// Allow invalid label-value in RequiredNodeSelector
+	AllowInvalidLabelValueInRequiredNodeAffinity bool
 }
 
 // ValidatePersistentVolumeName checks that a name is appropriate for a
@@ -1798,11 +1800,17 @@ var supportedVolumeModes = sets.New(core.PersistentVolumeBlock, core.PersistentV
 
 func ValidationOptionsForPersistentVolume(pv, oldPv *core.PersistentVolume) PersistentVolumeSpecValidationOptions {
 	opts := PersistentVolumeSpecValidationOptions{
-		EnableVolumeAttributesClass: utilfeature.DefaultMutableFeatureGate.Enabled(features.VolumeAttributesClass),
+		EnableVolumeAttributesClass:                  utilfeature.DefaultMutableFeatureGate.Enabled(features.VolumeAttributesClass),
+		AllowInvalidLabelValueInRequiredNodeAffinity: false,
 	}
 	if oldPv != nil && oldPv.Spec.VolumeAttributesClassName != nil {
 		opts.EnableVolumeAttributesClass = true
 	}
+	if oldPv != nil && oldPv.Spec.NodeAffinity != nil &&
+		oldPv.Spec.NodeAffinity.Required != nil {
+		terms := oldPv.Spec.NodeAffinity.Required.NodeSelectorTerms
+		opts.AllowInvalidLabelValueInRequiredNodeAffinity = helper.HasInvalidLabelValueInNodeSelectorTerms(terms)
+	}
 	return opts
 }
 
@@ -1874,7 +1882,7 @@ func ValidatePersistentVolumeSpec(pvSpec *core.PersistentVolumeSpec, pvName stri
 		if validateInlinePersistentVolumeSpec {
 			allErrs = append(allErrs, field.Forbidden(fldPath.Child("nodeAffinity"), "may not be specified in the context of inline volumes"))
 		} else {
-			nodeAffinitySpecified, errs = validateVolumeNodeAffinity(pvSpec.NodeAffinity, fldPath.Child("nodeAffinity"))
+			nodeAffinitySpecified, errs = validateVolumeNodeAffinity(pvSpec.NodeAffinity, opts, fldPath.Child("nodeAffinity"))
 			allErrs = append(allErrs, errs...)
 		}
 	}
@@ -3865,7 +3873,7 @@ func validateAffinity(affinity *core.Affinity, opts PodValidationOptions, fldPat
 
 	if affinity != nil {
 		if affinity.NodeAffinity != nil {
-			allErrs = append(allErrs, validateNodeAffinity(affinity.NodeAffinity, fldPath.Child("nodeAffinity"))...)
+			allErrs = append(allErrs, validateNodeAffinity(affinity.NodeAffinity, opts, fldPath.Child("nodeAffinity"))...)
 		}
 		if affinity.PodAffinity != nil {
 			allErrs = append(allErrs, validatePodAffinity(affinity.PodAffinity, opts.AllowInvalidLabelValueInSelector, fldPath.Child("podAffinity"))...)
@@ -4053,6 +4061,8 @@ type PodValidationOptions struct {
 	PodLevelResourcesEnabled bool
 	// Allow sidecar containers resize policy for backward compatibility
 	AllowSidecarResizePolicy bool
+	// Allow invalid label-value in RequiredNodeSelector
+	AllowInvalidLabelValueInRequiredNodeAffinity bool
 }
 
 // validatePodMetadataAndSpec tests if required fields in the pod.metadata and pod.spec are set,
@@ -4730,14 +4740,14 @@ func validatePodAntiAffinity(podAntiAffinity *core.PodAntiAffinity, allowInvalid
 }
 
 // validateNodeAffinity tests that the specified nodeAffinity fields have valid data
-func validateNodeAffinity(na *core.NodeAffinity, fldPath *field.Path) field.ErrorList {
+func validateNodeAffinity(na *core.NodeAffinity, opts PodValidationOptions, fldPath *field.Path) field.ErrorList {
 	allErrs := field.ErrorList{}
 	// TODO: Uncomment the next three lines once RequiredDuringSchedulingRequiredDuringExecution is implemented.
 	// if na.RequiredDuringSchedulingRequiredDuringExecution != nil {
 	//	allErrs = append(allErrs, ValidateNodeSelector(na.RequiredDuringSchedulingRequiredDuringExecution, fldPath.Child("requiredDuringSchedulingRequiredDuringExecution"))...)
 	// }
 	if na.RequiredDuringSchedulingIgnoredDuringExecution != nil {
-		allErrs = append(allErrs, ValidateNodeSelector(na.RequiredDuringSchedulingIgnoredDuringExecution, true /* TODO: opts.AllowInvalidLabelValueInRequiredNodeAffinity */, fldPath.Child("requiredDuringSchedulingIgnoredDuringExecution"))...)
+		allErrs = append(allErrs, ValidateNodeSelector(na.RequiredDuringSchedulingIgnoredDuringExecution, opts.AllowInvalidLabelValueInRequiredNodeAffinity, fldPath.Child("requiredDuringSchedulingIgnoredDuringExecution"))...)
 	}
 	if len(na.PreferredDuringSchedulingIgnoredDuringExecution) > 0 {
 		allErrs = append(allErrs, ValidatePreferredSchedulingTerms(na.PreferredDuringSchedulingIgnoredDuringExecution, fldPath.Child("preferredDuringSchedulingIgnoredDuringExecution"))...)
@@ -7783,7 +7793,7 @@ func ValidateLoadBalancerStatus(status *core.LoadBalancerStatus, fldPath *field.
 // returns:
 // - true if volumeNodeAffinity is set
 // - errorList if there are validation errors
-func validateVolumeNodeAffinity(nodeAffinity *core.VolumeNodeAffinity, fldPath *field.Path) (bool, field.ErrorList) {
+func validateVolumeNodeAffinity(nodeAffinity *core.VolumeNodeAffinity, opts PersistentVolumeSpecValidationOptions, fldPath *field.Path) (bool, field.ErrorList) {
 	allErrs := field.ErrorList{}
 
 	if nodeAffinity == nil {
@@ -7791,7 +7801,7 @@ func validateVolumeNodeAffinity(nodeAffinity *core.VolumeNodeAffinity, fldPath *
 	}
 
 	if nodeAffinity.Required != nil {
-		allErrs = append(allErrs, ValidateNodeSelector(nodeAffinity.Required, true /* TODO: opts.AllowInvalidLabelValueInRequiredNodeAffinity */, fldPath.Child("required"))...)
+		allErrs = append(allErrs, ValidateNodeSelector(nodeAffinity.Required, opts.AllowInvalidLabelValueInRequiredNodeAffinity, fldPath.Child("required"))...)
 	} else {
 		allErrs = append(allErrs, field.Required(fldPath.Child("required"), "must specify required node constraints"))
 	}


Test patch:
diff --git a/pkg/api/pod/util_test.go b/pkg/api/pod/util_test.go
index 2f53c8fdc6ee4..9773668240870 100644
--- a/pkg/api/pod/util_test.go
+++ b/pkg/api/pod/util_test.go
@@ -4204,3 +4204,77 @@ func TestValidateAllowSidecarResizePolicy(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateInvalidLabelValueInNodeSelectorOption(t *testing.T) {
+	testCases := []struct {
+		name       string
+		oldPodSpec *api.PodSpec
+		wantOption bool
+	}{
+		{
+			name:       "Create",
+			wantOption: false,
+		},
+		{
+			name: "UpdateInvalidLabelSelector",
+			oldPodSpec: &api.PodSpec{
+				Affinity: &api.Affinity{
+					NodeAffinity: &api.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{
+							NodeSelectorTerms: []api.NodeSelectorTerm{{
+								MatchExpressions: []api.NodeSelectorRequirement{{
+									Key:      "foo",
+									Operator: api.NodeSelectorOpIn,
+									Values:   []string{"-1"},
+								}},
+							}},
+						},
+					},
+				},
+			},
+			wantOption: true,
+		},
+		{
+			name: "UpdateValidLabelSelector",
+			oldPodSpec: &api.PodSpec{
+				Affinity: &api.Affinity{
+					NodeAffinity: &api.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{
+							NodeSelectorTerms: []api.NodeSelectorTerm{{
+								MatchExpressions: []api.NodeSelectorRequirement{{
+									Key:      "foo",
+									Operator: api.NodeSelectorOpIn,
+									Values:   []string{"bar"},
+								}},
+							}},
+						},
+					},
+				},
+			},
+			wantOption: false,
+		},
+		{
+			name: "UpdateEmptyLabelSelector",
+			oldPodSpec: &api.PodSpec{
+				Affinity: &api.Affinity{
+					NodeAffinity: &api.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{
+							NodeSelectorTerms: []api.NodeSelectorTerm{},
+						},
+					},
+				},
+			},
+			wantOption: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Pod meta doesn't impact the outcome.
+			gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.oldPodSpec, nil, nil)
+			if tc.wantOption != gotOptions.AllowInvalidLabelValueInRequiredNodeAffinity {
+				t.Errorf("Got AllowInvalidLabelValueInRequiredNodeAffinity=%t, want %t", gotOptions.AllowInvalidLabelValueInRequiredNodeAffinity, tc.wantOption)
+			}
+		})
+	}
+}
diff --git a/pkg/api/pod/warnings_test.go b/pkg/api/pod/warnings_test.go
index b285a4b9f094d..bea1b1df62acd 100644
--- a/pkg/api/pod/warnings_test.go
+++ b/pkg/api/pod/warnings_test.go
@@ -1711,6 +1711,62 @@ func TestWarnings(t *testing.T) {
 				`spec.containers[0].ports[1]: duplicate port name "test" with spec.initContainers[0].ports[0], services and probes that select ports by name will use spec.initContainers[0].ports[0]`,
 			},
 		},
+		{
+			name: "creating pod with invalid value in nodeaffinity",
+			template: &api.PodTemplateSpec{Spec: api.PodSpec{
+				Affinity: &api.Affinity{NodeAffinity: &api.NodeAffinity{
+					PreferredDuringSchedulingIgnoredDuringExecution: []api.PreferredSchedulingTerm{{
+						Weight: 10,
+						Preference: api.NodeSelectorTerm{
+							MatchExpressions: []api.NodeSelectorRequirement{{
+								Key:      "foo",
+								Operator: api.NodeSelectorOpIn,
+								Values:   []string{"-1"},
+							}},
+						},
+					}},
+				}},
+			}},
+			expected: []string{
+				`spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[0].values[0]: -1 is invalid, a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')`,
+			},
+		},
+		{
+			name: "updating pod with invalid value in nodeaffinity",
+			template: &api.PodTemplateSpec{Spec: api.PodSpec{
+				Affinity: &api.Affinity{NodeAffinity: &api.NodeAffinity{
+					PreferredDuringSchedulingIgnoredDuringExecution: []api.PreferredSchedulingTerm{{
+						Weight: 10,
+						Preference: api.NodeSelectorTerm{
+							MatchExpressions: []api.NodeSelectorRequirement{{
+								Key:      "foo",
+								Operator: api.NodeSelectorOpIn,
+								Values:   []string{"-1"},
+							}},
+						},
+					}},
+				}},
+				SchedulingGates: []api.PodSchedulingGate{{Name: "foo"}},
+			}},
+			oldTemplate: &api.PodTemplateSpec{Spec: api.PodSpec{
+				Affinity: &api.Affinity{NodeAffinity: &api.NodeAffinity{
+					PreferredDuringSchedulingIgnoredDuringExecution: []api.PreferredSchedulingTerm{{
+						Weight: 10,
+						Preference: api.NodeSelectorTerm{
+							MatchExpressions: []api.NodeSelectorRequirement{{
+								Key:      "foo",
+								Operator: api.NodeSelectorOpIn,
+								Values:   []string{"bar"},
+							}},
+						},
+					}},
+				}},
+				SchedulingGates: []api.PodSchedulingGate{{Name: "foo"}},
+			}},
+			expected: []string{
+				`spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[0].values[0]: -1 is invalid, a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')`,
+			},
+		},
 	}
 
 	for _, tc := range testcases {
diff --git a/pkg/apis/core/helper/helpers_test.go b/pkg/apis/core/helper/helpers_test.go
index 5f48b05b24483..f920cb38cc4ce 100644
--- a/pkg/apis/core/helper/helpers_test.go
+++ b/pkg/apis/core/helper/helpers_test.go
@@ -369,3 +369,48 @@ func TestIsServiceIPSet(t *testing.T) {
 		})
 	}
 }
+
+func TestHasInvalidLabelValueInNodeSelectorTerms(t *testing.T) {
+	testCases := []struct {
+		name   string
+		terms  []core.NodeSelectorTerm
+		expect bool
+	}{
+		{
+			name: "valid values",
+			terms: []core.NodeSelectorTerm{{
+				MatchExpressions: []core.NodeSelectorRequirement{{
+					Key:      "foo",
+					Operator: core.NodeSelectorOpIn,
+					Values:   []string{"far"},
+				}},
+			}},
+			expect: false,
+		},
+		{
+			name:   "empty terms",
+			terms:  []core.NodeSelectorTerm{},
+			expect: false,
+		},
+		{
+			name: "invalid label value",
+			terms: []core.NodeSelectorTerm{{
+				MatchExpressions: []core.NodeSelectorRequirement{{
+					Key:      "foo",
+					Operator: core.NodeSelectorOpIn,
+					Values:   []string{"-1"},
+				}},
+			}},
+			expect: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := HasInvalidLabelValueInNodeSelectorTerms(tc.terms)
+			if got != tc.expect {
+				t.Errorf("exepct %v, got %v", tc.expect, got)
+			}
+		})
+	}
+}
diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go
index 7c70af236e898..76e3a1dfb18e9 100644
--- a/pkg/apis/core/validation/validation_test.go
+++ b/pkg/apis/core/validation/validation_test.go
@@ -704,9 +704,28 @@ func TestValidatePersistentVolumeSpec(t *testing.T) {
 				MountOptions: []string{"soft", "read-write"},
 			},
 		},
+		"invalid-node-affinity": {
+			isExpectedFailure: true,
+			isInlineSpec:      false,
+			pvSpec: &core.PersistentVolumeSpec{
+				NodeAffinity: &core.VolumeNodeAffinity{
+					Required: &core.NodeSelector{
+						NodeSelectorTerms: []core.NodeSelectorTerm{{
+							MatchExpressions: []core.NodeSelectorRequirement{{
+								Key:      "foo",
+								Operator: core.NodeSelectorOpIn,
+								Values:   []string{"-1"},
+							}},
+						}},
+					},
+				},
+			},
+		},
 	}
 	for name, scenario := range scenarios {
-		opts := PersistentVolumeSpecValidationOptions{}
+		opts := ValidationOptionsForPersistentVolume(&core.PersistentVolume{
+			Spec: *scenario.pvSpec.DeepCopy(),
+		}, nil)
 		errs := ValidatePersistentVolumeSpec(scenario.pvSpec, "", scenario.isInlineSpec, field.NewPath("field"), opts)
 		if len(errs) == 0 && scenario.isExpectedFailure {
 			t.Errorf("Unexpected success for scenario: %s", name)
@@ -998,6 +1017,24 @@ func TestValidationOptionsForPersistentVolume(t *testing.T) {
 			enableVolumeAttributesClass: false,
 			expectValidationOpts:        PersistentVolumeSpecValidationOptions{EnableVolumeAttributesClass: true},
 		},
+		"old pv has invalid label-value in node affinity": {
+			oldPv: &core.PersistentVolume{
+				Spec: core.PersistentVolumeSpec{
+					NodeAffinity: &core.VolumeNodeAffinity{
+						Required: &core.NodeSelector{
+							NodeSelectorTerms: []core.NodeSelectorTerm{{
+								MatchExpressions: []core.NodeSelectorRequirement{{
+									Key:      "foo",
+									Operator: core.NodeSelectorOpIn,
+									Values:   []string{"-1"},
+								}},
+							}},
+						},
+					},
+				},
+			},
+			expectValidationOpts: PersistentVolumeSpecValidationOptions{AllowInvalidLabelValueInRequiredNodeAffinity: true},
+		},
 	}
 
 	for name, tc := range tests {
@@ -1464,6 +1501,11 @@ func TestValidateVolumeNodeAffinityUpdate(t *testing.T) {
 			oldPV:             testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")),
 			newPV:             testVolumeWithNodeAffinity(nil),
 		},
+		"old affinity already has invalid label-value": {
+			isExpectedFailure: false,
+			oldPV:             testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceType, "-1")),
+			newPV:             testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceTypeStable, "-1")),
+		},
 	}
 
 	for name, scenario := range scenarios {
@@ -12242,6 +12284,23 @@ func TestValidatePod(t *testing.T) {
 				podtest.SetAnnotations(map[string]string{core.PodDeletionCost: "+10"}),
 			),
 		},
+		"invalid required node affinity, value of NodeSelectorRequirement should be a valid label value": {
+			expectedError: "spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[0].values[0]: Invalid value: \"-1\": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')",
+			spec: *podtest.MakePod("123",
+				podtest.SetAffinity(&core.Affinity{
+					NodeAffinity: &core.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{
+							NodeSelectorTerms: []core.NodeSelectorTerm{{
+								MatchExpressions: []core.NodeSelectorRequirement{{
+									Key:      "foo",
+									Operator: core.NodeSelectorOpIn,
+									Values:   []string{"-1"},
+								}},
+							}},
+						}},
+				}),
+			),
+		},
 	}
 
 	for k, v := range errorCases {
@@ -12316,6 +12375,7 @@ func TestValidatePodUpdate(t *testing.T) {
 		old  core.Pod
 		new  core.Pod
 		err  string
+		opts PodValidationOptions
 	}{
 		{new: *podtest.MakePod(""), old: *podtest.MakePod(""), err: "", test: "nothing"}, {
 			new:  *podtest.MakePod("foo"),
@@ -13669,6 +13729,73 @@ func TestValidatePodUpdate(t *testing.T) {
 			err:  "pod updates may not change fields other than",
 			test: "the podAntiAffinity cannot be updated on gated pods",
 		},
+		{
+			old: *podtest.MakePod("foo",
+				podtest.SetAffinity(&core.Affinity{
+					NodeAffinity: &core.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{
+							NodeSelectorTerms: []core.NodeSelectorTerm{{
+								MatchExpressions: []core.NodeSelectorRequirement{{
+									Key:      "expr",
+									Operator: core.NodeSelectorOpIn,
+									Values:   []string{"-1"},
+								}},
+							}},
+						}},
+				}),
+				podtest.SetSchedulingGates(core.PodSchedulingGate{Name: "baz"}),
+			),
+			new: *podtest.MakePod("foo",
+				podtest.SetAffinity(&core.Affinity{
+					NodeAffinity: &core.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{
+							NodeSelectorTerms: []core.NodeSelectorTerm{{
+								MatchExpressions: []core.NodeSelectorRequirement{{
+									Key:      "expr",
+									Operator: core.NodeSelectorOpIn,
+									Values:   []string{"-1"},
+								}},
+							}},
+						}},
+				}),
+			),
+			test: "allow update pod if old pod already has invalid label-value in node affinity",
+			opts: PodValidationOptions{AllowInvalidLabelValueInRequiredNodeAffinity: true},
+		},
+		{
+			old: *podtest.MakePod("foo",
+				podtest.SetAffinity(&core.Affinity{
+					NodeAffinity: &core.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{
+							NodeSelectorTerms: []core.NodeSelectorTerm{{
+								MatchExpressions: []core.NodeSelectorRequirement{{
+									Key:      "expr",
+									Operator: core.NodeSelectorOpIn,
+									Values:   []string{"bar"},
+								}},
+							}},
+						}},
+				}),
+				podtest.SetSchedulingGates(core.PodSchedulingGate{Name: "baz"}),
+			),
+			new: *podtest.MakePod("foo",
+				podtest.SetAffinity(&core.Affinity{
+					NodeAffinity: &core.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{
+							NodeSelectorTerms: []core.NodeSelectorTerm{{
+								MatchExpressions: []core.NodeSelectorRequirement{{
+									Key:      "expr",
+									Operator: core.NodeSelectorOpIn,
+									Values:   []string{"-1"},
+								}},
+							}},
+						}},
+				}),
+				podtest.SetSchedulingGates(core.PodSchedulingGate{Name: "baz"}),
+			),
+			err:  `a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.'`,
+			test: "not allow update node affinity to an invalid label-value",
+		},
 		{
 			new: *podtest.MakePod("pod",
 				podtest.SetContainers(podtest.MakeContainer("container",
@@ -13730,7 +13857,7 @@ func TestValidatePodUpdate(t *testing.T) {
 			test.old.Spec.RestartPolicy = "Always"
 		}
 
-		errs := ValidatePodUpdate(&test.new, &test.old, PodValidationOptions{})
+		errs := ValidatePodUpdate(&test.new, &test.old, test.opts)
 		if test.err == "" {
 			if len(errs) != 0 {
 				t.Errorf("unexpected invalid: %s (%+v)\nA: %+v\nB: %+v", test.test, errs, test.new, test.old)