Commit 9a77899e authored by lichuqiang's avatar lichuqiang

API changes for Topology aware dynamic provisioning

parent 50178d3e
...@@ -2166,6 +2166,7 @@ type NodeSelector struct { ...@@ -2166,6 +2166,7 @@ type NodeSelector struct {
// A null or empty node selector term matches no objects. The requirements of // A null or empty node selector term matches no objects. The requirements of
// them are ANDed. // them are ANDed.
// The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
type NodeSelectorTerm struct { type NodeSelectorTerm struct {
// A list of node selector requirements by node's labels. // A list of node selector requirements by node's labels.
MatchExpressions []NodeSelectorRequirement MatchExpressions []NodeSelectorRequirement
...@@ -2203,6 +2204,27 @@ const ( ...@@ -2203,6 +2204,27 @@ const (
NodeSelectorOpLt NodeSelectorOperator = "Lt" NodeSelectorOpLt NodeSelectorOperator = "Lt"
) )
// A topology selector term represents the result of label queries.
// A null or empty topology selector term matches no objects.
// The requirements of them are ANDed.
// It provides a subset of functionality as NodeSelectorTerm.
// This is an alpha feature and may change in the future.
type TopologySelectorTerm struct {
// A list of topology selector requirements by labels.
// +optional
MatchLabelExpressions []TopologySelectorLabelRequirement
}
// A topology selector requirement is a selector that matches given label.
// This is an alpha feature and may change in the future.
type TopologySelectorLabelRequirement struct {
// The label key that the selector applies to.
Key string
// An array of string values. One value must match the label to be selected.
// Each entry in Values is ORed.
Values []string
}
// Affinity is a group of affinity scheduling rules. // Affinity is a group of affinity scheduling rules.
type Affinity struct { type Affinity struct {
// Describes node affinity scheduling rules for the pod. // Describes node affinity scheduling rules for the pod.
......
...@@ -316,6 +316,50 @@ func MatchNodeSelectorTerms( ...@@ -316,6 +316,50 @@ func MatchNodeSelectorTerms(
return false return false
} }
// TopologySelectorRequirementsAsSelector converts the []TopologySelectorLabelRequirement api type into a struct
// that implements labels.Selector.
func TopologySelectorRequirementsAsSelector(tsm []v1.TopologySelectorLabelRequirement) (labels.Selector, error) {
if len(tsm) == 0 {
return labels.Nothing(), nil
}
selector := labels.NewSelector()
for _, expr := range tsm {
r, err := labels.NewRequirement(expr.Key, selection.In, expr.Values)
if err != nil {
return nil, err
}
selector = selector.Add(*r)
}
return selector, nil
}
// MatchTopologySelectorTerms checks whether given labels match topology selector terms in ORed;
// nil or empty term matches no objects; while empty term list matches all objects.
func MatchTopologySelectorTerms(topologySelectorTerms []v1.TopologySelectorTerm, lbls labels.Set) bool {
if len(topologySelectorTerms) == 0 {
// empty term list matches all objects
return true
}
for _, req := range topologySelectorTerms {
// nil or empty term selects no objects
if len(req.MatchLabelExpressions) == 0 {
continue
}
labelSelector, err := TopologySelectorRequirementsAsSelector(req.MatchLabelExpressions)
if err != nil || !labelSelector.Matches(lbls) {
continue
}
return true
}
return false
}
// AddOrUpdateTolerationInPodSpec tries to add a toleration to the toleration list in PodSpec. // AddOrUpdateTolerationInPodSpec tries to add a toleration to the toleration list in PodSpec.
// Returns true if something was updated, false otherwise. // Returns true if something was updated, false otherwise.
func AddOrUpdateTolerationInPodSpec(spec *v1.PodSpec, toleration *v1.Toleration) bool { func AddOrUpdateTolerationInPodSpec(spec *v1.PodSpec, toleration *v1.Toleration) bool {
......
...@@ -301,6 +301,71 @@ func TestNodeSelectorRequirementsAsSelector(t *testing.T) { ...@@ -301,6 +301,71 @@ func TestNodeSelectorRequirementsAsSelector(t *testing.T) {
} }
} }
func TestTopologySelectorRequirementsAsSelector(t *testing.T) {
mustParse := func(s string) labels.Selector {
out, e := labels.Parse(s)
if e != nil {
panic(e)
}
return out
}
tc := []struct {
in []v1.TopologySelectorLabelRequirement
out labels.Selector
expectErr bool
}{
{in: nil, out: labels.Nothing()},
{in: []v1.TopologySelectorLabelRequirement{}, out: labels.Nothing()},
{
in: []v1.TopologySelectorLabelRequirement{{
Key: "foo",
Values: []string{"bar", "baz"},
}},
out: mustParse("foo in (baz,bar)"),
},
{
in: []v1.TopologySelectorLabelRequirement{{
Key: "foo",
Values: []string{},
}},
expectErr: true,
},
{
in: []v1.TopologySelectorLabelRequirement{
{
Key: "foo",
Values: []string{"bar", "baz"},
},
{
Key: "invalid",
Values: []string{},
},
},
expectErr: true,
},
{
in: []v1.TopologySelectorLabelRequirement{{
Key: "/invalidkey",
Values: []string{"bar", "baz"},
}},
expectErr: true,
},
}
for i, tc := range tc {
out, err := TopologySelectorRequirementsAsSelector(tc.in)
if err == nil && tc.expectErr {
t.Errorf("[%v]expected error but got none.", i)
}
if err != nil && !tc.expectErr {
t.Errorf("[%v]did not expect error but got: %v", i, err)
}
if !reflect.DeepEqual(out, tc.out) {
t.Errorf("[%v]expected:\n\t%+v\nbut got:\n\t%+v", i, tc.out, out)
}
}
}
func TestTolerationsTolerateTaintsWithFilter(t *testing.T) { func TestTolerationsTolerateTaintsWithFilter(t *testing.T) {
testCases := []struct { testCases := []struct {
description string description string
...@@ -792,3 +857,161 @@ func TestMatchNodeSelectorTerms(t *testing.T) { ...@@ -792,3 +857,161 @@ func TestMatchNodeSelectorTerms(t *testing.T) {
}) })
} }
} }
func TestMatchTopologySelectorTerms(t *testing.T) {
type args struct {
topologySelectorTerms []v1.TopologySelectorTerm
labels labels.Set
}
tests := []struct {
name string
args args
want bool
}{
{
name: "nil term list",
args: args{
topologySelectorTerms: nil,
labels: nil,
},
want: true,
},
{
name: "nil term",
args: args{
topologySelectorTerms: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{},
},
},
labels: nil,
},
want: false,
},
{
name: "label matches MatchLabelExpressions terms",
args: args{
topologySelectorTerms: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{{
Key: "label_1",
Values: []string{"label_1_val"},
}},
},
},
labels: map[string]string{"label_1": "label_1_val"},
},
want: true,
},
{
name: "label does not match MatchLabelExpressions terms",
args: args{
topologySelectorTerms: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{{
Key: "label_1",
Values: []string{"label_1_val"},
}},
},
},
labels: map[string]string{"label_1": "label_1_val-failed"},
},
want: false,
},
{
name: "multi-values in one requirement, one matched",
args: args{
topologySelectorTerms: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{{
Key: "label_1",
Values: []string{"label_1_val1", "label_1_val2"},
}},
},
},
labels: map[string]string{"label_1": "label_1_val2"},
},
want: true,
},
{
name: "multi-terms was set, one matched",
args: args{
topologySelectorTerms: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{{
Key: "label_1",
Values: []string{"label_1_val"},
}},
},
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{{
Key: "label_2",
Values: []string{"label_2_val"},
}},
},
},
labels: map[string]string{
"label_2": "label_2_val",
},
},
want: true,
},
{
name: "multi-requirement in one term, fully matched",
args: args{
topologySelectorTerms: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: "label_1",
Values: []string{"label_1_val"},
},
{
Key: "label_2",
Values: []string{"label_2_val"},
},
},
},
},
labels: map[string]string{
"label_1": "label_1_val",
"label_2": "label_2_val",
},
},
want: true,
},
{
name: "multi-requirement in one term, partial matched",
args: args{
topologySelectorTerms: []v1.TopologySelectorTerm{
{
MatchLabelExpressions: []v1.TopologySelectorLabelRequirement{
{
Key: "label_1",
Values: []string{"label_1_val"},
},
{
Key: "label_2",
Values: []string{"label_2_val"},
},
},
},
},
labels: map[string]string{
"label_1": "label_1_val-failed",
"label_2": "label_2_val",
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := MatchTopologySelectorTerms(tt.args.topologySelectorTerms, tt.args.labels); got != tt.want {
t.Errorf("MatchTopologySelectorTermsORed() = %v, want %v", got, tt.want)
}
})
}
}
...@@ -3104,6 +3104,32 @@ func ValidateNodeSelector(nodeSelector *core.NodeSelector, fldPath *field.Path) ...@@ -3104,6 +3104,32 @@ func ValidateNodeSelector(nodeSelector *core.NodeSelector, fldPath *field.Path)
return allErrs return allErrs
} }
// validateTopologySelectorLabelRequirement tests that the specified TopologySelectorLabelRequirement fields has valid data
func validateTopologySelectorLabelRequirement(rq core.TopologySelectorLabelRequirement, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(rq.Values) == 0 {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("values"), "must specify as least one value"))
}
allErrs = append(allErrs, unversionedvalidation.ValidateLabelName(rq.Key, fldPath.Child("key"))...)
return allErrs
}
// ValidateTopologySelectorTerm tests that the specified topology selector term has valid data
func ValidateTopologySelectorTerm(term core.TopologySelectorTerm, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicProvisioningScheduling) {
for i, req := range term.MatchLabelExpressions {
allErrs = append(allErrs, validateTopologySelectorLabelRequirement(req, fldPath.Child("matchLabelExpressions").Index(i))...)
}
} else if len(term.MatchLabelExpressions) != 0 {
allErrs = append(allErrs, field.Forbidden(fldPath, "field is disabled by feature-gate DynamicProvisioningScheduling"))
}
return allErrs
}
// ValidateAvoidPodsInNodeAnnotations tests that the serialized AvoidPods in Node.Annotations has valid data // ValidateAvoidPodsInNodeAnnotations tests that the serialized AvoidPods in Node.Annotations has valid data
func ValidateAvoidPodsInNodeAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList { func ValidateAvoidPodsInNodeAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
......
...@@ -72,6 +72,14 @@ type StorageClass struct { ...@@ -72,6 +72,14 @@ type StorageClass struct {
// the VolumeScheduling feature. // the VolumeScheduling feature.
// +optional // +optional
VolumeBindingMode *VolumeBindingMode VolumeBindingMode *VolumeBindingMode
// Restrict the node topologies where volumes can be dynamically provisioned.
// Each volume plugin defines its own supported topology specifications.
// An empty TopologySelectorTerm list means there is no topology restriction.
// This field is alpha-level and is only honored by servers that enable
// the DynamicProvisioningScheduling feature.
// +optional
AllowedTopologies []api.TopologySelectorTerm
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
......
...@@ -27,4 +27,7 @@ func DropDisabledAlphaFields(class *storage.StorageClass) { ...@@ -27,4 +27,7 @@ func DropDisabledAlphaFields(class *storage.StorageClass) {
if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) { if !utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) {
class.VolumeBindingMode = nil class.VolumeBindingMode = nil
} }
if !utilfeature.DefaultFeatureGate.Enabled(features.DynamicProvisioningScheduling) {
class.AllowedTopologies = nil
}
} }
...@@ -17,39 +17,60 @@ limitations under the License. ...@@ -17,39 +17,60 @@ limitations under the License.
package util package util
import ( import (
"reflect"
"testing" "testing"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/storage" "k8s.io/kubernetes/pkg/apis/storage"
) )
func TestDropAlphaFields(t *testing.T) { func TestDropAlphaFields(t *testing.T) {
bindingMode := storage.VolumeBindingWaitForFirstConsumer bindingMode := storage.VolumeBindingWaitForFirstConsumer
allowedTopologies := []api.TopologySelectorTerm{
{
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
{
Key: "kubernetes.io/hostname",
Values: []string{"node1"},
},
},
},
}
// Test that field gets dropped when feature gate is not set // Test that field gets dropped when feature gate is not set
if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false"); err != nil { if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false,DynamicProvisioningScheduling=false"); err != nil {
t.Fatalf("Failed to set feature gate for VolumeScheduling: %v", err) t.Fatalf("Failed to set feature gate for VolumeScheduling or DynamicProvisioningScheduling: %v", err)
} }
class := &storage.StorageClass{ class := &storage.StorageClass{
VolumeBindingMode: &bindingMode, VolumeBindingMode: &bindingMode,
AllowedTopologies: allowedTopologies,
} }
DropDisabledAlphaFields(class) DropDisabledAlphaFields(class)
if class.VolumeBindingMode != nil { if class.VolumeBindingMode != nil {
t.Errorf("VolumeBindingMode field didn't get dropped: %+v", class.VolumeBindingMode) t.Errorf("VolumeBindingMode field didn't get dropped: %+v", class.VolumeBindingMode)
} }
if class.AllowedTopologies != nil {
t.Errorf("AllowedTopologies field didn't get dropped: %+v", class.AllowedTopologies)
}
// Test that field does not get dropped when feature gate is set // Test that field does not get dropped when feature gate is set
class = &storage.StorageClass{ class = &storage.StorageClass{
VolumeBindingMode: &bindingMode, VolumeBindingMode: &bindingMode,
AllowedTopologies: allowedTopologies,
} }
if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true"); err != nil { if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true,DynamicProvisioningScheduling=true"); err != nil {
t.Fatalf("Failed to set feature gate for VolumeScheduling: %v", err) t.Fatalf("Failed to set feature gate for VolumeScheduling or DynamicProvisioningScheduling: %v", err)
} }
DropDisabledAlphaFields(class) DropDisabledAlphaFields(class)
if class.VolumeBindingMode != &bindingMode { if class.VolumeBindingMode != &bindingMode {
t.Errorf("VolumeBindingMode field got unexpectantly modified: %+v", class.VolumeBindingMode) t.Errorf("VolumeBindingMode field got unexpectantly modified: %+v", class.VolumeBindingMode)
} }
if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false"); err != nil { if !reflect.DeepEqual(class.AllowedTopologies, allowedTopologies) {
t.Fatalf("Failed to disable feature gate for VolumeScheduling: %v", err) t.Errorf("AllowedTopologies field got unexpectantly modified: %+v", class.AllowedTopologies)
}
if err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false,DynamicProvisioningScheduling=false"); err != nil {
t.Fatalf("Failed to disable feature gate for VolumeScheduling or DynamicProvisioningScheduling: %v", err)
} }
} }
...@@ -47,6 +47,7 @@ func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList { ...@@ -47,6 +47,7 @@ func ValidateStorageClass(storageClass *storage.StorageClass) field.ErrorList {
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"))...) allErrs = append(allErrs, validateVolumeBindingMode(storageClass.VolumeBindingMode, field.NewPath("volumeBindingMode"))...)
allErrs = append(allErrs, validateAllowedTopologies(storageClass.AllowedTopologies, field.NewPath("allowedTopologies"))...)
return allErrs return allErrs
} }
...@@ -239,3 +240,22 @@ func validateVolumeBindingMode(mode *storage.VolumeBindingMode, fldPath *field.P ...@@ -239,3 +240,22 @@ func validateVolumeBindingMode(mode *storage.VolumeBindingMode, fldPath *field.P
return allErrs return allErrs
} }
// validateAllowedTopology tests that AllowedTopologies specifies valid values.
func validateAllowedTopologies(topologies []api.TopologySelectorTerm, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if topologies == nil || len(topologies) == 0 {
return allErrs
}
if !utilfeature.DefaultFeatureGate.Enabled(features.DynamicProvisioningScheduling) {
allErrs = append(allErrs, field.Forbidden(fldPath, "field is disabled by feature-gate DynamicProvisioningScheduling"))
}
for i, term := range topologies {
allErrs = append(allErrs, apivalidation.ValidateTopologySelectorTerm(term, fldPath.Index(i))...)
}
return allErrs
}
...@@ -450,21 +450,22 @@ func TestVolumeAttachmentUpdateValidation(t *testing.T) { ...@@ -450,21 +450,22 @@ func TestVolumeAttachmentUpdateValidation(t *testing.T) {
} }
} }
func makeClassWithBinding(mode *storage.VolumeBindingMode) *storage.StorageClass { func makeClass(mode *storage.VolumeBindingMode, topologies []api.TopologySelectorTerm) *storage.StorageClass {
return &storage.StorageClass{ return &storage.StorageClass{
ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "foo"},
Provisioner: "kubernetes.io/foo-provisioner", Provisioner: "kubernetes.io/foo-provisioner",
ReclaimPolicy: &deleteReclaimPolicy, ReclaimPolicy: &deleteReclaimPolicy,
VolumeBindingMode: mode, VolumeBindingMode: mode,
AllowedTopologies: topologies,
} }
} }
// TODO: Remove these tests once feature gate is not required // TODO: Remove these tests once feature gate is not required
func TestValidateVolumeBindingModeAlphaDisabled(t *testing.T) { func TestValidateVolumeBindingModeAlphaDisabled(t *testing.T) {
errorCases := map[string]*storage.StorageClass{ errorCases := map[string]*storage.StorageClass{
"immediate mode": makeClassWithBinding(&immediateMode1), "immediate mode": makeClass(&immediateMode1, nil),
"waiting mode": makeClassWithBinding(&waitingMode), "waiting mode": makeClass(&waitingMode, nil),
"invalid mode": makeClassWithBinding(&invalidMode), "invalid mode": makeClass(&invalidMode, nil),
} }
err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false") err := utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
...@@ -486,19 +487,19 @@ type bindingTest struct { ...@@ -486,19 +487,19 @@ type bindingTest struct {
func TestValidateVolumeBindingMode(t *testing.T) { func TestValidateVolumeBindingMode(t *testing.T) {
cases := map[string]bindingTest{ cases := map[string]bindingTest{
"no mode": { "no mode": {
class: makeClassWithBinding(nil), class: makeClass(nil, nil),
shouldSucceed: false, shouldSucceed: false,
}, },
"immediate mode": { "immediate mode": {
class: makeClassWithBinding(&immediateMode1), class: makeClass(&immediateMode1, nil),
shouldSucceed: true, shouldSucceed: true,
}, },
"waiting mode": { "waiting mode": {
class: makeClassWithBinding(&waitingMode), class: makeClass(&waitingMode, nil),
shouldSucceed: true, shouldSucceed: true,
}, },
"invalid mode": { "invalid mode": {
class: makeClassWithBinding(&invalidMode), class: makeClass(&invalidMode, nil),
shouldSucceed: false, shouldSucceed: false,
}, },
} }
...@@ -532,10 +533,10 @@ type updateTest struct { ...@@ -532,10 +533,10 @@ type updateTest struct {
} }
func TestValidateUpdateVolumeBindingMode(t *testing.T) { func TestValidateUpdateVolumeBindingMode(t *testing.T) {
noBinding := makeClassWithBinding(nil) noBinding := makeClass(nil, nil)
immediateBinding1 := makeClassWithBinding(&immediateMode1) immediateBinding1 := makeClass(&immediateMode1, nil)
immediateBinding2 := makeClassWithBinding(&immediateMode2) immediateBinding2 := makeClass(&immediateMode2, nil)
waitBinding := makeClassWithBinding(&waitingMode) waitBinding := makeClass(&waitingMode, nil)
cases := map[string]updateTest{ cases := map[string]updateTest{
"old and new no mode": { "old and new no mode": {
...@@ -591,3 +592,102 @@ func TestValidateUpdateVolumeBindingMode(t *testing.T) { ...@@ -591,3 +592,102 @@ func TestValidateUpdateVolumeBindingMode(t *testing.T) {
t.Fatalf("Failed to disable feature gate for VolumeScheduling: %v", err) t.Fatalf("Failed to disable feature gate for VolumeScheduling: %v", err)
} }
} }
func TestValidateAllowedTopologies(t *testing.T) {
validTopology := []api.TopologySelectorTerm{
{
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
{
Key: "failure-domain.beta.kubernetes.io/zone",
Values: []string{"zone1"},
},
{
Key: "kubernetes.io/hostname",
Values: []string{"node1"},
},
},
},
{
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
{
Key: "failure-domain.beta.kubernetes.io/zone",
Values: []string{"zone2"},
},
{
Key: "kubernetes.io/hostname",
Values: []string{"node2"},
},
},
},
}
topologyInvalidKey := []api.TopologySelectorTerm{
{
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
{
Key: "/invalidkey",
Values: []string{"zone1"},
},
},
},
}
topologyLackOfValues := []api.TopologySelectorTerm{
{
MatchLabelExpressions: []api.TopologySelectorLabelRequirement{
{
Key: "kubernetes.io/hostname",
Values: []string{},
},
},
},
}
cases := map[string]bindingTest{
"no topology": {
class: makeClass(nil, nil),
shouldSucceed: true,
},
"valid topology": {
class: makeClass(nil, validTopology),
shouldSucceed: true,
},
"topology invalid key": {
class: makeClass(nil, topologyInvalidKey),
shouldSucceed: false,
},
"topology lack of values": {
class: makeClass(nil, topologyLackOfValues),
shouldSucceed: false,
},
}
// TODO: remove when feature gate not required
err := utilfeature.DefaultFeatureGate.Set("DynamicProvisioningScheduling=true")
if err != nil {
t.Fatalf("Failed to enable feature gate for DynamicProvisioningScheduling: %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("DynamicProvisioningScheduling=false")
if err != nil {
t.Fatalf("Failed to disable feature gate for DynamicProvisioningScheduling: %v", err)
}
for testName, testCase := range cases {
errs := ValidateStorageClass(testCase.class)
if len(errs) == 0 && testCase.class.AllowedTopologies != nil {
t.Errorf("Expected failure for test %q, got success", testName)
}
}
}
...@@ -2389,6 +2389,7 @@ type NodeSelector struct { ...@@ -2389,6 +2389,7 @@ type NodeSelector struct {
// A null or empty node selector term matches no objects. The requirements of // A null or empty node selector term matches no objects. The requirements of
// them are ANDed. // them are ANDed.
// The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
type NodeSelectorTerm struct { type NodeSelectorTerm struct {
// A list of node selector requirements by node's labels. // A list of node selector requirements by node's labels.
// +optional // +optional
...@@ -2428,6 +2429,27 @@ const ( ...@@ -2428,6 +2429,27 @@ const (
NodeSelectorOpLt NodeSelectorOperator = "Lt" NodeSelectorOpLt NodeSelectorOperator = "Lt"
) )
// A topology selector term represents the result of label queries.
// A null or empty topology selector term matches no objects.
// The requirements of them are ANDed.
// It provides a subset of functionality as NodeSelectorTerm.
// This is an alpha feature and may change in the future.
type TopologySelectorTerm struct {
// A list of topology selector requirements by labels.
// +optional
MatchLabelExpressions []TopologySelectorLabelRequirement `json:"matchLabelExpressions,omitempty" protobuf:"bytes,1,rep,name=matchLabelExpressions"`
}
// A topology selector requirement is a selector that matches given label.
// This is an alpha feature and may change in the future.
type TopologySelectorLabelRequirement struct {
// The label key that the selector applies to.
Key string `json:"key" protobuf:"bytes,1,opt,name=key"`
// An array of string values. One value must match the label to be selected.
// Each entry in Values is ORed.
Values []string `json:"values" protobuf:"bytes,2,rep,name=values"`
}
// Affinity is a group of affinity scheduling rules. // Affinity is a group of affinity scheduling rules.
type Affinity struct { type Affinity struct {
// Describes node affinity scheduling rules for the pod. // Describes node affinity scheduling rules for the pod.
......
...@@ -66,6 +66,14 @@ type StorageClass struct { ...@@ -66,6 +66,14 @@ type StorageClass struct {
// the VolumeScheduling feature. // the VolumeScheduling feature.
// +optional // +optional
VolumeBindingMode *VolumeBindingMode `json:"volumeBindingMode,omitempty" protobuf:"bytes,7,opt,name=volumeBindingMode"` VolumeBindingMode *VolumeBindingMode `json:"volumeBindingMode,omitempty" protobuf:"bytes,7,opt,name=volumeBindingMode"`
// Restrict the node topologies where volumes can be dynamically provisioned.
// Each volume plugin defines its own supported topology specifications.
// An empty TopologySelectorTerm list means there is no topology restriction.
// This field is alpha-level and is only honored by servers that enable
// the DynamicProvisioningScheduling feature.
// +optional
AllowedTopologies []v1.TopologySelectorTerm `json:"allowedTopologies,omitempty" protobuf:"bytes,8,rep,name=allowedTopologies"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
......
...@@ -66,6 +66,14 @@ type StorageClass struct { ...@@ -66,6 +66,14 @@ type StorageClass struct {
// the VolumeScheduling feature. // the VolumeScheduling feature.
// +optional // +optional
VolumeBindingMode *VolumeBindingMode `json:"volumeBindingMode,omitempty" protobuf:"bytes,7,opt,name=volumeBindingMode"` VolumeBindingMode *VolumeBindingMode `json:"volumeBindingMode,omitempty" protobuf:"bytes,7,opt,name=volumeBindingMode"`
// Restrict the node topologies where volumes can be dynamically provisioned.
// Each volume plugin defines its own supported topology specifications.
// An empty TopologySelectorTerm list means there is no topology restriction.
// This field is alpha-level and is only honored by servers that enable
// the DynamicProvisioningScheduling feature.
// +optional
AllowedTopologies []v1.TopologySelectorTerm `json:"allowedTopologies,omitempty" protobuf:"bytes,8,rep,name=allowedTopologies"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment