diff --git a/source/custom/expression/expression.go b/pkg/apis/nfd/v1alpha1/expression.go similarity index 76% rename from source/custom/expression/expression.go rename to pkg/apis/nfd/v1alpha1/expression.go index f5595306b..5c6c186c3 100644 --- a/source/custom/expression/expression.go +++ b/pkg/apis/nfd/v1alpha1/expression.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package expression +package v1alpha1 import ( "encoding/json" @@ -29,80 +29,6 @@ import ( "sigs.k8s.io/node-feature-discovery/pkg/api/feature" ) -// MatchExpressionSet contains a set of MatchExpressions, each of which is -// evaluated against a set of input values. -type MatchExpressionSet map[string]*MatchExpression - -// MatchExpression specifies an expression to evaluate against a set of input -// values. It contains an operator that is applied when matching the input and -// an array of values that the operator evaluates the input against. -// NB: CreateMatchExpression or MustCreateMatchExpression() should be used for -// creating new instances. -// NB: Validate() must be called if Op or Value fields are modified or if a new -// instance is created from scratch without using the helper functions. -type MatchExpression struct { - // Op is the operator to be applied. - Op MatchOp - - // Value is the list of values that the operand evaluates the input - // against. Value should be empty if the operator is Exists, DoesNotExist, - // IsTrue or IsFalse. Value should contain exactly one element if the - // operator is Gt or Lt and exactly two elements if the operator is GtLt. - // In other cases Value should contain at least one element. - Value MatchValue `json:",omitempty"` - - // valueRe caches compiled regexps for "InRegexp" operator - valueRe []*regexp.Regexp -} - -// MatchOp is the match operator that is applied on values when evaluating a -// MatchExpression. -type MatchOp string - -// MatchValue is the list of values associated with a MatchExpression. -type MatchValue []string - -const ( - // MatchAny returns always true. - MatchAny MatchOp = "" - // MatchIn returns true if any of the values stored in the expression is - // equal to the input. - MatchIn MatchOp = "In" - // MatchIn returns true if none of the values in the expression are equal - // to the input. - MatchNotIn MatchOp = "NotIn" - // MatchInRegexp treats values of the expression as regular expressions and - // returns true if any of them matches the input. - MatchInRegexp MatchOp = "InRegexp" - // MatchExists returns true if the input is valid. The expression must not - // have any values. - MatchExists MatchOp = "Exists" - // MatchDoesNotExist returns true if the input is not valid. The expression - // must not have any values. - MatchDoesNotExist MatchOp = "DoesNotExist" - // MatchGt returns true if the input is greater than the value of the - // expression (number of values in the expression must be exactly one). - // Both the input and value must be integer numbers, otherwise an error is - // returned. - MatchGt MatchOp = "Gt" - // MatchLt returns true if the input is less than the value of the - // expression (number of values in the expression must be exactly one). - // Both the input and value must be integer numbers, otherwise an error is - // returned. - MatchLt MatchOp = "Lt" - // MatchGtLt returns true if the input is between two values, i.e. greater - // than the first value and less than the second value of the expression - // (number of values in the expression must be exactly two). Both the input - // and values must be integer numbers, otherwise an error is returned. - MatchGtLt MatchOp = "GtLt" - // MatchIsTrue returns true if the input holds the value "true". The - // expression must not have any values. - MatchIsTrue MatchOp = "IsTrue" - // MatchIsTrue returns true if the input holds the value "false". The - // expression must not have any values. - MatchIsFalse MatchOp = "IsFalse" -) - var matchOps = map[MatchOp]struct{}{ MatchAny: struct{}{}, MatchIn: struct{}{}, diff --git a/pkg/apis/nfd/v1alpha1/expression_test.go b/pkg/apis/nfd/v1alpha1/expression_test.go new file mode 100644 index 000000000..33682a3b1 --- /dev/null +++ b/pkg/apis/nfd/v1alpha1/expression_test.go @@ -0,0 +1,439 @@ +/* +Copyright 2021 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 v1alpha1_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" + + "sigs.k8s.io/node-feature-discovery/pkg/api/feature" + api "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" +) + +type BoolAssertionFuncf func(assert.TestingT, bool, string, ...interface{}) bool + +type ValueAssertionFuncf func(assert.TestingT, interface{}, string, ...interface{}) bool + +func TestCreateMatchExpression(t *testing.T) { + type V = api.MatchValue + type TC struct { + op api.MatchOp + values V + err ValueAssertionFuncf + } + + tcs := []TC{ + {op: api.MatchAny, err: assert.Nilf}, // #0 + {op: api.MatchAny, values: V{"1"}, err: assert.NotNilf}, + + {op: api.MatchIn, err: assert.NotNilf}, + {op: api.MatchIn, values: V{"1"}, err: assert.Nilf}, + {op: api.MatchIn, values: V{"1", "2", "3", "4"}, err: assert.Nilf}, + + {op: api.MatchNotIn, err: assert.NotNilf}, + {op: api.MatchNotIn, values: V{"1"}, err: assert.Nilf}, + {op: api.MatchNotIn, values: V{"1", "2"}, err: assert.Nilf}, + + {op: api.MatchInRegexp, err: assert.NotNilf}, + {op: api.MatchInRegexp, values: V{"1"}, err: assert.Nilf}, + {op: api.MatchInRegexp, values: V{"()", "2", "3"}, err: assert.Nilf}, + {op: api.MatchInRegexp, values: V{"("}, err: assert.NotNilf}, + + {op: api.MatchExists, err: assert.Nilf}, + {op: api.MatchExists, values: V{"1"}, err: assert.NotNilf}, + + {op: api.MatchDoesNotExist, err: assert.Nilf}, + {op: api.MatchDoesNotExist, values: V{"1"}, err: assert.NotNilf}, + + {op: api.MatchGt, err: assert.NotNilf}, + {op: api.MatchGt, values: V{"1"}, err: assert.Nilf}, + {op: api.MatchGt, values: V{"-10"}, err: assert.Nilf}, + {op: api.MatchGt, values: V{"1", "2"}, err: assert.NotNilf}, + {op: api.MatchGt, values: V{""}, err: assert.NotNilf}, + + {op: api.MatchLt, err: assert.NotNilf}, + {op: api.MatchLt, values: V{"1"}, err: assert.Nilf}, + {op: api.MatchLt, values: V{"-1"}, err: assert.Nilf}, + {op: api.MatchLt, values: V{"1", "2", "3"}, err: assert.NotNilf}, + {op: api.MatchLt, values: V{"a"}, err: assert.NotNilf}, + + {op: api.MatchGtLt, err: assert.NotNilf}, + {op: api.MatchGtLt, values: V{"1"}, err: assert.NotNilf}, + {op: api.MatchGtLt, values: V{"1", "2"}, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"2", "1"}, err: assert.NotNilf}, + {op: api.MatchGtLt, values: V{"1", "2", "3"}, err: assert.NotNilf}, + {op: api.MatchGtLt, values: V{"a", "2"}, err: assert.NotNilf}, + + {op: api.MatchIsTrue, err: assert.Nilf}, + {op: api.MatchIsTrue, values: V{"1"}, err: assert.NotNilf}, + + {op: api.MatchIsFalse, err: assert.Nilf}, + {op: api.MatchIsFalse, values: V{"1", "2"}, err: assert.NotNilf}, + } + + for i, tc := range tcs { + _, err := api.CreateMatchExpression(tc.op, tc.values...) + tc.err(t, err, "test case #%d (%v) failed", i, tc) + } +} + +func TestMatch(t *testing.T) { + type V = api.MatchValue + type TC struct { + op api.MatchOp + values V + input interface{} + valid bool + result BoolAssertionFuncf + err ValueAssertionFuncf + } + + tcs := []TC{ + {op: api.MatchAny, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchAny, input: "2", valid: false, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf}, + + {op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchExists, input: nil, valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchExists, input: nil, valid: true, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchDoesNotExist, input: false, valid: false, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchDoesNotExist, input: false, valid: true, result: assert.Falsef, err: assert.Nilf}, + + {op: api.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchGt, values: V{"2"}, input: "3a", valid: true, result: assert.Falsef, err: assert.NotNilf}, + + {op: api.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchLt, values: V{"2"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf}, + + {op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"1", "10"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf}, + + {op: api.MatchIsTrue, input: true, valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIsTrue, input: true, valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchIsTrue, input: false, valid: true, result: assert.Falsef, err: assert.Nilf}, + + {op: api.MatchIsFalse, input: "false", valid: false, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIsFalse, input: "false", valid: true, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchIsFalse, input: "true", valid: true, result: assert.Falsef, err: assert.Nilf}, + } + + for i, tc := range tcs { + me := api.MustCreateMatchExpression(tc.op, tc.values...) + res, err := me.Match(tc.valid, tc.input) + tc.result(t, res, "test case #%d (%v) failed", i, tc) + tc.err(t, err, "test case #%d (%v) failed", i, tc) + } + + // Check some special error cases separately because MustCreateMatch panics + tcs = []TC{ + + {op: api.MatchGt, values: V{"3.0"}, input: 1, valid: true}, + {op: api.MatchLt, values: V{"0x2"}, input: 1, valid: true}, + {op: api.MatchGtLt, values: V{"1", "str"}, input: 1, valid: true}, + {op: "non-existent-op", values: V{"1"}, input: 1, valid: true}, + } + + for i, tc := range tcs { + me := api.MatchExpression{Op: tc.op, Value: tc.values} + res, err := me.Match(tc.valid, tc.input) + assert.Falsef(t, res, "err test case #%d (%v) failed", i, tc) + assert.NotNilf(t, err, "err test case #%d (%v) failed", i, tc) + } +} + +func TestMatchKeys(t *testing.T) { + type V = api.MatchValue + type I = map[string]feature.Nil + type TC struct { + op api.MatchOp + values V + name string + input I + result BoolAssertionFuncf + err ValueAssertionFuncf + } + + tcs := []TC{ + {op: api.MatchAny, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchExists, name: "foo", input: nil, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchExists, name: "foo", input: I{"bar": {}}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchExists, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchDoesNotExist, name: "foo", input: I{}, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchDoesNotExist, name: "foo", input: I{"bar": {}}, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchDoesNotExist, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Falsef, err: assert.Nilf}, + + // All other ops should return an error + {op: api.MatchIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + {op: api.MatchNotIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + {op: api.MatchInRegexp, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + {op: api.MatchGt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + {op: api.MatchLt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + {op: api.MatchGtLt, values: V{"1", "10"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + {op: api.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + {op: api.MatchIsFalse, name: "foo", result: assert.Falsef, err: assert.NotNilf}, + } + + for i, tc := range tcs { + me := api.MustCreateMatchExpression(tc.op, tc.values...) + res, err := me.MatchKeys(tc.name, tc.input) + tc.result(t, res, "test case #%d (%v) failed", i, tc) + tc.err(t, err, "test case #%d (%v) failed", i, tc) + } +} + +func TestMatchValues(t *testing.T) { + type V = []string + type I = map[string]string + + type TC struct { + op api.MatchOp + values V + name string + input I + result BoolAssertionFuncf + err ValueAssertionFuncf + } + + tcs := []TC{ + {op: api.MatchAny, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Falsef, err: assert.Nilf}, + + {op: api.MatchInRegexp, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "9"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchExists, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchExists, name: "foo", input: I{"foo": "1"}, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchDoesNotExist, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf}, + + {op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf}, + + {op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf}, + + {op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf}, + {op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf}, + + {op: api.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIsTrue, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIsTrue, name: "foo", input: I{"foo": "true"}, result: assert.Truef, err: assert.Nilf}, + + {op: api.MatchIsFalse, name: "foo", input: I{"foo": "true"}, result: assert.Falsef, err: assert.Nilf}, + {op: api.MatchIsFalse, name: "foo", input: I{"foo": "false"}, result: assert.Truef, err: assert.Nilf}, + } + + for i, tc := range tcs { + me := api.MustCreateMatchExpression(tc.op, tc.values...) + res, err := me.MatchValues(tc.name, tc.input) + tc.result(t, res, "test case #%d (%v) failed", i, tc) + tc.err(t, err, "test case #%d (%v) failed", i, tc) + } +} + +func TestMESMatchKeys(t *testing.T) { + type I = map[string]feature.Nil + type TC struct { + mes string + input I + result BoolAssertionFuncf + err ValueAssertionFuncf + } + + tcs := []TC{ + {result: assert.Truef, err: assert.Nilf}, + + {input: I{"foo": {}}, result: assert.Truef, err: assert.Nilf}, + + {mes: ` +foo: { op: DoesNotExist } +bar: { op: Exists } +`, + input: I{"bar": {}, "baz": {}}, + result: assert.Truef, err: assert.Nilf}, + + {mes: ` +foo: { op: DoesNotExist } +bar: { op: Exists } +`, + input: I{"foo": {}, "bar": {}, "baz": {}}, + result: assert.Falsef, err: assert.Nilf}, + + {mes: ` +foo: { op: In, value: ["bar"] } +bar: { op: Exists } +`, + input: I{"bar": {}, "baz": {}}, + result: assert.Falsef, err: assert.NotNilf}, + } + + for i, tc := range tcs { + mes := &api.MatchExpressionSet{} + if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { + t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err) + } + + res, err := mes.MatchKeys(tc.input) + tc.result(t, res, "test case #%d (%v) failed", i, tc) + tc.err(t, err, "test case #%d (%v) failed", i, tc) + } +} + +func TestMESMatchValues(t *testing.T) { + type I = map[string]string + type TC struct { + mes string + input I + result BoolAssertionFuncf + err ValueAssertionFuncf + } + + tcs := []TC{ + {result: assert.Truef, err: assert.Nilf}, + + {input: I{"foo": "bar"}, result: assert.Truef, err: assert.Nilf}, + + {mes: ` +foo: { op: Exists } +bar: { op: In, value: ["val", "wal"] } +baz: { op: Gt, value: ["10"] } +`, + input: I{"bar": "val"}, + result: assert.Falsef, err: assert.Nilf}, + + {mes: ` +foo: { op: Exists } +bar: { op: In, value: ["val", "wal"] } +baz: { op: Gt, value: ["10"] } +`, + input: I{"foo": "1", "bar": "val", "baz": "123"}, + result: assert.Truef, err: assert.Nilf}, + + {mes: ` +foo: { op: Exists } +bar: { op: In, value: ["val"] } +baz: { op: Gt, value: ["10"] } +`, + input: I{"foo": "1", "bar": "val", "baz": "123.0"}, + result: assert.Falsef, err: assert.NotNilf}, + } + + for i, tc := range tcs { + mes := &api.MatchExpressionSet{} + if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { + t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err) + } + + res, err := mes.MatchValues(tc.input) + tc.result(t, res, "test case #%d (%v) failed", i, tc) + tc.err(t, err, "test case #%d (%v) failed", i, tc) + } +} + +func TestMESMatchInstances(t *testing.T) { + type I = feature.InstanceFeature + type A = map[string]string + type TC struct { + mes string + input []I + result BoolAssertionFuncf + err ValueAssertionFuncf + } + + tcs := []TC{ + {result: assert.Falsef, err: assert.Nilf}, // nil instances -> false + + {input: []I{}, result: assert.Falsef, err: assert.Nilf}, // zero instances -> false + + {input: []I{I{Attributes: A{}}}, result: assert.Truef, err: assert.Nilf}, // one "empty" instance + + {mes: ` +foo: { op: Exists } +bar: { op: Lt, value: ["10"] } +`, + input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}}, + result: assert.Falsef, err: assert.Nilf}, + + {mes: ` +foo: { op: Exists } +bar: { op: Lt, value: ["10"] } +`, + input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}}, + result: assert.Truef, err: assert.Nilf}, + + {mes: ` +bar: { op: Lt, value: ["10"] } +`, + input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}}, + result: assert.Falsef, err: assert.NotNilf}, + } + + for i, tc := range tcs { + mes := &api.MatchExpressionSet{} + if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { + t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err) + } + + res, err := mes.MatchInstances(tc.input) + tc.result(t, res, "test case #%d (%v) failed", i, tc) + tc.err(t, err, "test case #%d (%v) failed", i, tc) + } +} diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go new file mode 100644 index 000000000..9dede54aa --- /dev/null +++ b/pkg/apis/nfd/v1alpha1/types.go @@ -0,0 +1,95 @@ +/* +Copyright 2021 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 v1alpha1 + +import ( + "regexp" +) + +// MatchExpressionSet contains a set of MatchExpressions, each of which is +// evaluated against a set of input values. +type MatchExpressionSet map[string]*MatchExpression + +// MatchExpression specifies an expression to evaluate against a set of input +// values. It contains an operator that is applied when matching the input and +// an array of values that the operator evaluates the input against. +// NB: CreateMatchExpression or MustCreateMatchExpression() should be used for +// creating new instances. +// NB: Validate() must be called if Op or Value fields are modified or if a new +// instance is created from scratch without using the helper functions. +type MatchExpression struct { + // Op is the operator to be applied. + Op MatchOp + + // Value is the list of values that the operand evaluates the input + // against. Value should be empty if the operator is Exists, DoesNotExist, + // IsTrue or IsFalse. Value should contain exactly one element if the + // operator is Gt or Lt and exactly two elements if the operator is GtLt. + // In other cases Value should contain at least one element. + Value MatchValue `json:",omitempty"` + + // valueRe caches compiled regexps for "InRegexp" operator + valueRe []*regexp.Regexp +} + +// MatchOp is the match operator that is applied on values when evaluating a +// MatchExpression. +type MatchOp string + +// MatchValue is the list of values associated with a MatchExpression. +type MatchValue []string + +const ( + // MatchAny returns always true. + MatchAny MatchOp = "" + // MatchIn returns true if any of the values stored in the expression is + // equal to the input. + MatchIn MatchOp = "In" + // MatchIn returns true if none of the values in the expression are equal + // to the input. + MatchNotIn MatchOp = "NotIn" + // MatchInRegexp treats values of the expression as regular expressions and + // returns true if any of them matches the input. + MatchInRegexp MatchOp = "InRegexp" + // MatchExists returns true if the input is valid. The expression must not + // have any values. + MatchExists MatchOp = "Exists" + // MatchDoesNotExist returns true if the input is not valid. The expression + // must not have any values. + MatchDoesNotExist MatchOp = "DoesNotExist" + // MatchGt returns true if the input is greater than the value of the + // expression (number of values in the expression must be exactly one). + // Both the input and value must be integer numbers, otherwise an error is + // returned. + MatchGt MatchOp = "Gt" + // MatchLt returns true if the input is less than the value of the + // expression (number of values in the expression must be exactly one). + // Both the input and value must be integer numbers, otherwise an error is + // returned. + MatchLt MatchOp = "Lt" + // MatchGtLt returns true if the input is between two values, i.e. greater + // than the first value and less than the second value of the expression + // (number of values in the expression must be exactly two). Both the input + // and values must be integer numbers, otherwise an error is returned. + MatchGtLt MatchOp = "GtLt" + // MatchIsTrue returns true if the input holds the value "true". The + // expression must not have any values. + MatchIsTrue MatchOp = "IsTrue" + // MatchIsTrue returns true if the input holds the value "false". The + // expression must not have any values. + MatchIsFalse MatchOp = "IsFalse" +) diff --git a/source/custom/custom.go b/source/custom/custom.go index 4ccc5f85a..84b5a502f 100644 --- a/source/custom/custom.go +++ b/source/custom/custom.go @@ -26,9 +26,9 @@ import ( "sigs.k8s.io/yaml" "sigs.k8s.io/node-feature-discovery/pkg/api/feature" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/pkg/utils" "sigs.k8s.io/node-feature-discovery/source" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" "sigs.k8s.io/node-feature-discovery/source/custom/rules" ) @@ -65,7 +65,7 @@ type FeatureMatcher []FeatureMatcherTerm type FeatureMatcherTerm struct { Feature string - MatchExpressions expression.MatchExpressionSet + MatchExpressions nfdv1alpha1.MatchExpressionSet } type config []CustomRule diff --git a/source/custom/custom_test.go b/source/custom/custom_test.go index 968413260..d29a7fbad 100644 --- a/source/custom/custom_test.go +++ b/source/custom/custom_test.go @@ -21,7 +21,7 @@ import ( "github.com/stretchr/testify/assert" "sigs.k8s.io/node-feature-discovery/pkg/api/feature" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" ) func TestRule(t *testing.T) { @@ -32,7 +32,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchExists)}, + MatchExpressions: nfdv1alpha1.MatchExpressionSet{"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists)}, }, }, } @@ -95,7 +95,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-1")}, + MatchExpressions: nfdv1alpha1.MatchExpressionSet{"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1")}, }, }, } @@ -114,7 +114,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: expression.MatchExpressionSet{"attr-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-1")}, + MatchExpressions: nfdv1alpha1.MatchExpressionSet{"attr-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1")}, }, }, } @@ -133,11 +133,11 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-x")}, + MatchExpressions: nfdv1alpha1.MatchExpressionSet{"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-x")}, }, FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: expression.MatchExpressionSet{"attr-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-1")}, + MatchExpressions: nfdv1alpha1.MatchExpressionSet{"attr-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1")}, }, }, } @@ -145,7 +145,7 @@ func TestRule(t *testing.T) { assert.Nilf(t, err, "unexpected error: %v", err) assert.Nil(t, m, "instances should not have matched") - r5.MatchFeatures[0].MatchExpressions["key-1"] = expression.MustCreateMatchExpression(expression.MatchIn, "val-1") + r5.MatchFeatures[0].MatchExpressions["key-1"] = nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1") m, err = r5.execute(f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r5.Labels, m, "instances should have matched") @@ -156,7 +156,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: expression.MatchExpressionSet{"key-na": expression.MustCreateMatchExpression(expression.MatchExists)}, + MatchExpressions: nfdv1alpha1.MatchExpressionSet{"key-na": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists)}, }, }, }, @@ -170,11 +170,11 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchExists)}, + MatchExpressions: nfdv1alpha1.MatchExpressionSet{"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists)}, }, }, }) - r5.MatchFeatures[0].MatchExpressions["key-1"] = expression.MustCreateMatchExpression(expression.MatchIn, "val-1") + r5.MatchFeatures[0].MatchExpressions["key-1"] = nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1") m, err = r5.execute(f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r5.Labels, m, "instances should have matched") diff --git a/source/custom/expression/expression_test.go b/source/custom/expression/expression_test.go deleted file mode 100644 index 94b1681e3..000000000 --- a/source/custom/expression/expression_test.go +++ /dev/null @@ -1,439 +0,0 @@ -/* -Copyright 2021 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 expression_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "sigs.k8s.io/yaml" - - "sigs.k8s.io/node-feature-discovery/pkg/api/feature" - e "sigs.k8s.io/node-feature-discovery/source/custom/expression" -) - -type BoolAssertionFuncf func(assert.TestingT, bool, string, ...interface{}) bool - -type ValueAssertionFuncf func(assert.TestingT, interface{}, string, ...interface{}) bool - -func TestCreateMatchExpression(t *testing.T) { - type V = e.MatchValue - type TC struct { - op e.MatchOp - values V - err ValueAssertionFuncf - } - - tcs := []TC{ - {op: e.MatchAny, err: assert.Nilf}, // #0 - {op: e.MatchAny, values: V{"1"}, err: assert.NotNilf}, - - {op: e.MatchIn, err: assert.NotNilf}, - {op: e.MatchIn, values: V{"1"}, err: assert.Nilf}, - {op: e.MatchIn, values: V{"1", "2", "3", "4"}, err: assert.Nilf}, - - {op: e.MatchNotIn, err: assert.NotNilf}, - {op: e.MatchNotIn, values: V{"1"}, err: assert.Nilf}, - {op: e.MatchNotIn, values: V{"1", "2"}, err: assert.Nilf}, - - {op: e.MatchInRegexp, err: assert.NotNilf}, - {op: e.MatchInRegexp, values: V{"1"}, err: assert.Nilf}, - {op: e.MatchInRegexp, values: V{"()", "2", "3"}, err: assert.Nilf}, - {op: e.MatchInRegexp, values: V{"("}, err: assert.NotNilf}, - - {op: e.MatchExists, err: assert.Nilf}, - {op: e.MatchExists, values: V{"1"}, err: assert.NotNilf}, - - {op: e.MatchDoesNotExist, err: assert.Nilf}, - {op: e.MatchDoesNotExist, values: V{"1"}, err: assert.NotNilf}, - - {op: e.MatchGt, err: assert.NotNilf}, - {op: e.MatchGt, values: V{"1"}, err: assert.Nilf}, - {op: e.MatchGt, values: V{"-10"}, err: assert.Nilf}, - {op: e.MatchGt, values: V{"1", "2"}, err: assert.NotNilf}, - {op: e.MatchGt, values: V{""}, err: assert.NotNilf}, - - {op: e.MatchLt, err: assert.NotNilf}, - {op: e.MatchLt, values: V{"1"}, err: assert.Nilf}, - {op: e.MatchLt, values: V{"-1"}, err: assert.Nilf}, - {op: e.MatchLt, values: V{"1", "2", "3"}, err: assert.NotNilf}, - {op: e.MatchLt, values: V{"a"}, err: assert.NotNilf}, - - {op: e.MatchGtLt, err: assert.NotNilf}, - {op: e.MatchGtLt, values: V{"1"}, err: assert.NotNilf}, - {op: e.MatchGtLt, values: V{"1", "2"}, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"2", "1"}, err: assert.NotNilf}, - {op: e.MatchGtLt, values: V{"1", "2", "3"}, err: assert.NotNilf}, - {op: e.MatchGtLt, values: V{"a", "2"}, err: assert.NotNilf}, - - {op: e.MatchIsTrue, err: assert.Nilf}, - {op: e.MatchIsTrue, values: V{"1"}, err: assert.NotNilf}, - - {op: e.MatchIsFalse, err: assert.Nilf}, - {op: e.MatchIsFalse, values: V{"1", "2"}, err: assert.NotNilf}, - } - - for i, tc := range tcs { - _, err := e.CreateMatchExpression(tc.op, tc.values...) - tc.err(t, err, "test case #%d (%v) failed", i, tc) - } -} - -func TestMatch(t *testing.T) { - type V = e.MatchValue - type TC struct { - op e.MatchOp - values V - input interface{} - valid bool - result BoolAssertionFuncf - err ValueAssertionFuncf - } - - tcs := []TC{ - {op: e.MatchAny, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchAny, input: "2", valid: false, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf}, - - {op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchExists, input: nil, valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchExists, input: nil, valid: true, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchDoesNotExist, input: false, valid: false, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchDoesNotExist, input: false, valid: true, result: assert.Falsef, err: assert.Nilf}, - - {op: e.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchGt, values: V{"2"}, input: "3a", valid: true, result: assert.Falsef, err: assert.NotNilf}, - - {op: e.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchLt, values: V{"2"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf}, - - {op: e.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"1", "10"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf}, - - {op: e.MatchIsTrue, input: true, valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIsTrue, input: true, valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchIsTrue, input: false, valid: true, result: assert.Falsef, err: assert.Nilf}, - - {op: e.MatchIsFalse, input: "false", valid: false, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIsFalse, input: "false", valid: true, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchIsFalse, input: "true", valid: true, result: assert.Falsef, err: assert.Nilf}, - } - - for i, tc := range tcs { - me := e.MustCreateMatchExpression(tc.op, tc.values...) - res, err := me.Match(tc.valid, tc.input) - tc.result(t, res, "test case #%d (%v) failed", i, tc) - tc.err(t, err, "test case #%d (%v) failed", i, tc) - } - - // Check some special error cases separately because MustCreateMatch panics - tcs = []TC{ - - {op: e.MatchGt, values: V{"3.0"}, input: 1, valid: true}, - {op: e.MatchLt, values: V{"0x2"}, input: 1, valid: true}, - {op: e.MatchGtLt, values: V{"1", "str"}, input: 1, valid: true}, - {op: "non-existent-op", values: V{"1"}, input: 1, valid: true}, - } - - for i, tc := range tcs { - me := e.MatchExpression{Op: tc.op, Value: tc.values} - res, err := me.Match(tc.valid, tc.input) - assert.Falsef(t, res, "err test case #%d (%v) failed", i, tc) - assert.NotNilf(t, err, "err test case #%d (%v) failed", i, tc) - } -} - -func TestMatchKeys(t *testing.T) { - type V = e.MatchValue - type I = map[string]feature.Nil - type TC struct { - op e.MatchOp - values V - name string - input I - result BoolAssertionFuncf - err ValueAssertionFuncf - } - - tcs := []TC{ - {op: e.MatchAny, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchExists, name: "foo", input: nil, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchExists, name: "foo", input: I{"bar": {}}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchExists, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchDoesNotExist, name: "foo", input: I{}, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchDoesNotExist, name: "foo", input: I{"bar": {}}, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchDoesNotExist, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Falsef, err: assert.Nilf}, - - // All other ops should return an error - {op: e.MatchIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - {op: e.MatchNotIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - {op: e.MatchInRegexp, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - {op: e.MatchGt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - {op: e.MatchLt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - {op: e.MatchGtLt, values: V{"1", "10"}, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - {op: e.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - {op: e.MatchIsFalse, name: "foo", result: assert.Falsef, err: assert.NotNilf}, - } - - for i, tc := range tcs { - me := e.MustCreateMatchExpression(tc.op, tc.values...) - res, err := me.MatchKeys(tc.name, tc.input) - tc.result(t, res, "test case #%d (%v) failed", i, tc) - tc.err(t, err, "test case #%d (%v) failed", i, tc) - } -} - -func TestMatchValues(t *testing.T) { - type V = []string - type I = map[string]string - - type TC struct { - op e.MatchOp - values V - name string - input I - result BoolAssertionFuncf - err ValueAssertionFuncf - } - - tcs := []TC{ - {op: e.MatchAny, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Falsef, err: assert.Nilf}, - - {op: e.MatchInRegexp, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "9"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchExists, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchExists, name: "foo", input: I{"foo": "1"}, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchDoesNotExist, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf}, - - {op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf}, - - {op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf}, - - {op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf}, - {op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf}, - - {op: e.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIsTrue, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIsTrue, name: "foo", input: I{"foo": "true"}, result: assert.Truef, err: assert.Nilf}, - - {op: e.MatchIsFalse, name: "foo", input: I{"foo": "true"}, result: assert.Falsef, err: assert.Nilf}, - {op: e.MatchIsFalse, name: "foo", input: I{"foo": "false"}, result: assert.Truef, err: assert.Nilf}, - } - - for i, tc := range tcs { - me := e.MustCreateMatchExpression(tc.op, tc.values...) - res, err := me.MatchValues(tc.name, tc.input) - tc.result(t, res, "test case #%d (%v) failed", i, tc) - tc.err(t, err, "test case #%d (%v) failed", i, tc) - } -} - -func TestMESMatchKeys(t *testing.T) { - type I = map[string]feature.Nil - type TC struct { - mes string - input I - result BoolAssertionFuncf - err ValueAssertionFuncf - } - - tcs := []TC{ - {result: assert.Truef, err: assert.Nilf}, - - {input: I{"foo": {}}, result: assert.Truef, err: assert.Nilf}, - - {mes: ` -foo: { op: DoesNotExist } -bar: { op: Exists } -`, - input: I{"bar": {}, "baz": {}}, - result: assert.Truef, err: assert.Nilf}, - - {mes: ` -foo: { op: DoesNotExist } -bar: { op: Exists } -`, - input: I{"foo": {}, "bar": {}, "baz": {}}, - result: assert.Falsef, err: assert.Nilf}, - - {mes: ` -foo: { op: In, value: ["bar"] } -bar: { op: Exists } -`, - input: I{"bar": {}, "baz": {}}, - result: assert.Falsef, err: assert.NotNilf}, - } - - for i, tc := range tcs { - mes := &e.MatchExpressionSet{} - if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { - t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err) - } - - res, err := mes.MatchKeys(tc.input) - tc.result(t, res, "test case #%d (%v) failed", i, tc) - tc.err(t, err, "test case #%d (%v) failed", i, tc) - } -} - -func TestMESMatchValues(t *testing.T) { - type I = map[string]string - type TC struct { - mes string - input I - result BoolAssertionFuncf - err ValueAssertionFuncf - } - - tcs := []TC{ - {result: assert.Truef, err: assert.Nilf}, - - {input: I{"foo": "bar"}, result: assert.Truef, err: assert.Nilf}, - - {mes: ` -foo: { op: Exists } -bar: { op: In, value: ["val", "wal"] } -baz: { op: Gt, value: ["10"] } -`, - input: I{"bar": "val"}, - result: assert.Falsef, err: assert.Nilf}, - - {mes: ` -foo: { op: Exists } -bar: { op: In, value: ["val", "wal"] } -baz: { op: Gt, value: ["10"] } -`, - input: I{"foo": "1", "bar": "val", "baz": "123"}, - result: assert.Truef, err: assert.Nilf}, - - {mes: ` -foo: { op: Exists } -bar: { op: In, value: ["val"] } -baz: { op: Gt, value: ["10"] } -`, - input: I{"foo": "1", "bar": "val", "baz": "123.0"}, - result: assert.Falsef, err: assert.NotNilf}, - } - - for i, tc := range tcs { - mes := &e.MatchExpressionSet{} - if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { - t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err) - } - - res, err := mes.MatchValues(tc.input) - tc.result(t, res, "test case #%d (%v) failed", i, tc) - tc.err(t, err, "test case #%d (%v) failed", i, tc) - } -} - -func TestMESMatchInstances(t *testing.T) { - type I = feature.InstanceFeature - type A = map[string]string - type TC struct { - mes string - input []I - result BoolAssertionFuncf - err ValueAssertionFuncf - } - - tcs := []TC{ - {result: assert.Falsef, err: assert.Nilf}, // nil instances -> false - - {input: []I{}, result: assert.Falsef, err: assert.Nilf}, // zero instances -> false - - {input: []I{I{Attributes: A{}}}, result: assert.Truef, err: assert.Nilf}, // one "empty" instance - - {mes: ` -foo: { op: Exists } -bar: { op: Lt, value: ["10"] } -`, - input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}}, - result: assert.Falsef, err: assert.Nilf}, - - {mes: ` -foo: { op: Exists } -bar: { op: Lt, value: ["10"] } -`, - input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}}, - result: assert.Truef, err: assert.Nilf}, - - {mes: ` -bar: { op: Lt, value: ["10"] } -`, - input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}}, - result: assert.Falsef, err: assert.NotNilf}, - } - - for i, tc := range tcs { - mes := &e.MatchExpressionSet{} - if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { - t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err) - } - - res, err := mes.MatchInstances(tc.input) - tc.result(t, res, "test case #%d (%v) failed", i, tc) - tc.err(t, err, "test case #%d (%v) failed", i, tc) - } -} diff --git a/source/custom/rules/cpuid_rule.go b/source/custom/rules/cpuid_rule.go index fda326aa1..6f13c0709 100644 --- a/source/custom/rules/cpuid_rule.go +++ b/source/custom/rules/cpuid_rule.go @@ -19,14 +19,14 @@ package rules import ( "fmt" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/source" "sigs.k8s.io/node-feature-discovery/source/cpu" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" ) // CpuIDRule implements Rule for the custom source type CpuIDRule struct { - expression.MatchExpressionSet + nfdv1alpha1.MatchExpressionSet } func (r *CpuIDRule) Match() (bool, error) { diff --git a/source/custom/rules/kconfig_rule.go b/source/custom/rules/kconfig_rule.go index a8c79a823..efb7674b4 100644 --- a/source/custom/rules/kconfig_rule.go +++ b/source/custom/rules/kconfig_rule.go @@ -19,14 +19,14 @@ package rules import ( "fmt" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/source" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" "sigs.k8s.io/node-feature-discovery/source/kernel" ) // KconfigRule implements Rule for the custom source type KconfigRule struct { - expression.MatchExpressionSet + nfdv1alpha1.MatchExpressionSet } func (r *KconfigRule) Match() (bool, error) { diff --git a/source/custom/rules/loaded_kmod_rule.go b/source/custom/rules/loaded_kmod_rule.go index 38a3be060..54e616d4a 100644 --- a/source/custom/rules/loaded_kmod_rule.go +++ b/source/custom/rules/loaded_kmod_rule.go @@ -19,14 +19,14 @@ package rules import ( "fmt" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/source" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" "sigs.k8s.io/node-feature-discovery/source/kernel" ) // LoadedKModRule matches loaded kernel modules in the system type LoadedKModRule struct { - expression.MatchExpressionSet + nfdv1alpha1.MatchExpressionSet } // Match loaded kernel modules on provided list of kernel modules diff --git a/source/custom/rules/nodename_rule.go b/source/custom/rules/nodename_rule.go index 7f5724f93..69eab4dad 100644 --- a/source/custom/rules/nodename_rule.go +++ b/source/custom/rules/nodename_rule.go @@ -20,14 +20,14 @@ import ( "encoding/json" "fmt" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/source" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" "sigs.k8s.io/node-feature-discovery/source/system" ) // NodenameRule matches on nodenames configured in a ConfigMap type NodenameRule struct { - expression.MatchExpression + nfdv1alpha1.MatchExpression } func (r *NodenameRule) Match() (bool, error) { @@ -43,8 +43,8 @@ func (r *NodenameRule) UnmarshalJSON(data []byte) error { return err } // Force regexp matching - if r.Op == expression.MatchIn { - r.Op = expression.MatchInRegexp + if r.Op == nfdv1alpha1.MatchIn { + r.Op = nfdv1alpha1.MatchInRegexp } // We need to run Validate() because operator forcing above return r.Validate() diff --git a/source/custom/rules/pci_id_rule.go b/source/custom/rules/pci_id_rule.go index 28793e341..9efa4e810 100644 --- a/source/custom/rules/pci_id_rule.go +++ b/source/custom/rules/pci_id_rule.go @@ -19,13 +19,13 @@ package rules import ( "fmt" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/source" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" "sigs.k8s.io/node-feature-discovery/source/pci" ) type PciIDRule struct { - expression.MatchExpressionSet + nfdv1alpha1.MatchExpressionSet } // Match PCI devices on provided PCI device attributes diff --git a/source/custom/rules/usb_id_rule.go b/source/custom/rules/usb_id_rule.go index 23f53e602..d0c83c106 100644 --- a/source/custom/rules/usb_id_rule.go +++ b/source/custom/rules/usb_id_rule.go @@ -19,13 +19,13 @@ package rules import ( "fmt" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/source" - "sigs.k8s.io/node-feature-discovery/source/custom/expression" "sigs.k8s.io/node-feature-discovery/source/usb" ) type UsbIDRule struct { - expression.MatchExpressionSet + nfdv1alpha1.MatchExpressionSet } // Match USB devices on provided USB device attributes diff --git a/source/custom/static_features.go b/source/custom/static_features.go index d0c4cd96e..fed8a22c5 100644 --- a/source/custom/static_features.go +++ b/source/custom/static_features.go @@ -17,7 +17,7 @@ limitations under the License. package custom import ( - "sigs.k8s.io/node-feature-discovery/source/custom/expression" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/source/custom/rules" ) @@ -31,8 +31,8 @@ func getStaticFeatureConfig() []CustomRule { MatchOn: []LegacyMatcher{ { PciID: &rules.PciIDRule{ - MatchExpressionSet: expression.MatchExpressionSet{ - "vendor": expression.MustCreateMatchExpression(expression.MatchIn, "15b3"), + MatchExpressionSet: nfdv1alpha1.MatchExpressionSet{ + "vendor": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "15b3"), }, }, }, @@ -45,9 +45,9 @@ func getStaticFeatureConfig() []CustomRule { MatchOn: []LegacyMatcher{ { LoadedKMod: &rules.LoadedKModRule{ - MatchExpressionSet: expression.MatchExpressionSet{ - "ib_uverbs": expression.MustCreateMatchExpression(expression.MatchExists), - "rdma_ucm": expression.MustCreateMatchExpression(expression.MatchExists), + MatchExpressionSet: nfdv1alpha1.MatchExpressionSet{ + "ib_uverbs": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists), + "rdma_ucm": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists), }, }, },