From b60bd37114ae5733d29e90a79f010d3b9f824949 Mon Sep 17 00:00:00 2001 From: Michelle Au Date: Wed, 4 Oct 2017 10:34:34 -0700 Subject: [PATCH] StorageClass API changes for VolumeBindingMode --- pkg/apis/storage/types.go | 22 +++ pkg/apis/storage/util/BUILD | 24 ++- pkg/apis/storage/util/util.go | 30 ++++ pkg/apis/storage/util/util_test.go | 52 +++++++ pkg/apis/storage/v1/util/BUILD | 14 +- pkg/apis/storage/v1/util/helpers.go | 12 +- pkg/apis/storage/v1/util/helpers_test.go | 55 +++++++ pkg/apis/storage/validation/validation.go | 20 +++ .../storage/validation/validation_test.go | 146 ++++++++++++++++++ pkg/features/kube_features.go | 9 +- pkg/printers/internalversion/describe.go | 3 + pkg/printers/internalversion/describe_test.go | 13 +- pkg/registry/storage/storageclass/BUILD | 1 + pkg/registry/storage/storageclass/strategy.go | 5 + .../storage/storageclass/strategy_test.go | 7 +- staging/src/k8s.io/api/storage/v1/types.go | 22 +++ .../src/k8s.io/api/storage/v1beta1/types.go | 22 +++ 17 files changed, 448 insertions(+), 9 deletions(-) create mode 100644 pkg/apis/storage/util/util.go create mode 100644 pkg/apis/storage/util/util_test.go create mode 100644 pkg/apis/storage/v1/util/helpers_test.go diff --git a/pkg/apis/storage/types.go b/pkg/apis/storage/types.go index 5a9024a70f9..e44b327d381 100644 --- a/pkg/apis/storage/types.go +++ b/pkg/apis/storage/types.go @@ -65,6 +65,13 @@ type StorageClass struct { // for all PVs created from this storageclass. // +optional AllowVolumeExpansion *bool + + // VolumeBindingMode indicates how PersistentVolumeClaims should be + // provisioned and bound. When unset, VolumeBindingImmediate is used. + // This field is alpha-level and is only honored by servers that enable + // the VolumeScheduling feature. + // +optional + VolumeBindingMode *VolumeBindingMode } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -187,3 +194,18 @@ type VolumeError struct { // +optional Message string } + +// VolumeBindingMode indicates how PersistentVolumeClaims should be bound. +type VolumeBindingMode string + +const ( + // VolumeBindingImmediate indicates that PersistentVolumeClaims should be + // immediately provisioned and bound. + VolumeBindingImmediate VolumeBindingMode = "Immediate" + + // VolumeBindingWaitForFirstConsumer indicates that PersistentVolumeClaims + // should not be provisioned and bound until the first Pod is created that + // references the PeristentVolumeClaim. The volume provisioning and + // binding will occur during Pod scheduing. + VolumeBindingWaitForFirstConsumer VolumeBindingMode = "WaitForFirstConsumer" +) diff --git a/pkg/apis/storage/util/BUILD b/pkg/apis/storage/util/BUILD index 297c1df9b02..490e8372149 100644 --- a/pkg/apis/storage/util/BUILD +++ b/pkg/apis/storage/util/BUILD @@ -3,13 +3,22 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( name = "go_default_library", - srcs = ["helpers.go"], + srcs = [ + "helpers.go", + "util.go", + ], importpath = "k8s.io/kubernetes/pkg/apis/storage/util", - deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library"], + deps = [ + "//pkg/apis/storage:go_default_library", + "//pkg/features:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", + ], ) filegroup( @@ -24,3 +33,14 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["util_test.go"], + importpath = "k8s.io/kubernetes/pkg/apis/storage/util", + library = ":go_default_library", + deps = [ + "//pkg/apis/storage:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", + ], +) diff --git a/pkg/apis/storage/util/util.go b/pkg/apis/storage/util/util.go new file mode 100644 index 00000000000..9d134043563 --- /dev/null +++ b/pkg/apis/storage/util/util.go @@ -0,0 +1,30 @@ +/* +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 util + +import ( + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/kubernetes/pkg/apis/storage" + "k8s.io/kubernetes/pkg/features" +) + +// DropDisabledAlphaFields removes disabled fields from the StorageClass object. +func DropDisabledAlphaFields(class *storage.StorageClass) { + if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) { + class.VolumeBindingMode = nil + } +} diff --git a/pkg/apis/storage/util/util_test.go b/pkg/apis/storage/util/util_test.go new file mode 100644 index 00000000000..d75b2455865 --- /dev/null +++ b/pkg/apis/storage/util/util_test.go @@ -0,0 +1,52 @@ +/* +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 util + +import ( + "testing" + + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/kubernetes/pkg/apis/storage" +) + +func TestDropAlphaFields(t *testing.T) { + bindingMode := storage.VolumeBindingWaitForFirstConsumer + + // Test that field gets dropped when feature gate is not set + class := &storage.StorageClass{ + VolumeBindingMode: &bindingMode, + } + DropDisabledAlphaFields(class) + if class.VolumeBindingMode != nil { + t.Errorf("VolumeBindingMode field didn't get dropped: %+v", class.VolumeBindingMode) + } + + // Test that field does not get dropped when feature gate is set + class = &storage.StorageClass{ + VolumeBindingMode: &bindingMode, + } + if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true"); err != nil { + t.Fatalf("Failed to set feature gate for VolumeScheduling: %v", err) + } + DropDisabledAlphaFields(class) + if class.VolumeBindingMode != &bindingMode { + t.Errorf("VolumeBindingMode field got unexpectantly modified: %+v", class.VolumeBindingMode) + } + if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false"); err != nil { + t.Fatalf("Failed to disable feature gate for VolumeScheduling: %v", err) + } +} diff --git a/pkg/apis/storage/v1/util/BUILD b/pkg/apis/storage/v1/util/BUILD index c6603b8e9c2..86106067c32 100644 --- a/pkg/apis/storage/v1/util/BUILD +++ b/pkg/apis/storage/v1/util/BUILD @@ -3,13 +3,17 @@ package(default_visibility = ["//visibility:public"]) load( "@io_bazel_rules_go//go:def.bzl", "go_library", + "go_test", ) go_library( name = "go_default_library", srcs = ["helpers.go"], importpath = "k8s.io/kubernetes/pkg/apis/storage/v1/util", - deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library"], + deps = [ + "//vendor/k8s.io/api/storage/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + ], ) filegroup( @@ -24,3 +28,11 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["helpers_test.go"], + importpath = "k8s.io/kubernetes/pkg/apis/storage/v1/util", + library = ":go_default_library", + deps = ["//vendor/k8s.io/api/storage/v1:go_default_library"], +) diff --git a/pkg/apis/storage/v1/util/helpers.go b/pkg/apis/storage/v1/util/helpers.go index 62c27ccfc19..6e81cfe35ec 100644 --- a/pkg/apis/storage/v1/util/helpers.go +++ b/pkg/apis/storage/v1/util/helpers.go @@ -16,7 +16,10 @@ limitations under the License. package util -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + storagev1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // IsDefaultStorageClassAnnotation represents a StorageClass annotation that // marks a class as the default StorageClass @@ -51,3 +54,10 @@ func IsDefaultAnnotation(obj metav1.ObjectMeta) bool { return false } + +// IsBindingModeWaitForFirstConsumer returns true if the VolumeBindingMode is set +// to VolumeBindingWaitForFirstConsumer +func IsBindingModeWaitForFirstConsumer(class *storagev1.StorageClass) bool { + mode := class.VolumeBindingMode + return mode != nil && *mode == storagev1.VolumeBindingWaitForFirstConsumer +} diff --git a/pkg/apis/storage/v1/util/helpers_test.go b/pkg/apis/storage/v1/util/helpers_test.go new file mode 100644 index 00000000000..4b01e41ece1 --- /dev/null +++ b/pkg/apis/storage/v1/util/helpers_test.go @@ -0,0 +1,55 @@ +/* +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 util + +import ( + "testing" + + storagev1 "k8s.io/api/storage/v1" +) + +type bindingTest struct { + class *storagev1.StorageClass + expected bool +} + +func TestIsBindingModeWaitForFirstConsumer(t *testing.T) { + immediateMode := storagev1.VolumeBindingImmediate + waitingMode := storagev1.VolumeBindingWaitForFirstConsumer + cases := map[string]bindingTest{ + "nil binding mode": { + &storagev1.StorageClass{}, + false, + }, + "immediate binding mode": { + &storagev1.StorageClass{VolumeBindingMode: &immediateMode}, + false, + }, + "waiting binding mode": { + &storagev1.StorageClass{VolumeBindingMode: &waitingMode}, + true, + }, + } + + for testName, testCase := range cases { + result := IsBindingModeWaitForFirstConsumer(testCase.class) + if result != testCase.expected { + t.Errorf("Test %q failed. Expected %v, got %v", testName, testCase.expected, result) + } + } + +} diff --git a/pkg/apis/storage/validation/validation.go b/pkg/apis/storage/validation/validation.go index 4ffc7108808..bcfc960e8aa 100644 --- a/pkg/apis/storage/validation/validation.go +++ b/pkg/apis/storage/validation/validation.go @@ -46,6 +46,7 @@ func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList { 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"))...) + allErrs = append(allErrs, validateVolumeBindingMode(storageClass.VolumeBindingMode, field.NewPath("volumeBindingMode"))...) return allErrs } @@ -64,6 +65,8 @@ func ValidateStorageClassUpdate(storageClass, oldStorageClass *storage.StorageCl if *storageClass.ReclaimPolicy != *oldStorageClass.ReclaimPolicy { allErrs = append(allErrs, field.Forbidden(field.NewPath("reclaimPolicy"), "updates to reclaimPolicy are forbidden.")) } + + allErrs = append(allErrs, apivalidation.ValidateImmutableField(storageClass.VolumeBindingMode, oldStorageClass.VolumeBindingMode, field.NewPath("volumeBindingMode"))...) return allErrs } @@ -218,3 +221,20 @@ func ValidateVolumeAttachmentUpdate(new, old *storage.VolumeAttachment) field.Er } return allErrs } + +var supportedVolumeBindingModes = sets.NewString(string(storage.VolumeBindingImmediate), string(storage.VolumeBindingWaitForFirstConsumer)) + +// validateVolumeBindingMode tests that VolumeBindingMode specifies valid values. +func validateVolumeBindingMode(mode *storage.VolumeBindingMode, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if mode != nil { + if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) { + allErrs = append(allErrs, field.Forbidden(fldPath, "field is disabled by feature-gate VolumeScheduling")) + } + if !supportedVolumeBindingModes.Has(string(*mode)) { + allErrs = append(allErrs, field.NotSupported(fldPath, mode, supportedVolumeBindingModes.List())) + } + } + + return allErrs +} diff --git a/pkg/apis/storage/validation/validation_test.go b/pkg/apis/storage/validation/validation_test.go index 1c0542a787e..c1701688780 100644 --- a/pkg/apis/storage/validation/validation_test.go +++ b/pkg/apis/storage/validation/validation_test.go @@ -27,6 +27,14 @@ import ( "k8s.io/kubernetes/pkg/apis/storage" ) +var ( + deleteReclaimPolicy = api.PersistentVolumeReclaimDelete + immediateMode1 = storage.VolumeBindingImmediate + immediateMode2 = storage.VolumeBindingImmediate + waitingMode = storage.VolumeBindingWaitForFirstConsumer + invalidMode = storage.VolumeBindingMode("foo") +) + func TestValidateStorageClass(t *testing.T) { deleteReclaimPolicy := api.PersistentVolumeReclaimPolicy("Delete") retainReclaimPolicy := api.PersistentVolumeReclaimPolicy("Retain") @@ -436,3 +444,141 @@ func TestVolumeAttachmentUpdateValidation(t *testing.T) { } } } + +func makeClassWithBinding(mode *storage.VolumeBindingMode) *storage.StorageClass { + return &storage.StorageClass{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "foo"}, + Provisioner: "kubernetes.io/foo-provisioner", + ReclaimPolicy: &deleteReclaimPolicy, + VolumeBindingMode: mode, + } +} + +// TODO: Remove these tests once feature gate is not required +func TestValidateVolumeBindingModeAlphaDisabled(t *testing.T) { + errorCases := map[string]*storage.StorageClass{ + "immediate mode": makeClassWithBinding(&immediateMode1), + "waiting mode": makeClassWithBinding(&waitingMode), + "invalid mode": makeClassWithBinding(&invalidMode), + } + + for testName, storageClass := range errorCases { + if errs := ValidateStorageClass(storageClass); len(errs) == 0 { + t.Errorf("Expected failure for test: %v", testName) + } + } +} + +type bindingTest struct { + class *storage.StorageClass + shouldSucceed bool +} + +func TestValidateVolumeBindingMode(t *testing.T) { + cases := map[string]bindingTest{ + "no mode": { + class: makeClassWithBinding(nil), + shouldSucceed: true, + }, + "immediate mode": { + class: makeClassWithBinding(&immediateMode1), + shouldSucceed: true, + }, + "waiting mode": { + class: makeClassWithBinding(&waitingMode), + shouldSucceed: true, + }, + "invalid mode": { + class: makeClassWithBinding(&invalidMode), + shouldSucceed: false, + }, + } + + // TODO: remove when feature gate not required + err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true") + if err != nil { + t.Fatalf("Failed to enable feature gate for VolumeScheduling: %v", err) + } + + for testName, testCase := range cases { + errs := ValidateStorageClass(testCase.class) + if testCase.shouldSucceed && len(errs) != 0 { + t.Errorf("Expected success for test %q, got %v", testName, errs) + } + if !testCase.shouldSucceed && len(errs) == 0 { + t.Errorf("Expected failure for test %q, got success", testName) + } + } + + err = utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false") + if err != nil { + t.Fatalf("Failed to disable feature gate for VolumeScheduling: %v", err) + } +} + +type updateTest struct { + oldClass *storage.StorageClass + newClass *storage.StorageClass + shouldSucceed bool +} + +func TestValidateUpdateVolumeBindingMode(t *testing.T) { + noBinding := makeClassWithBinding(nil) + immediateBinding1 := makeClassWithBinding(&immediateMode1) + immediateBinding2 := makeClassWithBinding(&immediateMode2) + waitBinding := makeClassWithBinding(&waitingMode) + + cases := map[string]updateTest{ + "old and new no mode": { + oldClass: noBinding, + newClass: noBinding, + shouldSucceed: true, + }, + "old and new same mode ptr": { + oldClass: immediateBinding1, + newClass: immediateBinding1, + shouldSucceed: true, + }, + "old and new same mode value": { + oldClass: immediateBinding1, + newClass: immediateBinding2, + shouldSucceed: true, + }, + "old no mode, new mode": { + oldClass: noBinding, + newClass: waitBinding, + shouldSucceed: false, + }, + "old mode, new no mode": { + oldClass: waitBinding, + newClass: noBinding, + shouldSucceed: false, + }, + "old and new different modes": { + oldClass: waitBinding, + newClass: immediateBinding1, + shouldSucceed: false, + }, + } + + // TODO: remove when feature gate not required + err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true") + if err != nil { + t.Fatalf("Failed to enable feature gate for VolumeScheduling: %v", err) + } + + for testName, testCase := range cases { + errs := ValidateStorageClassUpdate(testCase.newClass, testCase.oldClass) + if testCase.shouldSucceed && len(errs) != 0 { + t.Errorf("Expected success for %v, got %v", testName, errs) + } + if !testCase.shouldSucceed && len(errs) == 0 { + t.Errorf("Expected failure for %v, got success", testName) + } + } + + err = utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false") + if err != nil { + t.Fatalf("Failed to disable feature gate for VolumeScheduling: %v", err) + } +} diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 9e6a61274a0..910c7cfdbed 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -98,7 +98,7 @@ const ( // the API server as the certificate approaches expiration. RotateKubeletClientCertificate utilfeature.Feature = "RotateKubeletClientCertificate" - // owner: @msau + // owner: @msau42 // alpha: v1.7 // // A new volume type that supports local disks on a node. @@ -175,6 +175,12 @@ const ( // // Enable running mount utilities in containers. MountContainers utilfeature.Feature = "MountContainers" + + // owner: @msau42 + // alpha: v1.9 + // + // Extend the default scheduler to be aware of PV topology and handle PV binding + VolumeScheduling utilfeature.Feature = "VolumeScheduling" ) func init() { @@ -208,6 +214,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS CPUManager: {Default: false, PreRelease: utilfeature.Alpha}, ServiceNodeExclusion: {Default: false, PreRelease: utilfeature.Alpha}, MountContainers: {Default: false, PreRelease: utilfeature.Alpha}, + VolumeScheduling: {Default: false, PreRelease: utilfeature.Alpha}, // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: diff --git a/pkg/printers/internalversion/describe.go b/pkg/printers/internalversion/describe.go index 79295cf21fc..4392e8e7dbb 100644 --- a/pkg/printers/internalversion/describe.go +++ b/pkg/printers/internalversion/describe.go @@ -3214,6 +3214,9 @@ func describeStorageClass(sc *storage.StorageClass, events *api.EventList) (stri if sc.ReclaimPolicy != nil { w.Write(LEVEL_0, "ReclaimPolicy:\t%s\n", *sc.ReclaimPolicy) } + if sc.VolumeBindingMode != nil { + w.Write(LEVEL_0, "VolumeBindingMode:\t%s\n", *sc.VolumeBindingMode) + } if events != nil { DescribeEvents(events, w) } diff --git a/pkg/printers/internalversion/describe_test.go b/pkg/printers/internalversion/describe_test.go index 962ff3936f6..75030fea5df 100644 --- a/pkg/printers/internalversion/describe_test.go +++ b/pkg/printers/internalversion/describe_test.go @@ -943,6 +943,7 @@ func TestDescribeDeployment(t *testing.T) { func TestDescribeStorageClass(t *testing.T) { reclaimPolicy := api.PersistentVolumeReclaimRetain + bindingMode := storage.VolumeBindingMode("bindingmode") f := fake.NewSimpleClientset(&storage.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", @@ -956,14 +957,22 @@ func TestDescribeStorageClass(t *testing.T) { "param1": "value1", "param2": "value2", }, - ReclaimPolicy: &reclaimPolicy, + ReclaimPolicy: &reclaimPolicy, + VolumeBindingMode: &bindingMode, }) s := StorageClassDescriber{f} out, err := s.Describe("", "foo", printers.DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } - if !strings.Contains(out, "foo") { + if !strings.Contains(out, "foo") || + !strings.Contains(out, "my-provisioner") || + !strings.Contains(out, "param1") || + !strings.Contains(out, "param2") || + !strings.Contains(out, "value1") || + !strings.Contains(out, "value2") || + !strings.Contains(out, "Retain") || + !strings.Contains(out, "bindingmode") { t.Errorf("unexpected out: %s", out) } } diff --git a/pkg/registry/storage/storageclass/BUILD b/pkg/registry/storage/storageclass/BUILD index 44e2166842b..d8d1d0a375c 100644 --- a/pkg/registry/storage/storageclass/BUILD +++ b/pkg/registry/storage/storageclass/BUILD @@ -16,6 +16,7 @@ go_library( deps = [ "//pkg/api/legacyscheme:go_default_library", "//pkg/apis/storage:go_default_library", + "//pkg/apis/storage/util:go_default_library", "//pkg/apis/storage/validation:go_default_library", "//pkg/features:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/pkg/registry/storage/storageclass/strategy.go b/pkg/registry/storage/storageclass/strategy.go index 6ea4fb1250f..b1cbf49ebcf 100644 --- a/pkg/registry/storage/storageclass/strategy.go +++ b/pkg/registry/storage/storageclass/strategy.go @@ -24,6 +24,7 @@ import ( utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/storage" + storageutil "k8s.io/kubernetes/pkg/apis/storage/util" "k8s.io/kubernetes/pkg/apis/storage/validation" "k8s.io/kubernetes/pkg/features" ) @@ -49,6 +50,8 @@ func (storageClassStrategy) PrepareForCreate(ctx genericapirequest.Context, obj if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) { class.AllowVolumeExpansion = nil } + + storageutil.DropDisabledAlphaFields(class) } func (storageClassStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { @@ -73,6 +76,8 @@ func (storageClassStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, newClass.AllowVolumeExpansion = nil oldClass.AllowVolumeExpansion = nil } + storageutil.DropDisabledAlphaFields(oldClass) + storageutil.DropDisabledAlphaFields(newClass) } func (storageClassStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { diff --git a/pkg/registry/storage/storageclass/strategy_test.go b/pkg/registry/storage/storageclass/strategy_test.go index 1f455e7528c..33842efa775 100644 --- a/pkg/registry/storage/storageclass/strategy_test.go +++ b/pkg/registry/storage/storageclass/strategy_test.go @@ -35,6 +35,7 @@ func TestStorageClassStrategy(t *testing.T) { } deleteReclaimPolicy := api.PersistentVolumeReclaimDelete + bindingMode := storage.VolumeBindingWaitForFirstConsumer storageClass := &storage.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "valid-class", @@ -43,7 +44,8 @@ func TestStorageClassStrategy(t *testing.T) { Parameters: map[string]string{ "foo": "bar", }, - ReclaimPolicy: &deleteReclaimPolicy, + ReclaimPolicy: &deleteReclaimPolicy, + VolumeBindingMode: &bindingMode, } Strategy.PrepareForCreate(ctx, storageClass) @@ -62,7 +64,8 @@ func TestStorageClassStrategy(t *testing.T) { Parameters: map[string]string{ "foo": "bar", }, - ReclaimPolicy: &deleteReclaimPolicy, + ReclaimPolicy: &deleteReclaimPolicy, + VolumeBindingMode: &bindingMode, } Strategy.PrepareForUpdate(ctx, newStorageClass, storageClass) diff --git a/staging/src/k8s.io/api/storage/v1/types.go b/staging/src/k8s.io/api/storage/v1/types.go index 9afdafb62ac..288d40abb8f 100644 --- a/staging/src/k8s.io/api/storage/v1/types.go +++ b/staging/src/k8s.io/api/storage/v1/types.go @@ -59,6 +59,13 @@ type StorageClass struct { // AllowVolumeExpansion shows whether the storage class allow volume expand // +optional AllowVolumeExpansion *bool `json:"allowVolumeExpansion,omitempty" protobuf:"varint,6,opt,name=allowVolumeExpansion"` + + // VolumeBindingMode indicates how PersistentVolumeClaims should be + // provisioned and bound. When unset, VolumeBindingImmediate is used. + // This field is alpha-level and is only honored by servers that enable + // the VolumeScheduling feature. + // +optional + VolumeBindingMode *VolumeBindingMode `json:"volumeBindingMode,omitempty" protobuf:"bytes,7,opt,name=volumeBindingMode"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -74,3 +81,18 @@ type StorageClassList struct { // Items is the list of StorageClasses Items []StorageClass `json:"items" protobuf:"bytes,2,rep,name=items"` } + +// VolumeBindingMode indicates how PersistentVolumeClaims should be bound. +type VolumeBindingMode string + +const ( + // VolumeBindingImmediate indicates that PersistentVolumeClaims should be + // immediately provisioned and bound. This is the default mode. + VolumeBindingImmediate VolumeBindingMode = "Immediate" + + // VolumeBindingWaitForFirstConsumer indicates that PersistentVolumeClaims + // should not be provisioned and bound until the first Pod is created that + // references the PeristentVolumeClaim. The volume provisioning and + // binding will occur during Pod scheduing. + VolumeBindingWaitForFirstConsumer VolumeBindingMode = "WaitForFirstConsumer" +) diff --git a/staging/src/k8s.io/api/storage/v1beta1/types.go b/staging/src/k8s.io/api/storage/v1beta1/types.go index e5036b55b33..7fb9ad98077 100644 --- a/staging/src/k8s.io/api/storage/v1beta1/types.go +++ b/staging/src/k8s.io/api/storage/v1beta1/types.go @@ -59,6 +59,13 @@ type StorageClass struct { // AllowVolumeExpansion shows whether the storage class allow volume expand // +optional AllowVolumeExpansion *bool `json:"allowVolumeExpansion,omitempty" protobuf:"varint,6,opt,name=allowVolumeExpansion"` + + // VolumeBindingMode indicates how PersistentVolumeClaims should be + // provisioned and bound. When unset, VolumeBindingImmediate is used. + // This field is alpha-level and is only honored by servers that enable + // the VolumeScheduling feature. + // +optional + VolumeBindingMode *VolumeBindingMode `json:"volumeBindingMode,omitempty" protobuf:"bytes,7,opt,name=volumeBindingMode"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -74,3 +81,18 @@ type StorageClassList struct { // Items is the list of StorageClasses Items []StorageClass `json:"items" protobuf:"bytes,2,rep,name=items"` } + +// VolumeBindingMode indicates how PersistentVolumeClaims should be bound. +type VolumeBindingMode string + +const ( + // VolumeBindingImmediate indicates that PersistentVolumeClaims should be + // immediately provisioned and bound. This is the default mode. + VolumeBindingImmediate VolumeBindingMode = "Immediate" + + // VolumeBindingWaitForFirstConsumer indicates that PersistentVolumeClaims + // should not be provisioned and bound until the first Pod is created that + // references the PeristentVolumeClaim. The volume provisioning and + // binding will occur during Pod scheduing. + VolumeBindingWaitForFirstConsumer VolumeBindingMode = "WaitForFirstConsumer" +)