diff --git a/deployment/base/nfd-crds/cr-sample.yaml b/deployment/base/nfd-crds/cr-sample.yaml index dad9405d08..51192cf515 100644 --- a/deployment/base/nfd-crds/cr-sample.yaml +++ b/deployment/base/nfd-crds/cr-sample.yaml @@ -88,6 +88,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: | @@ -137,3 +144,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/nodefeaturerule-crd.yaml b/deployment/base/nfd-crds/nodefeaturerule-crd.yaml index c40af91193..920e54c4f0 100644 --- a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml +++ b/deployment/base/nfd-crds/nodefeaturerule-crd.yaml @@ -69,6 +69,8 @@ spec: in the feature set. properties: feature: + description: Feature is the name of the feature + set to match against. type: string matchExpressions: additionalProperties: @@ -113,13 +115,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: @@ -135,6 +170,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: @@ -176,12 +213,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 eb5030bd5e..0cfb8aa057 100644 --- a/deployment/components/worker-config/nfd-worker.conf.example +++ b/deployment/components/worker-config/nfd-worker.conf.example @@ -171,6 +171,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: | @@ -221,3 +228,10 @@ # 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/nodefeaturerule-crd.yaml b/deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml index c40af91193..920e54c4f0 100644 --- a/deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml +++ b/deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml @@ -69,6 +69,8 @@ spec: in the feature set. properties: feature: + description: Feature is the name of the feature + set to match against. type: string matchExpressions: additionalProperties: @@ -113,13 +115,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: @@ -135,6 +170,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: @@ -176,12 +213,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 bc24b15b55..60f944db0b 100644 --- a/deployment/helm/node-feature-discovery/values.yaml +++ b/deployment/helm/node-feature-discovery/values.yaml @@ -265,6 +265,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: | @@ -315,6 +322,13 @@ worker: # 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"]} ### daemonsetAnnotations: {} diff --git a/pkg/apis/nfd/v1alpha1/expression.go b/pkg/apis/nfd/v1alpha1/expression.go index b0e1f58840..afc03d47c1 100644 --- a/pkg/apis/nfd/v1alpha1/expression.go +++ b/pkg/apis/nfd/v1alpha1/expression.go @@ -260,6 +260,93 @@ 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]feature.Nil) (bool, []MatchedKey, error) { + ret := []MatchedKey{} + + for k := range keys { + if match, err := m.Match(true, k); err != nil { + return false, nil, err + } else if match { + ret = append(ret, MatchedKey{Name: k}) + } + } + // Sort for reproducible output + sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name }) + + if klog.V(3).Enabled() { + mk := make([]string, len(ret)) + for i, v := range ret { + mk[i] = v.Name + } + + k := make([]string, 0, len(keys)) + for n := range keys { + k = append(k, n) + } + sort.Strings(k) + + if len(keys) < 10 || klog.V(4).Enabled() { + klog.Infof("matched names %s when matching %q %q against %s", strings.Join(mk, ", "), m.Op, m.Value, strings.Join(k, " ")) + } else { + klog.Infof("matched names %s when matching %q %q against %s... (list truncated)", strings.Join(mk, ", "), m.Op, m.Value, strings.Join(k[0:10], ", ")) + } + } + + return true, ret, nil +} + +// MatchValueNames evaluates the MatchExpression against names of a set of value features. +func (m *MatchExpression) MatchValueNames(values map[string]string) (bool, []MatchedValue, error) { + ret := []MatchedValue{} + + for k, v := range values { + if match, err := m.Match(true, k); err != nil { + return false, nil, err + } else if match { + ret = append(ret, MatchedValue{Name: k, Value: v}) + } + } + // Sort for reproducible output + sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name }) + + if klog.V(3).Enabled() { + mk := make([]string, len(ret)) + for i, v := range ret { + mk[i] = v.Name + } + + k := make([]string, 0, len(values)) + for n := range values { + k = append(k, n) + } + sort.Strings(k) + + if len(values) < 10 || klog.V(4).Enabled() { + klog.Infof("matched names %s when matching %q %q against %s", strings.Join(mk, ", "), m.Op, m.Value, strings.Join(k, " ")) + } else { + klog.Infof("matched names %s when matching %q %q against %s... (list truncated)", strings.Join(mk, ", "), m.Op, m.Value, strings.Join(k[0:10], ", ")) + } + } + + return true, ret, nil +} + +// MatchInstanceAttributeNames evaluates the MatchExpression against a set of +// instance features, matching against the names of their attributes. +func (m *MatchExpression) MatchInstanceAttributeNames(instances []feature.InstanceFeature) ([]MatchedInstance, error) { + ret := []MatchedInstance{} + + 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 1237a6f192..5074a942b1 100644 --- a/pkg/apis/nfd/v1alpha1/rule.go +++ b/pkg/apis/nfd/v1alpha1/rule.go @@ -176,22 +176,49 @@ func (m *FeatureMatcher) match(features map[string]*feature.DomainFeatures) (boo matches[domain] = make(domainMatchedFeatures) } - var isMatch bool + var isMatch = true var err error if f, ok := domainFeatures.Keys[featureName]; ok { - m, v, e := term.MatchExpressions.MatchGetKeys(f.Elements) - isMatch = m - err = e + var v []MatchedKey + if term.MatchExpressions != nil { + isMatch, v, err = term.MatchExpressions.MatchGetKeys(f.Elements) + } + + if err == nil && isMatch && term.MatchName != nil { + mTmp, vTmp, e := term.MatchName.MatchKeyNames(f.Elements) + isMatch = mTmp + err = e + v = append(v, vTmp...) + } + matches[domain][featureName] = v } else if f, ok := domainFeatures.Values[featureName]; ok { - m, v, e := term.MatchExpressions.MatchGetValues(f.Elements) - isMatch = m - err = e + var v []MatchedValue + if term.MatchExpressions != nil { + isMatch, v, err = term.MatchExpressions.MatchGetValues(f.Elements) + } + + if err == nil && isMatch && term.MatchName != nil { + mTmp, vTmp, e := term.MatchName.MatchValueNames(f.Elements) + isMatch = mTmp + err = e + v = append(v, vTmp...) + } + matches[domain][featureName] = v } else if f, ok := domainFeatures.Instances[featureName]; ok { - v, e := term.MatchExpressions.MatchGetInstances(f.Elements) - isMatch = len(v) > 0 - err = e + var v []MatchedInstance + if term.MatchExpressions != nil { + v, err = term.MatchExpressions.MatchGetInstances(f.Elements) + isMatch = len(v) > 0 + } + + if err == nil && isMatch && term.MatchName != nil { + vTmp, e := term.MatchName.MatchInstanceAttributeNames(f.Elements) + isMatch = len(vTmp) > 0 + err = e + v = append(v, vTmp...) + } matches[domain][featureName] = v } else { return false, nil, fmt.Errorf("%q feature of source/domain %q not available", featureName, domain) diff --git a/pkg/apis/nfd/v1alpha1/rule_test.go b/pkg/apis/nfd/v1alpha1/rule_test.go index a13760c6ca..05cfd18e53 100644 --- a/pkg/apis/nfd/v1alpha1/rule_test.go +++ b/pkg/apis/nfd/v1alpha1/rule_test.go @@ -32,7 +32,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": MustCreateMatchExpression(MatchExists), }, }, @@ -86,7 +86,7 @@ func TestRule(t *testing.T) { r1.MatchFeatures = FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{}, + MatchExpressions: &MatchExpressionSet{}, }, } m, err = r1.Execute(f) @@ -110,7 +110,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": MustCreateMatchExpression(MatchIn, "val-1"), }, }, @@ -131,7 +131,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "attr-1": MustCreateMatchExpression(MatchIn, "val-1"), }, }, @@ -152,13 +152,13 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.vf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": MustCreateMatchExpression(MatchIn, "val-x"), }, }, FeatureMatcherTerm{ Feature: "domain-1.if-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "attr-1": MustCreateMatchExpression(MatchIn, "val-1"), }, }, @@ -168,7 +168,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"] = MustCreateMatchExpression(MatchIn, "val-1") + (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = MustCreateMatchExpression(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") @@ -179,7 +179,7 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-na": MustCreateMatchExpression(MatchExists), }, }, @@ -195,13 +195,13 @@ func TestRule(t *testing.T) { MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain-1.kf-1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": MustCreateMatchExpression(MatchExists), }, }, }, }) - r5.MatchFeatures[0].MatchExpressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1") + (*r5.MatchFeatures[0].MatchExpressions)["key-1"] = MustCreateMatchExpression(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") @@ -225,6 +225,7 @@ func TestTemplating(t *testing.T) { "key-1": "val-1", "keu-2": "val-2", "key-3": "val-3", + "key-4": "val-4", }, }, }, @@ -275,7 +276,7 @@ var-2= MatchFeatures: FeatureMatcher{ FeatureMatcherTerm{ Feature: "domain_1.kf_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-a": MustCreateMatchExpression(MatchExists), "key-c": MustCreateMatchExpression(MatchExists), "foo": MustCreateMatchExpression(MatchDoesNotExist), @@ -283,14 +284,14 @@ var-2= }, FeatureMatcherTerm{ Feature: "domain_1.vf_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-1": MustCreateMatchExpression(MatchIn, "val-1", "val-2"), "bar": MustCreateMatchExpression(MatchDoesNotExist), }, }, FeatureMatcherTerm{ Feature: "domain_1.if_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "attr-1": MustCreateMatchExpression(MatchLt, "100"), }, }, @@ -344,7 +345,7 @@ var-2= // Use a simple empty matchexpression set to match anything. FeatureMatcherTerm{ Feature: "domain_1.kf_1", - MatchExpressions: MatchExpressionSet{ + MatchExpressions: &MatchExpressionSet{ "key-a": MustCreateMatchExpression(MatchExists), }, }, @@ -385,4 +386,28 @@ 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": MustCreateMatchExpression(MatchDoesNotExist), + }, + MatchName: MustCreateMatchExpression(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") } diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go index dafdaf851a..27cf654e89 100644 --- a/pkg/apis/nfd/v1alpha1/types.go +++ b/pkg/apis/nfd/v1alpha1/types.go @@ -105,8 +105,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 @@ -196,3 +204,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 ff8f19f121..06f4b101d6 100644 --- a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go @@ -35,19 +35,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(map[string]*MatchExpression) + 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.