1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2024-12-14 11:57:51 +00:00

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.
This commit is contained in:
Markus Lehtonen 2023-11-29 14:32:33 +02:00
parent e162540e54
commit 97bf841140
8 changed files with 687 additions and 651 deletions

View file

@ -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)
})
}
}

View file

@ -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)
})
}
}

View file

@ -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)

View file

@ -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)
})
}
}

View file

@ -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...)

View file

@ -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")
}

View file

@ -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()

View file

@ -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