mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 20:53:33 +00:00
StorageClass API changes for VolumeBindingMode
This commit is contained in:
parent
1ced91f201
commit
b60bd37114
@ -65,6 +65,13 @@ type StorageClass struct {
|
|||||||
// for all PVs created from this storageclass.
|
// for all PVs created from this storageclass.
|
||||||
// +optional
|
// +optional
|
||||||
AllowVolumeExpansion *bool
|
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
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
@ -187,3 +194,18 @@ type VolumeError struct {
|
|||||||
// +optional
|
// +optional
|
||||||
Message string
|
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"
|
||||||
|
)
|
||||||
|
@ -3,13 +3,22 @@ package(default_visibility = ["//visibility:public"])
|
|||||||
load(
|
load(
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
"go_library",
|
"go_library",
|
||||||
|
"go_test",
|
||||||
)
|
)
|
||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = ["helpers.go"],
|
srcs = [
|
||||||
|
"helpers.go",
|
||||||
|
"util.go",
|
||||||
|
],
|
||||||
importpath = "k8s.io/kubernetes/pkg/apis/storage/util",
|
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(
|
filegroup(
|
||||||
@ -24,3 +33,14 @@ filegroup(
|
|||||||
srcs = [":package-srcs"],
|
srcs = [":package-srcs"],
|
||||||
tags = ["automanaged"],
|
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",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
30
pkg/apis/storage/util/util.go
Normal file
30
pkg/apis/storage/util/util.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
52
pkg/apis/storage/util/util_test.go
Normal file
52
pkg/apis/storage/util/util_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,17 @@ package(default_visibility = ["//visibility:public"])
|
|||||||
load(
|
load(
|
||||||
"@io_bazel_rules_go//go:def.bzl",
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
"go_library",
|
"go_library",
|
||||||
|
"go_test",
|
||||||
)
|
)
|
||||||
|
|
||||||
go_library(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = ["helpers.go"],
|
srcs = ["helpers.go"],
|
||||||
importpath = "k8s.io/kubernetes/pkg/apis/storage/v1/util",
|
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(
|
filegroup(
|
||||||
@ -24,3 +28,11 @@ filegroup(
|
|||||||
srcs = [":package-srcs"],
|
srcs = [":package-srcs"],
|
||||||
tags = ["automanaged"],
|
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"],
|
||||||
|
)
|
||||||
|
@ -16,7 +16,10 @@ limitations under the License.
|
|||||||
|
|
||||||
package util
|
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
|
// IsDefaultStorageClassAnnotation represents a StorageClass annotation that
|
||||||
// marks a class as the default StorageClass
|
// marks a class as the default StorageClass
|
||||||
@ -51,3 +54,10 @@ func IsDefaultAnnotation(obj metav1.ObjectMeta) bool {
|
|||||||
|
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
55
pkg/apis/storage/v1/util/helpers_test.go
Normal file
55
pkg/apis/storage/v1/util/helpers_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -46,6 +46,7 @@ func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList {
|
|||||||
allErrs = append(allErrs, validateParameters(storageClass.Parameters, field.NewPath("parameters"))...)
|
allErrs = append(allErrs, validateParameters(storageClass.Parameters, field.NewPath("parameters"))...)
|
||||||
allErrs = append(allErrs, validateReclaimPolicy(storageClass.ReclaimPolicy, field.NewPath("reclaimPolicy"))...)
|
allErrs = append(allErrs, validateReclaimPolicy(storageClass.ReclaimPolicy, field.NewPath("reclaimPolicy"))...)
|
||||||
allErrs = append(allErrs, validateAllowVolumeExpansion(storageClass.AllowVolumeExpansion, field.NewPath("allowVolumeExpansion"))...)
|
allErrs = append(allErrs, validateAllowVolumeExpansion(storageClass.AllowVolumeExpansion, field.NewPath("allowVolumeExpansion"))...)
|
||||||
|
allErrs = append(allErrs, validateVolumeBindingMode(storageClass.VolumeBindingMode, field.NewPath("volumeBindingMode"))...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
@ -64,6 +65,8 @@ func ValidateStorageClassUpdate(storageClass, oldStorageClass *storage.StorageCl
|
|||||||
if *storageClass.ReclaimPolicy != *oldStorageClass.ReclaimPolicy {
|
if *storageClass.ReclaimPolicy != *oldStorageClass.ReclaimPolicy {
|
||||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("reclaimPolicy"), "updates to reclaimPolicy are forbidden."))
|
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
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,3 +221,20 @@ func ValidateVolumeAttachmentUpdate(new, old *storage.VolumeAttachment) field.Er
|
|||||||
}
|
}
|
||||||
return allErrs
|
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
|
||||||
|
}
|
||||||
|
@ -27,6 +27,14 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/apis/storage"
|
"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) {
|
func TestValidateStorageClass(t *testing.T) {
|
||||||
deleteReclaimPolicy := api.PersistentVolumeReclaimPolicy("Delete")
|
deleteReclaimPolicy := api.PersistentVolumeReclaimPolicy("Delete")
|
||||||
retainReclaimPolicy := api.PersistentVolumeReclaimPolicy("Retain")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -98,7 +98,7 @@ const (
|
|||||||
// the API server as the certificate approaches expiration.
|
// the API server as the certificate approaches expiration.
|
||||||
RotateKubeletClientCertificate utilfeature.Feature = "RotateKubeletClientCertificate"
|
RotateKubeletClientCertificate utilfeature.Feature = "RotateKubeletClientCertificate"
|
||||||
|
|
||||||
// owner: @msau
|
// owner: @msau42
|
||||||
// alpha: v1.7
|
// alpha: v1.7
|
||||||
//
|
//
|
||||||
// A new volume type that supports local disks on a node.
|
// A new volume type that supports local disks on a node.
|
||||||
@ -175,6 +175,12 @@ const (
|
|||||||
//
|
//
|
||||||
// Enable running mount utilities in containers.
|
// Enable running mount utilities in containers.
|
||||||
MountContainers utilfeature.Feature = "MountContainers"
|
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() {
|
func init() {
|
||||||
@ -208,6 +214,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
|
|||||||
CPUManager: {Default: false, PreRelease: utilfeature.Alpha},
|
CPUManager: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
ServiceNodeExclusion: {Default: false, PreRelease: utilfeature.Alpha},
|
ServiceNodeExclusion: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
MountContainers: {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
|
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
|
||||||
// unintentionally on either side:
|
// unintentionally on either side:
|
||||||
|
@ -3214,6 +3214,9 @@ func describeStorageClass(sc *storage.StorageClass, events *api.EventList) (stri
|
|||||||
if sc.ReclaimPolicy != nil {
|
if sc.ReclaimPolicy != nil {
|
||||||
w.Write(LEVEL_0, "ReclaimPolicy:\t%s\n", *sc.ReclaimPolicy)
|
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 {
|
if events != nil {
|
||||||
DescribeEvents(events, w)
|
DescribeEvents(events, w)
|
||||||
}
|
}
|
||||||
|
@ -943,6 +943,7 @@ func TestDescribeDeployment(t *testing.T) {
|
|||||||
|
|
||||||
func TestDescribeStorageClass(t *testing.T) {
|
func TestDescribeStorageClass(t *testing.T) {
|
||||||
reclaimPolicy := api.PersistentVolumeReclaimRetain
|
reclaimPolicy := api.PersistentVolumeReclaimRetain
|
||||||
|
bindingMode := storage.VolumeBindingMode("bindingmode")
|
||||||
f := fake.NewSimpleClientset(&storage.StorageClass{
|
f := fake.NewSimpleClientset(&storage.StorageClass{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
@ -957,13 +958,21 @@ func TestDescribeStorageClass(t *testing.T) {
|
|||||||
"param2": "value2",
|
"param2": "value2",
|
||||||
},
|
},
|
||||||
ReclaimPolicy: &reclaimPolicy,
|
ReclaimPolicy: &reclaimPolicy,
|
||||||
|
VolumeBindingMode: &bindingMode,
|
||||||
})
|
})
|
||||||
s := StorageClassDescriber{f}
|
s := StorageClassDescriber{f}
|
||||||
out, err := s.Describe("", "foo", printers.DescriberSettings{ShowEvents: true})
|
out, err := s.Describe("", "foo", printers.DescriberSettings{ShowEvents: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
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)
|
t.Errorf("unexpected out: %s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ go_library(
|
|||||||
deps = [
|
deps = [
|
||||||
"//pkg/api/legacyscheme:go_default_library",
|
"//pkg/api/legacyscheme:go_default_library",
|
||||||
"//pkg/apis/storage:go_default_library",
|
"//pkg/apis/storage:go_default_library",
|
||||||
|
"//pkg/apis/storage/util:go_default_library",
|
||||||
"//pkg/apis/storage/validation:go_default_library",
|
"//pkg/apis/storage/validation:go_default_library",
|
||||||
"//pkg/features:go_default_library",
|
"//pkg/features:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
"k8s.io/kubernetes/pkg/apis/storage"
|
"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/apis/storage/validation"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
)
|
)
|
||||||
@ -49,6 +50,8 @@ func (storageClassStrategy) PrepareForCreate(ctx genericapirequest.Context, obj
|
|||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
||||||
class.AllowVolumeExpansion = nil
|
class.AllowVolumeExpansion = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storageutil.DropDisabledAlphaFields(class)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (storageClassStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
|
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
|
newClass.AllowVolumeExpansion = nil
|
||||||
oldClass.AllowVolumeExpansion = nil
|
oldClass.AllowVolumeExpansion = nil
|
||||||
}
|
}
|
||||||
|
storageutil.DropDisabledAlphaFields(oldClass)
|
||||||
|
storageutil.DropDisabledAlphaFields(newClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (storageClassStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
func (storageClassStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
@ -35,6 +35,7 @@ func TestStorageClassStrategy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteReclaimPolicy := api.PersistentVolumeReclaimDelete
|
deleteReclaimPolicy := api.PersistentVolumeReclaimDelete
|
||||||
|
bindingMode := storage.VolumeBindingWaitForFirstConsumer
|
||||||
storageClass := &storage.StorageClass{
|
storageClass := &storage.StorageClass{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid-class",
|
Name: "valid-class",
|
||||||
@ -44,6 +45,7 @@ func TestStorageClassStrategy(t *testing.T) {
|
|||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
ReclaimPolicy: &deleteReclaimPolicy,
|
ReclaimPolicy: &deleteReclaimPolicy,
|
||||||
|
VolumeBindingMode: &bindingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
Strategy.PrepareForCreate(ctx, storageClass)
|
Strategy.PrepareForCreate(ctx, storageClass)
|
||||||
@ -63,6 +65,7 @@ func TestStorageClassStrategy(t *testing.T) {
|
|||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
ReclaimPolicy: &deleteReclaimPolicy,
|
ReclaimPolicy: &deleteReclaimPolicy,
|
||||||
|
VolumeBindingMode: &bindingMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
Strategy.PrepareForUpdate(ctx, newStorageClass, storageClass)
|
Strategy.PrepareForUpdate(ctx, newStorageClass, storageClass)
|
||||||
|
@ -59,6 +59,13 @@ type StorageClass struct {
|
|||||||
// AllowVolumeExpansion shows whether the storage class allow volume expand
|
// AllowVolumeExpansion shows whether the storage class allow volume expand
|
||||||
// +optional
|
// +optional
|
||||||
AllowVolumeExpansion *bool `json:"allowVolumeExpansion,omitempty" protobuf:"varint,6,opt,name=allowVolumeExpansion"`
|
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
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
@ -74,3 +81,18 @@ type StorageClassList struct {
|
|||||||
// Items is the list of StorageClasses
|
// Items is the list of StorageClasses
|
||||||
Items []StorageClass `json:"items" protobuf:"bytes,2,rep,name=items"`
|
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"
|
||||||
|
)
|
||||||
|
@ -59,6 +59,13 @@ type StorageClass struct {
|
|||||||
// AllowVolumeExpansion shows whether the storage class allow volume expand
|
// AllowVolumeExpansion shows whether the storage class allow volume expand
|
||||||
// +optional
|
// +optional
|
||||||
AllowVolumeExpansion *bool `json:"allowVolumeExpansion,omitempty" protobuf:"varint,6,opt,name=allowVolumeExpansion"`
|
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
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
@ -74,3 +81,18 @@ type StorageClassList struct {
|
|||||||
// Items is the list of StorageClasses
|
// Items is the list of StorageClasses
|
||||||
Items []StorageClass `json:"items" protobuf:"bytes,2,rep,name=items"`
|
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"
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user