diff --git a/deployment/base/nfd-crds/cr-sample.yaml b/deployment/base/nfd-crds/cr-sample.yaml index 539757209b..1c89a92ea9 100644 --- a/deployment/base/nfd-crds/cr-sample.yaml +++ b/deployment/base/nfd-crds/cr-sample.yaml @@ -94,6 +94,13 @@ spec: vendor: {op: In, value: ["8086"]} class: {op: In, value: ["02"]} + - name: "avx wildcard rule" + labels: + "my-avx-feature": "true" + matchFeatures: + - feature: cpu.cpuid + matchName: {op: InRegexp, value: ["^AVX512"]} + # The following features demonstreate label templating capabilities - name: "my system template feature" labelsTemplate: | @@ -143,3 +150,11 @@ spec: matchExpressions: my.kernel.feature: {op: IsTrue} my.dummy.var: {op: Gt, value: ["0"]} + + - name: "kconfig template rule" + labelsTemplate: | + {{ range .kernel.config }}kconfig-{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: kernel.config + matchName: {op: In, value: ["SWAP", "X86", "ARM"]} diff --git a/deployment/base/nfd-crds/nfd-api-crds.yaml b/deployment/base/nfd-crds/nfd-api-crds.yaml index 6975a7f97f..4e63041630 100644 --- a/deployment/base/nfd-crds/nfd-api-crds.yaml +++ b/deployment/base/nfd-crds/nfd-api-crds.yaml @@ -190,6 +190,8 @@ spec: in the feature set. properties: feature: + description: Feature is the name of the feature + set to match against. type: string matchExpressions: additionalProperties: @@ -229,13 +231,46 @@ spec: required: - op type: object - description: MatchExpressionSet contains a set of - MatchExpressions, each of which is evaluated against - a set of input values. + description: MatchExpressions is the set of per-element + expressions evaluated. These match against the + value of the specified elements. + type: object + matchName: + description: MatchName in an expression that is + matched against the name of each element in the + feature set. + properties: + op: + description: Op is the operator to be applied. + enum: + - In + - NotIn + - InRegexp + - Exists + - DoesNotExist + - Gt + - Lt + - GtLt + - IsTrue + - IsFalse + type: string + value: + description: Value is the list of values that + the operand evaluates the input against. Value + should be empty if the operator is Exists, + DoesNotExist, IsTrue or IsFalse. Value should + contain exactly one element if the operator + is Gt or Lt and exactly two elements if the + operator is GtLt. In other cases Value should + contain at least one element. + items: + type: string + type: array + required: + - op type: object required: - feature - - matchExpressions type: object type: array required: @@ -251,6 +286,8 @@ spec: are evaluated against each element in the feature set. properties: feature: + description: Feature is the name of the feature set to + match against. type: string matchExpressions: additionalProperties: @@ -288,12 +325,44 @@ spec: required: - op type: object - description: MatchExpressionSet contains a set of MatchExpressions, - each of which is evaluated against a set of input values. + description: MatchExpressions is the set of per-element + expressions evaluated. These match against the value + of the specified elements. + type: object + matchName: + description: MatchName in an expression that is matched + against the name of each element in the feature set. + properties: + op: + description: Op is the operator to be applied. + enum: + - In + - NotIn + - InRegexp + - Exists + - DoesNotExist + - Gt + - Lt + - GtLt + - IsTrue + - IsFalse + type: string + value: + description: Value is the list of values that the + operand evaluates the input against. Value should + be empty if the operator is Exists, DoesNotExist, + IsTrue or IsFalse. Value should contain exactly + one element if the operator is Gt or Lt and exactly + two elements if the operator is GtLt. In other cases + Value should contain at least one element. + items: + type: string + type: array + required: + - op type: object required: - feature - - matchExpressions type: object type: array name: diff --git a/deployment/components/worker-config/nfd-worker.conf.example b/deployment/components/worker-config/nfd-worker.conf.example index f1450f1688..9a3bfd85b7 100644 --- a/deployment/components/worker-config/nfd-worker.conf.example +++ b/deployment/components/worker-config/nfd-worker.conf.example @@ -174,6 +174,13 @@ # vendor: {op: In, value: ["8086"]} # class: {op: In, value: ["02"]} # +# - name: "avx wildcard rule" +# labels: +# "my-avx-feature": "true" +# matchFeatures: +# - feature: cpu.cpuid +# matchName: {op: InRegexp, value: ["^AVX512"]} +# # # The following features demonstreate label templating capabilities # - name: "my template rule" # labelsTemplate: | @@ -224,3 +231,10 @@ # vendor.io/my.kernel.feature: {op: IsTrue} # my.dummy.var: {op: Gt, value: ["0"]} # +# - name: "kconfig template rule" +# labelsTemplate: | +# {{ range .kernel.config }}kconfig-{{ .Name }}={{ .Value }} +# {{ end }} +# matchFeatures: +# - feature: kernel.config +# matchName: {op: In, value: ["SWAP", "X86", "ARM"]} diff --git a/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml index 6975a7f97f..4e63041630 100644 --- a/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml +++ b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml @@ -190,6 +190,8 @@ spec: in the feature set. properties: feature: + description: Feature is the name of the feature + set to match against. type: string matchExpressions: additionalProperties: @@ -229,13 +231,46 @@ spec: required: - op type: object - description: MatchExpressionSet contains a set of - MatchExpressions, each of which is evaluated against - a set of input values. + description: MatchExpressions is the set of per-element + expressions evaluated. These match against the + value of the specified elements. + type: object + matchName: + description: MatchName in an expression that is + matched against the name of each element in the + feature set. + properties: + op: + description: Op is the operator to be applied. + enum: + - In + - NotIn + - InRegexp + - Exists + - DoesNotExist + - Gt + - Lt + - GtLt + - IsTrue + - IsFalse + type: string + value: + description: Value is the list of values that + the operand evaluates the input against. Value + should be empty if the operator is Exists, + DoesNotExist, IsTrue or IsFalse. Value should + contain exactly one element if the operator + is Gt or Lt and exactly two elements if the + operator is GtLt. In other cases Value should + contain at least one element. + items: + type: string + type: array + required: + - op type: object required: - feature - - matchExpressions type: object type: array required: @@ -251,6 +286,8 @@ spec: are evaluated against each element in the feature set. properties: feature: + description: Feature is the name of the feature set to + match against. type: string matchExpressions: additionalProperties: @@ -288,12 +325,44 @@ spec: required: - op type: object - description: MatchExpressionSet contains a set of MatchExpressions, - each of which is evaluated against a set of input values. + description: MatchExpressions is the set of per-element + expressions evaluated. These match against the value + of the specified elements. + type: object + matchName: + description: MatchName in an expression that is matched + against the name of each element in the feature set. + properties: + op: + description: Op is the operator to be applied. + enum: + - In + - NotIn + - InRegexp + - Exists + - DoesNotExist + - Gt + - Lt + - GtLt + - IsTrue + - IsFalse + type: string + value: + description: Value is the list of values that the + operand evaluates the input against. Value should + be empty if the operator is Exists, DoesNotExist, + IsTrue or IsFalse. Value should contain exactly + one element if the operator is Gt or Lt and exactly + two elements if the operator is GtLt. In other cases + Value should contain at least one element. + items: + type: string + type: array + required: + - op type: object required: - feature - - matchExpressions type: object type: array name: diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml index 66bd39787b..8cfaf7f861 100644 --- a/deployment/helm/node-feature-discovery/values.yaml +++ b/deployment/helm/node-feature-discovery/values.yaml @@ -312,6 +312,13 @@ worker: # vendor: {op: In, value: ["8086"]} # class: {op: In, value: ["02"]} # + # - name: "avx wildcard rule" + # labels: + # "my-avx-feature": "true" + # matchFeatures: + # - feature: cpu.cpuid + # matchName: {op: InRegexp, value: ["^AVX512"]} + # # # The following features demonstreate label templating capabilities # - name: "my template rule" # labelsTemplate: | @@ -362,6 +369,13 @@ worker: # vendor.io/my.kernel.feature: {op: IsTrue} # my.dummy.var: {op: Gt, value: ["0"]} # + # - name: "kconfig template rule" + # labelsTemplate: | + # {{ range .kernel.config }}kconfig-{{ .Name }}={{ .Value }} + # {{ end }} + # matchFeatures: + # - feature: kernel.config + # matchName: {op: In, value: ["SWAP", "X86", "ARM"]} ### metricsPort: 8081 diff --git a/docs/usage/customization-guide.md b/docs/usage/customization-guide.md index 52aa1aa23b..8f3652052a 100644 --- a/docs/usage/customization-guide.md +++ b/docs/usage/customization-guide.md @@ -761,16 +761,46 @@ of them must match for the rule to trigger. value: - - ... + matchName: + op: + value: + - + - ... +``` + +The `.matchFeatures[].feature` field specifies the feature which to evaluate. + +> **NOTE:**If both [`matchExpressions`](#matchexpressions) and +> [`matchName`](#matchname) are specified, they both must match. + +##### matchExpressions + +The `.matchFeatures[].matchExpressions` field is used to match against the +value(s) of a feature. The `matchExpressions` field consists of a set of +expressions, each of which is evaluated against all elements of the specified +feature. + +```yaml + matchExpressions: + : + op: + value: + - + - ... ``` -The `.matchFeatures[].feature` field specifies the feature against which to -match. +In each MatchExpression the `key` specifies the name of of the feature element +(*flag* and *attribute* features) or name of the attribute (*instance* +features) which to look for. The behavior of MatchExpression depends on the +[feature type](#feature-types): -The `.matchFeatures[].matchExpressions` field specifies a map of expressions -which to evaluate against the elements of the feature. +- for *flag* and *attribute* features the MatchExpression operates on the + feature element whose name matches the `` +- for *instance* features all MatchExpressions are evaluated against the + attributes of each instance separately -In each MatchExpression `op` specifies the operator to apply. Valid values are -described below. +The `op` field specifies the operator to apply. Valid values are described +below. | Operator | Number of values | Matches when | | --------------- | ---------------- | ----------- | @@ -788,11 +818,57 @@ described below. The `value` field of MatchExpression is a list of string arguments to the operator. -The behavior of MatchExpression depends on the [feature type](#feature-types): -for *flag* and *attribute* features the MatchExpression operates on the feature -element whose name matches the ``. However, for *instance* features all -MatchExpressions are evaluated against the attributes of each instance -separately. +##### matchName + +The `.matchFeatures[].matchName` field is used to match against the +name(s) of a feature (whereas the [`matchExpressions`](#matchexpressions) field +matches against the value(s). The `matchName` field consists of a single +expression which is evaulated against the name of each element of the specified +feature. + +```yaml + matchName: + op: + value: + - + - ... +``` + +The behavior of `matchName` depends on the [feature type](#feature-types): + +- for *flag* and *attribute* features the expression is evaluated against the + name of each element +- for *instance* features the expression is evaluated against the name of + each attribute, for each element (instance) separately (matches if the + attributes of any of the elements satisfy the expression) + +The `op` field specifies the operator to apply. Same operators as for +[`matchExpressions`](#matchexpressions) above are available. + +| Operator | Number of values | Matches | +| --------------- | ---------------- | ----------- | +| `In` | 1 or greater | All name is equal to one of the values | +| `NotIn` | 1 or greater | All name that is not equal to any of the values | +| `InRegexp` | 1 or greater | All name that matches any of the values (treated as regexps) | +| `Exists` | 0 | All elements | + +Other operators are not practical with `matchName` (`DoesNotExist` never +matches; `Gt`,`Lt` and `GtLt` are only usable if feature names are integers; +`IsTrue` and `IsFalse` are only usable if the feature name is `true` or +`false`). + +The `value` field is a list of string arguments to the operator. + +An example: + +```yaml + matchFeatures: + - feature: cpu.cpuid + matchName: {op: InRegexp, value: ["^AVX"]} +``` + +The snippet above would match if any CPUID feature starting with AVX is present +(e.g. AVX1 or AVX2 or AVX512F etc). #### matchAny @@ -992,6 +1068,10 @@ feature: ``` +> **NOTE:**If both `matchExpressions` and `matchName` for a feature matcher +> term (see [`matchFeatures`](#matchfeatures)) is specified, the list of +> matched features (for the template engine) is the union from both of these. + > **NOTE:** In case of matchAny is specified, the template is executed > separately against each individual `matchFeatures` field and the final set of > labels will be superset of all these separate template expansions. E.g. diff --git a/pkg/apis/nfd/v1alpha1/expression.go b/pkg/apis/nfd/v1alpha1/expression.go index 3b85005a81..b7fce66052 100644 --- a/pkg/apis/nfd/v1alpha1/expression.go +++ b/pkg/apis/nfd/v1alpha1/expression.go @@ -258,6 +258,88 @@ func (m *MatchExpression) MatchValues(name string, values map[string]string) (bo return matched, nil } +// MatchKeyNames evaluates the MatchExpression against names of a set of key features. +func (m *MatchExpression) MatchKeyNames(keys map[string]Nil) (bool, []MatchedElement, error) { + ret := []MatchedElement{} + + for k := range keys { + if match, err := m.Match(true, k); err != nil { + return false, nil, err + } else if match { + ret = append(ret, MatchedElement{"Name": k}) + } + } + // Sort for reproducible output + sort.Slice(ret, func(i, j int) bool { return ret[i]["Name"] < ret[j]["Name"] }) + + 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 (key) names", "matchResult", mkMsg, "matchOp", m.Op, "matchValue", m.Value, "inputKeys", k) + } else { + klogV3.InfoS("matched (key) names", "matchResult", mkMsg, "matchOp", m.Op, "matchValue", m.Value) + } + } + + return len(ret) > 0, ret, nil +} + +// MatchValueNames evaluates the MatchExpression against names of a set of value features. +func (m *MatchExpression) MatchValueNames(values map[string]string) (bool, []MatchedElement, error) { + ret := []MatchedElement{} + + for k, v := range values { + if match, err := m.Match(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"] }) + + 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() { + klogV3.InfoS("matched (value) names", "matchResult", mkMsg, "matchOp", m.Op, "matchValue", m.Value, "inputValues", values) + } else { + klogV3.InfoS("matched (value) names", "matchResult", mkMsg, "matchOp", m.Op, "matchValue", m.Value) + } + } + + return len(ret) > 0, ret, nil +} + +// 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) { + ret := []MatchedElement{} + + for _, i := range instances { + if match, _, err := m.MatchValueNames(i.Attributes); err != nil { + return nil, err + } else if match { + ret = append(ret, i.Attributes) + } + } + return ret, nil +} + // matchExpression is a helper type for unmarshalling MatchExpression type matchExpression MatchExpression diff --git a/pkg/apis/nfd/v1alpha1/rule.go b/pkg/apis/nfd/v1alpha1/rule.go index 7aeac42d84..c3c14bdc29 100644 --- a/pkg/apis/nfd/v1alpha1/rule.go +++ b/pkg/apis/nfd/v1alpha1/rule.go @@ -180,16 +180,39 @@ func (m *FeatureMatcher) match(features *Features) (bool, matchedFeatures, error matches[dom] = make(domainMatchedFeatures) } - var isMatch bool + var isMatch = true var matchedElems []MatchedElement var err error if f, ok := features.Flags[featureName]; ok { - isMatch, matchedElems, err = term.MatchExpressions.MatchGetKeys(f.Elements) + if term.MatchExpressions != nil { + isMatch, matchedElems, err = term.MatchExpressions.MatchGetKeys(f.Elements) + } + var meTmp []MatchedElement + if err == nil && isMatch && term.MatchName != nil { + isMatch, meTmp, err = term.MatchName.MatchKeyNames(f.Elements) + matchedElems = append(matchedElems, meTmp...) + } } else if f, ok := features.Attributes[featureName]; ok { - isMatch, matchedElems, err = term.MatchExpressions.MatchGetValues(f.Elements) + if term.MatchExpressions != nil { + isMatch, matchedElems, err = term.MatchExpressions.MatchGetValues(f.Elements) + } + var meTmp []MatchedElement + if err == nil && isMatch && term.MatchName != nil { + isMatch, meTmp, err = term.MatchName.MatchValueNames(f.Elements) + matchedElems = append(matchedElems, meTmp...) + } } else if f, ok := features.Instances[featureName]; ok { - matchedElems, err = term.MatchExpressions.MatchGetInstances(f.Elements) - isMatch = len(matchedElems) > 0 + if term.MatchExpressions != nil { + matchedElems, err = term.MatchExpressions.MatchGetInstances(f.Elements) + isMatch = len(matchedElems) > 0 + } + var meTmp []MatchedElement + if err == nil && isMatch && term.MatchName != nil { + meTmp, err = term.MatchName.MatchInstanceAttributeNames(f.Elements) + isMatch = len(meTmp) > 0 + matchedElems = append(matchedElems, meTmp...) + + } } else { return false, nil, fmt.Errorf("feature %q not available", featureName) } diff --git a/pkg/apis/nfd/v1alpha1/rule_test.go b/pkg/apis/nfd/v1alpha1/rule_test.go index 8608df99a3..b6ee6ec7ed 100644 --- a/pkg/apis/nfd/v1alpha1/rule_test.go +++ b/pkg/apis/nfd/v1alpha1/rule_test.go @@ -31,7 +31,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": newMatchExpression(MatchExists), }, }, @@ -84,7 +84,7 @@ func TestRule(t *testing.T) { r1.MatchFeatures = FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{}, + MatchExpressions: &MatchExpressionSet{}, }, } m, err = r1.Execute(f) @@ -108,7 +108,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": newMatchExpression(MatchIn, "val-1"), }, }, @@ -129,7 +129,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "attr-1": newMatchExpression(MatchIn, "val-1"), }, }, @@ -150,13 +150,13 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": newMatchExpression(MatchIn, "val-x"), }, }, FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "attr-1": newMatchExpression(MatchIn, "val-1"), }, }, @@ -166,7 +166,7 @@ func TestRule(t *testing.T) { 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") + (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(MatchIn, "val-1") m, err = r5.Execute(f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r5.Labels, m.Labels, "instances should have matched") @@ -177,7 +177,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-na": newMatchExpression(MatchExists), }, }, @@ -193,13 +193,13 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": newMatchExpression(MatchExists), }, }, }, }) - r5.MatchFeatures[0].MatchExpressions["key-1"] = newMatchExpression(MatchIn, "val-1") + (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(MatchIn, "val-1") m, err = r5.Execute(f) assert.Nilf(t, err, "unexpected error: %v", err) assert.Equal(t, r5.Labels, m.Labels, "instances should have matched") @@ -222,6 +222,7 @@ func TestTemplating(t *testing.T) { "key-1": "val-1", "keu-2": "val-2", "key-3": "val-3", + "key-4": "val-4", }, }, }, @@ -278,7 +279,7 @@ var-2= MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain_1.kf_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-a": newMatchExpression(MatchExists), "key-c": newMatchExpression(MatchExists), "foo": newMatchExpression(MatchDoesNotExist), @@ -286,20 +287,20 @@ var-2= }, FeatureMatcherTerm{ Feature: "domain_1.vf_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": newMatchExpression(MatchIn, "val-1", "val-2"), "bar": newMatchExpression(MatchDoesNotExist), }, }, FeatureMatcherTerm{ Feature: "domain_1.if_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "attr-1": newMatchExpression(MatchLt, "100"), }, }, FeatureMatcherTerm{ Feature: "domain_1.if_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "attr-1": newMatchExpression(MatchExists), "attr-2": newMatchExpression(MatchExists), "attr-3": newMatchExpression(MatchExists), @@ -356,7 +357,7 @@ var-2= // Use a simple empty matchexpression set to match anything. FeatureMatcherTerm{ Feature: "domain_1.kf_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-a": newMatchExpression(MatchExists), }, }, @@ -397,4 +398,42 @@ var-2= _, err = r2.Execute(f) assert.Error(t, err) + // + // Test matchName + // + r4 := Rule{ + LabelsTemplate: "{{range .domain_1.vf_1}}{{.Name}}={{.Value}}\n{{end}}", + MatchFeatures: FeatureMatcher{ + FeatureMatcherTerm{ + Feature: "domain_1.vf_1", + MatchExpressions: &MatchExpressionSet{ + "key-5": newMatchExpression(MatchDoesNotExist), + }, + MatchName: newMatchExpression(MatchIn, "key-1", "key-4"), + }, + }, + } + expectedLabels = map[string]string{ + "key-1": "val-1", + "key-4": "val-4", + "key-5": "", + } + + m, err = r4.Execute(f) + assert.Nilf(t, err, "unexpected error: %v", err) + assert.Equal(t, expectedLabels, m.Labels, "instances should have matched") + + r4 = Rule{ + Labels: map[string]string{"should-not-match": "true"}, + MatchFeatures: FeatureMatcher{ + FeatureMatcherTerm{ + Feature: "domain_1.vf_1", + MatchName: newMatchExpression(MatchIn, "key-not-exists"), + }, + }, + } + + m, err = r4.Execute(f) + assert.Nilf(t, err, "unexpected error: %v", err) + assert.Equal(t, map[string]string(nil), m.Labels, "instances should have matched") } diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go index a9c8bcbcbe..9c8f2b92d7 100644 --- a/pkg/apis/nfd/v1alpha1/types.go +++ b/pkg/apis/nfd/v1alpha1/types.go @@ -198,8 +198,16 @@ type FeatureMatcher []FeatureMatcherTerm // requirements (specified as MatchExpressions) are evaluated against each // element in the feature set. type FeatureMatcherTerm struct { - Feature string `json:"feature"` - MatchExpressions MatchExpressionSet `json:"matchExpressions"` + // Feature is the name of the feature set to match against. + Feature string `json:"feature"` + // MatchExpressions is the set of per-element expressions evaluated. These + // match against the value of the specified elements. + // +optional + MatchExpressions *MatchExpressionSet `json:"matchExpressions"` + // MatchName in an expression that is matched against the name of each + // element in the feature set. + // +optional + MatchName *MatchExpression `json:"matchName"` } // MatchExpressionSet contains a set of MatchExpressions, each of which is @@ -279,3 +287,7 @@ const ( // output of preceding rules. RuleBackrefFeature = "matched" ) + +// MatchAllNames is a special key in MatchExpressionSet to use field names +// (keys from the input) instead of values when matching. +const MatchAllNames = "*" diff --git a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go index 60335468c5..1ba7ea4623 100644 --- a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go @@ -58,19 +58,28 @@ func (in *FeatureMatcherTerm) DeepCopyInto(out *FeatureMatcherTerm) { *out = *in if in.MatchExpressions != nil { in, out := &in.MatchExpressions, &out.MatchExpressions - *out = make(MatchExpressionSet, len(*in)) - for key, val := range *in { - var outVal *MatchExpression - if val == nil { - (*out)[key] = nil - } else { - in, out := &val, &outVal - *out = new(MatchExpression) - (*in).DeepCopyInto(*out) + *out = new(MatchExpressionSet) + if **in != nil { + in, out := *in, *out + *out = make(map[string]*MatchExpression, len(*in)) + for key, val := range *in { + var outVal *MatchExpression + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(MatchExpression) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal } - (*out)[key] = outVal } } + if in.MatchName != nil { + in, out := &in.MatchName, &out.MatchName + *out = new(MatchExpression) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureMatcherTerm. diff --git a/samples/nodefeaturerule-cpu.yaml b/samples/nodefeaturerule-cpu.yaml new file mode 100644 index 0000000000..22ace7b5a5 --- /dev/null +++ b/samples/nodefeaturerule-cpu.yaml @@ -0,0 +1,112 @@ +# +# This NodeFeatureRule replicates all built-in cpu feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-cpu-features +spec: + rules: + - name: "nfd built-in cpu-cpuid labels" + labelsTemplate: | + {{ range .cpu.cpuid }}cpu-cpuid.{{ .Name }}=true + {{ end }} + matchFeatures: + - feature: cpu.cpuid + matchName: + op: NotIn + value: + - "BMI1" + - "BMI2" + - "CLMUL" + - "CMOV" + - "CX16" + - "ERMS" + - "F16C" + - "HTT" + - "LZCNT" + - "MMX" + - "MMXEXT" + - "NX" + - "POPCNT" + - "RDRAND" + - "RDSEED" + - "RDTSCP" + - "SGX" + - "SGXLC" + - "SSE" + - "SSE2" + - "SSE3" + - "SSE4" + - "SSE42" + - "SSSE3" + + - name: "nfd built-in cpu-hardware_multithreading label" + labelsTemplate: | + {{ range .cpu.topology }}cpu-{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: cpu.topology + matchName: + op: In + value: + - "hardware_multithreading" + + - name: "nfd built-in cpu-cstate and cpu-pstate labels" + labelsTemplate: | + {{ range .cpu.cstate }}cpu-cstate.{{ .Name }}={{ .Value }} + {{ end }} + {{ range .cpu.pstate }}cpu-pstate.{{ .Name }}={{ .Value }} + {{ end }} + matchAny: + - matchFeatures: + - feature: cpu.cstate + matchName: + op: Exists + - matchFeatures: + - feature: cpu.pstate + matchName: + op: Exists + + - name: "nfd built-in cpu-model labels" + labelsTemplate: | + {{ range .cpu.model }}cpu-model.{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: cpu.model + matchName: + op: Exists + + - name: "nfd built-in cpu-security labels" + labelsTemplate: | + {{ range .cpu.security }}cpu-security.{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: cpu.security + matchName: + op: NotIn + value: + - "tdx.total_keys" + - "sgx.epc" + - "sev.encrypted_state_ids" + - "sev.asids" + + - name: "nfd built-in cpu-sst labels" + labelsTemplate: | + {{ range .cpu.sst }}cpu-power.sst_{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: cpu.sst + matchName: + op: Exists + + - name: "nfd built-in cpu-coprocessor labels" + labelsTemplate: | + {{ range .cpu.sst }}cpu-coprocessor.{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: cpu.coprocessor + matchName: + op: In + value: + - "nx_gzip" diff --git a/samples/nodefeaturerule-custom.yaml b/samples/nodefeaturerule-custom.yaml new file mode 100644 index 0000000000..90427d59f4 --- /dev/null +++ b/samples/nodefeaturerule-custom.yaml @@ -0,0 +1,29 @@ +# +# This NodeFeatureRule replicates all built-in static custom feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-custom-features +spec: + rules: + - name: "nfd built-in static custom rdma.capable label" + labels: + "custom-rdma.capable": "true" + matchFeatures: + - feature: pci.device + matchExpressions: + vendor: + op: In + value: ["15b3"] + + - name: "nfd built-in static custom rdma.available label" + labels: + "custom-rdma.available": "true" + matchFeatures: + - feature: kernel.loadedmodule + matchExpressions: + "ib_uverbs": + op: Exists + "rdma_ucm": + op: Exists diff --git a/samples/nodefeaturerule-kernel.yaml b/samples/nodefeaturerule-kernel.yaml new file mode 100644 index 0000000000..b526b7fc40 --- /dev/null +++ b/samples/nodefeaturerule-kernel.yaml @@ -0,0 +1,38 @@ +# +# This NodeFeatureRule replicates all built-in kernel feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-kernel-features +spec: + rules: + - name: "nfd built-in kernel-version labels" + labelsTemplate: | + {{ range .kernel.version }}kernel-version.{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: kernel.version + matchName: + op: Exists + + - name: "nfd built-in kernel-config labels" + labelsTemplate: | + {{ range .kernel.config }}kernel-config.{{ .Name }}=true + {{ end }} + matchFeatures: + - feature: kernel.config + matchExpressions: + "NO_HZ": {op: In, value: ["y"]} + "NO_HZ_IDLE": {op: In, value: ["y"]} + "NO_HZ_FULL": {op: In, value: ["y"]} + "PREEMPT": {op: In, value: ["y"]} + + - name: "nfd built-in kernel-selinux labels" + labels: + "kernel-selinux.enabled": "true" + matchFeatures: + - feature: kernel.selinux + matchExpressions: + "enabled": + op: IsTrue diff --git a/samples/nodefeaturerule-local.yaml b/samples/nodefeaturerule-local.yaml new file mode 100644 index 0000000000..bc0fb998be --- /dev/null +++ b/samples/nodefeaturerule-local.yaml @@ -0,0 +1,17 @@ +# +# This NodeFeatureRule replicates all built-in local feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-local-features +spec: + rules: + - name: "nfd built-in labels from the local feature source" + labelsTemplate: | + {{ range .local.label }}{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: local.label + matchName: + op: Exists diff --git a/samples/nodefeaturerule-memory.yaml b/samples/nodefeaturerule-memory.yaml new file mode 100644 index 0000000000..110cfe96da --- /dev/null +++ b/samples/nodefeaturerule-memory.yaml @@ -0,0 +1,34 @@ +# +# This NodeFeatureRule replicates all built-in memory feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-memory-features +spec: + rules: + - name: "nfd built-in memory-numa labels" + labels: + "memory-numa": "true" + matchFeatures: + - feature: memory.numa + matchExpressions: + "is_numa": + op: IsTrue + + - name: "nfd built-in memory-nv.present label" + labelsTemplate: "{{ if gt (len .memory.nv ) 0 }}memory-nv.present=true{{ end }}" + matchFeatures: + - feature: memory.nv + matchName: + op: Exists + + - name: "nfd built-in memory-nv.dax label" + labels: + "memory.nv.dax": "true" + matchFeatures: + - feature: memory.nv + matchExpressions: + "devtype": + op: In + value: ["nd_dax"] diff --git a/samples/nodefeaturerule-network.yaml b/samples/nodefeaturerule-network.yaml new file mode 100644 index 0000000000..4ac0b9d1c0 --- /dev/null +++ b/samples/nodefeaturerule-network.yaml @@ -0,0 +1,28 @@ +# +# This NodeFeatureRule replicates all built-in networkfeature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-network-features +spec: + rules: + - name: "nfd built-in network-sriov.capable label" + labels: + "network-sriov.capable": "true" + matchFeatures: + - feature: network.device + matchExpressions: + "sriov_totalvfs": + op: Gt + value: ["0"] + + - name: "nfd built-in network-sriov.configured label" + labels: + "network-sriov.configured": "true" + matchFeatures: + - feature: network.device + matchExpressions: + "network-sriov_numvfs": + op: Gt + value: ["0"] diff --git a/samples/nodefeaturerule-pci.yaml b/samples/nodefeaturerule-pci.yaml new file mode 100644 index 0000000000..cbfdd02713 --- /dev/null +++ b/samples/nodefeaturerule-pci.yaml @@ -0,0 +1,32 @@ +# +# This NodeFeatureRule replicates all built-in pci feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-pci-features +spec: + rules: + - name: "nfd built-in pci-.present labels" + labelsTemplate: | + {{ range .pci.device }}pci-{{ .class }}_{{ .vendor }}.present=true + {{ end }} + matchFeatures: + - feature: pci.device + matchExpressions: + "class": + op: InRegexp + value: ["^03", "^0b40", "^12"] + + - name: "nfd built-in pci-.sriov.capable labels" + labelsTemplate: | + {{ range .pci.device }}pci-{{ .class }}_{{ .vendor }}.sriov.capable=true + {{ end }} + matchFeatures: + - feature: pci.device + matchExpressions: + "class": + op: InRegexp + value: ["^03", "^0b40", "^12"] + "sriov_totalvfs": + op: Exists diff --git a/samples/nodefeaturerule-storage.yaml b/samples/nodefeaturerule-storage.yaml new file mode 100644 index 0000000000..a97adddd56 --- /dev/null +++ b/samples/nodefeaturerule-storage.yaml @@ -0,0 +1,18 @@ +# +# This NodeFeatureRule replicates all built-in storage feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-storage-features +spec: + rules: + - name: "nfd built-in storage-nonrotationaldisk label" + labels: + "storage-nonrotationaldisk": "true" + matchFeatures: + - feature: storage.block + matchExpressions: + "rotational": + op: In + value: ["0"] diff --git a/samples/nodefeaturerule-system.yaml b/samples/nodefeaturerule-system.yaml new file mode 100644 index 0000000000..408fcbe0ff --- /dev/null +++ b/samples/nodefeaturerule-system.yaml @@ -0,0 +1,23 @@ +# +# This NodeFeatureRule replicates all built-in system feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-system-features +spec: + rules: + - name: "nfd built-in system-os_release labels" + labelsTemplate: | + {{ range .system.osrelease }}system-os_release.{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: system.osrelease + matchName: + op: In + value: + - "ID" + - "VERSION_ID" + - "VERSION_ID.major" + - "VERSION_ID.minor" + diff --git a/samples/nodefeaturerule-usb.yaml b/samples/nodefeaturerule-usb.yaml new file mode 100644 index 0000000000..9d56889f84 --- /dev/null +++ b/samples/nodefeaturerule-usb.yaml @@ -0,0 +1,19 @@ +# +# This NodeFeatureRule replicates all built-in usb feature labels of NFD. +# +apiVersion: nfd.k8s-sigs.io/v1alpha1 +kind: NodeFeatureRule +metadata: + name: nfd-builtin-usb-features +spec: + rules: + - name: "nfd built-in usb-.present labels" + labelsTemplate: | + {{ range .usb.device }}usb-{{ .class }}_{{ .vendor }}_{{ .device }}.present=true + {{ end }} + matchFeatures: + - feature: usb.device + matchExpressions: + "class": + op: In + value: ["0e", "ef", "fe", "ff"] diff --git a/source/custom/static_features.go b/source/custom/static_features.go index 6f3f458780..2c2d1ff05d 100644 --- a/source/custom/static_features.go +++ b/source/custom/static_features.go @@ -31,7 +31,7 @@ func getStaticFeatureConfig() []CustomRule { MatchFeatures: nfdv1alpha1.FeatureMatcher{ nfdv1alpha1.FeatureMatcherTerm{ Feature: "pci.device", - MatchExpressions: nfdv1alpha1.MatchExpressionSet{ + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ "vendor": &nfdv1alpha1.MatchExpression{ Op: nfdv1alpha1.MatchIn, Value: nfdv1alpha1.MatchValue{"15b3"}}, @@ -47,7 +47,7 @@ func getStaticFeatureConfig() []CustomRule { MatchFeatures: nfdv1alpha1.FeatureMatcher{ nfdv1alpha1.FeatureMatcherTerm{ Feature: "kernel.loadedmodule", - MatchExpressions: nfdv1alpha1.MatchExpressionSet{ + MatchExpressions: &nfdv1alpha1.MatchExpressionSet{ "ib_uverbs": &nfdv1alpha1.MatchExpression{ Op: nfdv1alpha1.MatchExists, }, diff --git a/test/e2e/data/nodefeaturerule-1.yaml b/test/e2e/data/nodefeaturerule-1.yaml index ea1f2f2fd1..02d6db60cd 100644 --- a/test/e2e/data/nodefeaturerule-1.yaml +++ b/test/e2e/data/nodefeaturerule-1.yaml @@ -17,6 +17,13 @@ spec: matchExpressions: "flag_1": {op: Exists} + - name: "e2e-flag-test-2" + labels: + e2e-flag-test-2: "true" + matchFeatures: + - feature: "fake.flag" + matchName: {op: In, value: ["flag_2"]} + # Negative test not supposed to create a label - name: "e2e-flag-test-neg-1" labels: @@ -26,6 +33,13 @@ spec: matchExpressions: "flag_1": {op: DoesNotExist} + - name: "e2e-flag-test-neg-2" + labels: + e2e-flag-test-neg-2: "true" + matchFeatures: + - feature: "fake.flag" + matchName: {op: In, value: ["flag_x"]} + # # Simple test rules for attribute features # @@ -40,6 +54,13 @@ spec: "attr_1": {op: IsTrue} "attr_2": {op: IsFalse} + - name: "e2e-attribute-test-2" + labels: + e2e-attribute-test-2: "true" + matchFeatures: + - feature: "fake.attribute" + matchName: {op: In, value: ["attr_2", "attr_x"]} + # Negative test not supposed to create a label - name: "e2e-attribute-test-neg-1" labels: @@ -50,8 +71,15 @@ spec: "attr_1": {op: IsTrue} "attr_2": {op: IsTrue} + - name: "e2e-attribute-test-neg-2" + labels: + e2e-attribute-test-neg-2: "true" + matchFeatures: + - feature: "fake.attribute" + matchName: {op: In, value: ["attr_x"]} + # - # Simple test rules for instnace features + # Simple test rules for instance features # - name: "e2e-instance-test-1" labels: @@ -65,6 +93,13 @@ spec: "attr_1": {op: In, value: ["true"]} "attr_3": {op: Gt, value: ["10"]} + - name: "e2e-instance-test-2" + labels: + e2e-instance-test-2: "true" + matchFeatures: + - feature: "fake.instance" + matchName: {op: In, value: ["attr_1", "attr_x"]} + # Negative test not supposed to create a label - name: "e2e-instance-test-neg-1" labels: @@ -74,3 +109,10 @@ spec: matchExpressions: "attr_1": {op: In, value: ["true"]} "attr_3": {op: Lt, value: ["10"]} + + - name: "e2e-instance-test-neg-2" + labels: + e2e-instance-test-neg-2: "true" + matchFeatures: + - feature: "fake.instance" + matchName: {op: In, value: ["attr_x"]} diff --git a/test/e2e/data/nodefeaturerule-2.yaml b/test/e2e/data/nodefeaturerule-2.yaml index bb3914fa9c..09dca8500e 100644 --- a/test/e2e/data/nodefeaturerule-2.yaml +++ b/test/e2e/data/nodefeaturerule-2.yaml @@ -40,3 +40,12 @@ spec: matchExpressions: "attr_1": {op: In, value: ["true"]} + - name: "e2e-template-test-2" + labelsTemplate: | + {{ range .fake.attribute }}e2e-template-test-2-{{ .Name }}={{ .Value }} + {{ end }} + matchFeatures: + - feature: "fake.attribute" + matchExpressions: + "attr_2": {op: IsFalse} + matchName: {op: In, value: ["attr_3"]} diff --git a/test/e2e/node_feature_discovery_test.go b/test/e2e/node_feature_discovery_test.go index 91bffea0fb..151af4b8a9 100644 --- a/test/e2e/node_feature_discovery_test.go +++ b/test/e2e/node_feature_discovery_test.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "maps" "path/filepath" "strings" "time" @@ -653,7 +654,7 @@ var _ = NFDDescribe(Label("nfd-master"), func() { // Test NodeFeatureRule // - Context("and nfd-worker and NodeFeatureRules objects deployed", func() { + Context("and nfd-worker and NodeFeatureRules objects deployed", Label("nodefeaturerule"), func() { testTolerations := []corev1.Toleration{ { Key: "feature.node.kubernetes.io/fake-special-node", @@ -714,8 +715,11 @@ core: expectedLabels := map[string]k8sLabels{ "*": { nfdv1alpha1.FeatureLabelNs + "/e2e-flag-test-1": "true", + nfdv1alpha1.FeatureLabelNs + "/e2e-flag-test-2": "true", nfdv1alpha1.FeatureLabelNs + "/e2e-attribute-test-1": "true", + nfdv1alpha1.FeatureLabelNs + "/e2e-attribute-test-2": "true", nfdv1alpha1.FeatureLabelNs + "/e2e-instance-test-1": "true", + nfdv1alpha1.FeatureLabelNs + "/e2e-instance-test-2": "true", }, } @@ -729,13 +733,17 @@ core: Expect(testutils.CreateNodeFeatureRulesFromFile(ctx, nfdClient, "nodefeaturerule-2.yaml")).NotTo(HaveOccurred()) // Add features from NodeFeatureRule #2 - expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-matchany-test-1"] = "true" - expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_1"] = "found" - expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_2"] = "found" - expectedLabels["*"][nfdv1alpha1.FeatureLabelNs+"/dynamic-label"] = "true" + maps.Copy(expectedLabels["*"], k8sLabels{ + nfdv1alpha1.FeatureLabelNs + "/e2e-matchany-test-1": "true", + nfdv1alpha1.FeatureLabelNs + "/e2e-template-test-1-instance_1": "found", + nfdv1alpha1.FeatureLabelNs + "/e2e-template-test-1-instance_2": "found", + nfdv1alpha1.FeatureLabelNs + "/e2e-template-test-2-attr_2": "false", + nfdv1alpha1.FeatureLabelNs + "/e2e-template-test-2-attr_3": "10", + nfdv1alpha1.FeatureLabelNs + "/dynamic-label": "true", + }) expectedAnnotations := map[string]k8sAnnotations{ "*": { - "nfd.node.kubernetes.io/feature-labels": "dynamic-label,e2e-attribute-test-1,e2e-flag-test-1,e2e-instance-test-1,e2e-matchany-test-1,e2e-template-test-1-instance_1,e2e-template-test-1-instance_2"}, + "nfd.node.kubernetes.io/feature-labels": "dynamic-label,e2e-attribute-test-1,e2e-attribute-test-2,e2e-flag-test-1,e2e-flag-test-2,e2e-instance-test-1,e2e-instance-test-2,e2e-matchany-test-1,e2e-template-test-1-instance_1,e2e-template-test-1-instance_2,e2e-template-test-2-attr_2,e2e-template-test-2-attr_3"}, } By("Verifying node labels from NodeFeatureRules #1 and #2")