mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 07:20:13 +00:00
Implement necessary API changes
Introduce feature gate for expanding PVs Add a field to SC Add new Conditions and feature tag pvc update Add tests for size update via feature gate register the resize admission plugin Update golint failures
This commit is contained in:
parent
034c40be6f
commit
e78d433150
@ -37,6 +37,7 @@ go_library(
|
||||
"//plugin/pkg/admission/namespace/exists:go_default_library",
|
||||
"//plugin/pkg/admission/noderestriction:go_default_library",
|
||||
"//plugin/pkg/admission/persistentvolume/label:go_default_library",
|
||||
"//plugin/pkg/admission/persistentvolume/resize:go_default_library",
|
||||
"//plugin/pkg/admission/podnodeselector:go_default_library",
|
||||
"//plugin/pkg/admission/podpreset:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction:go_default_library",
|
||||
|
@ -41,6 +41,7 @@ import (
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/namespace/exists"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/noderestriction"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/resize"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/podnodeselector"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/podpreset"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction"
|
||||
@ -81,4 +82,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
serviceaccount.Register(plugins)
|
||||
setdefault.Register(plugins)
|
||||
webhook.Register(plugins)
|
||||
resize.Register(plugins)
|
||||
}
|
||||
|
@ -190,6 +190,7 @@ pkg/controller/volume/attachdetach
|
||||
pkg/controller/volume/attachdetach/statusupdater
|
||||
pkg/controller/volume/attachdetach/testing
|
||||
pkg/controller/volume/events
|
||||
pkg/controller/volume/expand
|
||||
pkg/controller/volume/persistentvolume
|
||||
pkg/controller/volume/persistentvolume/options
|
||||
pkg/credentialprovider
|
||||
|
@ -547,6 +547,27 @@ type PersistentVolumeClaimSpec struct {
|
||||
StorageClassName *string
|
||||
}
|
||||
|
||||
type PersistentVolumeClaimConditionType string
|
||||
|
||||
// These are valid conditions of Pvc
|
||||
const (
|
||||
// An user trigger resize of pvc has been started
|
||||
PersistentVolumeClaimResizing PersistentVolumeClaimConditionType = "Resizing"
|
||||
)
|
||||
|
||||
type PersistentVolumeClaimCondition struct {
|
||||
Type PersistentVolumeClaimConditionType
|
||||
Status ConditionStatus
|
||||
// +optional
|
||||
LastProbeTime metav1.Time
|
||||
// +optional
|
||||
LastTransitionTime metav1.Time
|
||||
// +optional
|
||||
Reason string
|
||||
// +optional
|
||||
Message string
|
||||
}
|
||||
|
||||
type PersistentVolumeClaimStatus struct {
|
||||
// Phase represents the current phase of PersistentVolumeClaim
|
||||
// +optional
|
||||
@ -557,6 +578,8 @@ type PersistentVolumeClaimStatus struct {
|
||||
// Represents the actual resources of the underlying volume
|
||||
// +optional
|
||||
Capacity ResourceList
|
||||
// +optional
|
||||
Conditions []PersistentVolumeClaimCondition
|
||||
}
|
||||
|
||||
type PersistentVolumeAccessMode string
|
||||
|
@ -1588,10 +1588,31 @@ func ValidatePersistentVolumeClaimUpdate(newPvc, oldPvc *api.PersistentVolumeCla
|
||||
oldPvc.Spec.VolumeName = newPvc.Spec.VolumeName
|
||||
defer func() { oldPvc.Spec.VolumeName = "" }()
|
||||
}
|
||||
// changes to Spec are not allowed, but updates to label/and some annotations are OK.
|
||||
// no-op updates pass validation.
|
||||
if !apiequality.Semantic.DeepEqual(newPvc.Spec, oldPvc.Spec) {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "field is immutable after creation"))
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
||||
newPVCSpecCopy := newPvc.Spec.DeepCopy()
|
||||
|
||||
// lets make sure storage values are same.
|
||||
if newPvc.Status.Phase == api.ClaimBound && newPVCSpecCopy.Resources.Requests != nil {
|
||||
newPVCSpecCopy.Resources.Requests["storage"] = oldPvc.Spec.Resources.Requests["storage"]
|
||||
}
|
||||
|
||||
oldSize := oldPvc.Spec.Resources.Requests["storage"]
|
||||
newSize := newPvc.Spec.Resources.Requests["storage"]
|
||||
|
||||
if !apiequality.Semantic.DeepEqual(*newPVCSpecCopy, oldPvc.Spec) {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "is immutable after creation except resources.requests for bound claims"))
|
||||
}
|
||||
if newSize.Cmp(oldSize) < 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "resources", "requests", "storage"), "field can not be less than previous value"))
|
||||
}
|
||||
|
||||
} else {
|
||||
// changes to Spec are not allowed, but updates to label/and some annotations are OK.
|
||||
// no-op updates pass validation.
|
||||
if !apiequality.Semantic.DeepEqual(newPvc.Spec, oldPvc.Spec) {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "field is immutable after creation"))
|
||||
}
|
||||
}
|
||||
|
||||
// storageclass annotation should be immutable after creation
|
||||
@ -1611,6 +1632,10 @@ func ValidatePersistentVolumeClaimStatusUpdate(newPvc, oldPvc *api.PersistentVol
|
||||
if len(newPvc.Spec.AccessModes) == 0 {
|
||||
allErrs = append(allErrs, field.Required(field.NewPath("Spec", "accessModes"), ""))
|
||||
}
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) && len(newPvc.Status.Conditions) > 0 {
|
||||
conditionPath := field.NewPath("status", "conditions")
|
||||
allErrs = append(allErrs, field.Forbidden(conditionPath, "invalid field"))
|
||||
}
|
||||
capPath := field.NewPath("status", "capacity")
|
||||
for r, qty := range newPvc.Status.Capacity {
|
||||
allErrs = append(allErrs, validateBasicResource(qty, capPath.Key(string(r)))...)
|
||||
|
@ -530,6 +530,17 @@ func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeCla
|
||||
}
|
||||
}
|
||||
|
||||
func testVolumeClaimWithStatus(
|
||||
name, namespace string,
|
||||
spec api.PersistentVolumeClaimSpec,
|
||||
status api.PersistentVolumeClaimStatus) *api.PersistentVolumeClaim {
|
||||
return &api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
Spec: spec,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
func testVolumeClaimStorageClass(name string, namespace string, annval string, spec api.PersistentVolumeClaimSpec) *api.PersistentVolumeClaim {
|
||||
annotations := map[string]string{
|
||||
v1.BetaStorageClassAnnotation: annval,
|
||||
@ -728,7 +739,7 @@ func TestValidatePersistentVolumeClaim(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
|
||||
validClaim := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
|
||||
validClaim := testVolumeClaimWithStatus("foo", "ns", api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{
|
||||
api.ReadWriteOnce,
|
||||
api.ReadOnlyMany,
|
||||
@ -738,7 +749,10 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
|
||||
},
|
||||
},
|
||||
}, api.PersistentVolumeClaimStatus{
|
||||
Phase: api.ClaimBound,
|
||||
})
|
||||
|
||||
validClaimStorageClass := testVolumeClaimStorageClass("foo", "ns", "fast", api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{
|
||||
api.ReadOnlyMany,
|
||||
@ -828,50 +842,125 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
|
||||
},
|
||||
VolumeName: "volume",
|
||||
})
|
||||
validSizeUpdate := testVolumeClaimWithStatus("foo", "ns", api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{
|
||||
api.ReadWriteOnce,
|
||||
api.ReadOnlyMany,
|
||||
},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("15G"),
|
||||
},
|
||||
},
|
||||
}, api.PersistentVolumeClaimStatus{
|
||||
Phase: api.ClaimBound,
|
||||
})
|
||||
|
||||
invalidSizeUpdate := testVolumeClaimWithStatus("foo", "ns", api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{
|
||||
api.ReadWriteOnce,
|
||||
api.ReadOnlyMany,
|
||||
},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("5G"),
|
||||
},
|
||||
},
|
||||
}, api.PersistentVolumeClaimStatus{
|
||||
Phase: api.ClaimBound,
|
||||
})
|
||||
|
||||
unboundSizeUpdate := testVolumeClaimWithStatus("foo", "ns", api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{
|
||||
api.ReadWriteOnce,
|
||||
api.ReadOnlyMany,
|
||||
},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("12G"),
|
||||
},
|
||||
},
|
||||
}, api.PersistentVolumeClaimStatus{
|
||||
Phase: api.ClaimPending,
|
||||
})
|
||||
|
||||
scenarios := map[string]struct {
|
||||
isExpectedFailure bool
|
||||
oldClaim *api.PersistentVolumeClaim
|
||||
newClaim *api.PersistentVolumeClaim
|
||||
enableResize bool
|
||||
}{
|
||||
"valid-update-volumeName-only": {
|
||||
isExpectedFailure: false,
|
||||
oldClaim: validClaim,
|
||||
newClaim: validUpdateClaim,
|
||||
enableResize: false,
|
||||
},
|
||||
"valid-no-op-update": {
|
||||
isExpectedFailure: false,
|
||||
oldClaim: validUpdateClaim,
|
||||
newClaim: validUpdateClaim,
|
||||
enableResize: false,
|
||||
},
|
||||
"invalid-update-change-resources-on-bound-claim": {
|
||||
isExpectedFailure: true,
|
||||
oldClaim: validUpdateClaim,
|
||||
newClaim: invalidUpdateClaimResources,
|
||||
enableResize: false,
|
||||
},
|
||||
"invalid-update-change-access-modes-on-bound-claim": {
|
||||
isExpectedFailure: true,
|
||||
oldClaim: validUpdateClaim,
|
||||
newClaim: invalidUpdateClaimAccessModes,
|
||||
enableResize: false,
|
||||
},
|
||||
"invalid-update-change-storage-class-annotation-after-creation": {
|
||||
isExpectedFailure: true,
|
||||
oldClaim: validClaimStorageClass,
|
||||
newClaim: invalidUpdateClaimStorageClass,
|
||||
enableResize: false,
|
||||
},
|
||||
"valid-update-mutable-annotation": {
|
||||
isExpectedFailure: false,
|
||||
oldClaim: validClaimAnnotation,
|
||||
newClaim: validUpdateClaimMutableAnnotation,
|
||||
enableResize: false,
|
||||
},
|
||||
"valid-update-add-annotation": {
|
||||
isExpectedFailure: false,
|
||||
oldClaim: validClaim,
|
||||
newClaim: validAddClaimAnnotation,
|
||||
enableResize: false,
|
||||
},
|
||||
"valid-size-update-resize-disabled": {
|
||||
isExpectedFailure: true,
|
||||
oldClaim: validClaim,
|
||||
newClaim: validSizeUpdate,
|
||||
enableResize: false,
|
||||
},
|
||||
"valid-size-update-resize-enabled": {
|
||||
isExpectedFailure: false,
|
||||
oldClaim: validClaim,
|
||||
newClaim: validSizeUpdate,
|
||||
enableResize: true,
|
||||
},
|
||||
"invalid-size-update-resize-enabled": {
|
||||
isExpectedFailure: true,
|
||||
oldClaim: validClaim,
|
||||
newClaim: invalidSizeUpdate,
|
||||
enableResize: true,
|
||||
},
|
||||
"unbound-size-update-resize-enabled": {
|
||||
isExpectedFailure: true,
|
||||
oldClaim: validClaim,
|
||||
newClaim: unboundSizeUpdate,
|
||||
enableResize: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, scenario := range scenarios {
|
||||
// ensure we have a resource version specified for updates
|
||||
togglePVExpandFeature(scenario.enableResize, t)
|
||||
scenario.oldClaim.ResourceVersion = "1"
|
||||
scenario.newClaim.ResourceVersion = "1"
|
||||
errs := ValidatePersistentVolumeClaimUpdate(scenario.newClaim, scenario.oldClaim)
|
||||
@ -884,6 +973,23 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func togglePVExpandFeature(toggleFlag bool, t *testing.T) {
|
||||
if toggleFlag {
|
||||
// Enable alpha feature LocalStorageCapacityIsolation
|
||||
err := utilfeature.DefaultFeatureGate.Set("ExpandPersistentVolumes=true")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to enable feature gate for ExpandPersistentVolumes: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := utilfeature.DefaultFeatureGate.Set("ExpandPersistentVolumes=false")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to disable feature gate for ExpandPersistentVolumes: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateKeyToPath(t *testing.T) {
|
||||
testCases := []struct {
|
||||
kp api.KeyToPath
|
||||
@ -9232,6 +9338,68 @@ func TestValidateLimitRange(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestValidatePersistentVolumeClaimStatusUpdate(t *testing.T) {
|
||||
validClaim := testVolumeClaim("foo", "ns", api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{
|
||||
api.ReadWriteOnce,
|
||||
api.ReadOnlyMany,
|
||||
},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
|
||||
},
|
||||
},
|
||||
})
|
||||
validConditionUpdate := testVolumeClaimWithStatus("foo", "ns", api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []api.PersistentVolumeAccessMode{
|
||||
api.ReadWriteOnce,
|
||||
api.ReadOnlyMany,
|
||||
},
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: api.ResourceList{
|
||||
api.ResourceName(api.ResourceStorage): resource.MustParse("10G"),
|
||||
},
|
||||
},
|
||||
}, api.PersistentVolumeClaimStatus{
|
||||
Phase: api.ClaimPending,
|
||||
Conditions: []api.PersistentVolumeClaimCondition{
|
||||
{Type: api.PersistentVolumeClaimResizing, Status: api.ConditionTrue},
|
||||
},
|
||||
})
|
||||
scenarios := map[string]struct {
|
||||
isExpectedFailure bool
|
||||
oldClaim *api.PersistentVolumeClaim
|
||||
newClaim *api.PersistentVolumeClaim
|
||||
enableResize bool
|
||||
}{
|
||||
"condition-update-with-disabled-feature-gate": {
|
||||
isExpectedFailure: true,
|
||||
oldClaim: validClaim,
|
||||
newClaim: validConditionUpdate,
|
||||
enableResize: false,
|
||||
},
|
||||
"condition-update-with-enabled-feature-gate": {
|
||||
isExpectedFailure: false,
|
||||
oldClaim: validClaim,
|
||||
newClaim: validConditionUpdate,
|
||||
enableResize: true,
|
||||
},
|
||||
}
|
||||
for name, scenario := range scenarios {
|
||||
// ensure we have a resource version specified for updates
|
||||
togglePVExpandFeature(scenario.enableResize, t)
|
||||
scenario.oldClaim.ResourceVersion = "1"
|
||||
scenario.newClaim.ResourceVersion = "1"
|
||||
errs := ValidatePersistentVolumeClaimStatusUpdate(scenario.newClaim, scenario.oldClaim)
|
||||
if len(errs) == 0 && scenario.isExpectedFailure {
|
||||
t.Errorf("Unexpected success for scenario: %s", name)
|
||||
}
|
||||
if len(errs) > 0 && !scenario.isExpectedFailure {
|
||||
t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateResourceQuota(t *testing.T) {
|
||||
spec := api.ResourceQuotaSpec{
|
||||
Hard: api.ResourceList{
|
||||
|
@ -59,6 +59,12 @@ type StorageClass struct {
|
||||
// PersistentVolumes of this storage class are created with
|
||||
// +optional
|
||||
MountOptions []string
|
||||
|
||||
// AllowVolumeExpansion shows whether the storage class allow volume expand
|
||||
// If the field is nil or not set, it would amount to expansion disabled
|
||||
// for all PVs created from this storageclass.
|
||||
// +optional
|
||||
AllowVolumeExpansion *bool
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
@ -13,9 +13,11 @@ go_library(
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/validation:go_default_library",
|
||||
"//pkg/apis/storage:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@ -27,6 +29,7 @@ go_test(
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/apis/storage:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -23,9 +23,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
apivalidation "k8s.io/kubernetes/pkg/api/validation"
|
||||
"k8s.io/kubernetes/pkg/apis/storage"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
// ValidateStorageClass validates a StorageClass.
|
||||
@ -34,6 +36,7 @@ func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList {
|
||||
allErrs = append(allErrs, validateProvisioner(storageClass.Provisioner, field.NewPath("provisioner"))...)
|
||||
allErrs = append(allErrs, validateParameters(storageClass.Parameters, field.NewPath("parameters"))...)
|
||||
allErrs = append(allErrs, validateReclaimPolicy(storageClass.ReclaimPolicy, field.NewPath("reclaimPolicy"))...)
|
||||
allErrs = append(allErrs, validateAllowVolumeExpansion(storageClass.AllowVolumeExpansion, field.NewPath("allowVolumeExpansion"))...)
|
||||
|
||||
return allErrs
|
||||
}
|
||||
@ -108,3 +111,13 @@ func validateReclaimPolicy(reclaimPolicy *api.PersistentVolumeReclaimPolicy, fld
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateAllowVolumeExpansion tests that if ExpandPersistentVolumes feature gate is disabled, whether the AllowVolumeExpansion filed
|
||||
// of storage class is set
|
||||
func validateAllowVolumeExpansion(allowExpand *bool, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if allowExpand != nil && !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath, "field is disabled by feature-gate ExpandPersistentVolumes"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/storage"
|
||||
)
|
||||
@ -123,3 +124,36 @@ func TestValidateStorageClass(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphaExpandPersistentVolumesFeatureValidation(t *testing.T) {
|
||||
deleteReclaimPolicy := api.PersistentVolumeReclaimPolicy("Delete")
|
||||
falseVar := false
|
||||
testSC := &storage.StorageClass{
|
||||
// empty parameters
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
||||
Provisioner: "kubernetes.io/foo-provisioner",
|
||||
Parameters: map[string]string{},
|
||||
ReclaimPolicy: &deleteReclaimPolicy,
|
||||
AllowVolumeExpansion: &falseVar,
|
||||
}
|
||||
|
||||
// Enable alpha feature ExpandPersistentVolumes
|
||||
err := utilfeature.DefaultFeatureGate.Set("ExpandPersistentVolumes=true")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to enable feature gate for ExpandPersistentVolumes: %v", err)
|
||||
return
|
||||
}
|
||||
if errs := ValidateStorageClass(testSC); len(errs) != 0 {
|
||||
t.Errorf("expected success: %v", errs)
|
||||
}
|
||||
// Disable alpha feature ExpandPersistentVolumes
|
||||
err = utilfeature.DefaultFeatureGate.Set("ExpandPersistentVolumes=false")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to disable feature gate for ExpandPersistentVolumes: %v", err)
|
||||
return
|
||||
}
|
||||
if errs := ValidateStorageClass(testSC); len(errs) == 0 {
|
||||
t.Errorf("expected failure, but got no error")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -114,6 +114,11 @@ const (
|
||||
// New local storage types to support local storage capacity isolation
|
||||
LocalStorageCapacityIsolation utilfeature.Feature = "LocalStorageCapacityIsolation"
|
||||
|
||||
// owner: @gnufied
|
||||
// alpha: v1.8
|
||||
// Ability to Expand persistent volumes
|
||||
ExpandPersistentVolumes utilfeature.Feature = "ExpandPersistentVolumes"
|
||||
|
||||
// owner: @verb
|
||||
// alpha: v1.8
|
||||
//
|
||||
@ -179,6 +184,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
|
||||
EnableEquivalenceClassCache: {Default: false, PreRelease: utilfeature.Alpha},
|
||||
TaintNodesByCondition: {Default: false, PreRelease: utilfeature.Alpha},
|
||||
MountPropagation: {Default: false, PreRelease: utilfeature.Alpha},
|
||||
ExpandPersistentVolumes: {Default: false, PreRelease: utilfeature.Alpha},
|
||||
|
||||
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
|
||||
// unintentionally on either side:
|
||||
|
@ -45,6 +45,7 @@ const (
|
||||
FailedAttachVolume = "FailedAttachVolume"
|
||||
FailedDetachVolume = "FailedDetachVolume"
|
||||
FailedMountVolume = "FailedMount"
|
||||
VolumeResizeFailed = "VolumeResizeFailed"
|
||||
FailedUnMountVolume = "FailedUnMount"
|
||||
WarnAlreadyMountedVolume = "AlreadyMountedVolume"
|
||||
SuccessfulDetachVolume = "SuccessfulDetachVolume"
|
||||
|
@ -25,6 +25,7 @@ go_library(
|
||||
"//pkg/api/helper/qos:go_default_library",
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/api/validation:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//pkg/kubeapiserver/admission/util:go_default_library",
|
||||
"//pkg/quota:go_default_library",
|
||||
"//pkg/quota/generic:go_default_library",
|
||||
@ -36,6 +37,7 @@ go_library(
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//vendor/k8s.io/client-go/informers:go_default_library",
|
||||
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
|
||||
],
|
||||
|
@ -27,11 +27,13 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/helper"
|
||||
k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/kubeapiserver/admission/util"
|
||||
"k8s.io/kubernetes/pkg/quota"
|
||||
"k8s.io/kubernetes/pkg/quota/generic"
|
||||
@ -147,6 +149,10 @@ func (p *pvcEvaluator) Handles(a admission.Attributes) bool {
|
||||
if op == admission.Create {
|
||||
return true
|
||||
}
|
||||
if op == admission.Update && utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
||||
return true
|
||||
}
|
||||
|
||||
updateUninitialized, err := util.IsUpdatingUninitializedObject(a)
|
||||
if err != nil {
|
||||
// fail closed, will try to give an evaluation.
|
||||
|
@ -21,9 +21,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/storage"
|
||||
"k8s.io/kubernetes/pkg/apis/storage/validation"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
// storageClassStrategy implements behavior for StorageClass objects
|
||||
@ -42,7 +44,11 @@ func (storageClassStrategy) NamespaceScoped() bool {
|
||||
|
||||
// ResetBeforeCreate clears the Status field which is not allowed to be set by end users on creation.
|
||||
func (storageClassStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
|
||||
_ = obj.(*storage.StorageClass)
|
||||
class := obj.(*storage.StorageClass)
|
||||
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
||||
class.AllowVolumeExpansion = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (storageClassStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
|
||||
@ -60,8 +66,13 @@ func (storageClassStrategy) AllowCreateOnUpdate() bool {
|
||||
|
||||
// PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a PV
|
||||
func (storageClassStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
|
||||
_ = obj.(*storage.StorageClass)
|
||||
_ = old.(*storage.StorageClass)
|
||||
newClass := obj.(*storage.StorageClass)
|
||||
oldClass := old.(*storage.StorageClass)
|
||||
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
||||
newClass.AllowVolumeExpansion = nil
|
||||
oldClass.AllowVolumeExpansion = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (storageClassStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||
|
55
plugin/pkg/admission/persistentvolume/resize/BUILD
Normal file
55
plugin/pkg/admission/persistentvolume/resize/BUILD
Normal file
@ -0,0 +1,55 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/apis/storage:go_default_library",
|
||||
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/helper:go_default_library",
|
||||
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
|
||||
"//pkg/client/listers/core/internalversion:go_default_library",
|
||||
"//pkg/client/listers/storage/internalversion:go_default_library",
|
||||
"//pkg/kubeapiserver/admission:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
146
plugin/pkg/admission/persistentvolume/resize/admission.go
Normal file
146
plugin/pkg/admission/persistentvolume/resize/admission.go
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
apihelper "k8s.io/kubernetes/pkg/api/helper"
|
||||
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
|
||||
pvlister "k8s.io/kubernetes/pkg/client/listers/core/internalversion"
|
||||
storagelisters "k8s.io/kubernetes/pkg/client/listers/storage/internalversion"
|
||||
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
|
||||
)
|
||||
|
||||
const (
|
||||
// PluginName is the name of pvc resize admission plugin
|
||||
PluginName = "PersistentVolumeClaimResize"
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
plugin := newPlugin()
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ admission.Interface = &persistentVolumeClaimResize{}
|
||||
var _ = kubeapiserveradmission.WantsInternalKubeInformerFactory(&persistentVolumeClaimResize{})
|
||||
|
||||
type persistentVolumeClaimResize struct {
|
||||
*admission.Handler
|
||||
|
||||
pvLister pvlister.PersistentVolumeLister
|
||||
scLister storagelisters.StorageClassLister
|
||||
}
|
||||
|
||||
func newPlugin() *persistentVolumeClaimResize {
|
||||
return &persistentVolumeClaimResize{
|
||||
Handler: admission.NewHandler(admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
func (pvcr *persistentVolumeClaimResize) SetInternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
pvcInformer := f.Core().InternalVersion().PersistentVolumes()
|
||||
pvcr.pvLister = pvcInformer.Lister()
|
||||
scInformer := f.Storage().InternalVersion().StorageClasses()
|
||||
pvcr.scLister = scInformer.Lister()
|
||||
pvcr.SetReadyFunc(func() bool {
|
||||
return pvcInformer.Informer().HasSynced() && scInformer.Informer().HasSynced()
|
||||
})
|
||||
}
|
||||
|
||||
// Validate ensures lister is set.
|
||||
func (pvcr *persistentVolumeClaimResize) Validate() error {
|
||||
if pvcr.pvLister == nil {
|
||||
return fmt.Errorf("missing persistent volume lister")
|
||||
}
|
||||
if pvcr.scLister == nil {
|
||||
return fmt.Errorf("missing storageclass lister")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pvcr *persistentVolumeClaimResize) Admit(a admission.Attributes) error {
|
||||
if a.GetResource().GroupResource() != api.Resource("persistentvolumeclaims") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a.GetSubresource()) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pvc, ok := a.GetObject().(*api.PersistentVolumeClaim)
|
||||
// if we can't convert then we don't handle this object so just return
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
oldPvc, ok := a.GetOldObject().(*api.PersistentVolumeClaim)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Growing Persistent volumes is only allowed for PVCs for which their StorageClass
|
||||
// explicitly allows it
|
||||
if !pvcr.allowResize(pvc, oldPvc) {
|
||||
return admission.NewForbidden(a, fmt.Errorf("only dynamically provisioned pvc can be resized and "+
|
||||
"the storageclass that provisions the pvc must support resize"))
|
||||
}
|
||||
|
||||
// volume plugin must support resize
|
||||
pv, err := pvcr.pvLister.Get(pvc.Spec.VolumeName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !pvcr.checkVolumePlugin(pv) {
|
||||
return admission.NewForbidden(a, fmt.Errorf("volume plugin does not support resize"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Growing Persistent volumes is only allowed for PVCs for which their StorageClass
|
||||
// explicitly allows it.
|
||||
func (pvcr *persistentVolumeClaimResize) allowResize(pvc, oldPvc *api.PersistentVolumeClaim) bool {
|
||||
pvcStorageClass := apihelper.GetPersistentVolumeClaimClass(pvc)
|
||||
oldPvcStorageClass := apihelper.GetPersistentVolumeClaimClass(oldPvc)
|
||||
if pvcStorageClass == "" || oldPvcStorageClass == "" || pvcStorageClass != oldPvcStorageClass {
|
||||
return false
|
||||
}
|
||||
sc, err := pvcr.scLister.Get(pvcStorageClass)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if sc.AllowVolumeExpansion != nil {
|
||||
return *sc.AllowVolumeExpansion
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkVolumePlugin checks whether the volume plugin supports resize
|
||||
func (pvcr *persistentVolumeClaimResize) checkVolumePlugin(pv *api.PersistentVolume) bool {
|
||||
if pv.Spec.Glusterfs != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
265
plugin/pkg/admission/persistentvolume/resize/admission_test.go
Normal file
265
plugin/pkg/admission/persistentvolume/resize/admission_test.go
Normal file
@ -0,0 +1,265 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/storage"
|
||||
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
)
|
||||
|
||||
func getResourceList(storage string) api.ResourceList {
|
||||
res := api.ResourceList{}
|
||||
if storage != "" {
|
||||
res[api.ResourceStorage] = resource.MustParse(storage)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestPVCResizeAdmission(t *testing.T) {
|
||||
goldClassName := "gold"
|
||||
trueVal := true
|
||||
falseVar := false
|
||||
goldClass := &storage.StorageClass{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StorageClass",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: goldClassName,
|
||||
},
|
||||
Provisioner: "kubernetes.io/glusterfs",
|
||||
AllowVolumeExpansion: &trueVal,
|
||||
}
|
||||
silverClassName := "silver"
|
||||
silverClass := &storage.StorageClass{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "StorageClass",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: silverClassName,
|
||||
},
|
||||
Provisioner: "kubernetes.io/glusterfs",
|
||||
AllowVolumeExpansion: &falseVar,
|
||||
}
|
||||
expectNoError := func(err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
expectDynamicallyProvisionedError := func(err error) bool {
|
||||
return strings.Contains(err.Error(), "only dynamically provisioned pvc can be resized and "+
|
||||
"the storageclass that provisions the pvc must support resize")
|
||||
}
|
||||
expectVolumePluginError := func(err error) bool {
|
||||
return strings.Contains(err.Error(), "volume plugin does not support resize")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
resource schema.GroupVersionResource
|
||||
subresource string
|
||||
oldObj runtime.Object
|
||||
newObj runtime.Object
|
||||
|
||||
checkError func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "pvc-resize, update, no error",
|
||||
resource: api.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
|
||||
oldObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume1",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("1Gi"),
|
||||
},
|
||||
StorageClassName: &goldClassName,
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("1Gi"),
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume1",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("2Gi"),
|
||||
},
|
||||
StorageClassName: &goldClassName,
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("2Gi"),
|
||||
},
|
||||
},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "pvc-resize, update, volume plugin error",
|
||||
resource: api.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
|
||||
oldObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume2",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("1Gi"),
|
||||
},
|
||||
StorageClassName: &goldClassName,
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("1Gi"),
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume2",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("2Gi"),
|
||||
},
|
||||
StorageClassName: &goldClassName,
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("2Gi"),
|
||||
},
|
||||
},
|
||||
checkError: expectVolumePluginError,
|
||||
},
|
||||
{
|
||||
name: "pvc-resize, update, dynamically provisioned error",
|
||||
resource: api.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
|
||||
oldObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume3",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("1Gi"),
|
||||
},
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("1Gi"),
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume3",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("2Gi"),
|
||||
},
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("2Gi"),
|
||||
},
|
||||
},
|
||||
checkError: expectDynamicallyProvisionedError,
|
||||
},
|
||||
{
|
||||
name: "pvc-resize, update, dynamically provisioned error",
|
||||
resource: api.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
|
||||
oldObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume4",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("1Gi"),
|
||||
},
|
||||
StorageClassName: &silverClassName,
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("1Gi"),
|
||||
},
|
||||
},
|
||||
newObj: &api.PersistentVolumeClaim{
|
||||
Spec: api.PersistentVolumeClaimSpec{
|
||||
VolumeName: "volume4",
|
||||
Resources: api.ResourceRequirements{
|
||||
Requests: getResourceList("2Gi"),
|
||||
},
|
||||
StorageClassName: &silverClassName,
|
||||
},
|
||||
Status: api.PersistentVolumeClaimStatus{
|
||||
Capacity: getResourceList("2Gi"),
|
||||
},
|
||||
},
|
||||
checkError: expectDynamicallyProvisionedError,
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := newPlugin()
|
||||
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
|
||||
ctrl.SetInternalKubeInformerFactory(informerFactory)
|
||||
err := ctrl.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("neither pv lister nor storageclass lister can be nil")
|
||||
}
|
||||
|
||||
pv1 := &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "volume1"},
|
||||
Spec: api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: api.PersistentVolumeSource{
|
||||
Glusterfs: &api.GlusterfsVolumeSource{
|
||||
EndpointsName: "http://localhost:8080/",
|
||||
Path: "/heketi",
|
||||
ReadOnly: false,
|
||||
},
|
||||
},
|
||||
StorageClassName: goldClassName,
|
||||
},
|
||||
}
|
||||
pv2 := &api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "volume2"},
|
||||
Spec: api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: api.PersistentVolumeSource{
|
||||
HostPath: &api.HostPathVolumeSource{},
|
||||
},
|
||||
StorageClassName: goldClassName,
|
||||
},
|
||||
}
|
||||
|
||||
pvs := []*api.PersistentVolume{}
|
||||
pvs = append(pvs, pv1, pv2)
|
||||
|
||||
for _, pv := range pvs {
|
||||
err := informerFactory.Core().InternalVersion().PersistentVolumes().Informer().GetStore().Add(pv)
|
||||
if err != nil {
|
||||
fmt.Println("add pv error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
scs := []*storage.StorageClass{}
|
||||
scs = append(scs, goldClass, silverClass)
|
||||
for _, sc := range scs {
|
||||
err := informerFactory.Storage().InternalVersion().StorageClasses().Informer().GetStore().Add(sc)
|
||||
if err != nil {
|
||||
fmt.Println("add storageclass error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
operation := admission.Update
|
||||
attributes := admission.NewAttributesRecord(tc.newObj, tc.oldObj, schema.GroupVersionKind{}, metav1.NamespaceDefault, "foo", tc.resource, tc.subresource, operation, nil)
|
||||
|
||||
err := ctrl.Admit(attributes)
|
||||
fmt.Println(tc.name)
|
||||
fmt.Println(err)
|
||||
if !tc.checkError(err) {
|
||||
t.Errorf("%v: unexpected err: %v", tc.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -630,6 +630,34 @@ type PersistentVolumeClaimSpec struct {
|
||||
StorageClassName *string `json:"storageClassName,omitempty" protobuf:"bytes,5,opt,name=storageClassName"`
|
||||
}
|
||||
|
||||
// PersistentVolumeClaimConditionType is a valid value of PersistentVolumeClaimCondition.Type
|
||||
type PersistentVolumeClaimConditionType string
|
||||
|
||||
const (
|
||||
// PersistentVolumeClaimResizing - a user trigger resize of pvc has been started
|
||||
PersistentVolumeClaimResizing PersistentVolumeClaimConditionType = "Resizing"
|
||||
)
|
||||
|
||||
// PersistentVolumeClaimCondition contails details about state of pvc
|
||||
type PersistentVolumeClaimCondition struct {
|
||||
Type PersistentVolumeClaimConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=PersistentVolumeClaimConditionType"`
|
||||
Status ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=ConditionStatus"`
|
||||
// Last time we probed the condition.
|
||||
// +optional
|
||||
LastProbeTime metav1.Time `json:"lastProbeTime,omitempty" protobuf:"bytes,3,opt,name=lastProbeTime"`
|
||||
// Last time the condition transitioned from one status to another.
|
||||
// +optional
|
||||
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,4,opt,name=lastTransitionTime"`
|
||||
// Unique, this should be a short, machine understandable string that gives the reason
|
||||
// for condition's last transition. If it reports "ResizeStarted" that means the underlying
|
||||
// persistent volume is being resized.
|
||||
// +optional
|
||||
Reason string `json:"reason,omitempty" protobuf:"bytes,5,opt,name=reason"`
|
||||
// Human-readable message indicating details about last transition.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
|
||||
}
|
||||
|
||||
// PersistentVolumeClaimStatus is the current status of a persistent volume claim.
|
||||
type PersistentVolumeClaimStatus struct {
|
||||
// Phase represents the current phase of PersistentVolumeClaim.
|
||||
@ -642,6 +670,12 @@ type PersistentVolumeClaimStatus struct {
|
||||
// Represents the actual resources of the underlying volume.
|
||||
// +optional
|
||||
Capacity ResourceList `json:"capacity,omitempty" protobuf:"bytes,3,rep,name=capacity,casttype=ResourceList,castkey=ResourceName"`
|
||||
// Current Condition of persistent volume claim. If underlying persistent volume is being
|
||||
// resized then the Condition will be set to 'ResizeStarted'.
|
||||
// +optional
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
Conditions []PersistentVolumeClaimCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,4,rep,name=conditions"`
|
||||
}
|
||||
|
||||
type PersistentVolumeAccessMode string
|
||||
|
@ -55,6 +55,10 @@ type StorageClass struct {
|
||||
// mount of the PVs will simply fail if one is invalid.
|
||||
// +optional
|
||||
MountOptions []string `json:"mountOptions,omitempty" protobuf:"bytes,5,opt,name=mountOptions"`
|
||||
|
||||
// AllowVolumeExpansion shows whether the storage class allow volume expand
|
||||
// +optional
|
||||
AllowVolumeExpansion *bool `json:"allowVolumeExpansion,omitempty" protobuf:"varint,6,opt,name=allowVolumeExpansion"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
@ -55,6 +55,10 @@ type StorageClass struct {
|
||||
// mount of the PVs will simply fail if one is invalid.
|
||||
// +optional
|
||||
MountOptions []string `json:"mountOptions,omitempty" protobuf:"bytes,5,opt,name=mountOptions"`
|
||||
|
||||
// AllowVolumeExpansion shows whether the storage class allow volume expand
|
||||
// +optional
|
||||
AllowVolumeExpansion *bool `json:"allowVolumeExpansion,omitempty" protobuf:"varint,6,opt,name=allowVolumeExpansion"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
Loading…
Reference in New Issue
Block a user