Add runtime representation of v1.NodeSelector

to be used for repeatedly matching nodes.

Also provide a lazy version (that only reports errors if no terms match) for backwards compatibility of corev1.MatchNodeSelectorTerms

Change-Id: Ib1a0866979ce6cf75d1d9668c4bf8f6fb57298b2
This commit is contained in:
Aldo Culquicondor 2020-10-30 18:58:41 -04:00
parent 3d62aad55e
commit c2e2a0d0a2
8 changed files with 467 additions and 179 deletions

View File

@ -255,5 +255,6 @@
- k8s.io/api
- k8s.io/apimachinery
- k8s.io/client-go
- k8s.io/component-helpers
- k8s.io/klog
- k8s.io/utils

View File

@ -11,10 +11,7 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/selection:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity:go_default_library",
],
)
@ -26,7 +23,6 @@ go_test(
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
],
)
@ -39,7 +35,10 @@ filegroup(
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
srcs = [
":package-srcs",
"//staging/src/k8s.io/component-helpers/scheduling/corev1/nodeaffinity:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -17,13 +17,8 @@ limitations under the License.
package corev1
import (
"fmt"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
apierrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/component-helpers/scheduling/corev1/nodeaffinity"
)
// PodPriority returns priority of the given pod.
@ -46,103 +41,5 @@ func MatchNodeSelectorTerms(
if node == nil {
return false, nil
}
var errors []error
for _, req := range nodeSelector.NodeSelectorTerms {
// nil or empty term selects no objects
if len(req.MatchExpressions) == 0 && len(req.MatchFields) == 0 {
continue
}
if len(req.MatchExpressions) != 0 {
labelSelector, err := nodeSelectorRequirementsAsSelector(req.MatchExpressions)
if err != nil {
errors = append(errors, err)
continue
}
if labelSelector == nil || !labelSelector.Matches(labels.Set(node.Labels)) {
continue
}
}
if len(req.MatchFields) != 0 && len(node.Name) > 0 {
fieldSelector, err := nodeSelectorRequirementsAsFieldSelector(req.MatchFields)
if err != nil {
errors = append(errors, err)
continue
}
if fieldSelector == nil || !fieldSelector.Matches(fields.Set{"metadata.name": node.Name}) {
continue
}
}
return true, nil
}
return false, apierrors.NewAggregate(errors)
}
// nodeSelectorRequirementsAsSelector converts the []NodeSelectorRequirement api type into a struct that implements
// labels.Selector.
func nodeSelectorRequirementsAsSelector(nsm []v1.NodeSelectorRequirement) (labels.Selector, error) {
if len(nsm) == 0 {
return labels.Nothing(), nil
}
selector := labels.NewSelector()
for _, expr := range nsm {
var op selection.Operator
switch expr.Operator {
case v1.NodeSelectorOpIn:
op = selection.In
case v1.NodeSelectorOpNotIn:
op = selection.NotIn
case v1.NodeSelectorOpExists:
op = selection.Exists
case v1.NodeSelectorOpDoesNotExist:
op = selection.DoesNotExist
case v1.NodeSelectorOpGt:
op = selection.GreaterThan
case v1.NodeSelectorOpLt:
op = selection.LessThan
default:
return nil, fmt.Errorf("%q is not a valid node selector operator", expr.Operator)
}
r, err := labels.NewRequirement(expr.Key, op, expr.Values)
if err != nil {
return nil, err
}
selector = selector.Add(*r)
}
return selector, nil
}
// nodeSelectorRequirementsAsFieldSelector converts the []NodeSelectorRequirement core type into a struct that implements
// fields.Selector.
func nodeSelectorRequirementsAsFieldSelector(nsm []v1.NodeSelectorRequirement) (fields.Selector, error) {
if len(nsm) == 0 {
return fields.Nothing(), nil
}
selectors := []fields.Selector{}
for _, expr := range nsm {
switch expr.Operator {
case v1.NodeSelectorOpIn:
if len(expr.Values) != 1 {
return nil, fmt.Errorf("unexpected number of value (%d) for node field selector operator %q",
len(expr.Values), expr.Operator)
}
selectors = append(selectors, fields.OneTermEqualSelector(expr.Key, expr.Values[0]))
case v1.NodeSelectorOpNotIn:
if len(expr.Values) != 1 {
return nil, fmt.Errorf("unexpected number of value (%d) for node field selector operator %q",
len(expr.Values), expr.Operator)
}
selectors = append(selectors, fields.OneTermNotEqualSelector(expr.Key, expr.Values[0]))
default:
return nil, fmt.Errorf("%q is not a valid node field selector operator", expr.Operator)
}
}
return fields.AndSelectors(selectors...), nil
return nodeaffinity.NewLazyErrorNodeSelector(nodeSelector).Match(node)
}

View File

@ -17,13 +17,11 @@ limitations under the License.
package corev1
import (
"reflect"
"testing"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)
// TestPodPriority tests PodPriority function.
@ -121,7 +119,7 @@ func TestMatchNodeSelectorTerms(t *testing.T) {
MatchFields: []v1.NodeSelectorRequirement{{
Key: "metadata.name",
Operator: v1.NodeSelectorOpIn,
Values: []string{"host_1, host_2"},
Values: []string{"host_1", "host_2"},
}},
},
}},
@ -539,7 +537,7 @@ func TestMatchNodeSelectorTermsStateless(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
MatchNodeSelectorTerms(tt.args.node, tt.args.nodeSelector)
_, _ = MatchNodeSelectorTerms(tt.args.node, tt.args.nodeSelector)
if !apiequality.Semantic.DeepEqual(tt.args.nodeSelector, tt.want) {
// fail when tt.args.nodeSelector is deeply modified
t.Errorf("MatchNodeSelectorTerms() got = %v, want %v", tt.args.nodeSelector, tt.want)
@ -547,67 +545,3 @@ func TestMatchNodeSelectorTermsStateless(t *testing.T) {
})
}
}
func TestNodeSelectorRequirementsAsSelector(t *testing.T) {
matchExpressions := []v1.NodeSelectorRequirement{{
Key: "foo",
Operator: v1.NodeSelectorOpIn,
Values: []string{"bar", "baz"},
}}
mustParse := func(s string) labels.Selector {
out, e := labels.Parse(s)
if e != nil {
panic(e)
}
return out
}
tc := []struct {
in []v1.NodeSelectorRequirement
out labels.Selector
expectErr bool
}{
{in: nil, out: labels.Nothing()},
{in: []v1.NodeSelectorRequirement{}, out: labels.Nothing()},
{
in: matchExpressions,
out: mustParse("foo in (baz,bar)"),
},
{
in: []v1.NodeSelectorRequirement{{
Key: "foo",
Operator: v1.NodeSelectorOpExists,
Values: []string{"bar", "baz"},
}},
expectErr: true,
},
{
in: []v1.NodeSelectorRequirement{{
Key: "foo",
Operator: v1.NodeSelectorOpGt,
Values: []string{"1"},
}},
out: mustParse("foo>1"),
},
{
in: []v1.NodeSelectorRequirement{{
Key: "bar",
Operator: v1.NodeSelectorOpLt,
Values: []string{"7"},
}},
out: mustParse("bar<7"),
},
}
for i, tc := range tc {
out, err := nodeSelectorRequirementsAsSelector(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)
}
}
}

