diff --git a/api/nfd/v1alpha1/feature.go b/api/nfd/v1alpha1/feature.go index 22915378a..1f4f1d074 100644 --- a/api/nfd/v1alpha1/feature.go +++ b/api/nfd/v1alpha1/feature.go @@ -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) { diff --git a/api/nfd/v1alpha1/feature_test.go b/api/nfd/v1alpha1/feature_test.go index b432d3a7f..e73c4d5db 100644 --- a/api/nfd/v1alpha1/feature_test.go +++ b/api/nfd/v1alpha1/feature_test.go @@ -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"}) diff --git a/docs/usage/customization-guide.md b/docs/usage/customization-guide.md index a7b46f791..810c5b4ab 100644 --- a/docs/usage/customization-guide.md +++ b/docs/usage/customization-guide.md @@ -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 @@ -955,7 +955,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 is present | diff --git a/pkg/apis/nfd/nodefeaturerule/expression-api_test.go b/pkg/apis/nfd/nodefeaturerule/expression-api_test.go index 9e63b8737..ba15e2d94 100644 --- a/pkg/apis/nfd/nodefeaturerule/expression-api_test.go +++ b/pkg/apis/nfd/nodefeaturerule/expression-api_test.go @@ -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) + }) + } +} diff --git a/pkg/apis/nfd/nodefeaturerule/expression.go b/pkg/apis/nfd/nodefeaturerule/expression.go index be7ca3bbb..585ce2e1c 100644 --- a/pkg/apis/nfd/nodefeaturerule/expression.go +++ b/pkg/apis/nfd/nodefeaturerule/expression.go @@ -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 } diff --git a/pkg/apis/nfd/nodefeaturerule/rule.go b/pkg/apis/nfd/nodefeaturerule/rule.go index 5a689da5d..b9a562924 100644 --- a/pkg/apis/nfd/nodefeaturerule/rule.go +++ b/pkg/apis/nfd/nodefeaturerule/rule.go @@ -214,39 +214,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 { diff --git a/pkg/apis/nfd/nodefeaturerule/rule_test.go b/pkg/apis/nfd/nodefeaturerule/rule_test.go index 773e72efe..036ad23e9 100644 --- a/pkg/apis/nfd/nodefeaturerule/rule_test.go +++ b/pkg/apis/nfd/nodefeaturerule/rule_test.go @@ -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 // diff --git a/source/source.go b/source/source.go index 06cdc5076..24b27b4ff 100644 --- a/source/source.go +++ b/source/source.go @@ -164,25 +164,16 @@ func GetAllFeatures() *nfdv1alpha1.Features { for k, v := range f.Flags { // Prefix feature with the name of the source k = n + "." + k - if typ := features.Exists(k); typ != "" { - panic(fmt.Sprintf("feature source %q returned flag feature %q which already exists (type %q)", n, k, typ)) - } features.Flags[k] = v } for k, v := range f.Attributes { // Prefix feature with the name of the source k = n + "." + k - if typ := features.Exists(k); typ != "" { - panic(fmt.Sprintf("feature source %q returned attribute feature %q which already exists (type %q)", n, k, typ)) - } features.Attributes[k] = v } for k, v := range f.Instances { // Prefix feature with the name of the source k = n + "." + k - if typ := features.Exists(k); typ != "" { - panic(fmt.Sprintf("feature source %q returned instance feature %q which already exists (type %q)", n, k, typ)) - } features.Instances[k] = v } }