Repository: kubernetes-sigs/node-feature-discovery
Pull request #792 (2022-03-22 22:10:17)

Add cpu-model feature detection
Signed-off-by: Carlos Eduardo Arango Gutierrez <carangog@redhat.com>
First pass on enhancing the discovered model of the CPU
example:
```bash
                    feature.node.kubernetes.io/cpu-model.ID=113
                    feature.node.kubernetes.io/cpu-model.family=23
                    feature.node.kubernetes.io/cpu-model.vendorID=AMD
```

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/node-feature-discovery/pull/792#" title="Author self-approved">ArangoGutierrez</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **marquiz** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @marquiz` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fnode-feature-discovery).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["marquiz"]} -->

Patch:
diff --git a/.github/ISSUE_TEMPLATE/new-release.md b/.github/ISSUE_TEMPLATE/new-release.md
index 93c1687e2..28b27f28c 100644
--- a/.github/ISSUE_TEMPLATE/new-release.md
+++ b/.github/ISSUE_TEMPLATE/new-release.md
@@ -18,7 +18,7 @@ Please do not remove items from the checklist
   - [ ] An OWNER pushes the new release branch with
         `git push release-0.$MAJ`
 - [ ] Run `scripts/prepare-release.sh $VERSION` to turn references to point to the upcoming release
