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: allow different types of features of the same name

This patch changes the handling of NodeFeatureRules so that one feature
name (say "cpu.cpuid") can hold different types of features (flags,
attributes and/or instances). Requiring features to choose one single
type has not been a limitation of the API itself (and there has been no
validation on this) but an implementation decision.

The new evalutation logic of match expressions is such that "flags" and
"attributes" are basically evaluated as an union - they are both maps
but "flags" just don't have any value associated with the key. However,
"instances" are handled separately as that is basically an array of
maps and needs to be evaluated in a different way (loop over the array
of instances and evaluate expressions against the attributes of each).
Because of this difference care must be taken if mixing "instance"
features with "flag" and/or "attribute" features.

Note that the API types or their validation is not changed - just the
implementation of how the NodeFeatureRules are evaluated.
This commit is contained in:
Markus Lehtonen 2024-04-17 15:38:34 +03:00
parent c2e3de68f6
commit 3b448ae623
7 changed files with 700 additions and 79 deletions

View file

@ -80,21 +80,6 @@ func (f *Features) InsertAttributeFeatures(domain, feature string, values map[st
maps.Copy(f.Attributes[key].Elements, values)
}
// Exists returns a non-empty string if a feature exists. The return value is
// the type of the feautre, i.e. "flag", "attribute" or "instance".
func (f *Features) Exists(name string) string {
if _, ok := f.Flags[name]; ok {
return "flag"
}
if _, ok := f.Attributes[name]; ok {
return "attribute"
}
if _, ok := f.Instances[name]; ok {
return "instance"
}
return ""
}
// MergeInto merges two FeatureSpecs into one. Data in the input object takes
// precedence (overwrite) over data of the existing object we're merging into.
func (in *NodeFeatureSpec) MergeInto(out *NodeFeatureSpec) {

View file

@ -113,12 +113,9 @@ func TestInstanceFeatureSet(t *testing.T) {
func TestFeature(t *testing.T) {
f := Features{}
// Test Exists() and InsertAttributeFeatures()
assert.Empty(t, f.Exists("dom.attr"), "empty features shouldn't contain anything")
// Test InsertAttributeFeatures()
f.InsertAttributeFeatures("dom", "attr", map[string]string{"k1": "v1", "k2": "v2"})
expectedAttributes := map[string]string{"k1": "v1", "k2": "v2"}
assert.Equal(t, "attribute", f.Exists("dom.attr"), "attribute feature should exist")
assert.Equal(t, expectedAttributes, f.Attributes["dom.attr"].Elements)
f.InsertAttributeFeatures("dom", "attr", map[string]string{"k2": "v2.override", "k3": "v3"})

View file

@ -101,7 +101,7 @@ NodeFeature object as NFD uses it to determine the node which it is targeting.
### Feature types
Features are divided into three different types:
Features have three different types:
- **flag** features: a set of names without any associated values, e.g. CPUID
flags or loaded kernel modules
@ -912,7 +912,7 @@ true).
The following features are available for matching:
| Feature | [Feature type](#feature-types) | Elements | Value type | Description |
| Feature | [Feature types](#feature-types) | Elements | Value type | Description |
| ---------------- | ------------ | -------- | ---------- | ----------- |
| **`cpu.cpuid`** | flag | | | Supported CPU capabilities |
| | | **`<cpuid-flag>`** | | CPUID flag is present |

View file

@ -248,11 +248,12 @@ bar: { op: Lt, value: ["10"] }
t.Fatal("failed to parse data of test case")
}
out, err := api.MatchGetInstances(mes, tc.input)
res, out, err := api.MatchGetInstances(mes, tc.input)
assert.Equal(t, tc.output, out)
tc.result(t, res)
tc.err(t, err)
res, err := api.MatchInstances(mes, tc.input)
res, err = api.MatchInstances(mes, tc.input)
tc.result(t, res)
tc.err(t, err)
})
@ -440,6 +441,7 @@ func TestMatchInstanceAttributeNames(t *testing.T) {
name string
me *nfdv1alpha1.MatchExpression
input I
result bool
output O
err ValueAssertionFunc
}
@ -449,6 +451,7 @@ func TestMatchInstanceAttributeNames(t *testing.T) {
name: "empty input",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchAny},
input: I{},
result: false,
output: O{},
err: assert.Nil,
},
@ -462,6 +465,7 @@ func TestMatchInstanceAttributeNames(t *testing.T) {
{Attributes: A{"bar": "1"}},
{Attributes: A{"baz": "2"}},
},
result: false,
output: O{},
err: assert.Nil,
},
@ -476,6 +480,7 @@ func TestMatchInstanceAttributeNames(t *testing.T) {
{Attributes: A{"bar": "2"}},
{Attributes: A{"foo": "3", "baz": "4"}},
},
result: true,
output: O{
{"foo": "1"},
{"foo": "3", "baz": "4"},
@ -497,9 +502,416 @@ func TestMatchInstanceAttributeNames(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
matched, err := api.MatchInstanceAttributeNames(tc.me, tc.input)
match, matched, err := api.MatchInstanceAttributeNames(tc.me, tc.input)
assert.Equal(t, tc.result, match)
assert.Equal(t, tc.output, matched)
tc.err(t, err)
})
}
}
func TestMatchMulti(t *testing.T) {
type O = []api.MatchedElement
type IK = map[string]nfdv1alpha1.Nil
type IV = map[string]string
type II = []nfdv1alpha1.InstanceFeature
type A = map[string]string
type TC struct {
name string
mes *nfdv1alpha1.MatchExpressionSet
inputKeys IK
inputValues IV
inputInstances II
output O
result BoolAssertionFunc
expectErr bool
}
tcs := []TC{
{
name: "empty expression and nil input",
mes: &nfdv1alpha1.MatchExpressionSet{},
output: O{},
result: assert.False,
},
{
name: "empty expression and empty input keys",
mes: &nfdv1alpha1.MatchExpressionSet{},
inputKeys: IK{},
inputValues: IV{},
inputInstances: II{},
output: O{},
result: assert.True,
},
{
name: "empty expression and empty input values",
mes: &nfdv1alpha1.MatchExpressionSet{},
inputValues: IV{},
output: O{},
result: assert.True,
},
{
name: "empty expression and empty input instances",
mes: &nfdv1alpha1.MatchExpressionSet{},
inputInstances: II{},
output: O{},
result: assert.False,
},
{
name: "empty expression and one input instance with empty attributes",
mes: &nfdv1alpha1.MatchExpressionSet{},
inputInstances: II{{Attributes: A{}}},
output: O{A{}},
result: assert.True,
},
{
name: "empty expression",
mes: &nfdv1alpha1.MatchExpressionSet{},
inputValues: IV{"foo": "bar"},
output: O{},
result: assert.True,
},
{
name: "keys match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchDoesNotExist},
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
},
inputKeys: IK{"bar": {}, "baz": {}, "buzz": {}},
output: O{{"Name": "bar"}, {"Name": "foo"}},
result: assert.True,
},
{
name: "keys do not match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchDoesNotExist},
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
},
inputKeys: IK{"foo": {}, "bar": {}, "baz": {}},
output: O{},
result: assert.False,
},
{
name: "keys error",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists, Value: nfdv1alpha1.MatchValue{"val"}},
},
inputKeys: IK{"bar": {}, "baz": {}, "buzz": {}},
output: nil,
result: assert.False,
expectErr: true,
},
{
name: "values match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"val", "wal"}},
"baz": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchGt, Value: nfdv1alpha1.MatchValue{"10"}},
},
inputValues: IV{"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,
},
{
name: "values do not match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"val", "wal"}},
"baz": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchGt, Value: nfdv1alpha1.MatchValue{"10"}},
},
inputValues: IV{"bar": "val"},
output: O{},
result: assert.False,
},
{
name: "values error",
mes: &nfdv1alpha1.MatchExpressionSet{
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn},
},
inputValues: IV{"foo": "1", "bar": "val", "baz": "123", "buzz": "light"},
output: nil,
result: assert.False,
expectErr: true,
},
{
name: "instances match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchLt, Value: nfdv1alpha1.MatchValue{"10"}},
},
inputInstances: II{{Attributes: A{"foo": "1"}}, {Attributes: A{"foo": "2", "bar": "1"}}},
output: O{A{"foo": "2", "bar": "1"}},
result: assert.True,
},
{
name: "instances do not match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
"baz": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchLt, Value: nfdv1alpha1.MatchValue{"10"}},
},
inputInstances: II{{Attributes: A{"foo": "1"}}, {Attributes: A{"bar": "1"}}},
output: O{},
result: assert.False,
},
{
name: "instances error",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn},
},
inputInstances: II{{Attributes: A{"foo": "1"}}, {Attributes: A{"foo": "2", "bar": "1"}}},
output: nil,
result: assert.False,
expectErr: true,
},
{
name: "multi: keys and values either matches",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"1"}},
"baz": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
},
inputKeys: IK{"bar": {}, "baz": {}, "qux": {}},
inputValues: IV{"foo": "1", "bar": "2", "quux": "3"},
output: O{{"Name": "baz"}, {"Name": "foo", "Value": "1"}},
result: assert.True,
},
{
name: "multi: keys and values duplicate match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"1"}},
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
},
inputKeys: IK{"bar": {}, "baz": {}, "qux": {}},
inputValues: IV{"foo": "1", "bar": "2", "quux": "3"},
output: O{{"Name": "bar"}, {"Name": "bar", "Value": "2"}, {"Name": "foo", "Value": "1"}},
result: assert.True,
},
{
name: "multi: keys and values NotIn match",
mes: &nfdv1alpha1.MatchExpressionSet{
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchNotIn, Value: nfdv1alpha1.MatchValue{"1", "3"}},
},
inputKeys: IK{"bar": {}, "baz": {}, "qux": {}},
inputValues: IV{"foo": "1", "bar": "2", "quux": "3"},
output: O{{"Name": "bar", "Value": "2"}},
result: assert.True,
},
{
name: "multi: keys and values NotIn does not match",
mes: &nfdv1alpha1.MatchExpressionSet{
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchNotIn, Value: nfdv1alpha1.MatchValue{"2"}},
},
inputKeys: IK{"bar": {}, "baz": {}, "qux": {}},
inputValues: IV{"foo": "1", "bar": "2", "quux": "3"},
output: O{},
result: assert.False,
},
{
name: "multi: keys and values DoesNotExist match",
mes: &nfdv1alpha1.MatchExpressionSet{
"xyzzy": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchDoesNotExist},
},
inputKeys: IK{"bar": {}, "baz": {}, "qux": {}},
inputValues: IV{"foo": "1", "bar": "2", "quux": "3"},
output: O{{"Name": "xyzzy"}, {"Name": "xyzzy", "Value": ""}},
result: assert.True,
},
{
name: "multi: keys and values DoesNotExist does not match",
mes: &nfdv1alpha1.MatchExpressionSet{
"quux": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchDoesNotExist},
},
inputKeys: IK{"bar": {}, "baz": {}, "qux": {}},
inputValues: IV{"foo": "1", "bar": "2", "quux": "3"},
output: O{},
result: assert.False,
},
{
name: "multi: keys, values and instances all match",
mes: &nfdv1alpha1.MatchExpressionSet{
"foo": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"1"}},
"bar": &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
},
inputKeys: IK{"bar": {}, "baz": {}, "qux": {}},
inputValues: IV{"foo": "1", "bar": "2", "quux": "3"},
inputInstances: II{
{Attributes: A{"foo": "1", "bar": "2"}},
{Attributes: A{"foo": "10", "bar": "20"}},
},
output: O{{"Name": "bar"}, {"Name": "bar", "Value": "2"}, {"Name": "foo", "Value": "1"}, {"bar": "2", "foo": "1"}},
result: assert.True,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
res, out, err := api.MatchMulti(tc.mes, tc.inputKeys, tc.inputValues, tc.inputInstances)
if tc.expectErr {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
tc.result(t, res)
assert.Equal(t, tc.output, out)
})
}
}
func TestMatchNamesMulti(t *testing.T) {
type O = []api.MatchedElement
type IK = map[string]nfdv1alpha1.Nil
type IV = map[string]string
type II = []nfdv1alpha1.InstanceFeature
type A = map[string]string
type TC struct {
name string
me *nfdv1alpha1.MatchExpression
inputKeys IK
inputValues IV
inputInstances II
output O
result bool
expectErr bool
}
tcs := []TC{
{
name: "nil input",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchAny},
result: false,
output: O{},
},
{
name: "empty input keys",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchAny},
inputKeys: IK{},
result: false,
output: O{},
},
{
name: "empty input values",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchAny},
inputValues: IV{},
result: false,
output: O{},
},
{
name: "empty input instances",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchAny},
inputInstances: II{},
result: false,
output: O{},
},
{
name: "input keys match",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchExists},
inputKeys: IK{"key1": {}, "key2": {}},
result: true,
output: O{{"Name": "key1"}, {"Name": "key2"}},
},
{
name: "input keys do not match",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchDoesNotExist},
inputKeys: IK{"key1": {}, "key2": {}},
result: false,
output: O{},
},
{
name: "input keys error",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn},
inputKeys: IK{"key1": {}, "key2": {}},
result: false,
output: nil,
expectErr: true,
},
{
name: "input values match",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"key1"}},
inputValues: IV{"key1": "val1", "key2": "val2"},
result: true,
output: O{{"Name": "key1", "Value": "val1"}},
},
{
name: "input values do not match",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"key3"}},
inputValues: IV{"key1": "val1", "key2": "val2"},
result: false,
output: O{},
},
{
name: "input values error",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn},
inputValues: IV{"key1": "val1", "key2": "val2"},
result: false,
output: nil,
expectErr: true,
},
{
name: "input instances match",
me: &nfdv1alpha1.MatchExpression{
Op: nfdv1alpha1.MatchIn,
Value: nfdv1alpha1.MatchValue{"foo"},
},
inputInstances: II{
{Attributes: A{"foo": "1"}},
{Attributes: A{"bar": "2"}},
{Attributes: A{"foo": "3", "baz": "4"}},
},
result: true,
output: O{
{"foo": "1"},
{"foo": "3", "baz": "4"},
},
},
{
name: "input instances do not match",
me: &nfdv1alpha1.MatchExpression{
Op: nfdv1alpha1.MatchIn,
Value: nfdv1alpha1.MatchValue{"foo"},
},
inputInstances: II{
{Attributes: A{"bar": "1"}},
{Attributes: A{"baz": "2"}},
},
result: false,
output: O{},
},
{
name: "input instances error",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn},
inputInstances: II{
{Attributes: A{"bar": "1"}},
},
result: false,
output: nil,
expectErr: true,
},
{
name: "input keys, values and instances match",
me: &nfdv1alpha1.MatchExpression{Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"key2"}},
inputKeys: IK{"key1": {}, "key2": {}},
inputValues: IV{"key1": "val1", "key2": "val2"},
inputInstances: II{
{Attributes: A{"key1": "1"}},
{Attributes: A{"key1": "2"}},
{Attributes: A{"key1": "3", "key2": "4"}},
},
result: true,
output: O{{"Name": "key2"}, {"Name": "key2", "Value": "val2"}, {"key1": "3", "key2": "4"}},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
res, ret, err := api.MatchNamesMulti(tc.me, tc.inputKeys, tc.inputValues, tc.inputInstances)
if tc.expectErr {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
assert.Equal(t, tc.result, res)
assert.Equal(t, tc.output, ret)
})
}
}

