Implement API changes necessary for recovery from resize failure

This commit is contained in:
Hemant Kumar
2021-11-12 11:00:57 -05:00
parent 7b9f4f18fe
commit 63fffd37b1
33 changed files with 1898 additions and 1019 deletions

View File

@@ -74,6 +74,21 @@ func EnforceDataSourceBackwardsCompatibility(pvcSpec, oldPVCSpec *core.Persisten
}
}
func DropDisabledFieldsFromStatus(pvc, oldPVC *core.PersistentVolumeClaim) {
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) && oldPVC.Status.Conditions == nil {
pvc.Status.Conditions = nil
}
if !utilfeature.DefaultFeatureGate.Enabled(features.RecoverVolumeExpansionFailure) {
if !allocatedResourcesInUse(oldPVC) {
pvc.Status.AllocatedResources = nil
}
if !resizeStatusInUse(oldPVC) {
pvc.Status.ResizeStatus = nil
}
}
}
func dataSourceInUse(oldPVCSpec *core.PersistentVolumeClaimSpec) bool {
if oldPVCSpec == nil {
return false
@@ -118,3 +133,25 @@ func NormalizeDataSources(pvcSpec *core.PersistentVolumeClaimSpec) {
pvcSpec.DataSource = pvcSpec.DataSourceRef.DeepCopy()
}
}
func resizeStatusInUse(oldPVC *core.PersistentVolumeClaim) bool {
if oldPVC == nil {
return false
}
if oldPVC.Status.ResizeStatus != nil {
return true
}
return false
}
func allocatedResourcesInUse(oldPVC *core.PersistentVolumeClaim) bool {
if oldPVC == nil {
return false
}
if oldPVC.Status.AllocatedResources != nil {
return true
}
return false
}

View File

@@ -22,6 +22,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/api/resource"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
@@ -288,3 +289,111 @@ func TestDataSourceRef(t *testing.T) {
})
}
}
func TestDropDisabledFieldsFromStatus(t *testing.T) {
tests := []struct {
name string
feature bool
pvc *core.PersistentVolumeClaim
oldPVC *core.PersistentVolumeClaim
expected *core.PersistentVolumeClaim
}{
{
name: "for:newPVC=hasAllocatedResource,oldPVC=doesnot,featuregate=false; should drop field",
feature: false,
pvc: withAllocatedResource("5G"),
oldPVC: getPVC(),
expected: getPVC(),
},
{
name: "for:newPVC=hasAllocatedResource,oldPVC=doesnot,featuregate=true; should keep field",
feature: true,
pvc: withAllocatedResource("5G"),
oldPVC: getPVC(),
expected: withAllocatedResource("5G"),
},
{
name: "for:newPVC=hasAllocatedResource,oldPVC=hasAllocatedResource,featuregate=true; should keep field",
feature: true,
pvc: withAllocatedResource("5G"),
oldPVC: withAllocatedResource("5G"),
expected: withAllocatedResource("5G"),
},
{
name: "for:newPVC=hasAllocatedResource,oldPVC=hasAllocatedResource,featuregate=false; should keep field",
feature: false,
pvc: withAllocatedResource("10G"),
oldPVC: withAllocatedResource("5G"),
expected: withAllocatedResource("10G"),
},
{
name: "for:newPVC=hasAllocatedResource,oldPVC=nil,featuregate=false; should drop field",
feature: false,
pvc: withAllocatedResource("5G"),
oldPVC: nil,
expected: getPVC(),
},
{
name: "for:newPVC=hasResizeStatus,oldPVC=nil, featuregate=false should drop field",
feature: false,
pvc: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
oldPVC: nil,
expected: getPVC(),
},
{
name: "for:newPVC=hasResizeStatus,oldPVC=doesnot,featuregate=true; should keep field",
feature: true,
pvc: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
oldPVC: getPVC(),
expected: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
},
{
name: "for:newPVC=hasResizeStatus,oldPVC=hasResizeStatus,featuregate=true; should keep field",
feature: true,
pvc: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
oldPVC: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
expected: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
},
{
name: "for:newPVC=hasResizeStatus,oldPVC=hasResizeStatus,featuregate=false; should keep field",
feature: false,
pvc: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
oldPVC: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
expected: withResizeStatus(core.PersistentVolumeClaimNodeExpansionFailed),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, test.feature)()
DropDisabledFieldsFromStatus(test.pvc, test.oldPVC)
if !reflect.DeepEqual(*test.expected, *test.pvc) {
t.Errorf("Unexpected change: %+v", cmp.Diff(test.expected, test.pvc))
}
})
}
}
func getPVC() *core.PersistentVolumeClaim {
return &core.PersistentVolumeClaim{}
}
func withAllocatedResource(q string) *core.PersistentVolumeClaim {
return &core.PersistentVolumeClaim{
Status: core.PersistentVolumeClaimStatus{
AllocatedResources: core.ResourceList{
core.ResourceStorage: resource.MustParse(q),
},
},
}
}
func withResizeStatus(status core.PersistentVolumeClaimResizeStatus) *core.PersistentVolumeClaim {
return &core.PersistentVolumeClaim{
Status: core.PersistentVolumeClaimStatus{
ResizeStatus: &status,
},
}
}