View File

@ -0,0 +1,42 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["nodeaffinity.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/component-helpers/scheduling/corev1/nodeaffinity",
importpath = "k8s.io/component-helpers/scheduling/corev1/nodeaffinity",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/selection:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["nodeaffinity_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
],
)

View File

@ -0,0 +1,199 @@
/*
Copyright 2020 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 nodeaffinity
import (
"fmt"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/util/errors"
)
// NodeSelector is a runtime representation of v1.NodeSelector.
type NodeSelector struct {
lazy LazyErrorNodeSelector
}
// LazyErrorNodeSelector is a runtime representation of v1.NodeSelector that
// only reports parse errors when no terms match.
type LazyErrorNodeSelector struct {
terms []nodeSelectorTerm
}
// NewNodeSelector returns a NodeSelector or all parsing errors found.
func NewNodeSelector(ns *v1.NodeSelector) (*NodeSelector, error) {
lazy := NewLazyErrorNodeSelector(ns)
var errs []error
for _, term := range lazy.terms {
if term.parseErr != nil {
errs = append(errs, term.parseErr)
}
}
if len(errs) != 0 {
return nil, errors.NewAggregate(errs)
}
return &NodeSelector{lazy: *lazy}, nil
}
// NewLazyErrorNodeSelector creates a NodeSelector that only reports parse
// errors when no terms match.
func NewLazyErrorNodeSelector(ns *v1.NodeSelector) *LazyErrorNodeSelector {
parsedTerms := make([]nodeSelectorTerm, 0, len(ns.NodeSelectorTerms))
for _, term := range ns.NodeSelectorTerms {
// nil or empty term selects no objects
if len(term.MatchExpressions) == 0 && len(term.MatchFields) == 0 {
continue
}
parsedTerms = append(parsedTerms, nodeSelectorTerm{})
parsedTerm := &parsedTerms[len(parsedTerms)-1]
if len(term.MatchExpressions) != 0 {
parsedTerm.matchLabels, parsedTerm.parseErr = nodeSelectorRequirementsAsSelector(term.MatchExpressions)
if parsedTerm.parseErr != nil {
continue
}
}
if len(term.MatchFields) != 0 {
parsedTerm.matchFields, parsedTerm.parseErr = nodeSelectorRequirementsAsFieldSelector(term.MatchFields)
}
}
return &LazyErrorNodeSelector{
terms: parsedTerms,
}
}
// Match checks whether the node labels and fields match the selector terms, ORed;
// nil or empty term matches no objects.
func (ns *NodeSelector) Match(node *v1.Node) bool {
// parse errors are reported in NewNodeSelector.
match, _ := ns.lazy.Match(node)
return match
}
// Match checks whether the node labels and fields match the selector terms, ORed;
// nil or empty term matches no objects.
// Parse errors are only returned if no terms matched.
func (ns *LazyErrorNodeSelector) Match(node *v1.Node) (bool, error) {
if node == nil {
return false, nil
}
nodeLabels := labels.Set(node.Labels)
nodeFields := make(fields.Set)
if len(node.Name) > 0 {
nodeFields["metadata.name"] = node.Name
}
var errs []error
for _, term := range ns.terms {
match, err := term.match(nodeLabels, nodeFields)
if err != nil {
errs = append(errs, term.parseErr)
continue
}
if match {
return true, nil
}
}
return false, errors.NewAggregate(errs)
}
type nodeSelectorTerm struct {
matchLabels labels.Selector
matchFields fields.Selector
parseErr error
}
func (t *nodeSelectorTerm) match(nodeLabels labels.Set, nodeFields fields.Set) (bool, error) {
if t.parseErr != nil {
return false, t.parseErr
}
if t.matchLabels != nil && !t.matchLabels.Matches(nodeLabels) {
return false, nil
}
if t.matchFields != nil && len(nodeFields) > 0 && !t.matchFields.Matches(nodeFields) {
return false, nil
}
return true, nil
}
// nodeSelectorRequirementsAsSelector converts the []NodeSelectorRequirement api type into a struct that implements
// labels.Selector.
func nodeSelectorRequirementsAsSelector(nsm []v1.NodeSelectorRequirement) (labels.Selector, error) {
if len(nsm) == 0 {
return labels.Nothing(), nil
}
selector := labels.NewSelector()
for _, expr := range nsm {
var op selection.Operator
switch expr.Operator {
case v1.NodeSelectorOpIn:
op = selection.In
case v1.NodeSelectorOpNotIn:
op = selection.NotIn
case v1.NodeSelectorOpExists:
op = selection.Exists
case v1.NodeSelectorOpDoesNotExist:
op = selection.DoesNotExist
case v1.NodeSelectorOpGt:
op = selection.GreaterThan
case v1.NodeSelectorOpLt:
op = selection.LessThan
default:
return nil, fmt.Errorf("%q is not a valid node selector operator", expr.Operator)
}
r, err := labels.NewRequirement(expr.Key, op, expr.Values)
if err != nil {
return nil, err
}
selector = selector.Add(*r)
}
return selector, nil
}
// nodeSelectorRequirementsAsFieldSelector converts the []NodeSelectorRequirement core type into a struct that implements
// fields.Selector.
func nodeSelectorRequirementsAsFieldSelector(nsr []v1.NodeSelectorRequirement) (fields.Selector, error) {
if len(nsr) == 0 {
return fields.Nothing(), nil
}
var selectors []fields.Selector
for _, expr := range nsr {
switch expr.Operator {
case v1.NodeSelectorOpIn:
if len(expr.Values) != 1 {
return nil, fmt.Errorf("unexpected number of value (%d) for node field selector operator %q",
len(expr.Values), expr.Operator)
}
selectors = append(selectors, fields.OneTermEqualSelector(expr.Key, expr.Values[0]))
case v1.NodeSelectorOpNotIn:
if len(expr.Values) != 1 {
return nil, fmt.Errorf("unexpected number of value (%d) for node field selector operator %q",
len(expr.Values), expr.Operator)
}
selectors = append(selectors, fields.OneTermNotEqualSelector(expr.Key, expr.Values[0]))
default:
return nil, fmt.Errorf("%q is not a valid node field selector operator", expr.Operator)
}
}
return fields.AndSelectors(selectors...), nil
}

View File

@ -0,0 +1,215 @@
/*
Copyright 2020 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 nodeaffinity
import (
"errors"
"reflect"
"testing"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
apierrors "k8s.io/apimachinery/pkg/util/errors"
)
func TestNodeSelectorMatch(t *testing.T) {
tests := []struct {
name string
nodeSelector v1.NodeSelector
node *v1.Node
wantErr error
wantMatch bool
}{
{
name: "nil node",
wantMatch: false,
},
{
name: "invalid field selector and label selector",
nodeSelector: v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchFields: []v1.NodeSelectorRequirement{{
Key: "metadata.name",
Operator: v1.NodeSelectorOpIn,
Values: []string{"host_1", "host_2"},
}},
},
{
MatchExpressions: []v1.NodeSelectorRequirement{{
Key: "label_1",
Operator: v1.NodeSelectorOpIn,
Values: []string{"label_1_val"},
}},
MatchFields: []v1.NodeSelectorRequirement{{
Key: "metadata.name",
Operator: v1.NodeSelectorOpIn,
Values: []string{"host_1"},
}},
},
{
MatchExpressions: []v1.NodeSelectorRequirement{{
Key: "invalid key",
Operator: v1.NodeSelectorOpIn,
Values: []string{"label_value"},
}},
},
}},
wantErr: apierrors.NewAggregate([]error{
errors.New(`unexpected number of value (2) for node field selector operator "In"`),
errors.New(`invalid label key "invalid key": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`),
}),
},
{
name: "node matches field selector, but not labels",
nodeSelector: v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{{
Key: "label_1",
Operator: v1.NodeSelectorOpIn,
Values: []string{"label_1_val"},
}},
MatchFields: []v1.NodeSelectorRequirement{{
Key: "metadata.name",
Operator: v1.NodeSelectorOpIn,
Values: []string{"host_1"},
}},
},
}},
node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "host_1"}},
},
{
name: "node matches field selector and label selector",
nodeSelector: v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{{
Key: "label_1",
Operator: v1.NodeSelectorOpIn,
Values: []string{"label_1_val"},
}},
MatchFields: []v1.NodeSelectorRequirement{{
Key: "metadata.name",
Operator: v1.NodeSelectorOpIn,
Values: []string{"host_1"},
}},
},
}},
node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "host_1", Labels: map[string]string{"label_1": "label_1_val"}}},
wantMatch: true,
},
{
name: "second term matches",
nodeSelector: v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{{
Key: "label_1",
Operator: v1.NodeSelectorOpIn,
Values: []string{"label_1_val"},
}},
},
{
MatchFields: []v1.NodeSelectorRequirement{{
Key: "metadata.name",
Operator: v1.NodeSelectorOpIn,
Values: []string{"host_1"},
}},
},
}},
node: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "host_1"}},
wantMatch: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nodeSelector, err := NewNodeSelector(&tt.nodeSelector)
if !reflect.DeepEqual(err, tt.wantErr) {
t.Fatalf("NewNodeSelector returned error %q, want %q", err, tt.wantErr)
}
if tt.wantErr != nil {
return
}
match := nodeSelector.Match(tt.node)
if match != tt.wantMatch {
t.Errorf("NodeSelector.Match returned %t, want %t", match, tt.wantMatch)
}
})
}
}
func TestNodeSelectorRequirementsAsSelector(t *testing.T) {
matchExpressions := []v1.NodeSelectorRequirement{{
Key: "foo",
Operator: v1.NodeSelectorOpIn,
Values: []string{"bar", "baz"},
}}
mustParse := func(s string) labels.Selector {
out, e := labels.Parse(s)
if e != nil {
panic(e)
}
return out
}
tc := []struct {
in []v1.NodeSelectorRequirement
out labels.Selector
expectErr bool
}{
{in: nil, out: labels.Nothing()},
{in: []v1.NodeSelectorRequirement{}, out: labels.Nothing()},
{
in: matchExpressions,
out: mustParse("foo in (baz,bar)"),
},
{
in: []v1.NodeSelectorRequirement{{
Key: "foo",
Operator: v1.NodeSelectorOpExists,
Values: []string{"bar", "baz"},
}},
expectErr: true,
},
{
in: []v1.NodeSelectorRequirement{{
Key: "foo",
Operator: v1.NodeSelectorOpGt,
Values: []string{"1"},
}},
out: mustParse("foo>1"),
},
{
in: []v1.NodeSelectorRequirement{{
Key: "bar",
Operator: v1.NodeSelectorOpLt,
Values: []string{"7"},
}},
out: mustParse("bar<7"),
},
}
for i, tc := range tc {
out, err := nodeSelectorRequirementsAsSelector(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)
}
}
}

1
vendor/modules.txt vendored
View File

@ -2193,6 +2193,7 @@ k8s.io/component-base/version/verflag
# k8s.io/component-helpers => ./staging/src/k8s.io/component-helpers
k8s.io/component-helpers/lease
k8s.io/component-helpers/scheduling/corev1
k8s.io/component-helpers/scheduling/corev1/nodeaffinity
# k8s.io/controller-manager v0.0.0 => ./staging/src/k8s.io/controller-manager
## explicit
# k8s.io/controller-manager => ./staging/src/k8s.io/controller-manager