From 97bf84114027bd2f06b570c1dce1ed9117b3f4f7 Mon Sep 17 00:00:00 2001 From: Markus Lehtonen Date: Wed, 29 Nov 2023 14:32:33 +0200 Subject: [PATCH] apis/nfd: split rule processing into a separate package This patch tidies up the nfdv1alpha1 API package by refactoring out the implementation of (NodeFeature)Rule evaluation into a separate package. --- pkg/apis/nfd/v1alpha1/expression_test.go | 457 ------------------ .../nodefeaturerule/expression-api_test.go | 225 +++++++++ .../{ => nodefeaturerule}/expression.go | 107 ++-- .../nodefeaturerule/expression_test.go | 262 ++++++++++ .../v1alpha1/{ => nodefeaturerule}/rule.go | 39 +- .../{ => nodefeaturerule}/rule_test.go | 242 +++++----- pkg/nfd-master/nfd-master.go | 3 +- source/custom/custom.go | 3 +- 8 files changed, 687 insertions(+), 651 deletions(-) delete mode 100644 pkg/apis/nfd/v1alpha1/expression_test.go create mode 100644 pkg/apis/nfd/v1alpha1/nodefeaturerule/expression-api_test.go rename pkg/apis/nfd/v1alpha1/{ => nodefeaturerule}/expression.go (73%) create mode 100644 pkg/apis/nfd/v1alpha1/nodefeaturerule/expression_test.go rename pkg/apis/nfd/v1alpha1/{ => nodefeaturerule}/rule.go (80%) rename pkg/apis/nfd/v1alpha1/{ => nodefeaturerule}/rule_test.go (60%) diff --git a/pkg/apis/nfd/v1alpha1/expression_test.go b/pkg/apis/nfd/v1alpha1/expression_test.go deleted file mode 100644 index 150f45415..000000000 --- a/pkg/apis/nfd/v1alpha1/expression_test.go +++ /dev/null @@ -1,457 +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 v1alpha1_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "sigs.k8s.io/yaml" - - api "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" -) - -type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool - -type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool - -func TestMatch(t *testing.T) { - type V = api.MatchValue - type TC struct { - name string - op api.MatchOp - values V - input interface{} - valid bool - result BoolAssertionFunc - } - - tcs := []TC{ - {name: "MatchAny-1", op: api.MatchAny, result: assert.True}, - {name: "MatchAny-2", op: api.MatchAny, input: "2", valid: false, result: assert.True}, - - {name: "MatchIn-1", op: api.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.False}, - {name: "MatchIn-2", op: api.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.False}, - {name: "MatchIn-3", op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False}, - {name: "MatchIn-4", op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.True}, - - {name: "MatchNotIn-1", op: api.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.False}, - {name: "MatchNotIn-2", op: api.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.True}, - {name: "MatchNotIn-3", op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False}, - {name: "MatchNotIn-4", op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.False}, - - {name: "MatchInRegexp-1", op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.False}, - {name: "MatchInRegexp-2", op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.True}, - {name: "MatchInRegexp-3", op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.False}, - {name: "MatchInRegexp-4", op: api.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.True}, - - {name: "MatchExists-1", op: api.MatchExists, input: nil, valid: false, result: assert.False}, - {name: "MatchExists-2", op: api.MatchExists, input: nil, valid: true, result: assert.True}, - - {name: "MatchDoesNotExist-1", op: api.MatchDoesNotExist, input: false, valid: false, result: assert.True}, - {name: "MatchDoesNotExist-2", op: api.MatchDoesNotExist, input: false, valid: true, result: assert.False}, - - {name: "MatchGt-1", op: api.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.False}, - {name: "MatchGt-2", op: api.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.False}, - {name: "MatchGt-3", op: api.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.True}, - {name: "MatchGt-4", op: api.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.True}, - - {name: "MatchLt-1", op: api.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.False}, - {name: "MatchLt-2", op: api.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.False}, - {name: "MatchLt-3", op: api.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.False}, - {name: "MatchLt-4", op: api.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.True}, - - {name: "MatchGtLt-1", op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.False}, - {name: "MatchGtLt-2", op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.False}, - {name: "MatchGtLt-3", op: api.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.False}, - {name: "MatchGtLt-4", op: api.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.True}, - - {name: "MatchIsTrue-1", op: api.MatchIsTrue, input: true, valid: false, result: assert.False}, - {name: "MatchIsTrue-2", op: api.MatchIsTrue, input: true, valid: true, result: assert.True}, - {name: "MatchIsTrue-3", op: api.MatchIsTrue, input: false, valid: true, result: assert.False}, - - {name: "MatchIsFalse-1", op: api.MatchIsFalse, input: "false", valid: false, result: assert.False}, - {name: "MatchIsFalse-2", op: api.MatchIsFalse, input: "false", valid: true, result: assert.True}, - {name: "MatchIsFalse-3", op: api.MatchIsFalse, input: "true", valid: true, result: assert.False}, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - me := api.MatchExpression{Op: tc.op, Value: tc.values} - res, err := me.Match(tc.valid, tc.input) - tc.result(t, res) - assert.Nil(t, err) - }) - } - - // Error cases - tcs = []TC{ - {name: "MatchAny-err-1", op: api.MatchAny, values: V{"1"}, input: "val"}, - - {name: "MatchIn-err-1", op: api.MatchIn, input: "val"}, - - {name: "MatchNotIn-err-1", op: api.MatchNotIn, input: "val"}, - - {name: "MatchInRegexp-err-1", op: api.MatchInRegexp, input: "val"}, - {name: "MatchInRegexp-err-2", op: api.MatchInRegexp, values: V{"("}, input: "val"}, - - {name: "MatchExists-err-1", op: api.MatchExists, values: V{"1"}}, - - {name: "MatchDoesNotExist-err-1", op: api.MatchDoesNotExist, values: V{"1"}}, - - {name: "MatchGt-err-1", op: api.MatchGt, input: "1"}, - {name: "MatchGt-err-2", op: api.MatchGt, values: V{"1", "2"}, input: "1"}, - {name: "MatchGt-err-3", op: api.MatchGt, values: V{""}, input: "1"}, - {name: "MatchGt-err-4", op: api.MatchGt, values: V{"2"}, input: "3a"}, - - {name: "MatchLt-err-1", op: api.MatchLt, input: "1"}, - {name: "MatchLt-err-2", op: api.MatchLt, values: V{"1", "2", "3"}, input: "1"}, - {name: "MatchLt-err-3", op: api.MatchLt, values: V{"a"}, input: "1"}, - {name: "MatchLt-err-4", op: api.MatchLt, values: V{"2"}, input: "1.0"}, - - {name: "MatchGtLt-err-1", op: api.MatchGtLt, input: "1"}, - {name: "MatchGtLt-err-2", op: api.MatchGtLt, values: V{"1"}, input: "1"}, - {name: "MatchGtLt-err-3", op: api.MatchGtLt, values: V{"2", "1"}, input: "1"}, - {name: "MatchGtLt-err-4", op: api.MatchGtLt, values: V{"1", "2", "3"}, input: "1"}, - {name: "MatchGtLt-err-5", op: api.MatchGtLt, values: V{"a", "2"}, input: "1"}, - {name: "MatchGtLt-err-6", op: api.MatchGtLt, values: V{"1", "10"}, input: "1.0"}, - - {name: "MatchIsTrue-err-1", op: api.MatchIsTrue, values: V{"1"}, input: "true"}, - - {name: "MatchIsFalse-err-1", op: api.MatchIsFalse, values: V{"1", "2"}, input: "false"}, - - {name: "invalid-op-err", op: "non-existent-op", values: V{"1"}, input: 1}, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - me := api.MatchExpression{Op: tc.op, Value: tc.values} - res, err := me.Match(true, tc.input) - assert.False(t, res) - assert.NotNil(t, err) - }) - } -} - -func TestMatchKeys(t *testing.T) { - type V = api.MatchValue - type I = map[string]api.Nil - type TC struct { - name string - key string - op api.MatchOp - values V - input I - result BoolAssertionFunc - err ValueAssertionFunc - } - - tcs := []TC{ - {name: "1", op: api.MatchAny, result: assert.True, err: assert.Nil}, - - {name: "2", op: api.MatchExists, key: "foo", input: nil, result: assert.False, err: assert.Nil}, - {name: "3", op: api.MatchExists, key: "foo", input: I{"bar": {}}, result: assert.False, err: assert.Nil}, - {name: "4", op: api.MatchExists, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.True, err: assert.Nil}, - - {name: "5", op: api.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil}, - {name: "6", op: api.MatchDoesNotExist, key: "foo", input: I{}, result: assert.True, err: assert.Nil}, - {name: "7", op: api.MatchDoesNotExist, key: "foo", input: I{"bar": {}}, result: assert.True, err: assert.Nil}, - {name: "8", op: api.MatchDoesNotExist, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.False, err: assert.Nil}, - - // All other ops should return an error - {name: "9", op: api.MatchIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil}, - {name: "10", op: api.MatchNotIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil}, - {name: "11", op: api.MatchInRegexp, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil}, - {name: "12", op: api.MatchGt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil}, - {name: "13", op: api.MatchLt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil}, - {name: "14", op: api.MatchGtLt, values: V{"1", "10"}, key: "foo", result: assert.False, err: assert.NotNil}, - {name: "15", op: api.MatchIsTrue, key: "foo", result: assert.False, err: assert.NotNil}, - {name: "16", op: api.MatchIsFalse, key: "foo", result: assert.False, err: assert.NotNil}, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - me := api.MatchExpression{Op: tc.op, Value: tc.values} - res, err := me.MatchKeys(tc.key, tc.input) - tc.result(t, res) - tc.err(t, err) - }) - } -} - -func TestMatchValues(t *testing.T) { - type V = []string - type I = map[string]string - - type TC struct { - name string - op api.MatchOp - values V - key string - input I - result BoolAssertionFunc - err ValueAssertionFunc - } - - tcs := []TC{ - {name: "1", op: api.MatchAny, result: assert.True, err: assert.Nil}, - - {name: "2", op: api.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil}, - {name: "3", op: api.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.False, err: assert.Nil}, - {name: "4", op: api.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil}, - - {name: "5", op: api.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil}, - {name: "6", op: api.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.True, err: assert.Nil}, - {name: "7", op: api.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.False, err: assert.Nil}, - - {name: "8", op: api.MatchInRegexp, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil}, - {name: "9", op: api.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "9"}, result: assert.False, err: assert.Nil}, - {name: "10", op: api.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil}, - - {name: "11", op: api.MatchExists, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil}, - {name: "12", op: api.MatchExists, key: "foo", input: I{"foo": "1"}, result: assert.True, err: assert.Nil}, - - {name: "13", op: api.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil}, - {name: "14", op: api.MatchDoesNotExist, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil}, - - {name: "15", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3"}, result: assert.False, err: assert.Nil}, - {name: "16", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.False, err: assert.Nil}, - {name: "17", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.True, err: assert.Nil}, - {name: "18", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil}, - - {name: "19", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil}, - {name: "20", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.False, err: assert.Nil}, - {name: "21", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil}, - {name: "22", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil}, - - {name: "23", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil}, - {name: "24", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.False, err: assert.Nil}, - {name: "25", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.False, err: assert.Nil}, - {name: "26", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil}, - {name: "27", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil}, - - {name: "28", op: api.MatchIsTrue, key: "foo", result: assert.False, err: assert.Nil}, - {name: "29", op: api.MatchIsTrue, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil}, - {name: "30", op: api.MatchIsTrue, key: "foo", input: I{"foo": "true"}, result: assert.True, err: assert.Nil}, - - {name: "31", op: api.MatchIsFalse, key: "foo", input: I{"foo": "true"}, result: assert.False, err: assert.Nil}, - {name: "32", op: api.MatchIsFalse, key: "foo", input: I{"foo": "false"}, result: assert.True, err: assert.Nil}, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - me := api.MatchExpression{Op: tc.op, Value: tc.values} - res, err := me.MatchValues(tc.key, tc.input) - tc.result(t, res) - tc.err(t, err) - }) - } -} - -func TestMESMatchKeys(t *testing.T) { - type I = map[string]api.Nil - type O = []api.MatchedElement - type TC struct { - name string - mes string - input I - output O - result BoolAssertionFunc - err ValueAssertionFunc - } - - tcs := []TC{ - {output: O{}, result: assert.True, err: assert.Nil}, - - {input: I{}, output: O{}, result: assert.True, err: assert.Nil}, - - {input: I{"foo": {}}, output: O{}, result: assert.True, err: assert.Nil}, - - {mes: ` -foo: { op: DoesNotExist } -bar: { op: Exists } -`, - input: I{"bar": {}, "baz": {}, "buzz": {}}, - output: O{{"Name": "bar"}, {"Name": "foo"}}, - result: assert.True, err: assert.Nil}, - - {mes: ` -foo: { op: DoesNotExist } -bar: { op: Exists } -`, - input: I{"foo": {}, "bar": {}, "baz": {}}, - output: nil, - result: assert.False, err: assert.Nil}, - - {mes: ` -foo: { op: In, value: ["bar"] } -bar: { op: Exists } -`, - input: I{"bar": {}, "baz": {}}, - output: nil, - result: assert.False, err: assert.NotNil}, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - mes := &api.MatchExpressionSet{} - if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { - t.Fatal("failed to parse data of test case") - } - - res, out, err := mes.MatchGetKeys(tc.input) - tc.result(t, res) - assert.Equal(t, tc.output, out) - tc.err(t, err) - - res, err = mes.MatchKeys(tc.input) - tc.result(t, res) - tc.err(t, err) - }) - } -} - -func TestMESMatchValues(t *testing.T) { - type I = map[string]string - type O = []api.MatchedElement - type TC struct { - name string - mes string - input I - output O - result BoolAssertionFunc - err ValueAssertionFunc - } - - tcs := []TC{ - {name: "1", output: O{}, result: assert.True, err: assert.Nil}, - - {name: "2", input: I{}, output: O{}, result: assert.True, err: assert.Nil}, - - {name: "3", input: I{"foo": "bar"}, output: O{}, result: assert.True, err: assert.Nil}, - - {name: "4", - mes: ` -foo: { op: Exists } -bar: { op: In, value: ["val", "wal"] } -baz: { op: Gt, value: ["10"] } -`, - input: I{"bar": "val"}, - result: assert.False, err: assert.Nil}, - - {name: "5", - mes: ` -foo: { op: Exists } -bar: { op: In, value: ["val", "wal"] } -baz: { op: Gt, value: ["10"] } -`, - input: I{"foo": "1", "bar": "val", "baz": "123", "buzz": "light"}, - output: O{{"Name": "bar", "Value": "val"}, {"Name": "baz", "Value": "123"}, {"Name": "foo", "Value": "1"}}, - result: assert.True, err: assert.Nil}, - - {name: "5", - 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.False, err: assert.NotNil}, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - mes := &api.MatchExpressionSet{} - if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { - t.Fatal("failed to parse data of test case") - } - - res, out, err := mes.MatchGetValues(tc.input) - tc.result(t, res) - assert.Equal(t, tc.output, out) - tc.err(t, err) - - res, err = mes.MatchValues(tc.input) - tc.result(t, res) - tc.err(t, err) - }) - } -} - -func TestMESMatchInstances(t *testing.T) { - type I = api.InstanceFeature - type O = []api.MatchedElement - type A = map[string]string - type TC struct { - name string - mes string - input []I - output O - result BoolAssertionFunc - err ValueAssertionFunc - } - - tcs := []TC{ - {name: "1", output: O{}, result: assert.False, err: assert.Nil}, // nil instances -> false - - {name: "2", input: []I{}, output: O{}, result: assert.False, err: assert.Nil}, // zero instances -> false - - {name: "3", input: []I{I{Attributes: A{}}}, output: O{A{}}, result: assert.True, err: assert.Nil}, // one "empty" instance - - {name: "4", - mes: ` -foo: { op: Exists } -bar: { op: Lt, value: ["10"] } -`, - input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}}, - output: O{}, - result: assert.False, err: assert.Nil}, - - {name: "5", - mes: ` -foo: { op: Exists } -bar: { op: Lt, value: ["10"] } -`, - input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}}, - output: O{A{"foo": "2", "bar": "1"}}, - result: assert.True, err: assert.Nil}, - - {name: "6", - mes: ` -bar: { op: Lt, value: ["10"] } -`, - input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}}, - result: assert.False, err: assert.NotNil}, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - mes := &api.MatchExpressionSet{} - if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { - t.Fatal("failed to parse data of test case") - } - - out, err := mes.MatchGetInstances(tc.input) - assert.Equal(t, tc.output, out) - tc.err(t, err) - - res, err := mes.MatchInstances(tc.input) - tc.result(t, res) - tc.err(t, err) - }) - } -} diff --git a/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression-api_test.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression-api_test.go new file mode 100644 index 000000000..e9974d6df --- /dev/null +++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression-api_test.go @@ -0,0 +1,225 @@ +/* +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 nodefeaturerule_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" + + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" + api "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1/nodefeaturerule" +) + +type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool + +type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool + +func TestMatchKeys(t *testing.T) { + type I = map[string]nfdv1alpha1.Nil + type O = []api.MatchedElement + type TC struct { + name string + mes string + input I + output O + result BoolAssertionFunc + err ValueAssertionFunc + } + + tcs := []TC{ + {output: O{}, result: assert.True, err: assert.Nil}, + + {input: I{}, output: O{}, result: assert.True, err: assert.Nil}, + + {input: I{"foo": {}}, output: O{}, result: assert.True, err: assert.Nil}, + + {mes: ` +foo: { op: DoesNotExist } +bar: { op: Exists } +`, + input: I{"bar": {}, "baz": {}, "buzz": {}}, + output: O{{"Name": "bar"}, {"Name": "foo"}}, + result: assert.True, err: assert.Nil}, + + {mes: ` +foo: { op: DoesNotExist } +bar: { op: Exists } +`, + input: I{"foo": {}, "bar": {}, "baz": {}}, + output: nil, + result: assert.False, err: assert.Nil}, + + {mes: ` +foo: { op: In, value: ["bar"] } +bar: { op: Exists } +`, + input: I{"bar": {}, "baz": {}}, + output: nil, + result: assert.False, err: assert.NotNil}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + mes := &nfdv1alpha1.MatchExpressionSet{} + if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { + t.Fatal("failed to parse data of test case") + } + + res, out, err := api.MatchGetKeys(mes, tc.input) + tc.result(t, res) + assert.Equal(t, tc.output, out) + tc.err(t, err) + + res, err = api.MatchKeys(mes, tc.input) + tc.result(t, res) + tc.err(t, err) + }) + } +} + +func TestMatchValues(t *testing.T) { + type I = map[string]string + type O = []api.MatchedElement + type TC struct { + name string + mes string + input I + output O + result BoolAssertionFunc + err ValueAssertionFunc + } + + tcs := []TC{ + {name: "1", output: O{}, result: assert.True, err: assert.Nil}, + + {name: "2", input: I{}, output: O{}, result: assert.True, err: assert.Nil}, + + {name: "3", input: I{"foo": "bar"}, output: O{}, result: assert.True, err: assert.Nil}, + + {name: "4", + mes: ` +foo: { op: Exists } +bar: { op: In, value: ["val", "wal"] } +baz: { op: Gt, value: ["10"] } +`, + input: I{"bar": "val"}, + result: assert.False, err: assert.Nil}, + + {name: "5", + mes: ` +foo: { op: Exists } +bar: { op: In, value: ["val", "wal"] } +baz: { op: Gt, value: ["10"] } +`, + input: I{"foo": "1", "bar": "val", "baz": "123", "buzz": "light"}, + output: O{{"Name": "bar", "Value": "val"}, {"Name": "baz", "Value": "123"}, {"Name": "foo", "Value": "1"}}, + result: assert.True, err: assert.Nil}, + + {name: "5", + 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.False, err: assert.NotNil}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + mes := &nfdv1alpha1.MatchExpressionSet{} + if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { + t.Fatal("failed to parse data of test case") + } + + res, out, err := api.MatchGetValues(mes, tc.input) + tc.result(t, res) + assert.Equal(t, tc.output, out) + tc.err(t, err) + + res, err = api.MatchValues(mes, tc.input) + tc.result(t, res) + tc.err(t, err) + }) + } +} + +func TestMatchInstances(t *testing.T) { + type I = nfdv1alpha1.InstanceFeature + type O = []api.MatchedElement + type A = map[string]string + type TC struct { + name string + mes string + input []I + output O + result BoolAssertionFunc + err ValueAssertionFunc + } + + tcs := []TC{ + {name: "1", output: O{}, result: assert.False, err: assert.Nil}, // nil instances -> false + + {name: "2", input: []I{}, output: O{}, result: assert.False, err: assert.Nil}, // zero instances -> false + + {name: "3", input: []I{I{Attributes: A{}}}, output: O{A{}}, result: assert.True, err: assert.Nil}, // one "empty" instance + + {name: "4", + mes: ` +foo: { op: Exists } +bar: { op: Lt, value: ["10"] } +`, + input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}}, + output: O{}, + result: assert.False, err: assert.Nil}, + + {name: "5", + mes: ` +foo: { op: Exists } +bar: { op: Lt, value: ["10"] } +`, + input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}}, + output: O{A{"foo": "2", "bar": "1"}}, + result: assert.True, err: assert.Nil}, + + {name: "6", + mes: ` +bar: { op: Lt, value: ["10"] } +`, + input: []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}}, + result: assert.False, err: assert.NotNil}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + mes := &nfdv1alpha1.MatchExpressionSet{} + if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil { + t.Fatal("failed to parse data of test case") + } + + out, err := api.MatchGetInstances(mes, tc.input) + assert.Equal(t, tc.output, out) + tc.err(t, err) + + res, err := api.MatchInstances(mes, tc.input) + tc.result(t, res) + tc.err(t, err) + }) + } +} diff --git a/pkg/apis/nfd/v1alpha1/expression.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression.go similarity index 73% rename from pkg/apis/nfd/v1alpha1/expression.go rename to pkg/apis/nfd/v1alpha1/nodefeaturerule/expression.go index 9a0507498..7438f3bb1 100644 --- a/pkg/apis/nfd/v1alpha1/expression.go +++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +package nodefeaturerule import ( "fmt" @@ -25,40 +25,42 @@ import ( "golang.org/x/exp/maps" "k8s.io/klog/v2" + + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" ) -var matchOps = map[MatchOp]struct{}{ - MatchAny: {}, - MatchIn: {}, - MatchNotIn: {}, - MatchInRegexp: {}, - MatchExists: {}, - MatchDoesNotExist: {}, - MatchGt: {}, - MatchLt: {}, - MatchGtLt: {}, - MatchIsTrue: {}, - MatchIsFalse: {}, +var matchOps = map[nfdv1alpha1.MatchOp]struct{}{ + nfdv1alpha1.MatchAny: {}, + nfdv1alpha1.MatchIn: {}, + nfdv1alpha1.MatchNotIn: {}, + nfdv1alpha1.MatchInRegexp: {}, + nfdv1alpha1.MatchExists: {}, + nfdv1alpha1.MatchDoesNotExist: {}, + nfdv1alpha1.MatchGt: {}, + nfdv1alpha1.MatchLt: {}, + nfdv1alpha1.MatchGtLt: {}, + nfdv1alpha1.MatchIsTrue: {}, + nfdv1alpha1.MatchIsFalse: {}, } -// Match evaluates the MatchExpression against a single input value. -func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { +// evaluateMatchExpression evaluates the MatchExpression against a single input value. +func evaluateMatchExpression(m *nfdv1alpha1.MatchExpression, valid bool, value interface{}) (bool, error) { if _, ok := matchOps[m.Op]; !ok { return false, fmt.Errorf("invalid Op %q", m.Op) } switch m.Op { - case MatchAny: + case nfdv1alpha1.MatchAny: if len(m.Value) != 0 { return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value) } return true, nil - case MatchExists: + case nfdv1alpha1.MatchExists: if len(m.Value) != 0 { return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value) } return valid, nil - case MatchDoesNotExist: + case nfdv1alpha1.MatchDoesNotExist: if len(m.Value) != 0 { return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value) } @@ -68,7 +70,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { if valid { value := fmt.Sprintf("%v", value) switch m.Op { - case MatchIn: + case nfdv1alpha1.MatchIn: if len(m.Value) == 0 { return false, fmt.Errorf("invalid expression, 'value' field must be non-empty for Op %q", m.Op) } @@ -77,7 +79,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { return true, nil } } - case MatchNotIn: + case nfdv1alpha1.MatchNotIn: if len(m.Value) == 0 { return false, fmt.Errorf("invalid expression, 'value' field must be non-empty for Op %q", m.Op) } @@ -87,7 +89,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { } } return true, nil - case MatchInRegexp: + case nfdv1alpha1.MatchInRegexp: if len(m.Value) == 0 { return false, fmt.Errorf("invalid expression, 'value' field must be non-empty for Op %q", m.Op) } @@ -104,7 +106,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { return true, nil } } - case MatchGt, MatchLt: + case nfdv1alpha1.MatchGt, nfdv1alpha1.MatchLt: if len(m.Value) != 1 { return false, fmt.Errorf("invalid expression, 'value' field must contain exactly one element for Op %q (have %v)", m.Op, m.Value) } @@ -118,10 +120,10 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { return false, fmt.Errorf("not a number %q in %v", m.Value[0], m) } - if (l < r && m.Op == MatchLt) || (l > r && m.Op == MatchGt) { + if (l < r && m.Op == nfdv1alpha1.MatchLt) || (l > r && m.Op == nfdv1alpha1.MatchGt) { return true, nil } - case MatchGtLt: + case nfdv1alpha1.MatchGtLt: if len(m.Value) != 2 { return false, fmt.Errorf("invalid expression, value' field must contain exactly two elements for Op %q (have %v)", m.Op, m.Value) } @@ -140,12 +142,12 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { return false, fmt.Errorf("invalid expression, value[0] must be less than Value[1] for Op %q (have %v)", m.Op, m.Value) } return v > lr[0] && v < lr[1], nil - case MatchIsTrue: + case nfdv1alpha1.MatchIsTrue: if len(m.Value) != 0 { return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value) } return value == "true", nil - case MatchIsFalse: + case nfdv1alpha1.MatchIsFalse: if len(m.Value) != 0 { return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value) } @@ -157,17 +159,17 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) { return false, nil } -// MatchKeys evaluates the MatchExpression against a set of keys. -func (m *MatchExpression) MatchKeys(name string, keys map[string]Nil) (bool, error) { +// evaluateMatchExpressionKeys evaluates the MatchExpression against a set of keys. +func evaluateMatchExpressionKeys(m *nfdv1alpha1.MatchExpression, name string, keys map[string]nfdv1alpha1.Nil) (bool, error) { matched := false _, ok := keys[name] switch m.Op { - case MatchAny: + case nfdv1alpha1.MatchAny: matched = true - case MatchExists: + case nfdv1alpha1.MatchExists: matched = ok - case MatchDoesNotExist: + case nfdv1alpha1.MatchDoesNotExist: matched = !ok default: return false, fmt.Errorf("invalid Op %q when matching keys", m.Op) @@ -183,10 +185,10 @@ func (m *MatchExpression) MatchKeys(name string, keys map[string]Nil) (bool, err return matched, nil } -// MatchValues evaluates the MatchExpression against a set of key-value pairs. -func (m *MatchExpression) MatchValues(name string, values map[string]string) (bool, error) { +// evaluateMatchExpressionValues evaluates the MatchExpression against a set of key-value pairs. +func evaluateMatchExpressionValues(m *nfdv1alpha1.MatchExpression, name string, values map[string]string) (bool, error) { v, ok := values[name] - matched, err := m.Match(ok, v) + matched, err := evaluateMatchExpression(m, ok, v) if err != nil { return false, err } @@ -201,11 +203,11 @@ func (m *MatchExpression) MatchValues(name string, values map[string]string) (bo } // MatchKeyNames evaluates the MatchExpression against names of a set of key features. -func (m *MatchExpression) MatchKeyNames(keys map[string]Nil) (bool, []MatchedElement, error) { +func MatchKeyNames(m *nfdv1alpha1.MatchExpression, keys map[string]nfdv1alpha1.Nil) (bool, []MatchedElement, error) { ret := []MatchedElement{} for k := range keys { - if match, err := m.Match(true, k); err != nil { + if match, err := evaluateMatchExpression(m, true, k); err != nil { return false, nil, err } else if match { ret = append(ret, MatchedElement{"Name": k}) @@ -237,11 +239,11 @@ func (m *MatchExpression) MatchKeyNames(keys map[string]Nil) (bool, []MatchedEle } // MatchValueNames evaluates the MatchExpression against names of a set of value features. -func (m *MatchExpression) MatchValueNames(values map[string]string) (bool, []MatchedElement, error) { +func MatchValueNames(m *nfdv1alpha1.MatchExpression, values map[string]string) (bool, []MatchedElement, error) { ret := []MatchedElement{} for k, v := range values { - if match, err := m.Match(true, k); err != nil { + if match, err := evaluateMatchExpression(m, true, k); err != nil { return false, nil, err } else if match { ret = append(ret, MatchedElement{"Name": k, "Value": v}) @@ -269,11 +271,11 @@ func (m *MatchExpression) MatchValueNames(values map[string]string) (bool, []Mat // MatchInstanceAttributeNames evaluates the MatchExpression against a set of // instance features, matching against the names of their attributes. -func (m *MatchExpression) MatchInstanceAttributeNames(instances []InstanceFeature) ([]MatchedElement, error) { +func MatchInstanceAttributeNames(m *nfdv1alpha1.MatchExpression, instances []nfdv1alpha1.InstanceFeature) ([]MatchedElement, error) { ret := []MatchedElement{} for _, i := range instances { - if match, _, err := m.MatchValueNames(i.Attributes); err != nil { + if match, _, err := MatchValueNames(m, i.Attributes); err != nil { return nil, err } else if match { ret = append(ret, i.Attributes) @@ -283,23 +285,22 @@ func (m *MatchExpression) MatchInstanceAttributeNames(instances []InstanceFeatur } // MatchKeys evaluates the MatchExpressionSet against a set of keys. -func (m *MatchExpressionSet) MatchKeys(keys map[string]Nil) (bool, error) { - matched, _, err := m.MatchGetKeys(keys) +func MatchKeys(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil) (bool, error) { + matched, _, err := MatchGetKeys(m, keys) return matched, err } // MatchedElement holds one matched Instance. -// +k8s:deepcopy-gen=false type MatchedElement map[string]string // MatchGetKeys evaluates the MatchExpressionSet against a set of keys and // returns all matched keys or nil if no match was found. Note that an empty // MatchExpressionSet returns a match with an empty slice of matched features. -func (m *MatchExpressionSet) MatchGetKeys(keys map[string]Nil) (bool, []MatchedElement, error) { +func MatchGetKeys(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil) (bool, []MatchedElement, error) { ret := make([]MatchedElement, 0, len(*m)) for n, e := range *m { - match, err := e.MatchKeys(n, keys) + match, err := evaluateMatchExpressionKeys(e, n, keys) if err != nil { return false, nil, err } @@ -314,19 +315,19 @@ func (m *MatchExpressionSet) MatchGetKeys(keys map[string]Nil) (bool, []MatchedE } // MatchValues evaluates the MatchExpressionSet against a set of key-value pairs. -func (m *MatchExpressionSet) MatchValues(values map[string]string) (bool, error) { - matched, _, err := m.MatchGetValues(values) +func MatchValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string) (bool, error) { + matched, _, err := MatchGetValues(m, values) return matched, err } // MatchGetValues evaluates the MatchExpressionSet against a set of key-value // pairs and returns all matched key-value pairs. Note that an empty // MatchExpressionSet returns a match with an empty slice of matched features. -func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []MatchedElement, error) { +func MatchGetValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string) (bool, []MatchedElement, error) { ret := make([]MatchedElement, 0, len(*m)) for n, e := range *m { - match, err := e.MatchValues(n, values) + match, err := evaluateMatchExpressionValues(e, n, values) if err != nil { return false, nil, err } @@ -343,8 +344,8 @@ func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []M // MatchInstances evaluates the MatchExpressionSet against a set of instance // features, each of which is an individual set of key-value pairs // (attributes). -func (m *MatchExpressionSet) MatchInstances(instances []InstanceFeature) (bool, error) { - v, err := m.MatchGetInstances(instances) +func MatchInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) (bool, error) { + v, err := MatchGetInstances(m, instances) return len(v) > 0, err } @@ -352,11 +353,11 @@ func (m *MatchExpressionSet) MatchInstances(instances []InstanceFeature) (bool, // features, each of which is an individual set of key-value pairs // (attributes). A slice containing all matching instances is returned. An // empty (non-nil) slice is returned if no matching instances were found. -func (m *MatchExpressionSet) MatchGetInstances(instances []InstanceFeature) ([]MatchedElement, error) { +func MatchGetInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) ([]MatchedElement, error) { ret := []MatchedElement{} for _, i := range instances { - if match, err := m.MatchValues(i.Attributes); err != nil { + if match, err := MatchValues(m, i.Attributes); err != nil { return nil, err } else if match { ret = append(ret, i.Attributes) diff --git a/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression_test.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression_test.go new file mode 100644 index 000000000..3b51ca12e --- /dev/null +++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression_test.go @@ -0,0 +1,262 @@ +/* +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 nodefeaturerule + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" +) + +type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool + +type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool + +func TestEvaluateMatchExpression(t *testing.T) { + type V = nfdv1alpha1.MatchValue + type TC struct { + name string + op nfdv1alpha1.MatchOp + values V + input interface{} + valid bool + result BoolAssertionFunc + } + + tcs := []TC{ + {name: "MatchAny-1", op: nfdv1alpha1.MatchAny, result: assert.True}, + {name: "MatchAny-2", op: nfdv1alpha1.MatchAny, input: "2", valid: false, result: assert.True}, + + {name: "MatchIn-1", op: nfdv1alpha1.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.False}, + {name: "MatchIn-2", op: nfdv1alpha1.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.False}, + {name: "MatchIn-3", op: nfdv1alpha1.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False}, + {name: "MatchIn-4", op: nfdv1alpha1.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.True}, + + {name: "MatchNotIn-1", op: nfdv1alpha1.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.False}, + {name: "MatchNotIn-2", op: nfdv1alpha1.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.True}, + {name: "MatchNotIn-3", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False}, + {name: "MatchNotIn-4", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.False}, + + {name: "MatchInRegexp-1", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.False}, + {name: "MatchInRegexp-2", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.True}, + {name: "MatchInRegexp-3", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.False}, + {name: "MatchInRegexp-4", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.True}, + + {name: "MatchExists-1", op: nfdv1alpha1.MatchExists, input: nil, valid: false, result: assert.False}, + {name: "MatchExists-2", op: nfdv1alpha1.MatchExists, input: nil, valid: true, result: assert.True}, + + {name: "MatchDoesNotExist-1", op: nfdv1alpha1.MatchDoesNotExist, input: false, valid: false, result: assert.True}, + {name: "MatchDoesNotExist-2", op: nfdv1alpha1.MatchDoesNotExist, input: false, valid: true, result: assert.False}, + + {name: "MatchGt-1", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.False}, + {name: "MatchGt-2", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.False}, + {name: "MatchGt-3", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.True}, + {name: "MatchGt-4", op: nfdv1alpha1.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.True}, + + {name: "MatchLt-1", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.False}, + {name: "MatchLt-2", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.False}, + {name: "MatchLt-3", op: nfdv1alpha1.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.False}, + {name: "MatchLt-4", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.True}, + + {name: "MatchGtLt-1", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.False}, + {name: "MatchGtLt-2", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.False}, + {name: "MatchGtLt-3", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.False}, + {name: "MatchGtLt-4", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.True}, + + {name: "MatchIsTrue-1", op: nfdv1alpha1.MatchIsTrue, input: true, valid: false, result: assert.False}, + {name: "MatchIsTrue-2", op: nfdv1alpha1.MatchIsTrue, input: true, valid: true, result: assert.True}, + {name: "MatchIsTrue-3", op: nfdv1alpha1.MatchIsTrue, input: false, valid: true, result: assert.False}, + + {name: "MatchIsFalse-1", op: nfdv1alpha1.MatchIsFalse, input: "false", valid: false, result: assert.False}, + {name: "MatchIsFalse-2", op: nfdv1alpha1.MatchIsFalse, input: "false", valid: true, result: assert.True}, + {name: "MatchIsFalse-3", op: nfdv1alpha1.MatchIsFalse, input: "true", valid: true, result: assert.False}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values} + res, err := evaluateMatchExpression(me, tc.valid, tc.input) + tc.result(t, res) + assert.Nil(t, err) + }) + } + + // Error cases + tcs = []TC{ + {name: "MatchAny-err-1", op: nfdv1alpha1.MatchAny, values: V{"1"}, input: "val"}, + + {name: "MatchIn-err-1", op: nfdv1alpha1.MatchIn, input: "val"}, + + {name: "MatchNotIn-err-1", op: nfdv1alpha1.MatchNotIn, input: "val"}, + + {name: "MatchInRegexp-err-1", op: nfdv1alpha1.MatchInRegexp, input: "val"}, + {name: "MatchInRegexp-err-2", op: nfdv1alpha1.MatchInRegexp, values: V{"("}, input: "val"}, + + {name: "MatchExists-err-1", op: nfdv1alpha1.MatchExists, values: V{"1"}}, + + {name: "MatchDoesNotExist-err-1", op: nfdv1alpha1.MatchDoesNotExist, values: V{"1"}}, + + {name: "MatchGt-err-1", op: nfdv1alpha1.MatchGt, input: "1"}, + {name: "MatchGt-err-2", op: nfdv1alpha1.MatchGt, values: V{"1", "2"}, input: "1"}, + {name: "MatchGt-err-3", op: nfdv1alpha1.MatchGt, values: V{""}, input: "1"}, + {name: "MatchGt-err-4", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: "3a"}, + + {name: "MatchLt-err-1", op: nfdv1alpha1.MatchLt, input: "1"}, + {name: "MatchLt-err-2", op: nfdv1alpha1.MatchLt, values: V{"1", "2", "3"}, input: "1"}, + {name: "MatchLt-err-3", op: nfdv1alpha1.MatchLt, values: V{"a"}, input: "1"}, + {name: "MatchLt-err-4", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "1.0"}, + + {name: "MatchGtLt-err-1", op: nfdv1alpha1.MatchGtLt, input: "1"}, + {name: "MatchGtLt-err-2", op: nfdv1alpha1.MatchGtLt, values: V{"1"}, input: "1"}, + {name: "MatchGtLt-err-3", op: nfdv1alpha1.MatchGtLt, values: V{"2", "1"}, input: "1"}, + {name: "MatchGtLt-err-4", op: nfdv1alpha1.MatchGtLt, values: V{"1", "2", "3"}, input: "1"}, + {name: "MatchGtLt-err-5", op: nfdv1alpha1.MatchGtLt, values: V{"a", "2"}, input: "1"}, + {name: "MatchGtLt-err-6", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "1.0"}, + + {name: "MatchIsTrue-err-1", op: nfdv1alpha1.MatchIsTrue, values: V{"1"}, input: "true"}, + + {name: "MatchIsFalse-err-1", op: nfdv1alpha1.MatchIsFalse, values: V{"1", "2"}, input: "false"}, + + {name: "invalid-op-err", op: "non-existent-op", values: V{"1"}, input: 1}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values} + res, err := evaluateMatchExpression(me, true, tc.input) + assert.False(t, res) + assert.NotNil(t, err) + }) + } +} + +func TestEvaluateMatchExpressionKeys(t *testing.T) { + type V = nfdv1alpha1.MatchValue + type I = map[string]nfdv1alpha1.Nil + type TC struct { + name string + key string + op nfdv1alpha1.MatchOp + values V + input I + result BoolAssertionFunc + err ValueAssertionFunc + } + + tcs := []TC{ + {name: "1", op: nfdv1alpha1.MatchAny, result: assert.True, err: assert.Nil}, + + {name: "2", op: nfdv1alpha1.MatchExists, key: "foo", input: nil, result: assert.False, err: assert.Nil}, + {name: "3", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"bar": {}}, result: assert.False, err: assert.Nil}, + {name: "4", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.True, err: assert.Nil}, + + {name: "5", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil}, + {name: "6", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{}, result: assert.True, err: assert.Nil}, + {name: "7", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{"bar": {}}, result: assert.True, err: assert.Nil}, + {name: "8", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.False, err: assert.Nil}, + + // All other ops should return an error + {name: "9", op: nfdv1alpha1.MatchIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil}, + {name: "10", op: nfdv1alpha1.MatchNotIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil}, + {name: "11", op: nfdv1alpha1.MatchInRegexp, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil}, + {name: "12", op: nfdv1alpha1.MatchGt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil}, + {name: "13", op: nfdv1alpha1.MatchLt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil}, + {name: "14", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, key: "foo", result: assert.False, err: assert.NotNil}, + {name: "15", op: nfdv1alpha1.MatchIsTrue, key: "foo", result: assert.False, err: assert.NotNil}, + {name: "16", op: nfdv1alpha1.MatchIsFalse, key: "foo", result: assert.False, err: assert.NotNil}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values} + res, err := evaluateMatchExpressionKeys(me, tc.key, tc.input) + tc.result(t, res) + tc.err(t, err) + }) + } +} + +func TestEvaluateMatchExpressionValues(t *testing.T) { + type V = []string + type I = map[string]string + + type TC struct { + name string + op nfdv1alpha1.MatchOp + values V + key string + input I + result BoolAssertionFunc + err ValueAssertionFunc + } + + tcs := []TC{ + {name: "1", op: nfdv1alpha1.MatchAny, result: assert.True, err: assert.Nil}, + + {name: "2", op: nfdv1alpha1.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil}, + {name: "3", op: nfdv1alpha1.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.False, err: assert.Nil}, + {name: "4", op: nfdv1alpha1.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil}, + + {name: "5", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil}, + {name: "6", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.True, err: assert.Nil}, + {name: "7", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.False, err: assert.Nil}, + + {name: "8", op: nfdv1alpha1.MatchInRegexp, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil}, + {name: "9", op: nfdv1alpha1.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "9"}, result: assert.False, err: assert.Nil}, + {name: "10", op: nfdv1alpha1.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil}, + + {name: "11", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil}, + {name: "12", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"foo": "1"}, result: assert.True, err: assert.Nil}, + + {name: "13", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil}, + {name: "14", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil}, + + {name: "15", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3"}, result: assert.False, err: assert.Nil}, + {name: "16", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.False, err: assert.Nil}, + {name: "17", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.True, err: assert.Nil}, + {name: "18", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil}, + + {name: "19", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil}, + {name: "20", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.False, err: assert.Nil}, + {name: "21", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil}, + {name: "22", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil}, + + {name: "23", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil}, + {name: "24", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.False, err: assert.Nil}, + {name: "25", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.False, err: assert.Nil}, + {name: "26", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil}, + {name: "27", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil}, + + {name: "28", op: nfdv1alpha1.MatchIsTrue, key: "foo", result: assert.False, err: assert.Nil}, + {name: "29", op: nfdv1alpha1.MatchIsTrue, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil}, + {name: "30", op: nfdv1alpha1.MatchIsTrue, key: "foo", input: I{"foo": "true"}, result: assert.True, err: assert.Nil}, + + {name: "31", op: nfdv1alpha1.MatchIsFalse, key: "foo", input: I{"foo": "true"}, result: assert.False, err: assert.Nil}, + {name: "32", op: nfdv1alpha1.MatchIsFalse, key: "foo", input: I{"foo": "false"}, result: assert.True, err: assert.Nil}, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values} + res, err := evaluateMatchExpressionValues(me, tc.key, tc.input) + tc.result(t, res) + tc.err(t, err) + }) + } +} diff --git a/pkg/apis/nfd/v1alpha1/rule.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go similarity index 80% rename from pkg/apis/nfd/v1alpha1/rule.go rename to pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go index b0936efb8..dd436d115 100644 --- a/pkg/apis/nfd/v1alpha1/rule.go +++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +package nodefeaturerule import ( "bytes" @@ -27,6 +27,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/klog/v2" + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" "sigs.k8s.io/node-feature-discovery/pkg/utils" ) @@ -41,7 +42,7 @@ type RuleOutput struct { } // Execute the rule against a set of input features. -func (r *Rule) Execute(features *Features) (RuleOutput, error) { +func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features) (RuleOutput, error) { labels := make(map[string]string) vars := make(map[string]string) @@ -49,7 +50,7 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) { // Logical OR over the matchAny matchers matched := false for _, matcher := range r.MatchAny { - if isMatch, matches, err := matcher.match(features); err != nil { + if isMatch, matches, err := evaluateMatchAnyElem(&matcher, features); err != nil { return RuleOutput{}, err } else if isMatch { matched = true @@ -62,10 +63,10 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) { break } - if err := r.executeLabelsTemplate(matches, labels); err != nil { + if err := executeLabelsTemplate(r, matches, labels); err != nil { return RuleOutput{}, err } - if err := r.executeVarsTemplate(matches, vars); err != nil { + if err := executeVarsTemplate(r, matches, vars); err != nil { return RuleOutput{}, err } } @@ -77,17 +78,17 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) { } if len(r.MatchFeatures) > 0 { - if isMatch, matches, err := r.MatchFeatures.match(features); err != nil { + if isMatch, matches, err := evaluateFeatureMatcher(&r.MatchFeatures, features); err != nil { return RuleOutput{}, err } else if !isMatch { klog.V(2).InfoS("rule did not match", "ruleName", r.Name) return RuleOutput{}, nil } else { klog.V(4).InfoS("matchFeatures matched", "ruleName", r.Name, "matchedFeatures", utils.DelayedDumper(matches)) - if err := r.executeLabelsTemplate(matches, labels); err != nil { + if err := executeLabelsTemplate(r, matches, labels); err != nil { return RuleOutput{}, err } - if err := r.executeVarsTemplate(matches, vars); err != nil { + if err := executeVarsTemplate(r, matches, vars); err != nil { return RuleOutput{}, err } } @@ -107,7 +108,7 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) { return ret, nil } -func (r *Rule) executeLabelsTemplate(in matchedFeatures, out map[string]string) error { +func executeLabelsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string]string) error { if r.LabelsTemplate == "" { return nil } @@ -127,7 +128,7 @@ func (r *Rule) executeLabelsTemplate(in matchedFeatures, out map[string]string) return nil } -func (r *Rule) executeVarsTemplate(in matchedFeatures, out map[string]string) error { +func executeVarsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string]string) error { if r.VarsTemplate == "" { return nil } @@ -151,11 +152,11 @@ type matchedFeatures map[string]domainMatchedFeatures type domainMatchedFeatures map[string][]MatchedElement -func (e *MatchAnyElem) match(features *Features) (bool, matchedFeatures, error) { - return e.MatchFeatures.match(features) +func evaluateMatchAnyElem(e *nfdv1alpha1.MatchAnyElem, features *nfdv1alpha1.Features) (bool, matchedFeatures, error) { + return evaluateFeatureMatcher(&e.MatchFeatures, features) } -func (m *FeatureMatcher) match(features *Features) (bool, matchedFeatures, error) { +func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1.Features) (bool, matchedFeatures, error) { matches := make(matchedFeatures, len(*m)) // Logical AND over the terms @@ -180,30 +181,30 @@ func (m *FeatureMatcher) match(features *Features) (bool, matchedFeatures, error var err error if f, ok := features.Flags[featureName]; ok { if term.MatchExpressions != nil { - isMatch, matchedElems, err = term.MatchExpressions.MatchGetKeys(f.Elements) + isMatch, matchedElems, err = MatchGetKeys(term.MatchExpressions, f.Elements) } var meTmp []MatchedElement if err == nil && isMatch && term.MatchName != nil { - isMatch, meTmp, err = term.MatchName.MatchKeyNames(f.Elements) + isMatch, meTmp, err = MatchKeyNames(term.MatchName, f.Elements) matchedElems = append(matchedElems, meTmp...) } } else if f, ok := features.Attributes[featureName]; ok { if term.MatchExpressions != nil { - isMatch, matchedElems, err = term.MatchExpressions.MatchGetValues(f.Elements) + isMatch, matchedElems, err = MatchGetValues(term.MatchExpressions, f.Elements) } var meTmp []MatchedElement if err == nil && isMatch && term.MatchName != nil { - isMatch, meTmp, err = term.MatchName.MatchValueNames(f.Elements) + isMatch, meTmp, err = MatchValueNames(term.MatchName, f.Elements) matchedElems = append(matchedElems, meTmp...) } } else if f, ok := features.Instances[featureName]; ok { if term.MatchExpressions != nil { - matchedElems, err = term.MatchExpressions.MatchGetInstances(f.Elements) + matchedElems, err = MatchGetInstances(term.MatchExpressions, f.Elements) isMatch = len(matchedElems) > 0 } var meTmp []MatchedElement if err == nil && isMatch && term.MatchName != nil { - meTmp, err = term.MatchName.MatchInstanceAttributeNames(f.Elements) + meTmp, err = MatchInstanceAttributeNames(term.MatchName, f.Elements) isMatch = len(meTmp) > 0 matchedElems = append(matchedElems, meTmp...) diff --git a/pkg/apis/nfd/v1alpha1/rule_test.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule_test.go similarity index 60% rename from pkg/apis/nfd/v1alpha1/rule_test.go rename to pkg/apis/nfd/v1alpha1/nodefeaturerule/rule_test.go index b3728359e..a642404a7 100644 --- a/pkg/apis/nfd/v1alpha1/rule_test.go +++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule_test.go @@ -14,217 +14,219 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +package nodefeaturerule import ( "testing" "github.com/stretchr/testify/assert" + + nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" ) // newMatchExpression returns a new MatchExpression instance. -func newMatchExpression(op MatchOp, values ...string) *MatchExpression { - return &MatchExpression{ +func newMatchExpression(op nfdv1alpha1.MatchOp, values ...string) *nfdv1alpha1.MatchExpression { + return &nfdv1alpha1.MatchExpression{ Op: op, Value: values, } } func TestRule(t *testing.T) { - f := &Features{} - r1 := Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}} - r2 := Rule{ + f := &nfdv1alpha1.Features{} + r1 := &nfdv1alpha1.Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}} + r2 := &nfdv1alpha1.Rule{ Labels: map[string]string{"label-1": "label-val-1"}, Vars: map[string]string{"var-1": "var-val-1"}, - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: &MatchExpressionSet{ - "key-1": newMatchExpression(MatchExists), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-1": newMatchExpression(nfdv1alpha1.MatchExists), }, }, }, } // Test totally empty features - m, err := r1.Execute(f) + m, err := Execute(r1, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features") - _, err = r2.Execute(f) + _, err = Execute(r2, f) assert.Error(t, err, "matching against a missing feature should have returned an error") // Test properly initialized empty features - f = NewFeatures() + f = nfdv1alpha1.NewFeatures() - m, err = r1.Execute(f) + m, err = Execute(r1, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features") assert.Empty(t, r1.Vars, "vars should be empty") - _, err = r2.Execute(f) + _, err = Execute(r2, f) assert.Error(t, err, "matching against a missing feature type should have returned an error") // Test empty feature sets - f.Flags["domain-1.kf-1"] = NewFlagFeatures() - f.Attributes["domain-1.vf-1"] = NewAttributeFeatures(nil) - f.Instances["domain-1.if-1"] = NewInstanceFeatures(nil) + f.Flags["domain-1.kf-1"] = nfdv1alpha1.NewFlagFeatures() + f.Attributes["domain-1.vf-1"] = nfdv1alpha1.NewAttributeFeatures(nil) + f.Instances["domain-1.if-1"] = nfdv1alpha1.NewInstanceFeatures(nil) - m, err = r1.Execute(f) + m, err = Execute(r1, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features") - m, err = r2.Execute(f) + m, err = Execute(r2, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Nil(t, m.Labels, "unexpected match") // Test non-empty feature sets - f.Flags["domain-1.kf-1"].Elements["key-x"] = Nil{} + f.Flags["domain-1.kf-1"].Elements["key-x"] = nfdv1alpha1.Nil{} f.Attributes["domain-1.vf-1"].Elements["key-1"] = "val-x" - f.Instances["domain-1.if-1"] = NewInstanceFeatures([]InstanceFeature{ - *NewInstanceFeature(map[string]string{"attr-1": "val-x"})}) + f.Instances["domain-1.if-1"] = nfdv1alpha1.NewInstanceFeatures([]nfdv1alpha1.InstanceFeature{ + *nfdv1alpha1.NewInstanceFeature(map[string]string{"attr-1": "val-x"})}) - m, err = r1.Execute(f) + m, err = Execute(r1, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features") // Test empty MatchExpressions - r1.MatchFeatures = FeatureMatcher{ - FeatureMatcherTerm{ + r1.MatchFeatures = nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: &MatchExpressionSet{}, + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{}, }, } - m, err = r1.Execute(f) + m, err = Execute(r1, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r1.Labels, m.Labels, "empty match expression set mathces anything") // Match "key" features - m, err = r2.Execute(f) + m, err = Execute(r2, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Nil(t, m.Labels, "keys should not have matched") - f.Flags["domain-1.kf-1"].Elements["key-1"] = Nil{} - m, err = r2.Execute(f) + f.Flags["domain-1.kf-1"].Elements["key-1"] = nfdv1alpha1.Nil{} + m, err = Execute(r2, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r2.Labels, m.Labels, "keys should have matched") assert.Equal(t, r2.Vars, m.Vars, "vars should be present") // Match "value" features - r3 := Rule{ + r3 := &nfdv1alpha1.Rule{ Labels: map[string]string{"label-3": "label-val-3", "empty": ""}, - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: &MatchExpressionSet{ - "key-1": newMatchExpression(MatchIn, "val-1"), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1"), }, }, }, } - m, err = r3.Execute(f) + m, err = Execute(r3, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Nil(t, m.Labels, "values should not have matched") f.Attributes["domain-1.vf-1"].Elements["key-1"] = "val-1" - m, err = r3.Execute(f) + m, err = Execute(r3, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r3.Labels, m.Labels, "values should have matched") // Match "instance" features - r4 := Rule{ + r4 := &nfdv1alpha1.Rule{ Labels: map[string]string{"label-4": "label-val-4"}, - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: &MatchExpressionSet{ - "attr-1": newMatchExpression(MatchIn, "val-1"), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "attr-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1"), }, }, }, } - m, err = r4.Execute(f) + m, err = Execute(r4, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Nil(t, m.Labels, "instances should not have matched") f.Instances["domain-1.if-1"].Elements[0].Attributes["attr-1"] = "val-1" - m, err = r4.Execute(f) + m, err = Execute(r4, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r4.Labels, m.Labels, "instances should have matched") // Test multiple feature matchers - r5 := Rule{ + r5 := &nfdv1alpha1.Rule{ Labels: map[string]string{"label-5": "label-val-5"}, - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: &MatchExpressionSet{ - "key-1": newMatchExpression(MatchIn, "val-x"), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-x"), }, }, - FeatureMatcherTerm{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: &MatchExpressionSet{ - "attr-1": newMatchExpression(MatchIn, "val-1"), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "attr-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1"), }, }, }, } - m, err = r5.Execute(f) + m, err = Execute(r5, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Nil(t, m.Labels, "instances should not have matched") - (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(MatchIn, "val-1") - m, err = r5.Execute(f) + (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1") + m, err = Execute(r5, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r5.Labels, m.Labels, "instances should have matched") // Test MatchAny - r5.MatchAny = []MatchAnyElem{ + r5.MatchAny = []nfdv1alpha1.MatchAnyElem{ { - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: &MatchExpressionSet{ - "key-na": newMatchExpression(MatchExists), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-na": newMatchExpression(nfdv1alpha1.MatchExists), }, }, }, }, } - m, err = r5.Execute(f) + m, err = Execute(r5, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Nil(t, m.Labels, "instances should not have matched") r5.MatchAny = append(r5.MatchAny, - MatchAnyElem{ - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + nfdv1alpha1.MatchAnyElem{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: &MatchExpressionSet{ - "key-1": newMatchExpression(MatchExists), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-1": newMatchExpression(nfdv1alpha1.MatchExists), }, }, }, }) - (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(MatchIn, "val-1") - m, err = r5.Execute(f) + (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1") + m, err = Execute(r5, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r5.Labels, m.Labels, "instances should have matched") } func TestTemplating(t *testing.T) { - f := &Features{ - Flags: map[string]FlagFeatureSet{ + f := &nfdv1alpha1.Features{ + Flags: map[string]nfdv1alpha1.FlagFeatureSet{ "domain_1.kf_1": { - Elements: map[string]Nil{ + Elements: map[string]nfdv1alpha1.Nil{ "key-a": {}, "key-b": {}, "key-c": {}, }, }, }, - Attributes: map[string]AttributeFeatureSet{ + Attributes: map[string]nfdv1alpha1.AttributeFeatureSet{ "domain_1.vf_1": { Elements: map[string]string{ "key-1": "val-1", @@ -234,9 +236,9 @@ func TestTemplating(t *testing.T) { }, }, }, - Instances: map[string]InstanceFeatureSet{ + Instances: map[string]nfdv1alpha1.InstanceFeatureSet{ "domain_1.if_1": { - Elements: []InstanceFeature{ + Elements: []nfdv1alpha1.InstanceFeature{ { Attributes: map[string]string{ "attr-1": "1", @@ -267,7 +269,7 @@ func TestTemplating(t *testing.T) { }, } - r1 := Rule{ + r1 := &nfdv1alpha1.Rule{ Labels: map[string]string{"label-1": "label-val-1"}, LabelsTemplate: ` label-1=will-be-overridden @@ -284,34 +286,34 @@ var-1=value-will-be-overridden-by-vars var-2= {{range .domain_1.kf_1}}kf-{{.Name}}=true {{end}}`, - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain_1.kf_1", - MatchExpressions: &MatchExpressionSet{ - "key-a": newMatchExpression(MatchExists), - "key-c": newMatchExpression(MatchExists), - "foo": newMatchExpression(MatchDoesNotExist), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-a": newMatchExpression(nfdv1alpha1.MatchExists), + "key-c": newMatchExpression(nfdv1alpha1.MatchExists), + "foo": newMatchExpression(nfdv1alpha1.MatchDoesNotExist), }, }, - FeatureMatcherTerm{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain_1.vf_1", - MatchExpressions: &MatchExpressionSet{ - "key-1": newMatchExpression(MatchIn, "val-1", "val-2"), - "bar": newMatchExpression(MatchDoesNotExist), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1", "val-2"), + "bar": newMatchExpression(nfdv1alpha1.MatchDoesNotExist), }, }, - FeatureMatcherTerm{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain_1.if_1", - MatchExpressions: &MatchExpressionSet{ - "attr-1": newMatchExpression(MatchLt, "100"), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "attr-1": newMatchExpression(nfdv1alpha1.MatchLt, "100"), }, }, - FeatureMatcherTerm{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain_1.if_1", - MatchExpressions: &MatchExpressionSet{ - "attr-1": newMatchExpression(MatchExists), - "attr-2": newMatchExpression(MatchExists), - "attr-3": newMatchExpression(MatchExists), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "attr-1": newMatchExpression(nfdv1alpha1.MatchExists), + "attr-2": newMatchExpression(nfdv1alpha1.MatchExists), + "attr-3": newMatchExpression(nfdv1alpha1.MatchExists), }, }, }, @@ -319,7 +321,7 @@ var-2= // test with empty MatchFeatures, but with MatchAny r3 := r1.DeepCopy() - r3.MatchAny = []MatchAnyElem{{MatchFeatures: r3.MatchFeatures}} + r3.MatchAny = []nfdv1alpha1.MatchAnyElem{{MatchFeatures: r3.MatchFeatures}} r3.MatchFeatures = nil expectedLabels := map[string]string{ @@ -346,12 +348,12 @@ var-2= "kf-foo": "true", } - m, err := r1.Execute(f) + m, err := Execute(r1, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, expectedLabels, m.Labels, "instances should have matched") assert.Equal(t, expectedVars, m.Vars, "instances should have matched") - m, err = r3.Execute(f) + m, err = Execute(r3, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, expectedLabels, m.Labels, "instances should have matched") assert.Equal(t, expectedVars, m.Vars, "instances should have matched") @@ -359,60 +361,60 @@ var-2= // // Test error cases // - r2 := Rule{ - MatchFeatures: FeatureMatcher{ + r2 := &nfdv1alpha1.Rule{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ // We need at least one matcher to match to execute the template. // Use a simple empty matchexpression set to match anything. - FeatureMatcherTerm{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain_1.kf_1", - MatchExpressions: &MatchExpressionSet{ - "key-a": newMatchExpression(MatchExists), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-a": newMatchExpression(nfdv1alpha1.MatchExists), }, }, }, } r2.LabelsTemplate = "foo=bar" - m, err = r2.Execute(f) + m, err = Execute(r2, f) assert.Nil(t, err) assert.Equal(t, map[string]string{"foo": "bar"}, m.Labels, "instances should have matched") assert.Empty(t, m.Vars) r2.LabelsTemplate = "foo" - _, err = r2.Execute(f) + _, err = Execute(r2, f) assert.Error(t, err) r2.LabelsTemplate = "{{" - _, err = r2.Execute(f) + _, err = Execute(r2, f) assert.Error(t, err) r2.LabelsTemplate = "" r2.VarsTemplate = "bar=baz" - m, err = r2.Execute(f) + m, err = Execute(r2, f) assert.Nil(t, err) assert.Empty(t, m.Labels) assert.Equal(t, map[string]string{"bar": "baz"}, m.Vars, "instances should have matched") r2.VarsTemplate = "bar" - _, err = r2.Execute(f) + _, err = Execute(r2, f) assert.Error(t, err) r2.VarsTemplate = "{{" - _, err = r2.Execute(f) + _, err = Execute(r2, f) assert.Error(t, err) // // Test matchName // - r4 := Rule{ + r4 := &nfdv1alpha1.Rule{ LabelsTemplate: "{{range .domain_1.vf_1}}{{.Name}}={{.Value}}\n{{end}}", - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain_1.vf_1", - MatchExpressions: &MatchExpressionSet{ - "key-5": newMatchExpression(MatchDoesNotExist), + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ + "key-5": newMatchExpression(nfdv1alpha1.MatchDoesNotExist), }, - MatchName: newMatchExpression(MatchIn, "key-1", "key-4"), + MatchName: newMatchExpression(nfdv1alpha1.MatchIn, "key-1", "key-4"), }, }, } @@ -422,21 +424,21 @@ var-2= "key-5": "", } - m, err = r4.Execute(f) + m, err = Execute(r4, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, expectedLabels, m.Labels, "instances should have matched") - r4 = Rule{ + r4 = &nfdv1alpha1.Rule{ Labels: map[string]string{"should-not-match": "true"}, - MatchFeatures: FeatureMatcher{ - FeatureMatcherTerm{ + MatchFeatures: nfdv1alpha1.FeatureMatcher{ + nfdv1alpha1.FeatureMatcherTerm{ Feature: "domain_1.vf_1", - MatchName: newMatchExpression(MatchIn, "key-not-exists"), + MatchName: newMatchExpression(nfdv1alpha1.MatchIn, "key-not-exists"), }, }, } - m, err = r4.Execute(f) + m, err = Execute(r4, f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, map[string]string(nil), m.Labels, "instances should have matched") } diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go index 09f9582ae..2f5dfebfe 100644 --- a/pkg/nfd-master/nfd-master.go +++ b/pkg/nfd-master/nfd-master.go @@ -54,6 +54,7 @@ import ( "sigs.k8s.io/node-feature-discovery/pkg/apihelper" nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" + "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1/nodefeaturerule" "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate" pb "sigs.k8s.io/node-feature-discovery/pkg/labeler" "sigs.k8s.io/node-feature-discovery/pkg/utils" @@ -970,7 +971,7 @@ func (m *nfdMaster) processNodeFeatureRule(nodeName string, features *nfdv1alpha klog.InfoS("executing NodeFeatureRule", "nodefeaturerule", klog.KObj(spec), "nodeName", nodeName) } for _, rule := range spec.Spec.Rules { - ruleOut, err := rule.Execute(features) + ruleOut, err := nodefeaturerule.Execute(&rule, features) if err != nil { klog.ErrorS(err, "failed to process rule", "ruleName", rule.Name, "nodefeaturerule", klog.KObj(spec), "nodeName", nodeName) nfrProcessingErrors.Inc() diff --git a/source/custom/custom.go b/source/custom/custom.go index 17ef9fff9..2655279f0 100644 --- a/source/custom/custom.go +++ b/source/custom/custom.go @@ -23,6 +23,7 @@ import ( "k8s.io/klog/v2" nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1" + nodefeaturerule "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1/nodefeaturerule" "sigs.k8s.io/node-feature-discovery/pkg/utils" "sigs.k8s.io/node-feature-discovery/source" api "sigs.k8s.io/node-feature-discovery/source/custom/api" @@ -92,7 +93,7 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) { klog.V(2).InfoS("resolving custom features", "configuration", utils.DelayedDumper(allFeatureConfig)) // Iterate over features for _, rule := range allFeatureConfig { - ruleOut, err := rule.Execute(features) + ruleOut, err := nodefeaturerule.Execute(&rule, features) if err != nil { klog.ErrorS(err, "failed to execute rule") continue