-      (README, deployment templates, docs configuration, test/e2e flags), submit a PR agains the release branch
+      (README, deployment templates, docs configuration, test/e2e flags), submit a PR against the release branch
 - An OWNER prepares a draft release
   - [ ] Create a draft release at [Github releases page](https://github.com/kubernetes-sigs/node-feature-discovery/releases)
   - [ ] Write the change log into the draft release
diff --git a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
index 4647ab81a..36a12ecbe 100644
--- a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
+++ b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
@@ -10,9 +10,9 @@ rules:
   - ""
   resources:
   - nodes
-  # when using command line flag --resource-labels to create extended resources
-  # you will need to uncomment "- nodes/status"
-  # - nodes/status
+{{- if .Values.master.resourceLabels | empty | not }}
+  - nodes/status
+{{- end }}
   verbs:
   - get
   - patch
diff --git a/deployment/helm/node-feature-discovery/templates/master.yaml b/deployment/helm/node-feature-discovery/templates/master.yaml
index 78ad224b1..ce28646df 100644
--- a/deployment/helm/node-feature-discovery/templates/master.yaml
+++ b/deployment/helm/node-feature-discovery/templates/master.yaml
@@ -5,6 +5,8 @@ metadata:
   labels:
     {{- include "node-feature-discovery.labels" . | nindent 4 }}
     role: master
+  annotations:
+    {{- toYaml .Values.master.deploymentAnnotations | nindent 4 }}
 spec:
   replicas: {{ .Values.master.replicaCount }}
   selector:
@@ -78,6 +80,9 @@ spec:
             {{- if .Values.master.extraLabelNs | empty | not }}
             - "--extra-label-ns={{- join "," .Values.master.extraLabelNs }}"
             {{- end }}
+            {{- if .Values.master.resourceLabels | empty | not }}
+            - "--resource-labels={{- join "," .Values.master.resourceLabels }}"
+            {{- end }}
             {{- if .Values.master.featureRulesController | kindIs "invalid" | not }}
             - "-featurerules-controller={{ .Values.master.featureRulesController }}"
             {{- else }}
diff --git a/deployment/helm/node-feature-discovery/templates/worker.yaml b/deployment/helm/node-feature-discovery/templates/worker.yaml
index 3a471b38d..955b6a358 100644
--- a/deployment/helm/node-feature-discovery/templates/worker.yaml
+++ b/deployment/helm/node-feature-discovery/templates/worker.yaml
@@ -5,6 +5,8 @@ metadata:
   labels:
     {{- include "node-feature-discovery.labels" . | nindent 4 }}
     role: worker
+  annotations:
+    {{- toYaml .Values.worker.daemonsetAnnotations | nindent 4 }}
 spec:
   selector:
     matchLabels:
diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml
index e7feb5ea7..466c75dbb 100644
--- a/deployment/helm/node-feature-discovery/values.yaml
+++ b/deployment/helm/node-feature-discovery/values.yaml
@@ -15,8 +15,9 @@ nodeFeatureRule:
 master:
   instance:
   extraLabelNs: []
+  resourceLabels: []
   featureRulesController: null
-
+  deploymentAnnotations: {}
   replicaCount: 1
 
   podSecurityContext: {}
@@ -315,6 +316,7 @@ worker:
     #
 ### <NFD-WORKER-CONF-END-DO-NOT-REMOVE>
 
+  daemonsetAnnotations: {}
   podSecurityContext: {}
     # fsGroup: 2000
 
diff --git a/docs/advanced/customization-guide.md b/docs/advanced/customization-guide.md
index 57422100e..2374c3357 100644
--- a/docs/advanced/customization-guide.md
+++ b/docs/advanced/customization-guide.md
@@ -420,10 +420,6 @@ element whose name matches the `<key>`. However, for *instance* features all
 MatchExpressions are evaluated against the attributes of each instance
 separately.
 
-A special case of an empty `matchExpressions` field matches everything, i.e.
-matches/returns all elements of the feature. This makes it possible to write
-[templates](#templating) that run over all discovered features.
-
 #### MatchAny
 
 The `.matchAny` field is a list of of [`matchFeatures`](#matchfeatures)
@@ -482,6 +478,10 @@ The following features are available for matching:
 |                  |              | **`<cpuid-flag>`** |  | CPUID flag is present
 | **`cpu.cstate`** | attribute    |          |            | Status of cstates in the intel_idle cpuidle driver
 |                  |              | **`enabled`** | bool  | 'true' if cstates are set, otherwise 'false'. Does not exist of intel_idle driver is not active.
+| **`cpu.model`**  | attribute    |          |            | CPU model related attributes
+|                  |              | **`family`** | int    | CPU family
+|                  |              | **`vendor_id`** | string | CPU vendor ID
+|                  |              | **`id`** | int        | CPU model ID
 | **`cpu.pstate`** | attribute    |          |            | State of the Intel pstate driver. Does not exist if the driver is not enabled.
 |                  |              | **`status`** | string | Status of the driver, possible values are 'active' and 'passive'
 |                  |              | **`turbo`**  | bool   | 'true' if turbo frequencies are enabled, otherwise 'false'
@@ -624,27 +624,6 @@ would be executed on matcher#1 as well as on matcher#2 and/or matcher#3
 these separate expansions would be created, i.e. the end result would be a
 union of all the individual expansions.
 
-A special case of an empty `matchExpressions` field matches everything, i.e.
-matches/returns all elements of the feature. This makes it possible to write
-[templates](#templating) that run over all discovered features.
-
-Consider the following example:
-<!-- {% raw %} -->
-
-```yaml
-    labelsTemplate: |
-      {{ range .network.device }}net-{{ .name }}.speed-mbps={{ .speed }}
-      {{ end }}
-    matchFeatures:
-      - feature: network.device
-        matchExpressions: null
-```
-
-<!-- {% endraw %} -->
-This will create individual
-`feature.node.kubernetes.io/net-<if-name>.speed-mbpx=<speed-in-mbps>` label for
-each (physical) network device of the system.
-
 Rule templates use the Golang [text/template](https://pkg.go.dev/text/template)
 package and all its built-in functionality (e.g. pipelines and functions) can
 be used. An example template taking use of the built-in `len` function,
@@ -710,7 +689,7 @@ Consider the following configuration:
 The `feature.node.kubernetes.io/high-level-feature = true` label depends on thw
 two previous rules.
 
-Note that when referencing rules accross multiple
+Note that when referencing rules across multiple
 [`NodeFeatureRule`](#nodefeaturerule-custom-resource) objects attention must be
 paid to the ordering. `NodeFeatureRule` objects are processed in alphabetical
 order (based on their `.metadata.name`).
diff --git a/docs/advanced/developer-guide.md b/docs/advanced/developer-guide.md
index 3d130101e..8595c2af4 100644
--- a/docs/advanced/developer-guide.md
+++ b/docs/advanced/developer-guide.md
@@ -66,7 +66,7 @@ make push-all
 ```
 
 The resulting container image can be used in the same way on each arch by pulling
-e.g. `node-feature-discovery:v0.10.0` without specifying the architechture. The
+e.g. `node-feature-discovery:v0.10.0` without specifying the architecture. The
 manifest-list will take care of providing the right architecture image.
 
 #### Change the job spec to use your custom image (optional)
diff --git a/docs/get-started/deployment-and-usage.md b/docs/get-started/deployment-and-usage.md
index 4728bc508..15dc9293e 100644
--- a/docs/get-started/deployment-and-usage.md
+++ b/docs/get-started/deployment-and-usage.md
@@ -116,7 +116,7 @@ scenarios under
 [`deployment/overlays`](https://github.com/kubernetes-sigs/node-feature-discovery/blob/{{site.release}}/deployment/overlays)
 
 - [`default`](https://github.com/kubernetes-sigs/node-feature-discovery/blob/{{site.release}}/deployment/overlays/default):
-  default deployment of nfd-worker as a daemonset, descibed above
+  default deployment of nfd-worker as a daemonset, described above
 - [`default-combined`](https://github.com/kubernetes-sigs/node-feature-discovery/blob/{{site.release}}/deployment/overlays/default-combined)
   see [Master-worker pod](#master-worker-pod) below
 - [`default-job`](https://github.com/kubernetes-sigs/node-feature-discovery/blob/{{site.release}}/deployment/overlays/default-job):
@@ -299,6 +299,7 @@ We have introduced the following Chart parameters.
 | `master.*`                  | dict    |                                         | NFD master deployment configuration                                                                                                      |
 | `master.instance`           | string  |                                         | Instance name. Used to separate annotation namespaces for multiple parallel deployments                                                  |
 | `master.extraLabelNs`       | array   | []                                      | List of allowed extra label namespaces                                                                                                   |
+| `master.resourceLabels`     | array   | []                                      | List of labels to be registered as extended resources                                                                                          |
 | `master.featureRulesController` | bool | null                                   | Specifies whether the controller for processing of NodeFeatureRule objects is enabled. If not set, controller will be enabled if `master.instance` is empty. |
 | `master.replicaCount`       | integer | 1                                       | Number of desired pods. This is a pointer to distinguish between explicit zero and not specified                                         |
 | `master.podSecurityContext` | dict    | {}                                      | [PodSecurityContext](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod) holds pod-level security attributes and common container settings |
@@ -312,8 +313,9 @@ We have introduced the following Chart parameters.
 | `master.resources`          | dict    | {}                                      | NFD master pod [resources management](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)                    |
 | `master.nodeSelector`       | dict    | {}                                      | NFD master pod [node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector)                    |
 | `master.tolerations`        | dict    | _Scheduling to master node is disabled_ | NFD master pod [tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)                              |
-| `master.annotations`        | dict    | {}                                      | NFD master pod [metadata](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)                                |
+| `master.annotations`        | dict    | {}                                      | NFD master pod [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)                                |
 | `master.affinity`           | dict    |                                         | NFD master pod required [node affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/) |
+| `master.deploymentAnnotations` | dict | {}                                      | NFD master deployment [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) |
 
 ##### Worker pod parameters
 
@@ -327,7 +329,8 @@ We have introduced the following Chart parameters.
 | `worker.resources` | dict | {} | NFD worker pod [resources management](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) |
 | `worker.nodeSelector` | dict | {} | NFD worker pod [node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) |
 | `worker.tolerations` | dict | {} | NFD worker pod [node tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) |
-| `worker.annotations` | dict | {} | NFD worker pod [metadata](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) |
+| `worker.annotations` | dict | {} | NFD worker pod [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) |
+| `worker.daemonsetAnnotations` | dict | {} | NFD worker daemonset [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) |
 
 ##### Topology updater parameters
 
@@ -339,7 +342,7 @@ We have introduced the following Chart parameters.
 | `topologyUpdater.serviceAccount.create`       | bool   | true    | Specifies whether the service account for topology updater should be created                                                                                               |
 | `topologyUpdater.serviceAccount.annotations`  | dict   | {}      | Annotations to add to the service account for topology updater                                                                                                             |
 | `topologyUpdater.serviceAccount.name`         | string |         | The name of the service account for topology updater to use. If not set and create is true, a name is generated using the fullname template and `-topology-updater` suffix |
-| `topologyUpdater.rbac`                        | dict   |         | RBAC [parameteres](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) for the topology updater                                                                 |
+| `topologyUpdater.rbac`                        | dict   |         | RBAC [parameters](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) for the topology updater                                                                 |
 | `topologyUpdater.rbac.create`                 | bool   | false   | Specifies whether the cluster role and binding for topology updater should be created                                                                                      |
 | `topologyUpdater.kubeletConfigPath`           | string | ""      | Specifies the kubelet config host path                                                                                                                                     |
 | `topologyUpdater.kubeletPodResourcesSockPath` | string | ""      | Specifies the kubelet sock path to read pod resources                                                                                                                      |
@@ -350,7 +353,7 @@ We have introduced the following Chart parameters.
 | `topologyUpdater.resources`                   | dict   | {}      | Topology updater pod [resources management](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)                                                |
 | `topologyUpdater.nodeSelector`                | dict   | {}      | Topology updater pod [node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector)                                                |
 | `topologyUpdater.tolerations`                 | dict   | {}      | Topology updater pod [node tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)                                                     |
-| `topologyUpdater.annotations`                 | dict   | {}      | Topology updater pod [metadata](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)                                                            |
+| `topologyUpdater.annotations`                 | dict   | {}      | Topology updater pod [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)                                                            |
 | `topologyUpdater.affinity`                    | dict   | {}      | Topology updater pod [affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/)                                           |
 
 ### Build your own
diff --git a/docs/get-started/features.md b/docs/get-started/features.md
index c38b90991..f0bb1b2b2 100644
--- a/docs/get-started/features.md
+++ b/docs/get-started/features.md
@@ -51,6 +51,9 @@ such as restricting discovered features with the -label-whitelist option.*
 | **`cpu-cstate.enabled`**          | bool   | Set to 'true' if cstates are set in the intel_idle driver, otherwise set to 'false'. Unset if intel_idle cpuidle driver is not active.
 | **`cpu-rdt.<rdt-flag>`**          | true   | [Intel RDT][intel-rdt] capability is supported. See [RDT flags](#intel-rdt-flags) for details.
 | **`cpu-sgx.enabled`**             | true   | Set to 'true' if Intel SGX is enabled in BIOS (based a non-zero sum value of SGX EPC section sizes).
+| **`cpu-model.vendor_id`**         | string | Comparable CPU vendor ID.
+| **`cpu-model.family`**            | int    | CPU family.
+| **`cpu-model.id`**                | int    | CPU model number.
 
 The CPU label source is configurable, see
 [worker configuration](deployment-and-usage#worker-configuration) and
diff --git a/pkg/apis/nfd/v1alpha1/expression.go b/pkg/apis/nfd/v1alpha1/expression.go
index 303b9b239..41a032b40 100644
--- a/pkg/apis/nfd/v1alpha1/expression.go
+++ b/pkg/apis/nfd/v1alpha1/expression.go
@@ -314,8 +314,8 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
 
 // MatchKeys evaluates the MatchExpressionSet against a set of keys.
 func (m *MatchExpressionSet) MatchKeys(keys map[string]feature.Nil) (bool, error) {
-	v, err := m.MatchGetKeys(keys)
-	return v != nil, err
+	matched, _, err := m.MatchGetKeys(keys)
+	return matched, err
 }
 
 // MatchedKey holds one matched key.
@@ -328,35 +328,28 @@ type MatchedKey struct {
 // empty MatchExpressionSet returns all existing keys are returned. Note that
 // an empty MatchExpressionSet and an empty set of keys returns an empty slice
 // which is not nil and is treated as a match.
-func (m *MatchExpressionSet) MatchGetKeys(keys map[string]feature.Nil) ([]MatchedKey, error) {
+func (m *MatchExpressionSet) MatchGetKeys(keys map[string]feature.Nil) (bool, []MatchedKey, error) {
 	ret := make([]MatchedKey, 0, m.Len())
 
-	// An empty rule matches all existing keys
-	if m.Len() == 0 {
-		for n := range keys {
-			ret = append(ret, MatchedKey{Name: n})
-		}
-	}
-
 	for n, e := range (*m).Expressions {
 		match, err := e.MatchKeys(n, keys)
 		if err != nil {
-			return nil, err
+			return false, nil, err
 		}
 		if !match {
-			return nil, nil
+			return false, nil, nil
 		}
 		ret = append(ret, MatchedKey{Name: n})
 	}
 	// Sort for reproducible output
 	sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
-	return ret, nil
+	return true, ret, nil
 }
 
 // MatchValues evaluates the MatchExpressionSet against a set of key-value pairs.
 func (m *MatchExpressionSet) MatchValues(values map[string]string) (bool, error) {
-	v, err := m.MatchGetValues(values)
-	return v != nil, err
+	matched, _, err := m.MatchGetValues(values)
+	return matched, err
 }
 
 // MatchedValue holds one matched key-value pair.
@@ -370,29 +363,22 @@ type MatchedValue struct {
 // MatchExpressionSet returns all existing key-value pairs. Note that an empty
 // MatchExpressionSet and an empty set of values returns an empty non-nil map
 // which is treated as a match.
-func (m *MatchExpressionSet) MatchGetValues(values map[string]string) ([]MatchedValue, error) {
+func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []MatchedValue, error) {
 	ret := make([]MatchedValue, 0, m.Len())
 
-	// An empty rule matches all existing values
-	if m.Len() == 0 {
-		for n, v := range values {
-			ret = append(ret, MatchedValue{Name: n, Value: v})
-		}
-	}
-
 	for n, e := range (*m).Expressions {
 		match, err := e.MatchValues(n, values)
 		if err != nil {
-			return nil, err
+			return false, nil, err
 		}
 		if !match {
-			return nil, nil
+			return false, nil, nil
 		}
 		ret = append(ret, MatchedValue{Name: n, Value: values[n]})
 	}
 	// Sort for reproducible output
 	sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
-	return ret, nil
+	return true, ret, nil
 }
 
 // MatchInstances evaluates the MatchExpressionSet against a set of instance
diff --git a/pkg/apis/nfd/v1alpha1/rule.go b/pkg/apis/nfd/v1alpha1/rule.go
index 3a4350fc2..267680326 100644
--- a/pkg/apis/nfd/v1alpha1/rule.go
+++ b/pkg/apis/nfd/v1alpha1/rule.go
@@ -44,21 +44,21 @@ func (r *Rule) Execute(features feature.Features) (RuleOutput, error) {
 		// Logical OR over the matchAny matchers
 		matched := false
 		for _, matcher := range r.MatchAny {
-			if m, err := matcher.match(features); err != nil {
+			if isMatch, matches, err := matcher.match(features); err != nil {
 				return RuleOutput{}, err
-			} else if m != nil {
+			} else if isMatch {
 				matched = true
-				utils.KlogDump(4, "matches for matchAny "+r.Name, "  ", m)
+				utils.KlogDump(4, "matches for matchAny "+r.Name, "  ", matches)
 
 				if r.labelsTemplate == nil {
 					// No templating so we stop here (further matches would just
 					// produce the same labels)
 					break
 				}
-				if err := r.executeLabelsTemplate(m, labels); err != nil {
+				if err := r.executeLabelsTemplate(matches, labels); err != nil {
 					return RuleOutput{}, err
 				}
-				if err := r.executeVarsTemplate(m, vars); err != nil {
+				if err := r.executeVarsTemplate(matches, vars); err != nil {
 					return RuleOutput{}, err
 				}
 			}
@@ -70,17 +70,17 @@ func (r *Rule) Execute(features feature.Features) (RuleOutput, error) {
 	}
 
 	if len(r.MatchFeatures) > 0 {
-		if m, err := r.MatchFeatures.match(features); err != nil {
+		if isMatch, matches, err := r.MatchFeatures.match(features); err != nil {
 			return RuleOutput{}, err
-		} else if m == nil {
+		} else if !isMatch {
 			klog.V(2).Infof("rule %q did not match", r.Name)
 			return RuleOutput{}, nil
 		} else {
-			utils.KlogDump(4, "matches for matchFeatures "+r.Name, "  ", m)
-			if err := r.executeLabelsTemplate(m, labels); err != nil {
+			utils.KlogDump(4, "matches for matchFeatures "+r.Name, "  ", matches)
+			if err := r.executeLabelsTemplate(matches, labels); err != nil {
 				return RuleOutput{}, err
 			}
-			if err := r.executeVarsTemplate(m, vars); err != nil {
+			if err := r.executeVarsTemplate(matches, vars); err != nil {
 				return RuleOutput{}, err
 			}
 		}
@@ -148,18 +148,18 @@ type matchedFeatures map[string]domainMatchedFeatures
 
 type domainMatchedFeatures map[string]interface{}
 
-func (e *MatchAnyElem) match(features map[string]*feature.DomainFeatures) (matchedFeatures, error) {
+func (e *MatchAnyElem) match(features map[string]*feature.DomainFeatures) (bool, matchedFeatures, error) {
 	return e.MatchFeatures.match(features)
 }
 
-func (m *FeatureMatcher) match(features map[string]*feature.DomainFeatures) (matchedFeatures, error) {
-	ret := make(matchedFeatures, len(*m))
+func (m *FeatureMatcher) match(features map[string]*feature.DomainFeatures) (bool, matchedFeatures, error) {
+	matches := make(matchedFeatures, len(*m))
 
 	// Logical AND over the terms
 	for _, term := range *m {
 		split := strings.SplitN(term.Feature, ".", 2)
 		if len(split) != 2 {
-			return nil, fmt.Errorf("invalid feature %q: must be <domain>.<feature>", term.Feature)
+			return false, nil, fmt.Errorf("invalid feature %q: must be <domain>.<feature>", term.Feature)
 		}
 		domain := split[0]
 		// Ignore case
@@ -167,41 +167,41 @@ func (m *FeatureMatcher) match(features map[string]*feature.DomainFeatures) (mat
 
 		domainFeatures, ok := features[domain]
 		if !ok {
-			return nil, fmt.Errorf("unknown feature source/domain %q", domain)
+			return false, nil, fmt.Errorf("unknown feature source/domain %q", domain)
 		}
 
-		if _, ok := ret[domain]; !ok {
-			ret[domain] = make(domainMatchedFeatures)
+		if _, ok := matches[domain]; !ok {
+			matches[domain] = make(domainMatchedFeatures)
 		}
 
-		var m bool
-		var e error
+		var isMatch bool
+		var err error
 		if f, ok := domainFeatures.Keys[featureName]; ok {
-			v, err := term.MatchExpressions.MatchGetKeys(f.Elements)
-			m = len(v) > 0
-			e = err
-			ret[domain][featureName] = v
+			m, v, e := term.MatchExpressions.MatchGetKeys(f.Elements)
+			isMatch = m
+			err = e
+			matches[domain][featureName] = v
 		} else if f, ok := domainFeatures.Values[featureName]; ok {
-			v, err := term.MatchExpressions.MatchGetValues(f.Elements)
-			m = len(v) > 0
-			e = err
-			ret[domain][featureName] = v
+			m, v, e := term.MatchExpressions.MatchGetValues(f.Elements)
+			isMatch = m
+			err = e
+			matches[domain][featureName] = v
 		} else if f, ok := domainFeatures.Instances[featureName]; ok {
-			v, err := term.MatchExpressions.MatchGetInstances(f.Elements)
-			m = len(v) > 0
-			e = err
-			ret[domain][featureName] = v
+			v, e := term.MatchExpressions.MatchGetInstances(f.Elements)
+			isMatch = len(v) > 0
+			err = e
+			matches[domain][featureName] = v
 		} else {
-			return nil, fmt.Errorf("%q feature of source/domain %q not available", featureName, domain)
+			return false, nil, fmt.Errorf("%q feature of source/domain %q not available", featureName, domain)
 		}
 
-		if e != nil {
-			return nil, e
-		} else if !m {
-			return nil, nil
+		if err != nil {
+			return false, nil, err
+		} else if !isMatch {
+			return false, nil, nil
 		}
 	}
-	return ret, nil
+	return true, matches, nil
 }
 
 type templateHelper struct {
diff --git a/source/cpu/cpu.go b/source/cpu/cpu.go
index 5c0fd8b85..5367026ac 100644
--- a/source/cpu/cpu.go
+++ b/source/cpu/cpu.go
@@ -22,6 +22,8 @@ import (
 
 	"k8s.io/klog/v2"
 
+	"github.com/klauspost/cpuid/v2"
+
 	"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/source"
@@ -32,6 +34,7 @@ const Name = "cpu"
 
 const (
 	CpuidFeature    = "cpuid"
+	Cpumodel        = "model"
 	CstateFeature   = "cstate"
 	PstateFeature   = "pstate"
 	RdtFeature      = "rdt"
@@ -141,6 +144,11 @@ func (s *cpuSource) GetLabels() (source.FeatureLabels, error) {
 		}
 	}
 
+	// CPU model
+	for k, v := range features.Values[Cpumodel].Elements {
+		labels["model."+k] = v
+	}
+
 	// Cstate
 	for k, v := range features.Values[CstateFeature].Elements {
 		labels["cstate."+k] = v
@@ -181,6 +189,9 @@ func (s *cpuSource) Discover() error {
 	// Detect CPUID
 	s.features.Keys[CpuidFeature] = feature.NewKeyFeatures(getCpuidFlags()...)
 
+	// Detect CPU model
+	s.features.Values[Cpumodel] = feature.NewValueFeatures(getCPUModel())
+
 	// Detect cstate configuration
 	cstate, err := detectCstate()
 	if err != nil {
@@ -221,6 +232,15 @@ func (s *cpuSource) GetFeatures() *feature.DomainFeatures {
 	return s.features
 }
 
+func getCPUModel() map[string]string {
+	cpuModelInfo := make(map[string]string)
+	cpuModelInfo["vendor_id"] = cpuid.CPU.VendorID.String()
+	cpuModelInfo["family"] = strconv.Itoa(cpuid.CPU.Family)
+	cpuModelInfo["id"] = strconv.Itoa(cpuid.CPU.Model)
+
+	return cpuModelInfo
+}
+
 func discoverTopology() map[string]string {
 	features := make(map[string]string)
 


Test patch:
diff --git a/pkg/apis/nfd/v1alpha1/expression_test.go b/pkg/apis/nfd/v1alpha1/expression_test.go
index c84a5f3c7..6dcd5f3e9 100644
--- a/pkg/apis/nfd/v1alpha1/expression_test.go
+++ b/pkg/apis/nfd/v1alpha1/expression_test.go
@@ -306,7 +306,7 @@ func TestMESMatchKeys(t *testing.T) {
 
 		{input: I{}, output: O{}, result: assert.Truef, err: assert.Nilf},
 
-		{input: I{"foo": {}}, output: O{MK{Name: "foo"}}, result: assert.Truef, err: assert.Nilf},
+		{input: I{"foo": {}}, output: O{}, result: assert.Truef, err: assert.Nilf},
 
 		{mes: `
 foo: { op: DoesNotExist }
@@ -339,11 +339,12 @@ bar: { op: Exists }
 			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
 		}
 
-		out, err := mes.MatchGetKeys(tc.input)
+		res, out, err := mes.MatchGetKeys(tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
 		assert.Equalf(t, tc.output, out, "test case #%d (%v) failed", i, tc)
 		tc.err(t, err, "test case #%d (%v) failed", i, tc)
 
-		res, err := mes.MatchKeys(tc.input)
+		res, err = mes.MatchKeys(tc.input)
 		tc.result(t, res, "test case #%d (%v) failed", i, tc)
 		tc.err(t, err, "test case #%d (%v) failed", i, tc)
 	}
@@ -366,7 +367,7 @@ func TestMESMatchValues(t *testing.T) {
 
 		{input: I{}, output: O{}, result: assert.Truef, err: assert.Nilf},
 
-		{input: I{"foo": "bar"}, output: O{MV{Name: "foo", Value: "bar"}}, result: assert.Truef, err: assert.Nilf},
+		{input: I{"foo": "bar"}, output: O{}, result: assert.Truef, err: assert.Nilf},
 
 		{mes: `
 foo: { op: Exists }
@@ -400,11 +401,12 @@ baz: { op: Gt, value: ["10"] }
 			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
 		}
 
-		out, err := mes.MatchGetValues(tc.input)
+		res, out, err := mes.MatchGetValues(tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
 		assert.Equalf(t, tc.output, out, "test case #%d (%v) failed", i, tc)
 		tc.err(t, err, "test case #%d (%v) failed", i, tc)
 
-		res, err := mes.MatchValues(tc.input)
+		res, err = mes.MatchValues(tc.input)
 		tc.result(t, res, "test case #%d (%v) failed", i, tc)
 		tc.err(t, err, "test case #%d (%v) failed", i, tc)
 	}
diff --git a/pkg/apis/nfd/v1alpha1/rule_test.go b/pkg/apis/nfd/v1alpha1/rule_test.go
index c4be8050c..87d9c7b3b 100644
--- a/pkg/apis/nfd/v1alpha1/rule_test.go
+++ b/pkg/apis/nfd/v1alpha1/rule_test.go
@@ -82,6 +82,17 @@ func TestRule(t *testing.T) {
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
 
+	// Test empty MatchExpressions
+	r1.MatchFeatures = FeatureMatcher{
+		FeatureMatcherTerm{
+			Feature:          "domain-1.kf-1",
+			MatchExpressions: MatchExpressionSet{},
+		},
+	}
+	m, err = r1.Execute(f)
+	assert.Nilf(t, err, "unexpected error: %v", err)
+	assert.Equal(t, r1.Labels, m.Labels, "empty match expression set mathces anything")
+
 	// Match "key" features
 	m, err = r2.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
@@ -325,8 +336,11 @@ var-2=
 			// We need at least one matcher to match to execute the template.
 			// Use a simple empty matchexpression set to match anything.
 			FeatureMatcherTerm{
-				Feature:          "domain_1.kf_1",
-				MatchExpressions: MatchExpressionSet{},
+				Feature: "domain_1.kf_1",
+				MatchExpressions: MatchExpressionSet{Expressions: Expressions{
+					"key-a": MustCreateMatchExpression(MatchExists),
+				},
+				},
 			},
 		},
 	}