diff --git a/pkg/apis/storage/v1/zz_generated.validations.go b/pkg/apis/storage/v1/zz_generated.validations.go index 9666724d831..237a72b4d57 100644 --- a/pkg/apis/storage/v1/zz_generated.validations.go +++ b/pkg/apis/storage/v1/zz_generated.validations.go @@ -25,6 +25,7 @@ import ( context "context" fmt "fmt" + corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" equality "k8s.io/apimachinery/pkg/api/equality" operation "k8s.io/apimachinery/pkg/api/operation" @@ -106,7 +107,27 @@ func Validate_StorageClass(ctx context.Context, op operation.Operation, fldPath return }(fldPath.Child("parameters"), obj.Parameters, safe.Field(oldObj, func(oldObj *storagev1.StorageClass) map[string]string { return oldObj.Parameters }), oldObj != nil)...) - // field storagev1.StorageClass.ReclaimPolicy has no validation + // field storagev1.StorageClass.ReclaimPolicy + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *corev1.PersistentVolumeReclaimPolicy, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("reclaimPolicy"), obj.ReclaimPolicy, safe.Field(oldObj, func(oldObj *storagev1.StorageClass) *corev1.PersistentVolumeReclaimPolicy { + return oldObj.ReclaimPolicy + }), oldObj != nil)...) + // field storagev1.StorageClass.MountOptions has no validation // field storagev1.StorageClass.AllowVolumeExpansion has no validation // field storagev1.StorageClass.VolumeBindingMode has no validation diff --git a/pkg/apis/storage/v1beta1/zz_generated.validations.go b/pkg/apis/storage/v1beta1/zz_generated.validations.go index 864cf631f8c..b31c45ddcb0 100644 --- a/pkg/apis/storage/v1beta1/zz_generated.validations.go +++ b/pkg/apis/storage/v1beta1/zz_generated.validations.go @@ -25,6 +25,7 @@ import ( context "context" fmt "fmt" + v1 "k8s.io/api/core/v1" storagev1beta1 "k8s.io/api/storage/v1beta1" equality "k8s.io/apimachinery/pkg/api/equality" operation "k8s.io/apimachinery/pkg/api/operation" @@ -106,7 +107,27 @@ func Validate_StorageClass(ctx context.Context, op operation.Operation, fldPath return }(fldPath.Child("parameters"), obj.Parameters, safe.Field(oldObj, func(oldObj *storagev1beta1.StorageClass) map[string]string { return oldObj.Parameters }), oldObj != nil)...) - // field storagev1beta1.StorageClass.ReclaimPolicy has no validation + // field storagev1beta1.StorageClass.ReclaimPolicy + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *v1.PersistentVolumeReclaimPolicy, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("reclaimPolicy"), obj.ReclaimPolicy, safe.Field(oldObj, func(oldObj *storagev1beta1.StorageClass) *v1.PersistentVolumeReclaimPolicy { + return oldObj.ReclaimPolicy + }), oldObj != nil)...) + // field storagev1beta1.StorageClass.MountOptions has no validation // field storagev1beta1.StorageClass.AllowVolumeExpansion has no validation // field storagev1beta1.StorageClass.VolumeBindingMode has no validation diff --git a/pkg/apis/storage/validation/validation.go b/pkg/apis/storage/validation/validation.go index 565e1446d07..554b4e6d581 100644 --- a/pkg/apis/storage/validation/validation.go +++ b/pkg/apis/storage/validation/validation.go @@ -66,9 +66,7 @@ func ValidateStorageClassUpdate(storageClass, oldStorageClass *storage.StorageCl allErrs = append(allErrs, apivalidation.ValidateImmutableField(storageClass.Provisioner, oldStorageClass.Provisioner, field.NewPath("provisioner")).WithOrigin("immutable").MarkCoveredByDeclarative()...) - if *storageClass.ReclaimPolicy != *oldStorageClass.ReclaimPolicy { - allErrs = append(allErrs, field.Forbidden(field.NewPath("reclaimPolicy"), "updates to reclaimPolicy are forbidden.")) - } + allErrs = append(allErrs, apivalidation.ValidateImmutableField(storageClass.ReclaimPolicy, oldStorageClass.ReclaimPolicy, field.NewPath("reclaimPolicy")).WithOrigin("immutable").MarkCoveredByDeclarative()...) allErrs = append(allErrs, apivalidation.ValidateImmutableField(storageClass.VolumeBindingMode, oldStorageClass.VolumeBindingMode, field.NewPath("volumeBindingMode"))...) return allErrs diff --git a/pkg/registry/storage/storageclass/declarative_validation_test.go b/pkg/registry/storage/storageclass/declarative_validation_test.go index d55d99a160c..b2651f4bfc8 100644 --- a/pkg/registry/storage/storageclass/declarative_validation_test.go +++ b/pkg/registry/storage/storageclass/declarative_validation_test.go @@ -102,6 +102,13 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { field.Invalid(field.NewPath("parameters"), map[string]string{"new": "value"}, "field is immutable").WithOrigin("immutable"), }, }, + "invalid update reclaimPolicy changed": { + oldObj: mkValidStorageClass(), + updateObj: mkValidStorageClass(TweakReclaimPolicy(api.PersistentVolumeReclaimRetain)), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("reclaimPolicy"), api.PersistentVolumeReclaimRetain, "field is immutable").WithOrigin("immutable"), + }, + }, } for k, tc := range testCases { t.Run(k, func(t *testing.T) { @@ -149,3 +156,9 @@ func TweakParameters(parameters map[string]string) func(obj *storage.StorageClas obj.Parameters = parameters } } + +func TweakReclaimPolicy(reclaimPolicy api.PersistentVolumeReclaimPolicy) func(obj *storage.StorageClass) { + return func(obj *storage.StorageClass) { + obj.ReclaimPolicy = &reclaimPolicy + } +} diff --git a/staging/src/k8s.io/api/storage/v1/types.go b/staging/src/k8s.io/api/storage/v1/types.go index 0ef7e701bfa..975600d9cff 100644 --- a/staging/src/k8s.io/api/storage/v1/types.go +++ b/staging/src/k8s.io/api/storage/v1/types.go @@ -55,6 +55,7 @@ type StorageClass struct { // reclaimPolicy controls the reclaimPolicy for dynamically provisioned PersistentVolumes of this storage class. // Defaults to Delete. // +optional + // +k8s:immutable ReclaimPolicy *v1.PersistentVolumeReclaimPolicy `json:"reclaimPolicy,omitempty" protobuf:"bytes,4,opt,name=reclaimPolicy,casttype=k8s.io/api/core/v1.PersistentVolumeReclaimPolicy"` // mountOptions controls the mountOptions for dynamically provisioned PersistentVolumes of this storage class. diff --git a/staging/src/k8s.io/api/storage/v1beta1/types.go b/staging/src/k8s.io/api/storage/v1beta1/types.go index 70d995b8f74..9d46d0909af 100644 --- a/staging/src/k8s.io/api/storage/v1beta1/types.go +++ b/staging/src/k8s.io/api/storage/v1beta1/types.go @@ -57,6 +57,7 @@ type StorageClass struct { // reclaimPolicy controls the reclaimPolicy for dynamically provisioned PersistentVolumes of this storage class. // Defaults to Delete. // +optional + // +k8s:immutable ReclaimPolicy *v1.PersistentVolumeReclaimPolicy `json:"reclaimPolicy,omitempty" protobuf:"bytes,4,opt,name=reclaimPolicy,casttype=k8s.io/api/core/v1.PersistentVolumeReclaimPolicy"` // mountOptions controls the mountOptions for dynamically provisioned PersistentVolumes of this storage class.