View File

@@ -924,6 +924,7 @@ func SetObjectDefaults_StatefulSet(in *v1.StatefulSet) {
corev1.SetDefaults_ResourceList(&a.Spec.Resources.Limits)
corev1.SetDefaults_ResourceList(&a.Spec.Resources.Requests)
corev1.SetDefaults_ResourceList(&a.Status.Capacity)
corev1.SetDefaults_ResourceList(&a.Status.AllocatedResources)
}
}

View File

@@ -478,6 +478,7 @@ func SetObjectDefaults_StatefulSet(in *v1beta1.StatefulSet) {
v1.SetDefaults_ResourceList(&a.Spec.Resources.Limits)
v1.SetDefaults_ResourceList(&a.Spec.Resources.Requests)
v1.SetDefaults_ResourceList(&a.Status.Capacity)
v1.SetDefaults_ResourceList(&a.Status.AllocatedResources)
}
}

View File

@@ -924,6 +924,7 @@ func SetObjectDefaults_StatefulSet(in *v1beta2.StatefulSet) {
v1.SetDefaults_ResourceList(&a.Spec.Resources.Limits)
v1.SetDefaults_ResourceList(&a.Spec.Resources.Requests)
v1.SetDefaults_ResourceList(&a.Status.Capacity)
v1.SetDefaults_ResourceList(&a.Status.AllocatedResources)
}
}

View File

