From b622363659b8f0ee7368557d7f9e7f73cad96aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=8E=AE=E6=96=87?= Date: Mon, 29 Sep 2025 18:25:25 +0800 Subject: [PATCH 1/3] add feature gate MutablePVNodeAffinity --- pkg/features/kube_features.go | 13 +++++++++++++ .../reference/versioned_feature_list.yaml | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index ce26a4ec706..855c99f7163 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -596,6 +596,13 @@ const ( // update the number of volumes that can be allocated on a node MutableCSINodeAllocatableCount featuregate.Feature = "MutableCSINodeAllocatableCount" + // owner: huww98 + // kep: https://kep.k8s.io/5381 + // + // Makes PersistentVolume.Spec.NodeAffinity mutable, allowing CSI drivers to + // update the topology info when the data is migrated + MutablePVNodeAffinity featuregate.Feature = "MutablePVNodeAffinity" + // owner: @kannon92 // kep: https://kep.k8s.io/5440 // @@ -1474,6 +1481,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.35"), Default: true, PreRelease: featuregate.Beta}, }, + MutablePVNodeAffinity: { + {Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Alpha}, + }, + MutablePodResourcesForSuspendedJobs: { {Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Alpha}, }, @@ -2234,6 +2245,8 @@ var defaultKubernetesFeatureGateDependencies = map[featuregate.Feature][]feature MutableCSINodeAllocatableCount: {}, + MutablePVNodeAffinity: {}, + MutablePodResourcesForSuspendedJobs: {}, MutableSchedulingDirectivesForSuspendedJobs: {}, diff --git a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml index 59c9e9ec01d..5682b3b7497 100644 --- a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml +++ b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml @@ -1071,6 +1071,12 @@ lockToDefault: false preRelease: Alpha version: "1.35" +- name: MutablePVNodeAffinity + versionedSpecs: + - default: false + lockToDefault: false + preRelease: Alpha + version: "1.35" - name: MutableSchedulingDirectivesForSuspendedJobs versionedSpecs: - default: false From 3882f0cf177984e941e6b7ae49eba0cd2e9bb623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=8E=AE=E6=96=87?= Date: Mon, 29 Sep 2025 18:31:24 +0800 Subject: [PATCH 2/3] allow PV.Spec.NodeAffinity update --- pkg/apis/core/validation/validation.go | 3 ++- pkg/apis/core/validation/validation_test.go | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index e0077f73d0b..55fa6e93510 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -2268,7 +2268,8 @@ func ValidatePersistentVolumeUpdate(newPv, oldPv *core.PersistentVolume, opts Pe allErrs = append(allErrs, ValidateImmutableField(newPv.Spec.VolumeMode, oldPv.Spec.VolumeMode, field.NewPath("volumeMode"))...) // Allow setting NodeAffinity if oldPv NodeAffinity was not set - if oldPv.Spec.NodeAffinity != nil { + if !utilfeature.DefaultFeatureGate.Enabled(features.MutablePVNodeAffinity) && + oldPv.Spec.NodeAffinity != nil { allErrs = append(allErrs, validatePvNodeAffinity(newPv.Spec.NodeAffinity, oldPv.Spec.NodeAffinity, field.NewPath("nodeAffinity"))...) } diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index db19836e802..b407f6747bc 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -1240,9 +1240,10 @@ func multipleVolumeNodeAffinity(terms [][]topologyPair) *core.VolumeNodeAffinity func TestValidateVolumeNodeAffinityUpdate(t *testing.T) { scenarios := map[string]struct { - isExpectedFailure bool - oldPV *core.PersistentVolume - newPV *core.PersistentVolume + mutablePVNodeAffinity bool + isExpectedFailure bool + oldPV *core.PersistentVolume + newPV *core.PersistentVolume }{ "nil-nothing-changed": { isExpectedFailure: false, @@ -1507,9 +1508,16 @@ func TestValidateVolumeNodeAffinityUpdate(t *testing.T) { oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceType, "-1")), newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity(v1.LabelInstanceTypeStable, "-1")), }, + "MutablePVNodeAffinity": { + mutablePVNodeAffinity: true, + isExpectedFailure: false, + oldPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "bar")), + newPV: testVolumeWithNodeAffinity(simpleVolumeNodeAffinity("foo", "baz")), + }, } for name, scenario := range scenarios { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MutablePVNodeAffinity, scenario.mutablePVNodeAffinity) originalNewPV := scenario.newPV.DeepCopy() originalOldPV := scenario.oldPV.DeepCopy() opts := ValidationOptionsForPersistentVolume(scenario.newPV, scenario.oldPV) From 78a8c2e6a35b5621a7afa9066770a819d417f707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=8E=AE=E6=96=87?= Date: Mon, 20 Oct 2025 21:22:02 +0800 Subject: [PATCH 3/3] mention MutablePVNodeAffinity in the API doc --- api/openapi-spec/swagger.json | 2 +- api/openapi-spec/v3/api__v1_openapi.json | 2 +- api/openapi-spec/v3/apis__storage.k8s.io__v1_openapi.json | 2 +- pkg/apis/core/types.go | 1 + pkg/generated/openapi/zz_generated.openapi.go | 2 +- staging/src/k8s.io/api/core/v1/generated.proto | 1 + staging/src/k8s.io/api/core/v1/types.go | 1 + staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go | 2 +- .../applyconfigurations/core/v1/persistentvolumespec.go | 1 + 9 files changed, 9 insertions(+), 5 deletions(-) diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 77ef3f8ccf5..97e1ac05a1b 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -9480,7 +9480,7 @@ }, "nodeAffinity": { "$ref": "#/definitions/io.k8s.api.core.v1.VolumeNodeAffinity", - "description": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume." + "description": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume. This field is mutable if MutablePVNodeAffinity feature gate is enabled." }, "persistentVolumeReclaimPolicy": { "description": "persistentVolumeReclaimPolicy defines what happens to a persistent volume when released from its claim. Valid options are Retain (default for manually created PersistentVolumes), Delete (default for dynamically provisioned PersistentVolumes), and Recycle (deprecated). Recycle must be supported by the volume plugin underlying this PersistentVolume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#reclaiming", diff --git a/api/openapi-spec/v3/api__v1_openapi.json b/api/openapi-spec/v3/api__v1_openapi.json index e272339ddea..fc73552a51b 100644 --- a/api/openapi-spec/v3/api__v1_openapi.json +++ b/api/openapi-spec/v3/api__v1_openapi.json @@ -5009,7 +5009,7 @@ "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeNodeAffinity" } ], - "description": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume." + "description": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume. This field is mutable if MutablePVNodeAffinity feature gate is enabled." }, "persistentVolumeReclaimPolicy": { "description": "persistentVolumeReclaimPolicy defines what happens to a persistent volume when released from its claim. Valid options are Retain (default for manually created PersistentVolumes), Delete (default for dynamically provisioned PersistentVolumes), and Recycle (deprecated). Recycle must be supported by the volume plugin underlying this PersistentVolume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#reclaiming", diff --git a/api/openapi-spec/v3/apis__storage.k8s.io__v1_openapi.json b/api/openapi-spec/v3/apis__storage.k8s.io__v1_openapi.json index 1f343a7ee12..6212f1d5363 100644 --- a/api/openapi-spec/v3/apis__storage.k8s.io__v1_openapi.json +++ b/api/openapi-spec/v3/apis__storage.k8s.io__v1_openapi.json @@ -800,7 +800,7 @@ "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeNodeAffinity" } ], - "description": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume." + "description": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume. This field is mutable if MutablePVNodeAffinity feature gate is enabled." }, "persistentVolumeReclaimPolicy": { "description": "persistentVolumeReclaimPolicy defines what happens to a persistent volume when released from its claim. Valid options are Retain (default for manually created PersistentVolumes), Delete (default for dynamically provisioned PersistentVolumes), and Recycle (deprecated). Recycle must be supported by the volume plugin underlying this PersistentVolume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#reclaiming", diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index a342146fcd3..64ca029b7b5 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -398,6 +398,7 @@ type PersistentVolumeSpec struct { VolumeMode *PersistentVolumeMode // NodeAffinity defines constraints that limit what nodes this volume can be accessed from. // This field influences the scheduling of pods that use this volume. + // This field is mutable if MutablePVNodeAffinity feature gate is enabled. // +optional NodeAffinity *VolumeNodeAffinity // Name of VolumeAttributesClass to which this persistent volume belongs. Empty value diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 4f8f1cf3ee0..01e3b5f0c26 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -28394,7 +28394,7 @@ func schema_k8sio_api_core_v1_PersistentVolumeSpec(ref common.ReferenceCallback) }, "nodeAffinity": { SchemaProps: spec.SchemaProps{ - Description: "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume.", + Description: "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume. This field is mutable if MutablePVNodeAffinity feature gate is enabled.", Ref: ref(corev1.VolumeNodeAffinity{}.OpenAPIModelName()), }, }, diff --git a/staging/src/k8s.io/api/core/v1/generated.proto b/staging/src/k8s.io/api/core/v1/generated.proto index f275a7d1b36..1862a7e229d 100644 --- a/staging/src/k8s.io/api/core/v1/generated.proto +++ b/staging/src/k8s.io/api/core/v1/generated.proto @@ -3598,6 +3598,7 @@ message PersistentVolumeSpec { // nodeAffinity defines constraints that limit what nodes this volume can be accessed from. // This field influences the scheduling of pods that use this volume. + // This field is mutable if MutablePVNodeAffinity feature gate is enabled. // +optional optional VolumeNodeAffinity nodeAffinity = 9; diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 6e63408f4c3..15045d331a8 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -427,6 +427,7 @@ type PersistentVolumeSpec struct { VolumeMode *PersistentVolumeMode `json:"volumeMode,omitempty" protobuf:"bytes,8,opt,name=volumeMode,casttype=PersistentVolumeMode"` // nodeAffinity defines constraints that limit what nodes this volume can be accessed from. // This field influences the scheduling of pods that use this volume. + // This field is mutable if MutablePVNodeAffinity feature gate is enabled. // +optional NodeAffinity *VolumeNodeAffinity `json:"nodeAffinity,omitempty" protobuf:"bytes,9,opt,name=nodeAffinity"` // Name of VolumeAttributesClass to which this persistent volume belongs. Empty value diff --git a/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go index 75bef7d522b..62db7faa420 100644 --- a/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/core/v1/types_swagger_doc_generated.go @@ -1584,7 +1584,7 @@ var map_PersistentVolumeSpec = map[string]string{ "storageClassName": "storageClassName is the name of StorageClass to which this persistent volume belongs. Empty value means that this volume does not belong to any StorageClass.", "mountOptions": "mountOptions is the list of mount options, e.g. [\"ro\", \"soft\"]. Not validated - mount will simply fail if one is invalid. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#mount-options", "volumeMode": "volumeMode defines if a volume is intended to be used with a formatted filesystem or to remain in raw block state. Value of Filesystem is implied when not included in spec.", - "nodeAffinity": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume.", + "nodeAffinity": "nodeAffinity defines constraints that limit what nodes this volume can be accessed from. This field influences the scheduling of pods that use this volume. This field is mutable if MutablePVNodeAffinity feature gate is enabled.", "volumeAttributesClassName": "Name of VolumeAttributesClass to which this persistent volume belongs. Empty value is not allowed. When this field is not set, it indicates that this volume does not belong to any VolumeAttributesClass. This field is mutable and can be changed by the CSI driver after a volume has been updated successfully to a new class. For an unbound PersistentVolume, the volumeAttributesClassName will be matched with unbound PersistentVolumeClaims during the binding process.", } diff --git a/staging/src/k8s.io/client-go/applyconfigurations/core/v1/persistentvolumespec.go b/staging/src/k8s.io/client-go/applyconfigurations/core/v1/persistentvolumespec.go index 7dc11740586..8c166102228 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/core/v1/persistentvolumespec.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/core/v1/persistentvolumespec.go @@ -58,6 +58,7 @@ type PersistentVolumeSpecApplyConfiguration struct { VolumeMode *corev1.PersistentVolumeMode `json:"volumeMode,omitempty"` // nodeAffinity defines constraints that limit what nodes this volume can be accessed from. // This field influences the scheduling of pods that use this volume. + // This field is mutable if MutablePVNodeAffinity feature gate is enabled. NodeAffinity *VolumeNodeAffinityApplyConfiguration `json:"nodeAffinity,omitempty"` // Name of VolumeAttributesClass to which this persistent volume belongs. Empty value // is not allowed. When this field is not set, it indicates that this volume does not belong to any