View file

@ -263,17 +263,17 @@ func MatchValueNames(m *nfdv1alpha1.MatchExpression, values map[string]string) (
// MatchInstanceAttributeNames evaluates the MatchExpression against a set of
// instance features, matching against the names of their attributes.
func MatchInstanceAttributeNames(m *nfdv1alpha1.MatchExpression, instances []nfdv1alpha1.InstanceFeature) ([]MatchedElement, error) {
func MatchInstanceAttributeNames(m *nfdv1alpha1.MatchExpression, instances []nfdv1alpha1.InstanceFeature) (bool, []MatchedElement, error) {
ret := []MatchedElement{}
for _, i := range instances {
if match, _, err := MatchValueNames(m, i.Attributes); err != nil {
return nil, err
return false, nil, err
} else if match {
ret = append(ret, i.Attributes)
}
}
return ret, nil
return len(ret) > 0, ret, nil
}
// MatchKeys evaluates the MatchExpressionSet against a set of keys.
@ -337,23 +337,153 @@ func MatchGetValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string)
// features, each of which is an individual set of key-value pairs
// (attributes).
func MatchInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) (bool, error) {
v, err := MatchGetInstances(m, instances)
return len(v) > 0, err
isMatch, _, err := MatchGetInstances(m, instances)
return isMatch, err
}
// MatchGetInstances evaluates the MatchExpressionSet against a set of instance
// 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 MatchGetInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) ([]MatchedElement, error) {
// (attributes). Returns a boolean that reports whether the expression matched.
// Also, returns a slice containing all matching instances. An empty (non-nil)
// slice is returned if no matching instances were found.
func MatchGetInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) (bool, []MatchedElement, error) {
ret := []MatchedElement{}
for _, i := range instances {
if match, err := MatchValues(m, i.Attributes); err != nil {
return nil, err
return false, nil, err
} else if match {
ret = append(ret, i.Attributes)
}
}
return ret, nil
return len(ret) > 0, ret, nil
}
// MatchMulti evaluates a MatchExpressionSet against key, value and instance
// features all at once. Key and values features are evaluated together so that
// a match in either (or both) of them is accepted as success. Instances are
// handled separately as the way of evaluating match expressions is different.
// This function is written to handle "multi-type" features where one feature
// (say "cpu.cpuid") contains multiple types (flag, attribute and/or instance).
func MatchMulti(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil, values map[string]string, instances []nfdv1alpha1.InstanceFeature) (bool, []MatchedElement, error) {
matchedElems := []MatchedElement{}
isMatch := false
// Keys and values are handled as a union, it is enough to find a match in
// either of them
if keys != nil || values != nil {
// Handle the special case of empty match expression
isMatch = true
}
for n, e := range *m {
var (
matchK bool
matchV bool
err error
)
if keys != nil {
matchK, err = evaluateMatchExpressionKeys(e, n, keys)
if err != nil {
return false, nil, err
}
if matchK {
matchedElems = append(matchedElems, MatchedElement{"Name": n})
} else if e.Op == nfdv1alpha1.MatchDoesNotExist {
// DoesNotExist is special in that both "keys" and "values" should match (i.e. the name is not found in either of them).
isMatch = false
matchedElems = []MatchedElement{}
break
}
}
if values != nil {
matchV, err = evaluateMatchExpressionValues(e, n, values)
if err != nil {
return false, nil, err
}
if matchV {
matchedElems = append(matchedElems, MatchedElement{"Name": n, "Value": values[n]})
} else if e.Op == nfdv1alpha1.MatchDoesNotExist {
// DoesNotExist is special in that both "keys" and "values" should match (i.e. the name is not found in either of them).
isMatch = false
matchedElems = []MatchedElement{}
break
}
}
if !matchK && !matchV {
isMatch = false
matchedElems = []MatchedElement{}
break
}
}
// Sort for reproducible output
sort.Slice(matchedElems, func(i, j int) bool { return matchedElems[i]["Name"] < matchedElems[j]["Name"] })
// Instances are handled separately as the logic is fundamentally different
// from keys and values and cannot be combined with them. We want to find
// instance(s) that match all match expressions. I.e. the set of all match
// expressions are evaluated against every instance separately.
ma, me, err := MatchGetInstances(m, instances)
if err != nil {
return false, nil, err
}
isMatch = isMatch || ma
matchedElems = append(matchedElems, me...)
return isMatch, matchedElems, nil
}
// MatchNamesMulti evaluates the MatchExpression against the names of key,
// value and attributes of instance features all at once. It is meant to handle
// "multi-type" features where one feature (say "cpu.cpuid") contains multiple
// types (flag, attribute and/or instance).
func MatchNamesMulti(m *nfdv1alpha1.MatchExpression, keys map[string]nfdv1alpha1.Nil, values map[string]string, instances []nfdv1alpha1.InstanceFeature) (bool, []MatchedElement, error) {
ret := []MatchedElement{}
for k := range keys {
if match, err := evaluateMatchExpression(m, true, k); err != nil {
return false, nil, err
} else if match {
ret = append(ret, MatchedElement{"Name": k})
}
}
for k, v := range values {
if match, err := evaluateMatchExpression(m, true, k); err != nil {
return false, nil, err
} else if match {
ret = append(ret, MatchedElement{"Name": k, "Value": v})
}
}
// Sort for reproducible output
sort.Slice(ret, func(i, j int) bool { return ret[i]["Name"] < ret[j]["Name"] })
_, me, err := MatchInstanceAttributeNames(m, instances)
if err != nil {
return false, nil, err
}
ret = append(ret, me...)
if klogV3 := klog.V(3); klogV3.Enabled() {
mk := make([]string, len(ret))
for i, v := range ret {
mk[i] = v["Name"]
}
mkMsg := strings.Join(mk, ", ")
if klogV4 := klog.V(4); klogV4.Enabled() {
k := make([]string, 0, len(keys))
for n := range keys {
k = append(k, n)
}
sort.Strings(k)
klogV3.InfoS("matched names", "matchResult", mkMsg, "matchOp", m.Op, "matchValue", m.Value, "inputKeys", k)
} else {
klogV3.InfoS("matched names", "matchResult", mkMsg, "matchOp", m.Op, "matchValue", m.Value)
}
}
return len(ret) > 0, ret, nil
}