@@ -430,6 +430,9 @@ type PersistentVolumeClaimSpec struct {
// +optional
Selector *metav1.LabelSelector
// Resources represents the minimum resources required
// If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
// that are lower than previous value but must still be higher than capacity recorded in the
// status field of the claim.
// +optional
Resources ResourceRequirements
// VolumeName is the binding reference to the PersistentVolume backing this
@@ -486,6 +489,26 @@ const (
PersistentVolumeClaimFileSystemResizePending PersistentVolumeClaimConditionType = "FileSystemResizePending"
)
// +enum
type PersistentVolumeClaimResizeStatus string
const (
// When expansion is complete, the empty string is set by resize controller or kubelet.
PersistentVolumeClaimNoExpansionInProgress PersistentVolumeClaimResizeStatus = ""
// State set when resize controller starts expanding the volume in control-plane
PersistentVolumeClaimControllerExpansionInProgress PersistentVolumeClaimResizeStatus = "ControllerExpansionInProgress"
// State set when expansion has failed in resize controller with a terminal error.
// Transient errors such as timeout should not set this status and should leave ResizeStatus
// unmodified, so as resize controller can resume the volume expansion.
PersistentVolumeClaimControllerExpansionFailed PersistentVolumeClaimResizeStatus = "ControllerExpansionFailed"
// State set when resize controller has finished expanding the volume but further expansion is needed on the node.
PersistentVolumeClaimNodeExpansionPending PersistentVolumeClaimResizeStatus = "NodeExpansionPending"
// State set when kubelet starts expanding the volume.
PersistentVolumeClaimNodeExpansionInProgress PersistentVolumeClaimResizeStatus = "NodeExpansionInProgress"
// State set when expansion has failed in kubelet with a terminal error. Transient errors don't set NodeExpansionFailed.
PersistentVolumeClaimNodeExpansionFailed PersistentVolumeClaimResizeStatus = "NodeExpansionFailed"
)
// PersistentVolumeClaimCondition represents the current condition of PV claim
type PersistentVolumeClaimCondition struct {
Type PersistentVolumeClaimConditionType
@@ -513,6 +536,24 @@ type PersistentVolumeClaimStatus struct {
Capacity ResourceList
// +optional
Conditions []PersistentVolumeClaimCondition
// The storage resource within AllocatedResources tracks the capacity allocated to a PVC. It may
// be larger than the actual capacity when a volume expansion operation is requested.
// For storage quota, the larger value from allocatedResources and PVC.spec.resources is used.
// If allocatedResources is not set, PVC.spec.resources alone is used for quota calculation.
// If a volume expansion capacity request is lowered, allocatedResources is only
// lowered if there are no expansion operations in progress and if the actual volume capacity
// is equal or lower than the requested capacity.
// This is an alpha field and requires enabling RecoverVolumeExpansionFailure feature.
// +featureGate=RecoverVolumeExpansionFailure
// +optional
AllocatedResources ResourceList
// ResizeStatus stores status of resize operation.
// ResizeStatus is not set by default but when expansion is complete resizeStatus is set to empty
// string by resize controller or kubelet.
// This is an alpha field and requires enabling RecoverVolumeExpansionFailure feature.
// +featureGate=RecoverVolumeExpansionFailure
// +optional
ResizeStatus *PersistentVolumeClaimResizeStatus
}
// PersistentVolumeAccessMode defines various access modes for PV.

View File

@@ -5171,6 +5171,8 @@ func autoConvert_v1_PersistentVolumeClaimStatus_To_core_PersistentVolumeClaimSta
out.AccessModes = *(*[]core.PersistentVolumeAccessMode)(unsafe.Pointer(&in.AccessModes))
out.Capacity = *(*core.ResourceList)(unsafe.Pointer(&in.Capacity))
out.Conditions = *(*[]core.PersistentVolumeClaimCondition)(unsafe.Pointer(&in.Conditions))
out.AllocatedResources = *(*core.ResourceList)(unsafe.Pointer(&in.AllocatedResources))
out.ResizeStatus = (*core.PersistentVolumeClaimResizeStatus)(unsafe.Pointer(in.ResizeStatus))
return nil
}
@@ -5184,6 +5186,8 @@ func autoConvert_core_PersistentVolumeClaimStatus_To_v1_PersistentVolumeClaimSta
out.AccessModes = *(*[]v1.PersistentVolumeAccessMode)(unsafe.Pointer(&in.AccessModes))
out.Capacity = *(*v1.ResourceList)(unsafe.Pointer(&in.Capacity))
out.Conditions = *(*[]v1.PersistentVolumeClaimCondition)(unsafe.Pointer(&in.Conditions))
out.AllocatedResources = *(*v1.ResourceList)(unsafe.Pointer(&in.AllocatedResources))
out.ResizeStatus = (*v1.PersistentVolumeClaimResizeStatus)(unsafe.Pointer(in.ResizeStatus))
return nil
}

View File

