Implement API changes necessary for recovery from resize failure
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
1
pkg/apis/apps/v1/zz_generated.defaults.go
generated
1
pkg/apis/apps/v1/zz_generated.defaults.go
generated
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
1
pkg/apis/apps/v1beta1/zz_generated.defaults.go
generated
1
pkg/apis/apps/v1beta1/zz_generated.defaults.go
generated
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
1
pkg/apis/apps/v1beta2/zz_generated.defaults.go
generated
1
pkg/apis/apps/v1beta2/zz_generated.defaults.go
generated
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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.
|
||||
|
4
pkg/apis/core/v1/zz_generated.conversion.go
generated
4
pkg/apis/core/v1/zz_generated.conversion.go
generated
@@ -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
|
||||
}
|
||||
|
||||
|
1
pkg/apis/core/v1/zz_generated.defaults.go
generated
1
pkg/apis/core/v1/zz_generated.defaults.go
generated
@@ -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) {
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
12
pkg/apis/core/zz_generated.deepcopy.go
generated
12
pkg/apis/core/zz_generated.deepcopy.go
generated
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user