View file

@ -179,39 +179,24 @@ func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1
var isMatch = true
var matchedElems []MatchedElement
var err error
if f, ok := features.Flags[featureName]; ok {
if term.MatchExpressions != nil {
isMatch, matchedElems, err = MatchGetKeys(term.MatchExpressions, f.Elements)
}
var meTmp []MatchedElement
if err == nil && isMatch && term.MatchName != nil {
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 = MatchGetValues(term.MatchExpressions, f.Elements)
}
var meTmp []MatchedElement
if err == nil && isMatch && term.MatchName != nil {
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 = MatchGetInstances(term.MatchExpressions, f.Elements)
isMatch = len(matchedElems) > 0
}
var meTmp []MatchedElement
if err == nil && isMatch && term.MatchName != nil {
meTmp, err = MatchInstanceAttributeNames(term.MatchName, f.Elements)
isMatch = len(meTmp) > 0
matchedElems = append(matchedElems, meTmp...)
}
} else {
fF, okF := features.Flags[featureName]
fA, okA := features.Attributes[featureName]
fI, okI := features.Instances[featureName]
if !okF && !okA && !okI {
return false, nil, fmt.Errorf("feature %q not available", featureName)
}
if term.MatchExpressions != nil {
isMatch, matchedElems, err = MatchMulti(term.MatchExpressions, fF.Elements, fA.Elements, fI.Elements)
}
if err == nil && isMatch && term.MatchName != nil {
var meTmp []MatchedElement
isMatch, meTmp, err = MatchNamesMulti(term.MatchName, fF.Elements, fA.Elements, fI.Elements)
matchedElems = append(matchedElems, meTmp...)
}
matches[dom][nam] = append(matches[dom][nam], matchedElems...)
if err != nil {

View file

@ -133,7 +133,7 @@ func TestRule(t *testing.T) {
assert.Equal(t, r3.Labels, m.Labels, "values should have matched")
// Match "instance" features
r4 := &nfdv1alpha1.Rule{
r3 = &nfdv1alpha1.Rule{
Labels: map[string]string{"label-4": "label-val-4"},
MatchFeatures: nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
@ -144,17 +144,90 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r4, f)
m, err = Execute(r3, 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 = Execute(r4, f)
m, err = Execute(r3, f)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r4.Labels, m.Labels, "instances should have matched")
assert.Equal(t, r3.Labels, m.Labels, "instances should have matched")
// Match "multi-type" features
f2 := nfdv1alpha1.NewFeatures()
f2.Flags["dom.feat"] = nfdv1alpha1.NewFlagFeatures("k-1", "k-2")
f2.Attributes["dom.feat"] = nfdv1alpha1.NewAttributeFeatures(map[string]string{"a-1": "v-1", "a-2": "v-2"})
f2.Instances["dom.feat"] = nfdv1alpha1.NewInstanceFeatures(
*nfdv1alpha1.NewInstanceFeature(map[string]string{"ia-1": "iv-1"}),
*nfdv1alpha1.NewInstanceFeature(map[string]string{"ia-2": "iv-2"}),
)
r3 = &nfdv1alpha1.Rule{
Labels: map[string]string{"feat": "val-1"},
MatchFeatures: nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
Feature: "dom.feat",
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
"k-1": newMatchExpression(nfdv1alpha1.MatchExists),
},
},
},
}
m, err = Execute(r3, f2)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "key in multi-type feature should have matched")
r3.MatchFeatures = nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
Feature: "dom.feat",
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
"a-1": newMatchExpression(nfdv1alpha1.MatchIn, "v-1"),
},
},
}
m, err = Execute(r3, f2)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "attribute in multi-type feature should have matched")
r3.MatchFeatures = nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
Feature: "dom.feat",
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
"ia-1": newMatchExpression(nfdv1alpha1.MatchIn, "iv-1"),
},
},
}
m, err = Execute(r3, f2)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "attribute in multi-type feature should have matched")
r3.MatchFeatures = nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
Feature: "dom.feat",
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
"k-2": newMatchExpression(nfdv1alpha1.MatchExists),
"a-2": newMatchExpression(nfdv1alpha1.MatchIn, "v-2"),
},
},
}
m, err = Execute(r3, f2)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "features in multi-type feature should have matched flags and attributes")
r3.MatchFeatures = nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
Feature: "dom.feat",
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
"ia-2": newMatchExpression(nfdv1alpha1.MatchIn, "iv-2"),
},
},
}
m, err = Execute(r3, f2)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r3.Labels, m.Labels, "features in multi-type feature should have matched instance")
// Test multiple feature matchers
r5 := &nfdv1alpha1.Rule{
r3 = &nfdv1alpha1.Rule{
Labels: map[string]string{"label-5": "label-val-5"},
MatchFeatures: nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
@ -171,17 +244,17 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r5, f)
m, err = Execute(r3, 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(nfdv1alpha1.MatchIn, "val-1")
m, err = Execute(r5, f)
(*r3.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1")
m, err = Execute(r3, f)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
assert.Equal(t, r3.Labels, m.Labels, "instances should have matched")
// Test MatchAny
r5.MatchAny = []nfdv1alpha1.MatchAnyElem{
r3.MatchAny = []nfdv1alpha1.MatchAnyElem{
{
MatchFeatures: nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
@ -193,11 +266,11 @@ func TestRule(t *testing.T) {
},
},
}
m, err = Execute(r5, f)
m, err = Execute(r3, f)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Nil(t, m.Labels, "instances should not have matched")
r5.MatchAny = append(r5.MatchAny,
r3.MatchAny = append(r3.MatchAny,
nfdv1alpha1.MatchAnyElem{
MatchFeatures: nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
@ -208,10 +281,10 @@ func TestRule(t *testing.T) {
},
},
})
(*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1")
m, err = Execute(r5, f)
(*r3.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1")
m, err = Execute(r3, f)
assert.Nilf(t, err, "unexpected error: %v", err)
assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
assert.Equal(t, r3.Labels, m.Labels, "instances should have matched")
}
func TestTemplating(t *testing.T) {
@ -224,6 +297,13 @@ func TestTemplating(t *testing.T) {
"key-c": {},
},
},
"domain.mf": {
Elements: map[string]nfdv1alpha1.Nil{
"key-a": {},
"key-b": {},
"key-c": {},
},
},
},
Attributes: map[string]nfdv1alpha1.AttributeFeatureSet{
"domain_1.vf_1": {
@ -234,6 +314,12 @@ func TestTemplating(t *testing.T) {
"key-4": "val-4",
},
},
"domain.mf": {
Elements: map[string]string{
"key-d": "val-d",
"key-e": "val-e",
},
},
},
Instances: map[string]nfdv1alpha1.InstanceFeatureSet{
"domain_1.if_1": {
@ -357,6 +443,32 @@ var-2=
assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
assert.Equal(t, expectedVars, m.Vars, "instances should have matched")
// Test "multi-type" feature
r3 = &nfdv1alpha1.Rule{
LabelsTemplate: `
{{range .domain.mf}}mf-{{.Name}}=found
{{end}}`,
MatchFeatures: nfdv1alpha1.FeatureMatcher{
nfdv1alpha1.FeatureMatcherTerm{
Feature: "domain.mf",
MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
"key-a": newMatchExpression(nfdv1alpha1.MatchExists),
"key-d": newMatchExpression(nfdv1alpha1.MatchIn, "val-d"),
},
},
},
}
expectedLabels = map[string]string{
"mf-key-a": "found",
"mf-key-d": "found",
}
expectedVars = map[string]string{}
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")
//
// Test error cases
//