@@ -155,6 +155,7 @@ func SetObjectDefaults_PersistentVolumeClaim(in *v1.PersistentVolumeClaim) {
SetDefaults_ResourceList(&in.Spec.Resources.Limits)
SetDefaults_ResourceList(&in.Spec.Resources.Requests)
SetDefaults_ResourceList(&in.Status.Capacity)
SetDefaults_ResourceList(&in.Status.AllocatedResources)
}
func SetObjectDefaults_PersistentVolumeClaimList(in *v1.PersistentVolumeClaimList) {

View File

@@ -2020,20 +2020,26 @@ func ValidatePersistentVolumeStatusUpdate(newPv, oldPv *core.PersistentVolume) f
return allErrs
}
// PersistentVolumeClaimSpecValidationOptions contains the different settings for PersistentVolumeClaim validation
type PersistentVolumeClaimSpecValidationOptions struct {
// Allow spec to contain the "ReadWiteOncePod" access mode
AllowReadWriteOncePod bool
// Allow pvc expansion after PVC is created and bound to a PV
EnableExpansion bool
// Allow users to recover from previously failing expansion operation
EnableRecoverFromExpansionFailure bool
}
func ValidationOptionsForPersistentVolumeClaim(pvc, oldPvc *core.PersistentVolumeClaim) PersistentVolumeClaimSpecValidationOptions {
opts := PersistentVolumeClaimSpecValidationOptions{
AllowReadWriteOncePod: utilfeature.DefaultFeatureGate.Enabled(features.ReadWriteOncePod),
AllowReadWriteOncePod: utilfeature.DefaultFeatureGate.Enabled(features.ReadWriteOncePod),
EnableExpansion: utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes),
EnableRecoverFromExpansionFailure: utilfeature.DefaultFeatureGate.Enabled(features.RecoverVolumeExpansionFailure),
}
if oldPvc == nil {
// If there's no old PVC, use the options based solely on feature enablement
return opts
}
if helper.ContainsAccessMode(oldPvc.Spec.AccessModes, core.ReadWriteOncePod) {
// If the old object allowed "ReadWriteOncePod", continue to allow it in the new object
opts.AllowReadWriteOncePod = true
@@ -2173,7 +2179,7 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *core.PersistentVolumeCl
allErrs = append(allErrs, ValidateImmutableAnnotation(newPvc.ObjectMeta.Annotations[v1.BetaStorageClassAnnotation], oldPvc.ObjectMeta.Annotations[v1.BetaStorageClassAnnotation], v1.BetaStorageClassAnnotation, field.NewPath("metadata"))...)
}
if utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
if opts.EnableExpansion {
// lets make sure storage values are same.
if newPvc.Status.Phase == core.ClaimBound && newPvcClone.Spec.Resources.Requests != nil {
newPvcClone.Spec.Resources.Requests["storage"] = oldPvc.Spec.Resources.Requests["storage"] // +k8s:verify-mutation:reason=clone
@@ -2181,13 +2187,23 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *core.PersistentVolumeCl
oldSize := oldPvc.Spec.Resources.Requests["storage"]
newSize := newPvc.Spec.Resources.Requests["storage"]
statusSize := oldPvc.Status.Capacity["storage"]
if !apiequality.Semantic.DeepEqual(newPvcClone.Spec, oldPvcClone.Spec) {
specDiff := diff.ObjectDiff(newPvcClone.Spec, oldPvcClone.Spec)
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), fmt.Sprintf("spec is immutable after creation except resources.requests for bound claims\n%v", specDiff)))
}
if newSize.Cmp(oldSize) < 0 {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "resources", "requests", "storage"), "field can not be less than previous value"))
if !opts.EnableRecoverFromExpansionFailure {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "resources", "requests", "storage"), "field can not be less than previous value"))
} else {
// This validation permits reducing pvc requested size up to capacity recorded in pvc.status
// so that users can recover from volume expansion failure, but Kubernetes does not actually
// support volume shrinking
if newSize.Cmp(statusSize) <= 0 {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "resources", "requests", "storage"), "field can not be less than status.capacity"))
}
}
}
} else {
@@ -2220,8 +2236,15 @@ func validateStorageClassUpgrade(oldAnnotations, newAnnotations map[string]strin
(!newAnnotationExist || newScInAnnotation == oldSc) /* condition 4 */
}
var resizeStatusSet = sets.NewString(string(core.PersistentVolumeClaimNoExpansionInProgress),
string(core.PersistentVolumeClaimControllerExpansionInProgress),
string(core.PersistentVolumeClaimControllerExpansionFailed),
string(core.PersistentVolumeClaimNodeExpansionPending),
string(core.PersistentVolumeClaimNodeExpansionInProgress),
string(core.PersistentVolumeClaimNodeExpansionFailed))
// ValidatePersistentVolumeClaimStatusUpdate validates an update to status of a PersistentVolumeClaim
func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *core.PersistentVolumeClaim) field.ErrorList {
func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *core.PersistentVolumeClaim, validationOpts PersistentVolumeClaimSpecValidationOptions) field.ErrorList {
allErrs := ValidateObjectMetaUpdate(&newPvc.ObjectMeta, &oldPvc.ObjectMeta, field.NewPath("metadata"))
if len(newPvc.ResourceVersion) == 0 {
allErrs = append(allErrs, field.Required(field.NewPath("resourceVersion"), ""))
@@ -2229,10 +2252,32 @@ func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *core.PersistentVo
if len(newPvc.Spec.AccessModes) == 0 {
allErrs = append(allErrs, field.Required(field.NewPath("Spec", "accessModes"), ""))
}
capPath := field.NewPath("status", "capacity")
for r, qty := range newPvc.Status.Capacity {
allErrs = append(allErrs, validateBasicResource(qty, capPath.Key(string(r)))...)
}
if validationOpts.EnableRecoverFromExpansionFailure {
resizeStatusPath := field.NewPath("status", "resizeStatus")
if newPvc.Status.ResizeStatus != nil {
resizeStatus := *newPvc.Status.ResizeStatus
if !resizeStatusSet.Has(string(resizeStatus)) {
allErrs = append(allErrs, field.NotSupported(resizeStatusPath, resizeStatus, resizeStatusSet.List()))
}
}
allocPath := field.NewPath("status", "allocatedResources")
for r, qty := range newPvc.Status.AllocatedResources {
if r != core.ResourceStorage {
allErrs = append(allErrs, field.NotSupported(allocPath, r, []string{string(core.ResourceStorage)}))
continue
}
if errs := validateBasicResource(qty, allocPath.Key(string(r))); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else {
allErrs = append(allErrs, ValidateResourceQuantityValue(string(core.ResourceStorage), qty, allocPath.Key(string(r)))...)
}
}
}
return allErrs
}

View File

@@ -1862,12 +1862,97 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
},
VolumeName: "volume",
})
validClaimShrinkInitial := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
core.ReadOnlyMany,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceName(core.ResourceStorage): resource.MustParse("15G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimBound,
Capacity: core.ResourceList{
core.ResourceStorage: resource.MustParse("10G"),
},
})
unboundShrink := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
core.ReadOnlyMany,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceName(core.ResourceStorage): resource.MustParse("12G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimPending,
Capacity: core.ResourceList{
core.ResourceStorage: resource.MustParse("10G"),
},
})
validClaimShrink := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
core.ReadOnlyMany,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceStorage: resource.MustParse("13G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimBound,
Capacity: core.ResourceList{
core.ResourceStorage: resource.MustParse("10G"),
},
})
invalidShrinkToStatus := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
core.ReadOnlyMany,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceStorage: resource.MustParse("10G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimBound,
Capacity: core.ResourceList{
core.ResourceStorage: resource.MustParse("10G"),
},
})
invalidClaimShrink := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
core.ReadOnlyMany,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceStorage: resource.MustParse("3G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimBound,
Capacity: core.ResourceList{
core.ResourceStorage: resource.MustParse("10G"),
},
})
scenarios := map[string]struct {
isExpectedFailure bool
oldClaim *core.PersistentVolumeClaim
newClaim *core.PersistentVolumeClaim
enableResize bool
isExpectedFailure bool
oldClaim *core.PersistentVolumeClaim
newClaim *core.PersistentVolumeClaim
enableResize bool
enableRecoverFromExpansion bool
}{
"valid-update-volumeName-only": {
isExpectedFailure: false,
@@ -2037,12 +2122,53 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
newClaim: validClaimRWOPAccessModeAddAnnotation,
enableResize: false,
},
"valid-expand-shrink-resize-enabled": {
oldClaim: validClaimShrinkInitial,
newClaim: validClaimShrink,
enableResize: true,
enableRecoverFromExpansion: true,
},
"invalid-expand-shrink-resize-enabled": {
oldClaim: validClaimShrinkInitial,
newClaim: invalidClaimShrink,
enableResize: true,
enableRecoverFromExpansion: true,
isExpectedFailure: true,
},
"invalid-expand-shrink-to-status-resize-enabled": {
oldClaim: validClaimShrinkInitial,
newClaim: invalidShrinkToStatus,
enableResize: true,
enableRecoverFromExpansion: true,
isExpectedFailure: true,
},
"invalid-expand-shrink-recover-disabled": {
oldClaim: validClaimShrinkInitial,
newClaim: validClaimShrink,
enableResize: true,
enableRecoverFromExpansion: false,
isExpectedFailure: true,
},
"invalid-expand-shrink-resize-disabled": {
oldClaim: validClaimShrinkInitial,
newClaim: validClaimShrink,
enableResize: false,
enableRecoverFromExpansion: true,
isExpectedFailure: true,
},
"unbound-size-shrink-resize-enabled": {
oldClaim: validClaimShrinkInitial,
newClaim: unboundShrink,
enableResize: true,
enableRecoverFromExpansion: true,
isExpectedFailure: true,
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
// ensure we have a resource version specified for updates
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExpandPersistentVolumes, scenario.enableResize)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, scenario.enableRecoverFromExpansion)()
scenario.oldClaim.ResourceVersion = "1"
scenario.newClaim.ResourceVersion = "1"
opts := ValidationOptionsForPersistentVolumeClaim(scenario.newClaim, scenario.oldClaim)
@@ -2067,35 +2193,45 @@ func TestValidationOptionsForPersistentVolumeClaim(t *testing.T) {
oldPvc: nil,
enableReadWriteOncePod: true,
expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{
AllowReadWriteOncePod: true,
AllowReadWriteOncePod: true,
EnableExpansion: true,
EnableRecoverFromExpansionFailure: false,
},
},
"rwop allowed because feature enabled": {
oldPvc: pvcWithAccessModes([]core.PersistentVolumeAccessMode{core.ReadWriteOnce}),
enableReadWriteOncePod: true,
expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{
AllowReadWriteOncePod: true,
AllowReadWriteOncePod: true,
EnableExpansion: true,
EnableRecoverFromExpansionFailure: false,
},
},
"rwop not allowed because not used and feature disabled": {
oldPvc: pvcWithAccessModes([]core.PersistentVolumeAccessMode{core.ReadWriteOnce}),
enableReadWriteOncePod: false,
expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{
AllowReadWriteOncePod: false,
AllowReadWriteOncePod: false,
EnableExpansion: true,
EnableRecoverFromExpansionFailure: false,
},
},
"rwop allowed because used and feature enabled": {
oldPvc: pvcWithAccessModes([]core.PersistentVolumeAccessMode{core.ReadWriteOncePod}),
enableReadWriteOncePod: true,
expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{
AllowReadWriteOncePod: true,
AllowReadWriteOncePod: true,
EnableExpansion: true,
EnableRecoverFromExpansionFailure: false,
},
},
"rwop allowed because used and feature disabled": {
oldPvc: pvcWithAccessModes([]core.PersistentVolumeAccessMode{core.ReadWriteOncePod}),
enableReadWriteOncePod: false,
expectValidationOpts: PersistentVolumeClaimSpecValidationOptions{
AllowReadWriteOncePod: true,
AllowReadWriteOncePod: true,
EnableExpansion: true,
EnableRecoverFromExpansionFailure: false,
},
},
}
@@ -15771,11 +15907,86 @@ func TestValidatePersistentVolumeClaimStatusUpdate(t *testing.T) {
{Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue},
},
})
validAllocatedResources := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
core.ReadOnlyMany,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceName(core.ResourceStorage): resource.MustParse("10G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimPending,
Conditions: []core.PersistentVolumeClaimCondition{
{Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue},
},
AllocatedResources: core.ResourceList{
core.ResourceName(core.ResourceStorage): resource.MustParse("10G"),
},
})
invalidAllocatedResources := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
core.ReadOnlyMany,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceName(core.ResourceStorage): resource.MustParse("10G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimPending,
Conditions: []core.PersistentVolumeClaimCondition{
{Type: core.PersistentVolumeClaimResizing, Status: core.ConditionTrue},
},
AllocatedResources: core.ResourceList{
core.ResourceName(core.ResourceStorage): resource.MustParse("-10G"),
},
})
noStoraegeClaimStatus := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
},
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceName(core.ResourceStorage): resource.MustParse("10G"),
},
},
}, core.PersistentVolumeClaimStatus{
Phase: core.ClaimPending,
AllocatedResources: core.ResourceList{
core.ResourceName(core.ResourceCPU): resource.MustParse("10G"),
},
})
progressResizeStatus := core.PersistentVolumeClaimControllerExpansionInProgress
invalidResizeStatus := core.PersistentVolumeClaimResizeStatus("foo")
validResizeStatusPVC := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
},
}, core.PersistentVolumeClaimStatus{
ResizeStatus: &progressResizeStatus,
})
invalidResizeStatusPVC := testVolumeClaimWithStatus("foo", "ns", core.PersistentVolumeClaimSpec{
AccessModes: []core.PersistentVolumeAccessMode{
core.ReadWriteOnce,
},
}, core.PersistentVolumeClaimStatus{
ResizeStatus: &invalidResizeStatus,
})
scenarios := map[string]struct {
isExpectedFailure bool
oldClaim *core.PersistentVolumeClaim
newClaim *core.PersistentVolumeClaim
enableResize bool
isExpectedFailure bool
oldClaim *core.PersistentVolumeClaim
newClaim *core.PersistentVolumeClaim
enableResize bool
enableRecoverFromExpansion bool
}{
"condition-update-with-enabled-feature-gate": {
isExpectedFailure: false,
@@ -15783,13 +15994,51 @@ func TestValidatePersistentVolumeClaimStatusUpdate(t *testing.T) {
newClaim: validConditionUpdate,
enableResize: true,
},
"status-update-with-valid-allocatedResources-feature-enabled": {
isExpectedFailure: false,
oldClaim: validClaim,
newClaim: validAllocatedResources,
enableResize: true,
enableRecoverFromExpansion: true,
},
"status-update-with-invalid-allocatedResources-feature-enabled": {
isExpectedFailure: true,
oldClaim: validClaim,
newClaim: invalidAllocatedResources,
enableResize: true,
enableRecoverFromExpansion: true,
},
"status-update-with-no-storage-update": {
isExpectedFailure: true,
oldClaim: validClaim,
newClaim: noStoraegeClaimStatus,
enableResize: true,
enableRecoverFromExpansion: true,
},
"status-update-with-valid-pvc-resize-status": {
isExpectedFailure: false,
oldClaim: validClaim,
newClaim: validResizeStatusPVC,
enableResize: true,
enableRecoverFromExpansion: true,
},
"status-update-with-invalid-pvc-resize-status": {
isExpectedFailure: true,
oldClaim: validClaim,
newClaim: invalidResizeStatusPVC,
enableResize: true,
enableRecoverFromExpansion: true,
},
}
for name, scenario := range scenarios {
t.Run(name, func(t *testing.T) {
validateOpts := PersistentVolumeClaimSpecValidationOptions{
EnableRecoverFromExpansionFailure: scenario.enableRecoverFromExpansion,
}
// ensure we have a resource version specified for updates
scenario.oldClaim.ResourceVersion = "1"
scenario.newClaim.ResourceVersion = "1"
errs := ValidatePersistentVolumeClaimStatusUpdate(scenario.newClaim, scenario.oldClaim)
errs := ValidatePersistentVolumeClaimStatusUpdate(scenario.newClaim, scenario.oldClaim, validateOpts)
if len(errs) == 0 && scenario.isExpectedFailure {
t.Errorf("Unexpected success for scenario: %s", name)
}

View File

@@ -2977,6 +2977,18 @@ func (in *PersistentVolumeClaimStatus) DeepCopyInto(out *PersistentVolumeClaimSt
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.AllocatedResources != nil {
in, out := &in.AllocatedResources, &out.AllocatedResources
*out = make(ResourceList, len(*in))
for key, val := range *in {
(*out)[key] = val.DeepCopy()
}
}
if in.ResizeStatus != nil {
in, out := &in.ResizeStatus, &out.ResizeStatus
*out = new(PersistentVolumeClaimResizeStatus)
**out = **in
}
return
}

View File

@@ -803,6 +803,13 @@ const (
// Honor Persistent Volume Reclaim Policy when it is "Delete" irrespective of PV-PVC
// deletion ordering.
HonorPVReclaimPolicy featuregate.Feature = "HonorPVReclaimPolicy"
// owner: @gnufied
// kep: http://kep.k8s.io/1790
// alpha: v1.23
//
// Allow users to recover from volume expansion failure
RecoverVolumeExpansionFailure featuregate.Feature = "RecoverVolumeExpansionFailure"
)
func init() {
@@ -920,6 +927,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
IdentifyPodOS: {Default: false, PreRelease: featuregate.Alpha},
PodAndContainerStatsFromCRI: {Default: false, PreRelease: featuregate.Alpha},
HonorPVReclaimPolicy: {Default: false, PreRelease: featuregate.Alpha},
RecoverVolumeExpansionFailure: {Default: false, PreRelease: featuregate.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:

View File

@@ -27,12 +27,10 @@ import (
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme"
pvcutil "k8s.io/kubernetes/pkg/api/persistentvolumeclaim"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/features"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
@@ -66,7 +64,6 @@ func (persistentvolumeclaimStrategy) GetResetFields() map[fieldpath.APIVersion]*
func (persistentvolumeclaimStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
pvc := obj.(*api.PersistentVolumeClaim)
pvc.Status = api.PersistentVolumeClaimStatus{}
pvcutil.DropDisabledFields(&pvc.Spec)
// For data sources, we need to do 2 things to implement KEP 1495
@@ -153,16 +150,17 @@ func (persistentvolumeclaimStatusStrategy) GetResetFields() map[fieldpath.APIVer
// PrepareForUpdate sets the Spec field which is not allowed to be changed when updating a PV's Status
func (persistentvolumeclaimStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newPv := obj.(*api.PersistentVolumeClaim)
oldPv := old.(*api.PersistentVolumeClaim)
newPv.Spec = oldPv.Spec
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) && oldPv.Status.Conditions == nil {
newPv.Status.Conditions = nil
}
newPVC := obj.(*api.PersistentVolumeClaim)
oldPVC := old.(*api.PersistentVolumeClaim)
newPVC.Spec = oldPVC.Spec
pvcutil.DropDisabledFieldsFromStatus(newPVC, oldPVC)
}
func (persistentvolumeclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidatePersistentVolumeClaimStatusUpdate(obj.(*api.PersistentVolumeClaim), old.(*api.PersistentVolumeClaim))
newPvc := obj.(*api.PersistentVolumeClaim)
oldPvc := old.(*api.PersistentVolumeClaim)
opts := validation.ValidationOptionsForPersistentVolumeClaim(newPvc, oldPvc)
return validation.ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc, opts)
}
// WarningsOnUpdate returns warnings for the given update.