Repository: kubernetes-sigs/node-feature-discovery
Pull request #1508 (2023-12-19 10:08:24)

apis/nfd: split rule processing into a separate package
This patch tidies up the nfdv1alpha1 API package by refactoring out the implementation of (NodeFeature)Rule evaluation into a separate package.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *kubernetes-sigs-nfd* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 1255c78f7e4c9746280bfee3cf2ed469d922ef2a |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kubernetes-sigs-nfd/deploys/65816b9bcf062c00086e6203 |
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/node-feature-discovery/pull/1508#" title="Author self-approved">marquiz</a>*

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).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

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

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":[]} -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) Report
> Merging [#1508](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) (1255c78) into [master](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/commit/e162540e543707ebc22b8eda2a3cda5e703d6793?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) (e162540) will **decrease** coverage by `0.64%`.
> The diff coverage is `80.00%`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/graphs/tree.svg?width=650&height=150&src=pr&token=mEZgtJQ1vg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

```diff
@@            Coverage Diff             @@
##           master    #1508      +/-   ##
==========================================
- Coverage   31.94%   31.31%   -0.64%     
==========================================
  Files          61       61              
  Lines        7722     7722              
==========================================
- Hits         2467     2418      -49     
- Misses       5004     5060      +56     
+ Partials      251      244       -7     
```


| [Files](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) | Coverage Δ | |
|---|---|---|
| [pkg/nfd-master/nfd-master.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL25mZC1tYXN0ZXIvbmZkLW1hc3Rlci5nbw==) | `46.04% <0.00%> (ø)` | |
| [...kg/apis/nfd/v1alpha1/nodefeaturerule/expression.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9leHByZXNzaW9uLmdv) | `72.31% <86.11%> (ø)` | |
| [pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9ydWxlLmdv) | `82.02% <72.22%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) Report
> Merging [#1508](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) (1255c78) into [master](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/commit/e162540e543707ebc22b8eda2a3cda5e703d6793?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) (e162540) will **decrease** coverage by `0.64%`.
> The diff coverage is `80.00%`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/graphs/tree.svg?width=650&height=150&src=pr&token=mEZgtJQ1vg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

```diff
@@            Coverage Diff             @@
##           master    #1508      +/-   ##
==========================================
- Coverage   31.94%   31.31%   -0.64%     
==========================================
  Files          61       61              
  Lines        7722     7722              
==========================================
- Hits         2467     2418      -49     
- Misses       5004     5060      +56     
+ Partials      251      244       -7     
```


| [Files](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) | Coverage Δ | |
|---|---|---|
| [pkg/nfd-master/nfd-master.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL25mZC1tYXN0ZXIvbmZkLW1hc3Rlci5nbw==) | `46.04% <0.00%> (ø)` | |
| [...kg/apis/nfd/v1alpha1/nodefeaturerule/expression.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9leHByZXNzaW9uLmdv) | `72.31% <86.11%> (ø)` | |
| [pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9ydWxlLmdv) | `82.02% <72.22%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) Report
Attention: `11 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`e162540`)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/commit/e162540e543707ebc22b8eda2a3cda5e703d6793?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) 31.94% compared to head [(`1255c78`)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) 31.31%.

> :exclamation: Current head 1255c78 differs from pull request most recent head 15202f1. Consider uploading reports for the commit 15202f1 to get more accurate results

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/graphs/tree.svg?width=650&height=150&src=pr&token=mEZgtJQ1vg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

```diff
@@            Coverage Diff             @@
##           master    #1508      +/-   ##
==========================================
- Coverage   31.94%   31.31%   -0.64%     
==========================================
  Files          61       61              
  Lines        7722     7722              
==========================================
- Hits         2467     2418      -49     
- Misses       5004     5060      +56     
+ Partials      251      244       -7     
```


| [Files](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) | Coverage Δ | |
|---|---|---|
| [pkg/nfd-master/nfd-master.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL25mZC1tYXN0ZXIvbmZkLW1hc3Rlci5nbw==) | `46.04% <0.00%> (ø)` | |
| [...kg/apis/nfd/v1alpha1/nodefeaturerule/expression.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9leHByZXNzaW9uLmdv) | `72.31% <86.11%> (ø)` | |
| [pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9ydWxlLmdv) | `82.02% <72.22%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) Report
Attention: `11 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`e162540`)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/commit/e162540e543707ebc22b8eda2a3cda5e703d6793?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) 31.94% compared to head [(`15202f1`)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) 31.31%.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/graphs/tree.svg?width=650&height=150&src=pr&token=mEZgtJQ1vg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

```diff
@@            Coverage Diff             @@
##           master    #1508      +/-   ##
==========================================
- Coverage   31.94%   31.31%   -0.64%     
==========================================
  Files          61       61              
  Lines        7722     7722              
==========================================
- Hits         2467     2418      -49     
- Misses       5004     5060      +56     
+ Partials      251      244       -7     
```


| [Files](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) | Coverage Δ | |
|---|---|---|
| [pkg/nfd-master/nfd-master.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL25mZC1tYXN0ZXIvbmZkLW1hc3Rlci5nbw==) | `46.04% <0.00%> (ø)` | |
| [...kg/apis/nfd/v1alpha1/nodefeaturerule/expression.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9leHByZXNzaW9uLmdv) | `72.31% <86.11%> (ø)` | |
| [pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs#diff-cGtnL2FwaXMvbmZkL3YxYWxwaGExL25vZGVmZWF0dXJlcnVsZS9ydWxlLmdv) | `82.02% <72.22%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/kubernetes-sigs/node-feature-discovery/pull/1508/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs)

</details>

Patch:
diff --git a/pkg/apis/nfd/v1alpha1/expression.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression.go
similarity index 73%
rename from pkg/apis/nfd/v1alpha1/expression.go
rename to pkg/apis/nfd/v1alpha1/nodefeaturerule/expression.go
index 9a0507498..7438f3bb1 100644
--- a/pkg/apis/nfd/v1alpha1/expression.go
+++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1alpha1
+package nodefeaturerule
 
 import (
 	"fmt"
@@ -25,40 +25,42 @@ import (
 
 	"golang.org/x/exp/maps"
 	"k8s.io/klog/v2"
+
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 )
 
-var matchOps = map[MatchOp]struct{}{
-	MatchAny:          {},
-	MatchIn:           {},
-	MatchNotIn:        {},
-	MatchInRegexp:     {},
-	MatchExists:       {},
-	MatchDoesNotExist: {},
-	MatchGt:           {},
-	MatchLt:           {},
-	MatchGtLt:         {},
-	MatchIsTrue:       {},
-	MatchIsFalse:      {},
+var matchOps = map[nfdv1alpha1.MatchOp]struct{}{
+	nfdv1alpha1.MatchAny:          {},
+	nfdv1alpha1.MatchIn:           {},
+	nfdv1alpha1.MatchNotIn:        {},
+	nfdv1alpha1.MatchInRegexp:     {},
+	nfdv1alpha1.MatchExists:       {},
+	nfdv1alpha1.MatchDoesNotExist: {},
+	nfdv1alpha1.MatchGt:           {},
+	nfdv1alpha1.MatchLt:           {},
+	nfdv1alpha1.MatchGtLt:         {},
+	nfdv1alpha1.MatchIsTrue:       {},
+	nfdv1alpha1.MatchIsFalse:      {},
 }
 
-// Match evaluates the MatchExpression against a single input value.
-func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
+// evaluateMatchExpression evaluates the MatchExpression against a single input value.
+func evaluateMatchExpression(m *nfdv1alpha1.MatchExpression, valid bool, value interface{}) (bool, error) {
 	if _, ok := matchOps[m.Op]; !ok {
 		return false, fmt.Errorf("invalid Op %q", m.Op)
 	}
 
 	switch m.Op {
-	case MatchAny:
+	case nfdv1alpha1.MatchAny:
 		if len(m.Value) != 0 {
 			return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value)
 		}
 		return true, nil
-	case MatchExists:
+	case nfdv1alpha1.MatchExists:
 		if len(m.Value) != 0 {
 			return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value)
 		}
 		return valid, nil
-	case MatchDoesNotExist:
+	case nfdv1alpha1.MatchDoesNotExist:
 		if len(m.Value) != 0 {
 			return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value)
 		}
@@ -68,7 +70,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
 	if valid {
 		value := fmt.Sprintf("%v", value)
 		switch m.Op {
-		case MatchIn:
+		case nfdv1alpha1.MatchIn:
 			if len(m.Value) == 0 {
 				return false, fmt.Errorf("invalid expression, 'value' field must be non-empty for Op %q", m.Op)
 			}
@@ -77,7 +79,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
 					return true, nil
 				}
 			}
-		case MatchNotIn:
+		case nfdv1alpha1.MatchNotIn:
 			if len(m.Value) == 0 {
 				return false, fmt.Errorf("invalid expression, 'value' field must be non-empty for Op %q", m.Op)
 			}
@@ -87,7 +89,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
 				}
 			}
 			return true, nil
-		case MatchInRegexp:
+		case nfdv1alpha1.MatchInRegexp:
 			if len(m.Value) == 0 {
 				return false, fmt.Errorf("invalid expression, 'value' field must be non-empty for Op %q", m.Op)
 			}
@@ -104,7 +106,7 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
 					return true, nil
 				}
 			}
-		case MatchGt, MatchLt:
+		case nfdv1alpha1.MatchGt, nfdv1alpha1.MatchLt:
 			if len(m.Value) != 1 {
 				return false, fmt.Errorf("invalid expression, 'value' field must contain exactly one element for Op %q (have %v)", m.Op, m.Value)
 			}
@@ -118,10 +120,10 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
 				return false, fmt.Errorf("not a number %q in %v", m.Value[0], m)
 			}
 
-			if (l < r && m.Op == MatchLt) || (l > r && m.Op == MatchGt) {
+			if (l < r && m.Op == nfdv1alpha1.MatchLt) || (l > r && m.Op == nfdv1alpha1.MatchGt) {
 				return true, nil
 			}
-		case MatchGtLt:
+		case nfdv1alpha1.MatchGtLt:
 			if len(m.Value) != 2 {
 				return false, fmt.Errorf("invalid expression, value' field must contain exactly two elements for Op %q (have %v)", m.Op, m.Value)
 			}
@@ -140,12 +142,12 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
 				return false, fmt.Errorf("invalid expression, value[0] must be less than Value[1] for Op %q (have %v)", m.Op, m.Value)
 			}
 			return v > lr[0] && v < lr[1], nil
-		case MatchIsTrue:
+		case nfdv1alpha1.MatchIsTrue:
 			if len(m.Value) != 0 {
 				return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value)
 			}
 			return value == "true", nil
-		case MatchIsFalse:
+		case nfdv1alpha1.MatchIsFalse:
 			if len(m.Value) != 0 {
 				return false, fmt.Errorf("invalid expression, 'value' field must be empty for Op %q (have %v)", m.Op, m.Value)
 			}
@@ -157,17 +159,17 @@ func (m *MatchExpression) Match(valid bool, value interface{}) (bool, error) {
 	return false, nil
 }
 
-// MatchKeys evaluates the MatchExpression against a set of keys.
-func (m *MatchExpression) MatchKeys(name string, keys map[string]Nil) (bool, error) {
+// evaluateMatchExpressionKeys evaluates the MatchExpression against a set of keys.
+func evaluateMatchExpressionKeys(m *nfdv1alpha1.MatchExpression, name string, keys map[string]nfdv1alpha1.Nil) (bool, error) {
 	matched := false
 
 	_, ok := keys[name]
 	switch m.Op {
-	case MatchAny:
+	case nfdv1alpha1.MatchAny:
 		matched = true
-	case MatchExists:
+	case nfdv1alpha1.MatchExists:
 		matched = ok
-	case MatchDoesNotExist:
+	case nfdv1alpha1.MatchDoesNotExist:
 		matched = !ok
 	default:
 		return false, fmt.Errorf("invalid Op %q when matching keys", m.Op)
@@ -183,10 +185,10 @@ func (m *MatchExpression) MatchKeys(name string, keys map[string]Nil) (bool, err
 	return matched, nil
 }
 
-// MatchValues evaluates the MatchExpression against a set of key-value pairs.
-func (m *MatchExpression) MatchValues(name string, values map[string]string) (bool, error) {
+// evaluateMatchExpressionValues evaluates the MatchExpression against a set of key-value pairs.
+func evaluateMatchExpressionValues(m *nfdv1alpha1.MatchExpression, name string, values map[string]string) (bool, error) {
 	v, ok := values[name]
-	matched, err := m.Match(ok, v)
+	matched, err := evaluateMatchExpression(m, ok, v)
 	if err != nil {
 		return false, err
 	}
@@ -201,11 +203,11 @@ func (m *MatchExpression) MatchValues(name string, values map[string]string) (bo
 }
 
 // MatchKeyNames evaluates the MatchExpression against names of a set of key features.
-func (m *MatchExpression) MatchKeyNames(keys map[string]Nil) (bool, []MatchedElement, error) {
+func MatchKeyNames(m *nfdv1alpha1.MatchExpression, keys map[string]nfdv1alpha1.Nil) (bool, []MatchedElement, error) {
 	ret := []MatchedElement{}
 
 	for k := range keys {
-		if match, err := m.Match(true, k); err != nil {
+		if match, err := evaluateMatchExpression(m, true, k); err != nil {
 			return false, nil, err
 		} else if match {
 			ret = append(ret, MatchedElement{"Name": k})
@@ -237,11 +239,11 @@ func (m *MatchExpression) MatchKeyNames(keys map[string]Nil) (bool, []MatchedEle
 }
 
 // MatchValueNames evaluates the MatchExpression against names of a set of value features.
-func (m *MatchExpression) MatchValueNames(values map[string]string) (bool, []MatchedElement, error) {
+func MatchValueNames(m *nfdv1alpha1.MatchExpression, values map[string]string) (bool, []MatchedElement, error) {
 	ret := []MatchedElement{}
 
 	for k, v := range values {
-		if match, err := m.Match(true, k); err != nil {
+		if match, err := evaluateMatchExpression(m, true, k); err != nil {
 			return false, nil, err
 		} else if match {
 			ret = append(ret, MatchedElement{"Name": k, "Value": v})
@@ -269,11 +271,11 @@ func (m *MatchExpression) MatchValueNames(values map[string]string) (bool, []Mat
 
 // 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) {
+func MatchInstanceAttributeNames(m *nfdv1alpha1.MatchExpression, instances []nfdv1alpha1.InstanceFeature) ([]MatchedElement, error) {
 	ret := []MatchedElement{}
 
 	for _, i := range instances {
-		if match, _, err := m.MatchValueNames(i.Attributes); err != nil {
+		if match, _, err := MatchValueNames(m, i.Attributes); err != nil {
 			return nil, err
 		} else if match {
 			ret = append(ret, i.Attributes)
@@ -283,23 +285,22 @@ func (m *MatchExpression) MatchInstanceAttributeNames(instances []InstanceFeatur
 }
 
 // MatchKeys evaluates the MatchExpressionSet against a set of keys.
-func (m *MatchExpressionSet) MatchKeys(keys map[string]Nil) (bool, error) {
-	matched, _, err := m.MatchGetKeys(keys)
+func MatchKeys(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil) (bool, error) {
+	matched, _, err := MatchGetKeys(m, keys)
 	return matched, err
 }
 
 // MatchedElement holds one matched Instance.
-// +k8s:deepcopy-gen=false
 type MatchedElement map[string]string
 
 // MatchGetKeys evaluates the MatchExpressionSet against a set of keys and
 // returns all matched keys or nil if no match was found. Note that an empty
 // MatchExpressionSet returns a match with an empty slice of matched features.
-func (m *MatchExpressionSet) MatchGetKeys(keys map[string]Nil) (bool, []MatchedElement, error) {
+func MatchGetKeys(m *nfdv1alpha1.MatchExpressionSet, keys map[string]nfdv1alpha1.Nil) (bool, []MatchedElement, error) {
 	ret := make([]MatchedElement, 0, len(*m))
 
 	for n, e := range *m {
-		match, err := e.MatchKeys(n, keys)
+		match, err := evaluateMatchExpressionKeys(e, n, keys)
 		if err != nil {
 			return false, nil, err
 		}
@@ -314,19 +315,19 @@ func (m *MatchExpressionSet) MatchGetKeys(keys map[string]Nil) (bool, []MatchedE
 }
 
 // MatchValues evaluates the MatchExpressionSet against a set of key-value pairs.
-func (m *MatchExpressionSet) MatchValues(values map[string]string) (bool, error) {
-	matched, _, err := m.MatchGetValues(values)
+func MatchValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string) (bool, error) {
+	matched, _, err := MatchGetValues(m, values)
 	return matched, err
 }
 
 // MatchGetValues evaluates the MatchExpressionSet against a set of key-value
 // pairs and returns all matched key-value pairs. Note that an empty
 // MatchExpressionSet returns a match with an empty slice of matched features.
-func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []MatchedElement, error) {
+func MatchGetValues(m *nfdv1alpha1.MatchExpressionSet, values map[string]string) (bool, []MatchedElement, error) {
 	ret := make([]MatchedElement, 0, len(*m))
 
 	for n, e := range *m {
-		match, err := e.MatchValues(n, values)
+		match, err := evaluateMatchExpressionValues(e, n, values)
 		if err != nil {
 			return false, nil, err
 		}
@@ -343,8 +344,8 @@ func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []M
 // MatchInstances evaluates the MatchExpressionSet against a set of instance
 // features, each of which is an individual set of key-value pairs
 // (attributes).
-func (m *MatchExpressionSet) MatchInstances(instances []InstanceFeature) (bool, error) {
-	v, err := m.MatchGetInstances(instances)
+func MatchInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) (bool, error) {
+	v, err := MatchGetInstances(m, instances)
 	return len(v) > 0, err
 }
 
@@ -352,11 +353,11 @@ func (m *MatchExpressionSet) MatchInstances(instances []InstanceFeature) (bool,
 // features, each of which is an individual set of key-value pairs
 // (attributes). A slice containing all matching instances is returned. An
 // empty (non-nil) slice is returned if no matching instances were found.
-func (m *MatchExpressionSet) MatchGetInstances(instances []InstanceFeature) ([]MatchedElement, error) {
+func MatchGetInstances(m *nfdv1alpha1.MatchExpressionSet, instances []nfdv1alpha1.InstanceFeature) ([]MatchedElement, error) {
 	ret := []MatchedElement{}
 
 	for _, i := range instances {
-		if match, err := m.MatchValues(i.Attributes); err != nil {
+		if match, err := MatchValues(m, i.Attributes); err != nil {
 			return nil, err
 		} else if match {
 			ret = append(ret, i.Attributes)
diff --git a/pkg/apis/nfd/v1alpha1/rule.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go
similarity index 80%
rename from pkg/apis/nfd/v1alpha1/rule.go
rename to pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go
index b0936efb8..dd436d115 100644
--- a/pkg/apis/nfd/v1alpha1/rule.go
+++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1alpha1
+package nodefeaturerule
 
 import (
 	"bytes"
@@ -27,6 +27,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/klog/v2"
 
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 )
 
@@ -41,7 +42,7 @@ type RuleOutput struct {
 }
 
 // Execute the rule against a set of input features.
-func (r *Rule) Execute(features *Features) (RuleOutput, error) {
+func Execute(r *nfdv1alpha1.Rule, features *nfdv1alpha1.Features) (RuleOutput, error) {
 	labels := make(map[string]string)
 	vars := make(map[string]string)
 
@@ -49,7 +50,7 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) {
 		// Logical OR over the matchAny matchers
 		matched := false
 		for _, matcher := range r.MatchAny {
-			if isMatch, matches, err := matcher.match(features); err != nil {
+			if isMatch, matches, err := evaluateMatchAnyElem(&matcher, features); err != nil {
 				return RuleOutput{}, err
 			} else if isMatch {
 				matched = true
@@ -62,10 +63,10 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) {
 					break
 				}
 
-				if err := r.executeLabelsTemplate(matches, labels); err != nil {
+				if err := executeLabelsTemplate(r, matches, labels); err != nil {
 					return RuleOutput{}, err
 				}
-				if err := r.executeVarsTemplate(matches, vars); err != nil {
+				if err := executeVarsTemplate(r, matches, vars); err != nil {
 					return RuleOutput{}, err
 				}
 			}
@@ -77,17 +78,17 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) {
 	}
 
 	if len(r.MatchFeatures) > 0 {
-		if isMatch, matches, err := r.MatchFeatures.match(features); err != nil {
+		if isMatch, matches, err := evaluateFeatureMatcher(&r.MatchFeatures, features); err != nil {
 			return RuleOutput{}, err
 		} else if !isMatch {
 			klog.V(2).InfoS("rule did not match", "ruleName", r.Name)
 			return RuleOutput{}, nil
 		} else {
 			klog.V(4).InfoS("matchFeatures matched", "ruleName", r.Name, "matchedFeatures", utils.DelayedDumper(matches))
-			if err := r.executeLabelsTemplate(matches, labels); err != nil {
+			if err := executeLabelsTemplate(r, matches, labels); err != nil {
 				return RuleOutput{}, err
 			}
-			if err := r.executeVarsTemplate(matches, vars); err != nil {
+			if err := executeVarsTemplate(r, matches, vars); err != nil {
 				return RuleOutput{}, err
 			}
 		}
@@ -107,7 +108,7 @@ func (r *Rule) Execute(features *Features) (RuleOutput, error) {
 	return ret, nil
 }
 
-func (r *Rule) executeLabelsTemplate(in matchedFeatures, out map[string]string) error {
+func executeLabelsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string]string) error {
 	if r.LabelsTemplate == "" {
 		return nil
 	}
@@ -127,7 +128,7 @@ func (r *Rule) executeLabelsTemplate(in matchedFeatures, out map[string]string)
 	return nil
 }
 
-func (r *Rule) executeVarsTemplate(in matchedFeatures, out map[string]string) error {
+func executeVarsTemplate(r *nfdv1alpha1.Rule, in matchedFeatures, out map[string]string) error {
 	if r.VarsTemplate == "" {
 		return nil
 	}
@@ -151,11 +152,11 @@ type matchedFeatures map[string]domainMatchedFeatures
 
 type domainMatchedFeatures map[string][]MatchedElement
 
-func (e *MatchAnyElem) match(features *Features) (bool, matchedFeatures, error) {
-	return e.MatchFeatures.match(features)
+func evaluateMatchAnyElem(e *nfdv1alpha1.MatchAnyElem, features *nfdv1alpha1.Features) (bool, matchedFeatures, error) {
+	return evaluateFeatureMatcher(&e.MatchFeatures, features)
 }
 
-func (m *FeatureMatcher) match(features *Features) (bool, matchedFeatures, error) {
+func evaluateFeatureMatcher(m *nfdv1alpha1.FeatureMatcher, features *nfdv1alpha1.Features) (bool, matchedFeatures, error) {
 	matches := make(matchedFeatures, len(*m))
 
 	// Logical AND over the terms
@@ -180,30 +181,30 @@ func (m *FeatureMatcher) match(features *Features) (bool, matchedFeatures, error
 		var err error
 		if f, ok := features.Flags[featureName]; ok {
 			if term.MatchExpressions != nil {
-				isMatch, matchedElems, err = term.MatchExpressions.MatchGetKeys(f.Elements)
+				isMatch, matchedElems, err = MatchGetKeys(term.MatchExpressions, f.Elements)
 			}
 			var meTmp []MatchedElement
 			if err == nil && isMatch && term.MatchName != nil {
-				isMatch, meTmp, err = term.MatchName.MatchKeyNames(f.Elements)
+				isMatch, meTmp, err = MatchKeyNames(term.MatchName, f.Elements)
 				matchedElems = append(matchedElems, meTmp...)
 			}
 		} else if f, ok := features.Attributes[featureName]; ok {
 			if term.MatchExpressions != nil {
-				isMatch, matchedElems, err = term.MatchExpressions.MatchGetValues(f.Elements)
+				isMatch, matchedElems, err = MatchGetValues(term.MatchExpressions, f.Elements)
 			}
 			var meTmp []MatchedElement
 			if err == nil && isMatch && term.MatchName != nil {
-				isMatch, meTmp, err = term.MatchName.MatchValueNames(f.Elements)
+				isMatch, meTmp, err = MatchValueNames(term.MatchName, f.Elements)
 				matchedElems = append(matchedElems, meTmp...)
 			}
 		} else if f, ok := features.Instances[featureName]; ok {
 			if term.MatchExpressions != nil {
-				matchedElems, err = term.MatchExpressions.MatchGetInstances(f.Elements)
+				matchedElems, err = MatchGetInstances(term.MatchExpressions, f.Elements)
 				isMatch = len(matchedElems) > 0
 			}
 			var meTmp []MatchedElement
 			if err == nil && isMatch && term.MatchName != nil {
-				meTmp, err = term.MatchName.MatchInstanceAttributeNames(f.Elements)
+				meTmp, err = MatchInstanceAttributeNames(term.MatchName, f.Elements)
 				isMatch = len(meTmp) > 0
 				matchedElems = append(matchedElems, meTmp...)
 
diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go
index 09f9582ae..2f5dfebfe 100644
--- a/pkg/nfd-master/nfd-master.go
+++ b/pkg/nfd-master/nfd-master.go
@@ -54,6 +54,7 @@ import (
 
 	"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
 	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+	"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1/nodefeaturerule"
 	"sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/validate"
 	pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
@@ -970,7 +971,7 @@ func (m *nfdMaster) processNodeFeatureRule(nodeName string, features *nfdv1alpha
 			klog.InfoS("executing NodeFeatureRule", "nodefeaturerule", klog.KObj(spec), "nodeName", nodeName)
 		}
 		for _, rule := range spec.Spec.Rules {
-			ruleOut, err := rule.Execute(features)
+			ruleOut, err := nodefeaturerule.Execute(&rule, features)
 			if err != nil {
 				klog.ErrorS(err, "failed to process rule", "ruleName", rule.Name, "nodefeaturerule", klog.KObj(spec), "nodeName", nodeName)
 				nfrProcessingErrors.Inc()
diff --git a/source/custom/custom.go b/source/custom/custom.go
index 17ef9fff9..2655279f0 100644
--- a/source/custom/custom.go
+++ b/source/custom/custom.go
@@ -23,6 +23,7 @@ import (
 	"k8s.io/klog/v2"
 
 	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+	nodefeaturerule "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1/nodefeaturerule"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/source"
 	api "sigs.k8s.io/node-feature-discovery/source/custom/api"
@@ -92,7 +93,7 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
 	klog.V(2).InfoS("resolving custom features", "configuration", utils.DelayedDumper(allFeatureConfig))
 	// Iterate over features
 	for _, rule := range allFeatureConfig {
-		ruleOut, err := rule.Execute(features)
+		ruleOut, err := nodefeaturerule.Execute(&rule, features)
 		if err != nil {
 			klog.ErrorS(err, "failed to execute rule")
 			continue


Test patch:
diff --git a/pkg/apis/nfd/v1alpha1/expression_test.go b/pkg/apis/nfd/v1alpha1/expression_test.go
deleted file mode 100644
index 150f45415..000000000
--- a/pkg/apis/nfd/v1alpha1/expression_test.go
+++ /dev/null
@@ -1,457 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v1alpha1_test
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"sigs.k8s.io/yaml"
-
-	api "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
-)
-
-type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool
-
-type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool
-
-func TestMatch(t *testing.T) {
-	type V = api.MatchValue
-	type TC struct {
-		name   string
-		op     api.MatchOp
-		values V
-		input  interface{}
-		valid  bool
-		result BoolAssertionFunc
-	}
-
-	tcs := []TC{
-		{name: "MatchAny-1", op: api.MatchAny, result: assert.True},
-		{name: "MatchAny-2", op: api.MatchAny, input: "2", valid: false, result: assert.True},
-
-		{name: "MatchIn-1", op: api.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.False},
-		{name: "MatchIn-2", op: api.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.False},
-		{name: "MatchIn-3", op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False},
-		{name: "MatchIn-4", op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.True},
-
-		{name: "MatchNotIn-1", op: api.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.False},
-		{name: "MatchNotIn-2", op: api.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.True},
-		{name: "MatchNotIn-3", op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False},
-		{name: "MatchNotIn-4", op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.False},
-
-		{name: "MatchInRegexp-1", op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.False},
-		{name: "MatchInRegexp-2", op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.True},
-		{name: "MatchInRegexp-3", op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.False},
-		{name: "MatchInRegexp-4", op: api.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.True},
-
-		{name: "MatchExists-1", op: api.MatchExists, input: nil, valid: false, result: assert.False},
-		{name: "MatchExists-2", op: api.MatchExists, input: nil, valid: true, result: assert.True},
-
-		{name: "MatchDoesNotExist-1", op: api.MatchDoesNotExist, input: false, valid: false, result: assert.True},
-		{name: "MatchDoesNotExist-2", op: api.MatchDoesNotExist, input: false, valid: true, result: assert.False},
-
-		{name: "MatchGt-1", op: api.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.False},
-		{name: "MatchGt-2", op: api.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.False},
-		{name: "MatchGt-3", op: api.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.True},
-		{name: "MatchGt-4", op: api.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.True},
-
-		{name: "MatchLt-1", op: api.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.False},
-		{name: "MatchLt-2", op: api.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.False},
-		{name: "MatchLt-3", op: api.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.False},
-		{name: "MatchLt-4", op: api.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.True},
-
-		{name: "MatchGtLt-1", op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.False},
-		{name: "MatchGtLt-2", op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.False},
-		{name: "MatchGtLt-3", op: api.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.False},
-		{name: "MatchGtLt-4", op: api.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.True},
-
-		{name: "MatchIsTrue-1", op: api.MatchIsTrue, input: true, valid: false, result: assert.False},
-		{name: "MatchIsTrue-2", op: api.MatchIsTrue, input: true, valid: true, result: assert.True},
-		{name: "MatchIsTrue-3", op: api.MatchIsTrue, input: false, valid: true, result: assert.False},
-
-		{name: "MatchIsFalse-1", op: api.MatchIsFalse, input: "false", valid: false, result: assert.False},
-		{name: "MatchIsFalse-2", op: api.MatchIsFalse, input: "false", valid: true, result: assert.True},
-		{name: "MatchIsFalse-3", op: api.MatchIsFalse, input: "true", valid: true, result: assert.False},
-	}
-
-	for _, tc := range tcs {
-		t.Run(tc.name, func(t *testing.T) {
-			me := api.MatchExpression{Op: tc.op, Value: tc.values}
-			res, err := me.Match(tc.valid, tc.input)
-			tc.result(t, res)
-			assert.Nil(t, err)
-		})
-	}
-
-	// Error cases
-	tcs = []TC{
-		{name: "MatchAny-err-1", op: api.MatchAny, values: V{"1"}, input: "val"},
-
-		{name: "MatchIn-err-1", op: api.MatchIn, input: "val"},
-
-		{name: "MatchNotIn-err-1", op: api.MatchNotIn, input: "val"},
-
-		{name: "MatchInRegexp-err-1", op: api.MatchInRegexp, input: "val"},
-		{name: "MatchInRegexp-err-2", op: api.MatchInRegexp, values: V{"("}, input: "val"},
-
-		{name: "MatchExists-err-1", op: api.MatchExists, values: V{"1"}},
-
-		{name: "MatchDoesNotExist-err-1", op: api.MatchDoesNotExist, values: V{"1"}},
-
-		{name: "MatchGt-err-1", op: api.MatchGt, input: "1"},
-		{name: "MatchGt-err-2", op: api.MatchGt, values: V{"1", "2"}, input: "1"},
-		{name: "MatchGt-err-3", op: api.MatchGt, values: V{""}, input: "1"},
-		{name: "MatchGt-err-4", op: api.MatchGt, values: V{"2"}, input: "3a"},
-
-		{name: "MatchLt-err-1", op: api.MatchLt, input: "1"},
-		{name: "MatchLt-err-2", op: api.MatchLt, values: V{"1", "2", "3"}, input: "1"},
-		{name: "MatchLt-err-3", op: api.MatchLt, values: V{"a"}, input: "1"},
-		{name: "MatchLt-err-4", op: api.MatchLt, values: V{"2"}, input: "1.0"},
-
-		{name: "MatchGtLt-err-1", op: api.MatchGtLt, input: "1"},
-		{name: "MatchGtLt-err-2", op: api.MatchGtLt, values: V{"1"}, input: "1"},
-		{name: "MatchGtLt-err-3", op: api.MatchGtLt, values: V{"2", "1"}, input: "1"},
-		{name: "MatchGtLt-err-4", op: api.MatchGtLt, values: V{"1", "2", "3"}, input: "1"},
-		{name: "MatchGtLt-err-5", op: api.MatchGtLt, values: V{"a", "2"}, input: "1"},
-		{name: "MatchGtLt-err-6", op: api.MatchGtLt, values: V{"1", "10"}, input: "1.0"},
-
-		{name: "MatchIsTrue-err-1", op: api.MatchIsTrue, values: V{"1"}, input: "true"},
-
-		{name: "MatchIsFalse-err-1", op: api.MatchIsFalse, values: V{"1", "2"}, input: "false"},
-
-		{name: "invalid-op-err", op: "non-existent-op", values: V{"1"}, input: 1},
-	}
-
-	for _, tc := range tcs {
-		t.Run(tc.name, func(t *testing.T) {
-			me := api.MatchExpression{Op: tc.op, Value: tc.values}
-			res, err := me.Match(true, tc.input)
-			assert.False(t, res)
-			assert.NotNil(t, err)
-		})
-	}
-}
-
-func TestMatchKeys(t *testing.T) {
-	type V = api.MatchValue
-	type I = map[string]api.Nil
-	type TC struct {
-		name   string
-		key    string
-		op     api.MatchOp
-		values V
-		input  I
-		result BoolAssertionFunc
-		err    ValueAssertionFunc
-	}
-
-	tcs := []TC{
-		{name: "1", op: api.MatchAny, result: assert.True, err: assert.Nil},
-
-		{name: "2", op: api.MatchExists, key: "foo", input: nil, result: assert.False, err: assert.Nil},
-		{name: "3", op: api.MatchExists, key: "foo", input: I{"bar": {}}, result: assert.False, err: assert.Nil},
-		{name: "4", op: api.MatchExists, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.True, err: assert.Nil},
-
-		{name: "5", op: api.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil},
-		{name: "6", op: api.MatchDoesNotExist, key: "foo", input: I{}, result: assert.True, err: assert.Nil},
-		{name: "7", op: api.MatchDoesNotExist, key: "foo", input: I{"bar": {}}, result: assert.True, err: assert.Nil},
-		{name: "8", op: api.MatchDoesNotExist, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.False, err: assert.Nil},
-
-		// All other ops should return an error
-		{name: "9", op: api.MatchIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil},
-		{name: "10", op: api.MatchNotIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil},
-		{name: "11", op: api.MatchInRegexp, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil},
-		{name: "12", op: api.MatchGt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil},
-		{name: "13", op: api.MatchLt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil},
-		{name: "14", op: api.MatchGtLt, values: V{"1", "10"}, key: "foo", result: assert.False, err: assert.NotNil},
-		{name: "15", op: api.MatchIsTrue, key: "foo", result: assert.False, err: assert.NotNil},
-		{name: "16", op: api.MatchIsFalse, key: "foo", result: assert.False, err: assert.NotNil},
-	}
-
-	for _, tc := range tcs {
-		t.Run(tc.name, func(t *testing.T) {
-			me := api.MatchExpression{Op: tc.op, Value: tc.values}
-			res, err := me.MatchKeys(tc.key, tc.input)
-			tc.result(t, res)
-			tc.err(t, err)
-		})
-	}
-}
-
-func TestMatchValues(t *testing.T) {
-	type V = []string
-	type I = map[string]string
-
-	type TC struct {
-		name   string
-		op     api.MatchOp
-		values V
-		key    string
-		input  I
-		result BoolAssertionFunc
-		err    ValueAssertionFunc
-	}
-
-	tcs := []TC{
-		{name: "1", op: api.MatchAny, result: assert.True, err: assert.Nil},
-
-		{name: "2", op: api.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil},
-		{name: "3", op: api.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.False, err: assert.Nil},
-		{name: "4", op: api.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil},
-
-		{name: "5", op: api.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil},
-		{name: "6", op: api.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.True, err: assert.Nil},
-		{name: "7", op: api.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.False, err: assert.Nil},
-
-		{name: "8", op: api.MatchInRegexp, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil},
-		{name: "9", op: api.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "9"}, result: assert.False, err: assert.Nil},
-		{name: "10", op: api.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil},
-
-		{name: "11", op: api.MatchExists, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil},
-		{name: "12", op: api.MatchExists, key: "foo", input: I{"foo": "1"}, result: assert.True, err: assert.Nil},
-
-		{name: "13", op: api.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil},
-		{name: "14", op: api.MatchDoesNotExist, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil},
-
-		{name: "15", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3"}, result: assert.False, err: assert.Nil},
-		{name: "16", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.False, err: assert.Nil},
-		{name: "17", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.True, err: assert.Nil},
-		{name: "18", op: api.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil},
-
-		{name: "19", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil},
-		{name: "20", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.False, err: assert.Nil},
-		{name: "21", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil},
-		{name: "22", op: api.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil},
-
-		{name: "23", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil},
-		{name: "24", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.False, err: assert.Nil},
-		{name: "25", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.False, err: assert.Nil},
-		{name: "26", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil},
-		{name: "27", op: api.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil},
-
-		{name: "28", op: api.MatchIsTrue, key: "foo", result: assert.False, err: assert.Nil},
-		{name: "29", op: api.MatchIsTrue, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil},
-		{name: "30", op: api.MatchIsTrue, key: "foo", input: I{"foo": "true"}, result: assert.True, err: assert.Nil},
-
-		{name: "31", op: api.MatchIsFalse, key: "foo", input: I{"foo": "true"}, result: assert.False, err: assert.Nil},
-		{name: "32", op: api.MatchIsFalse, key: "foo", input: I{"foo": "false"}, result: assert.True, err: assert.Nil},
-	}
-
-	for _, tc := range tcs {
-		t.Run(tc.name, func(t *testing.T) {
-			me := api.MatchExpression{Op: tc.op, Value: tc.values}
-			res, err := me.MatchValues(tc.key, tc.input)
-			tc.result(t, res)
-			tc.err(t, err)
-		})
-	}
-}
-
-func TestMESMatchKeys(t *testing.T) {
-	type I = map[string]api.Nil
-	type O = []api.MatchedElement
-	type TC struct {
-		name   string
-		mes    string
-		input  I
-		output O
-		result BoolAssertionFunc
-		err    ValueAssertionFunc
-	}
-
-	tcs := []TC{
-		{output: O{}, result: assert.True, err: assert.Nil},
-
-		{input: I{}, output: O{}, result: assert.True, err: assert.Nil},
-
-		{input: I{"foo": {}}, output: O{}, result: assert.True, err: assert.Nil},
-
-		{mes: `
-foo: { op: DoesNotExist }
-bar: { op: Exists }
-`,
-			input:  I{"bar": {}, "baz": {}, "buzz": {}},
-			output: O{{"Name": "bar"}, {"Name": "foo"}},
-			result: assert.True, err: assert.Nil},
-
-		{mes: `
-foo: { op: DoesNotExist }
-bar: { op: Exists }
-`,
-			input:  I{"foo": {}, "bar": {}, "baz": {}},
-			output: nil,
-			result: assert.False, err: assert.Nil},
-
-		{mes: `
-foo: { op: In, value: ["bar"] }
-bar: { op: Exists }
-`,
-			input:  I{"bar": {}, "baz": {}},
-			output: nil,
-			result: assert.False, err: assert.NotNil},
-	}
-
-	for _, tc := range tcs {
-		t.Run(tc.name, func(t *testing.T) {
-			mes := &api.MatchExpressionSet{}
-			if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
-				t.Fatal("failed to parse data of test case")
-			}
-
-			res, out, err := mes.MatchGetKeys(tc.input)
-			tc.result(t, res)
-			assert.Equal(t, tc.output, out)
-			tc.err(t, err)
-
-			res, err = mes.MatchKeys(tc.input)
-			tc.result(t, res)
-			tc.err(t, err)
-		})
-	}
-}
-
-func TestMESMatchValues(t *testing.T) {
-	type I = map[string]string
-	type O = []api.MatchedElement
-	type TC struct {
-		name   string
-		mes    string
-		input  I
-		output O
-		result BoolAssertionFunc
-		err    ValueAssertionFunc
-	}
-
-	tcs := []TC{
-		{name: "1", output: O{}, result: assert.True, err: assert.Nil},
-
-		{name: "2", input: I{}, output: O{}, result: assert.True, err: assert.Nil},
-
-		{name: "3", input: I{"foo": "bar"}, output: O{}, result: assert.True, err: assert.Nil},
-
-		{name: "4",
-			mes: `
-foo: { op: Exists }
-bar: { op: In, value: ["val", "wal"] }
-baz: { op: Gt, value: ["10"] }
-`,
-			input:  I{"bar": "val"},
-			result: assert.False, err: assert.Nil},
-
-		{name: "5",
-			mes: `
-foo: { op: Exists }
-bar: { op: In, value: ["val", "wal"] }
-baz: { op: Gt, value: ["10"] }
-`,
-			input:  I{"foo": "1", "bar": "val", "baz": "123", "buzz": "light"},
-			output: O{{"Name": "bar", "Value": "val"}, {"Name": "baz", "Value": "123"}, {"Name": "foo", "Value": "1"}},
-			result: assert.True, err: assert.Nil},
-
-		{name: "5",
-			mes: `
-foo: { op: Exists }
-bar: { op: In, value: ["val"] }
-baz: { op: Gt, value: ["10"] }
-`,
-			input:  I{"foo": "1", "bar": "val", "baz": "123.0"},
-			result: assert.False, err: assert.NotNil},
-	}
-
-	for _, tc := range tcs {
-		t.Run(tc.name, func(t *testing.T) {
-			mes := &api.MatchExpressionSet{}
-			if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
-				t.Fatal("failed to parse data of test case")
-			}
-
-			res, out, err := mes.MatchGetValues(tc.input)
-			tc.result(t, res)
-			assert.Equal(t, tc.output, out)
-			tc.err(t, err)
-
-			res, err = mes.MatchValues(tc.input)
-			tc.result(t, res)
-			tc.err(t, err)
-		})
-	}
-}
-
-func TestMESMatchInstances(t *testing.T) {
-	type I = api.InstanceFeature
-	type O = []api.MatchedElement
-	type A = map[string]string
-	type TC struct {
-		name   string
-		mes    string
-		input  []I
-		output O
-		result BoolAssertionFunc
-		err    ValueAssertionFunc
-	}
-
-	tcs := []TC{
-		{name: "1", output: O{}, result: assert.False, err: assert.Nil}, // nil instances -> false
-
-		{name: "2", input: []I{}, output: O{}, result: assert.False, err: assert.Nil}, // zero instances -> false
-
-		{name: "3", input: []I{I{Attributes: A{}}}, output: O{A{}}, result: assert.True, err: assert.Nil}, // one "empty" instance
-
-		{name: "4",
-			mes: `
-foo: { op: Exists }
-bar: { op: Lt, value: ["10"] }
-`,
-			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}},
-			output: O{},
-			result: assert.False, err: assert.Nil},
-
-		{name: "5",
-			mes: `
-foo: { op: Exists }
-bar: { op: Lt, value: ["10"] }
-`,
-			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}},
-			output: O{A{"foo": "2", "bar": "1"}},
-			result: assert.True, err: assert.Nil},
-
-		{name: "6",
-			mes: `
-bar: { op: Lt, value: ["10"] }
-`,
-			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}},
-			result: assert.False, err: assert.NotNil},
-	}
-
-	for _, tc := range tcs {
-		t.Run(tc.name, func(t *testing.T) {
-			mes := &api.MatchExpressionSet{}
-			if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
-				t.Fatal("failed to parse data of test case")
-			}
-
-			out, err := mes.MatchGetInstances(tc.input)
-			assert.Equal(t, tc.output, out)
-			tc.err(t, err)
-
-			res, err := mes.MatchInstances(tc.input)
-			tc.result(t, res)
-			tc.err(t, err)
-		})
-	}
-}
diff --git a/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression-api_test.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression-api_test.go
new file mode 100644
index 000000000..e9974d6df
--- /dev/null
+++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression-api_test.go
@@ -0,0 +1,225 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package nodefeaturerule_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/yaml"
+
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+	api "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1/nodefeaturerule"
+)
+
+type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool
+
+type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool
+
+func TestMatchKeys(t *testing.T) {
+	type I = map[string]nfdv1alpha1.Nil
+	type O = []api.MatchedElement
+	type TC struct {
+		name   string
+		mes    string
+		input  I
+		output O
+		result BoolAssertionFunc
+		err    ValueAssertionFunc
+	}
+
+	tcs := []TC{
+		{output: O{}, result: assert.True, err: assert.Nil},
+
+		{input: I{}, output: O{}, result: assert.True, err: assert.Nil},
+
+		{input: I{"foo": {}}, output: O{}, result: assert.True, err: assert.Nil},
+
+		{mes: `
+foo: { op: DoesNotExist }
+bar: { op: Exists }
+`,
+			input:  I{"bar": {}, "baz": {}, "buzz": {}},
+			output: O{{"Name": "bar"}, {"Name": "foo"}},
+			result: assert.True, err: assert.Nil},
+
+		{mes: `
+foo: { op: DoesNotExist }
+bar: { op: Exists }
+`,
+			input:  I{"foo": {}, "bar": {}, "baz": {}},
+			output: nil,
+			result: assert.False, err: assert.Nil},
+
+		{mes: `
+foo: { op: In, value: ["bar"] }
+bar: { op: Exists }
+`,
+			input:  I{"bar": {}, "baz": {}},
+			output: nil,
+			result: assert.False, err: assert.NotNil},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			mes := &nfdv1alpha1.MatchExpressionSet{}
+			if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
+				t.Fatal("failed to parse data of test case")
+			}
+
+			res, out, err := api.MatchGetKeys(mes, tc.input)
+			tc.result(t, res)
+			assert.Equal(t, tc.output, out)
+			tc.err(t, err)
+
+			res, err = api.MatchKeys(mes, tc.input)
+			tc.result(t, res)
+			tc.err(t, err)
+		})
+	}
+}
+
+func TestMatchValues(t *testing.T) {
+	type I = map[string]string
+	type O = []api.MatchedElement
+	type TC struct {
+		name   string
+		mes    string
+		input  I
+		output O
+		result BoolAssertionFunc
+		err    ValueAssertionFunc
+	}
+
+	tcs := []TC{
+		{name: "1", output: O{}, result: assert.True, err: assert.Nil},
+
+		{name: "2", input: I{}, output: O{}, result: assert.True, err: assert.Nil},
+
+		{name: "3", input: I{"foo": "bar"}, output: O{}, result: assert.True, err: assert.Nil},
+
+		{name: "4",
+			mes: `
+foo: { op: Exists }
+bar: { op: In, value: ["val", "wal"] }
+baz: { op: Gt, value: ["10"] }
+`,
+			input:  I{"bar": "val"},
+			result: assert.False, err: assert.Nil},
+
+		{name: "5",
+			mes: `
+foo: { op: Exists }
+bar: { op: In, value: ["val", "wal"] }
+baz: { op: Gt, value: ["10"] }
+`,
+			input:  I{"foo": "1", "bar": "val", "baz": "123", "buzz": "light"},
+			output: O{{"Name": "bar", "Value": "val"}, {"Name": "baz", "Value": "123"}, {"Name": "foo", "Value": "1"}},
+			result: assert.True, err: assert.Nil},
+
+		{name: "5",
+			mes: `
+foo: { op: Exists }
+bar: { op: In, value: ["val"] }
+baz: { op: Gt, value: ["10"] }
+`,
+			input:  I{"foo": "1", "bar": "val", "baz": "123.0"},
+			result: assert.False, err: assert.NotNil},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			mes := &nfdv1alpha1.MatchExpressionSet{}
+			if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
+				t.Fatal("failed to parse data of test case")
+			}
+
+			res, out, err := api.MatchGetValues(mes, tc.input)
+			tc.result(t, res)
+			assert.Equal(t, tc.output, out)
+			tc.err(t, err)
+
+			res, err = api.MatchValues(mes, tc.input)
+			tc.result(t, res)
+			tc.err(t, err)
+		})
+	}
+}
+
+func TestMatchInstances(t *testing.T) {
+	type I = nfdv1alpha1.InstanceFeature
+	type O = []api.MatchedElement
+	type A = map[string]string
+	type TC struct {
+		name   string
+		mes    string
+		input  []I
+		output O
+		result BoolAssertionFunc
+		err    ValueAssertionFunc
+	}
+
+	tcs := []TC{
+		{name: "1", output: O{}, result: assert.False, err: assert.Nil}, // nil instances -> false
+
+		{name: "2", input: []I{}, output: O{}, result: assert.False, err: assert.Nil}, // zero instances -> false
+
+		{name: "3", input: []I{I{Attributes: A{}}}, output: O{A{}}, result: assert.True, err: assert.Nil}, // one "empty" instance
+
+		{name: "4",
+			mes: `
+foo: { op: Exists }
+bar: { op: Lt, value: ["10"] }
+`,
+			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}},
+			output: O{},
+			result: assert.False, err: assert.Nil},
+
+		{name: "5",
+			mes: `
+foo: { op: Exists }
+bar: { op: Lt, value: ["10"] }
+`,
+			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}},
+			output: O{A{"foo": "2", "bar": "1"}},
+			result: assert.True, err: assert.Nil},
+
+		{name: "6",
+			mes: `
+bar: { op: Lt, value: ["10"] }
+`,
+			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}},
+			result: assert.False, err: assert.NotNil},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			mes := &nfdv1alpha1.MatchExpressionSet{}
+			if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
+				t.Fatal("failed to parse data of test case")
+			}
+
+			out, err := api.MatchGetInstances(mes, tc.input)
+			assert.Equal(t, tc.output, out)
+			tc.err(t, err)
+
+			res, err := api.MatchInstances(mes, tc.input)
+			tc.result(t, res)
+			tc.err(t, err)
+		})
+	}
+}
diff --git a/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression_test.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression_test.go
new file mode 100644
index 000000000..3b51ca12e
--- /dev/null
+++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/expression_test.go
@@ -0,0 +1,262 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package nodefeaturerule
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+)
+
+type BoolAssertionFunc func(assert.TestingT, bool, ...interface{}) bool
+
+type ValueAssertionFunc func(assert.TestingT, interface{}, ...interface{}) bool
+
+func TestEvaluateMatchExpression(t *testing.T) {
+	type V = nfdv1alpha1.MatchValue
+	type TC struct {
+		name   string
+		op     nfdv1alpha1.MatchOp
+		values V
+		input  interface{}
+		valid  bool
+		result BoolAssertionFunc
+	}
+
+	tcs := []TC{
+		{name: "MatchAny-1", op: nfdv1alpha1.MatchAny, result: assert.True},
+		{name: "MatchAny-2", op: nfdv1alpha1.MatchAny, input: "2", valid: false, result: assert.True},
+
+		{name: "MatchIn-1", op: nfdv1alpha1.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.False},
+		{name: "MatchIn-2", op: nfdv1alpha1.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.False},
+		{name: "MatchIn-3", op: nfdv1alpha1.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False},
+		{name: "MatchIn-4", op: nfdv1alpha1.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.True},
+
+		{name: "MatchNotIn-1", op: nfdv1alpha1.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.False},
+		{name: "MatchNotIn-2", op: nfdv1alpha1.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.True},
+		{name: "MatchNotIn-3", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.False},
+		{name: "MatchNotIn-4", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.False},
+
+		{name: "MatchInRegexp-1", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.False},
+		{name: "MatchInRegexp-2", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.True},
+		{name: "MatchInRegexp-3", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.False},
+		{name: "MatchInRegexp-4", op: nfdv1alpha1.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.True},
+
+		{name: "MatchExists-1", op: nfdv1alpha1.MatchExists, input: nil, valid: false, result: assert.False},
+		{name: "MatchExists-2", op: nfdv1alpha1.MatchExists, input: nil, valid: true, result: assert.True},
+
+		{name: "MatchDoesNotExist-1", op: nfdv1alpha1.MatchDoesNotExist, input: false, valid: false, result: assert.True},
+		{name: "MatchDoesNotExist-2", op: nfdv1alpha1.MatchDoesNotExist, input: false, valid: true, result: assert.False},
+
+		{name: "MatchGt-1", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.False},
+		{name: "MatchGt-2", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.False},
+		{name: "MatchGt-3", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.True},
+		{name: "MatchGt-4", op: nfdv1alpha1.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.True},
+
+		{name: "MatchLt-1", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.False},
+		{name: "MatchLt-2", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.False},
+		{name: "MatchLt-3", op: nfdv1alpha1.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.False},
+		{name: "MatchLt-4", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.True},
+
+		{name: "MatchGtLt-1", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.False},
+		{name: "MatchGtLt-2", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.False},
+		{name: "MatchGtLt-3", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.False},
+		{name: "MatchGtLt-4", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.True},
+
+		{name: "MatchIsTrue-1", op: nfdv1alpha1.MatchIsTrue, input: true, valid: false, result: assert.False},
+		{name: "MatchIsTrue-2", op: nfdv1alpha1.MatchIsTrue, input: true, valid: true, result: assert.True},
+		{name: "MatchIsTrue-3", op: nfdv1alpha1.MatchIsTrue, input: false, valid: true, result: assert.False},
+
+		{name: "MatchIsFalse-1", op: nfdv1alpha1.MatchIsFalse, input: "false", valid: false, result: assert.False},
+		{name: "MatchIsFalse-2", op: nfdv1alpha1.MatchIsFalse, input: "false", valid: true, result: assert.True},
+		{name: "MatchIsFalse-3", op: nfdv1alpha1.MatchIsFalse, input: "true", valid: true, result: assert.False},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values}
+			res, err := evaluateMatchExpression(me, tc.valid, tc.input)
+			tc.result(t, res)
+			assert.Nil(t, err)
+		})
+	}
+
+	// Error cases
+	tcs = []TC{
+		{name: "MatchAny-err-1", op: nfdv1alpha1.MatchAny, values: V{"1"}, input: "val"},
+
+		{name: "MatchIn-err-1", op: nfdv1alpha1.MatchIn, input: "val"},
+
+		{name: "MatchNotIn-err-1", op: nfdv1alpha1.MatchNotIn, input: "val"},
+
+		{name: "MatchInRegexp-err-1", op: nfdv1alpha1.MatchInRegexp, input: "val"},
+		{name: "MatchInRegexp-err-2", op: nfdv1alpha1.MatchInRegexp, values: V{"("}, input: "val"},
+
+		{name: "MatchExists-err-1", op: nfdv1alpha1.MatchExists, values: V{"1"}},
+
+		{name: "MatchDoesNotExist-err-1", op: nfdv1alpha1.MatchDoesNotExist, values: V{"1"}},
+
+		{name: "MatchGt-err-1", op: nfdv1alpha1.MatchGt, input: "1"},
+		{name: "MatchGt-err-2", op: nfdv1alpha1.MatchGt, values: V{"1", "2"}, input: "1"},
+		{name: "MatchGt-err-3", op: nfdv1alpha1.MatchGt, values: V{""}, input: "1"},
+		{name: "MatchGt-err-4", op: nfdv1alpha1.MatchGt, values: V{"2"}, input: "3a"},
+
+		{name: "MatchLt-err-1", op: nfdv1alpha1.MatchLt, input: "1"},
+		{name: "MatchLt-err-2", op: nfdv1alpha1.MatchLt, values: V{"1", "2", "3"}, input: "1"},
+		{name: "MatchLt-err-3", op: nfdv1alpha1.MatchLt, values: V{"a"}, input: "1"},
+		{name: "MatchLt-err-4", op: nfdv1alpha1.MatchLt, values: V{"2"}, input: "1.0"},
+
+		{name: "MatchGtLt-err-1", op: nfdv1alpha1.MatchGtLt, input: "1"},
+		{name: "MatchGtLt-err-2", op: nfdv1alpha1.MatchGtLt, values: V{"1"}, input: "1"},
+		{name: "MatchGtLt-err-3", op: nfdv1alpha1.MatchGtLt, values: V{"2", "1"}, input: "1"},
+		{name: "MatchGtLt-err-4", op: nfdv1alpha1.MatchGtLt, values: V{"1", "2", "3"}, input: "1"},
+		{name: "MatchGtLt-err-5", op: nfdv1alpha1.MatchGtLt, values: V{"a", "2"}, input: "1"},
+		{name: "MatchGtLt-err-6", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, input: "1.0"},
+
+		{name: "MatchIsTrue-err-1", op: nfdv1alpha1.MatchIsTrue, values: V{"1"}, input: "true"},
+
+		{name: "MatchIsFalse-err-1", op: nfdv1alpha1.MatchIsFalse, values: V{"1", "2"}, input: "false"},
+
+		{name: "invalid-op-err", op: "non-existent-op", values: V{"1"}, input: 1},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values}
+			res, err := evaluateMatchExpression(me, true, tc.input)
+			assert.False(t, res)
+			assert.NotNil(t, err)
+		})
+	}
+}
+
+func TestEvaluateMatchExpressionKeys(t *testing.T) {
+	type V = nfdv1alpha1.MatchValue
+	type I = map[string]nfdv1alpha1.Nil
+	type TC struct {
+		name   string
+		key    string
+		op     nfdv1alpha1.MatchOp
+		values V
+		input  I
+		result BoolAssertionFunc
+		err    ValueAssertionFunc
+	}
+
+	tcs := []TC{
+		{name: "1", op: nfdv1alpha1.MatchAny, result: assert.True, err: assert.Nil},
+
+		{name: "2", op: nfdv1alpha1.MatchExists, key: "foo", input: nil, result: assert.False, err: assert.Nil},
+		{name: "3", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"bar": {}}, result: assert.False, err: assert.Nil},
+		{name: "4", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.True, err: assert.Nil},
+
+		{name: "5", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil},
+		{name: "6", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{}, result: assert.True, err: assert.Nil},
+		{name: "7", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{"bar": {}}, result: assert.True, err: assert.Nil},
+		{name: "8", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{"bar": {}, "foo": {}}, result: assert.False, err: assert.Nil},
+
+		// All other ops should return an error
+		{name: "9", op: nfdv1alpha1.MatchIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil},
+		{name: "10", op: nfdv1alpha1.MatchNotIn, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil},
+		{name: "11", op: nfdv1alpha1.MatchInRegexp, values: V{"foo"}, key: "foo", result: assert.False, err: assert.NotNil},
+		{name: "12", op: nfdv1alpha1.MatchGt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil},
+		{name: "13", op: nfdv1alpha1.MatchLt, values: V{"1"}, key: "foo", result: assert.False, err: assert.NotNil},
+		{name: "14", op: nfdv1alpha1.MatchGtLt, values: V{"1", "10"}, key: "foo", result: assert.False, err: assert.NotNil},
+		{name: "15", op: nfdv1alpha1.MatchIsTrue, key: "foo", result: assert.False, err: assert.NotNil},
+		{name: "16", op: nfdv1alpha1.MatchIsFalse, key: "foo", result: assert.False, err: assert.NotNil},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values}
+			res, err := evaluateMatchExpressionKeys(me, tc.key, tc.input)
+			tc.result(t, res)
+			tc.err(t, err)
+		})
+	}
+}
+
+func TestEvaluateMatchExpressionValues(t *testing.T) {
+	type V = []string
+	type I = map[string]string
+
+	type TC struct {
+		name   string
+		op     nfdv1alpha1.MatchOp
+		values V
+		key    string
+		input  I
+		result BoolAssertionFunc
+		err    ValueAssertionFunc
+	}
+
+	tcs := []TC{
+		{name: "1", op: nfdv1alpha1.MatchAny, result: assert.True, err: assert.Nil},
+
+		{name: "2", op: nfdv1alpha1.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil},
+		{name: "3", op: nfdv1alpha1.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.False, err: assert.Nil},
+		{name: "4", op: nfdv1alpha1.MatchIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil},
+
+		{name: "5", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil},
+		{name: "6", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "3"}, result: assert.True, err: assert.Nil},
+		{name: "7", op: nfdv1alpha1.MatchNotIn, values: V{"1", "2"}, key: "foo", input: I{"foo": "2"}, result: assert.False, err: assert.Nil},
+
+		{name: "8", op: nfdv1alpha1.MatchInRegexp, values: V{"1", "2"}, key: "foo", input: I{"bar": "2"}, result: assert.False, err: assert.Nil},
+		{name: "9", op: nfdv1alpha1.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "9"}, result: assert.False, err: assert.Nil},
+		{name: "10", op: nfdv1alpha1.MatchInRegexp, values: V{"1", "[0-8]"}, key: "foo", input: I{"foo": "2"}, result: assert.True, err: assert.Nil},
+
+		{name: "11", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil},
+		{name: "12", op: nfdv1alpha1.MatchExists, key: "foo", input: I{"foo": "1"}, result: assert.True, err: assert.Nil},
+
+		{name: "13", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: nil, result: assert.True, err: assert.Nil},
+		{name: "14", op: nfdv1alpha1.MatchDoesNotExist, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil},
+
+		{name: "15", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3"}, result: assert.False, err: assert.Nil},
+		{name: "16", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.False, err: assert.Nil},
+		{name: "17", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.True, err: assert.Nil},
+		{name: "18", op: nfdv1alpha1.MatchGt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil},
+
+		{name: "19", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil},
+		{name: "20", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.False, err: assert.Nil},
+		{name: "21", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil},
+		{name: "22", op: nfdv1alpha1.MatchLt, values: V{"2"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil},
+
+		{name: "23", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1"}, result: assert.False, err: assert.Nil},
+		{name: "24", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.False, err: assert.Nil},
+		{name: "25", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.False, err: assert.Nil},
+		{name: "26", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.True, err: assert.Nil},
+		{name: "27", op: nfdv1alpha1.MatchGtLt, values: V{"-10", "10"}, key: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.False, err: assert.NotNil},
+
+		{name: "28", op: nfdv1alpha1.MatchIsTrue, key: "foo", result: assert.False, err: assert.Nil},
+		{name: "29", op: nfdv1alpha1.MatchIsTrue, key: "foo", input: I{"foo": "1"}, result: assert.False, err: assert.Nil},
+		{name: "30", op: nfdv1alpha1.MatchIsTrue, key: "foo", input: I{"foo": "true"}, result: assert.True, err: assert.Nil},
+
+		{name: "31", op: nfdv1alpha1.MatchIsFalse, key: "foo", input: I{"foo": "true"}, result: assert.False, err: assert.Nil},
+		{name: "32", op: nfdv1alpha1.MatchIsFalse, key: "foo", input: I{"foo": "false"}, result: assert.True, err: assert.Nil},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			me := &nfdv1alpha1.MatchExpression{Op: tc.op, Value: tc.values}
+			res, err := evaluateMatchExpressionValues(me, tc.key, tc.input)
+			tc.result(t, res)
+			tc.err(t, err)
+		})
+	}
+}
diff --git a/pkg/apis/nfd/v1alpha1/rule_test.go b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule_test.go
similarity index 60%
rename from pkg/apis/nfd/v1alpha1/rule_test.go
rename to pkg/apis/nfd/v1alpha1/nodefeaturerule/rule_test.go
index b3728359e..a642404a7 100644
--- a/pkg/apis/nfd/v1alpha1/rule_test.go
+++ b/pkg/apis/nfd/v1alpha1/nodefeaturerule/rule_test.go
@@ -14,217 +14,219 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1alpha1
+package nodefeaturerule
 
 import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 )
 
 // newMatchExpression returns a new MatchExpression instance.
-func newMatchExpression(op MatchOp, values ...string) *MatchExpression {
-	return &MatchExpression{
+func newMatchExpression(op nfdv1alpha1.MatchOp, values ...string) *nfdv1alpha1.MatchExpression {
+	return &nfdv1alpha1.MatchExpression{
 		Op:    op,
 		Value: values,
 	}
 }
 
 func TestRule(t *testing.T) {
-	f := &Features{}
-	r1 := Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}}
-	r2 := Rule{
+	f := &nfdv1alpha1.Features{}
+	r1 := &nfdv1alpha1.Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}}
+	r2 := &nfdv1alpha1.Rule{
 		Labels: map[string]string{"label-1": "label-val-1"},
 		Vars:   map[string]string{"var-1": "var-val-1"},
-		MatchFeatures: FeatureMatcher{
-			FeatureMatcherTerm{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain-1.kf-1",
-				MatchExpressions: &MatchExpressionSet{
-					"key-1": newMatchExpression(MatchExists),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"key-1": newMatchExpression(nfdv1alpha1.MatchExists),
 				},
 			},
 		},
 	}
 
 	// Test totally empty features
-	m, err := r1.Execute(f)
+	m, err := Execute(r1, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
 
-	_, err = r2.Execute(f)
+	_, err = Execute(r2, f)
 	assert.Error(t, err, "matching against a missing feature should have returned an error")
 
 	// Test properly initialized empty features
-	f = NewFeatures()
+	f = nfdv1alpha1.NewFeatures()
 
-	m, err = r1.Execute(f)
+	m, err = Execute(r1, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
 	assert.Empty(t, r1.Vars, "vars should be empty")
 
-	_, err = r2.Execute(f)
+	_, err = Execute(r2, f)
 	assert.Error(t, err, "matching against a missing feature type should have returned an error")
 
 	// Test empty feature sets
-	f.Flags["domain-1.kf-1"] = NewFlagFeatures()
-	f.Attributes["domain-1.vf-1"] = NewAttributeFeatures(nil)
-	f.Instances["domain-1.if-1"] = NewInstanceFeatures(nil)
+	f.Flags["domain-1.kf-1"] = nfdv1alpha1.NewFlagFeatures()
+	f.Attributes["domain-1.vf-1"] = nfdv1alpha1.NewAttributeFeatures(nil)
+	f.Instances["domain-1.if-1"] = nfdv1alpha1.NewInstanceFeatures(nil)
 
-	m, err = r1.Execute(f)
+	m, err = Execute(r1, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
 
-	m, err = r2.Execute(f)
+	m, err = Execute(r2, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "unexpected match")
 
 	// Test non-empty feature sets
-	f.Flags["domain-1.kf-1"].Elements["key-x"] = Nil{}
+	f.Flags["domain-1.kf-1"].Elements["key-x"] = nfdv1alpha1.Nil{}
 	f.Attributes["domain-1.vf-1"].Elements["key-1"] = "val-x"
-	f.Instances["domain-1.if-1"] = NewInstanceFeatures([]InstanceFeature{
-		*NewInstanceFeature(map[string]string{"attr-1": "val-x"})})
+	f.Instances["domain-1.if-1"] = nfdv1alpha1.NewInstanceFeatures([]nfdv1alpha1.InstanceFeature{
+		*nfdv1alpha1.NewInstanceFeature(map[string]string{"attr-1": "val-x"})})
 
-	m, err = r1.Execute(f)
+	m, err = Execute(r1, f)
 	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{
+	r1.MatchFeatures = nfdv1alpha1.FeatureMatcher{
+		nfdv1alpha1.FeatureMatcherTerm{
 			Feature:          "domain-1.kf-1",
-			MatchExpressions: &MatchExpressionSet{},
+			MatchExpressions: &nfdv1alpha1.MatchExpressionSet{},
 		},
 	}
-	m, err = r1.Execute(f)
+	m, err = Execute(r1, 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)
+	m, err = Execute(r2, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "keys should not have matched")
 
-	f.Flags["domain-1.kf-1"].Elements["key-1"] = Nil{}
-	m, err = r2.Execute(f)
+	f.Flags["domain-1.kf-1"].Elements["key-1"] = nfdv1alpha1.Nil{}
+	m, err = Execute(r2, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r2.Labels, m.Labels, "keys should have matched")
 	assert.Equal(t, r2.Vars, m.Vars, "vars should be present")
 
 	// Match "value" features
-	r3 := Rule{
+	r3 := &nfdv1alpha1.Rule{
 		Labels: map[string]string{"label-3": "label-val-3", "empty": ""},
-		MatchFeatures: FeatureMatcher{
-			FeatureMatcherTerm{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain-1.vf-1",
-				MatchExpressions: &MatchExpressionSet{
-					"key-1": newMatchExpression(MatchIn, "val-1"),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"key-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1"),
 				},
 			},
 		},
 	}
-	m, err = r3.Execute(f)
+	m, err = Execute(r3, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "values should not have matched")
 
 	f.Attributes["domain-1.vf-1"].Elements["key-1"] = "val-1"
-	m, err = r3.Execute(f)
+	m, err = Execute(r3, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r3.Labels, m.Labels, "values should have matched")
 
 	// Match "instance" features
-	r4 := Rule{
+	r4 := &nfdv1alpha1.Rule{
 		Labels: map[string]string{"label-4": "label-val-4"},
-		MatchFeatures: FeatureMatcher{
-			FeatureMatcherTerm{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain-1.if-1",
-				MatchExpressions: &MatchExpressionSet{
-					"attr-1": newMatchExpression(MatchIn, "val-1"),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"attr-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1"),
 				},
 			},
 		},
 	}
-	m, err = r4.Execute(f)
+	m, err = Execute(r4, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "instances should not have matched")
 
 	f.Instances["domain-1.if-1"].Elements[0].Attributes["attr-1"] = "val-1"
-	m, err = r4.Execute(f)
+	m, err = Execute(r4, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r4.Labels, m.Labels, "instances should have matched")
 
 	// Test multiple feature matchers
-	r5 := Rule{
+	r5 := &nfdv1alpha1.Rule{
 		Labels: map[string]string{"label-5": "label-val-5"},
-		MatchFeatures: FeatureMatcher{
-			FeatureMatcherTerm{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain-1.vf-1",
-				MatchExpressions: &MatchExpressionSet{
-					"key-1": newMatchExpression(MatchIn, "val-x"),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"key-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-x"),
 				},
 			},
-			FeatureMatcherTerm{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain-1.if-1",
-				MatchExpressions: &MatchExpressionSet{
-					"attr-1": newMatchExpression(MatchIn, "val-1"),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"attr-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1"),
 				},
 			},
 		},
 	}
-	m, err = r5.Execute(f)
+	m, err = Execute(r5, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "instances should not have matched")
 
-	(*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(MatchIn, "val-1")
-	m, err = r5.Execute(f)
+	(*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1")
+	m, err = Execute(r5, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
 
 	// Test MatchAny
-	r5.MatchAny = []MatchAnyElem{
+	r5.MatchAny = []nfdv1alpha1.MatchAnyElem{
 		{
-			MatchFeatures: FeatureMatcher{
-				FeatureMatcherTerm{
+			MatchFeatures: nfdv1alpha1.FeatureMatcher{
+				nfdv1alpha1.FeatureMatcherTerm{
 					Feature: "domain-1.kf-1",
-					MatchExpressions: &MatchExpressionSet{
-						"key-na": newMatchExpression(MatchExists),
+					MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+						"key-na": newMatchExpression(nfdv1alpha1.MatchExists),
 					},
 				},
 			},
 		},
 	}
-	m, err = r5.Execute(f)
+	m, err = Execute(r5, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "instances should not have matched")
 
 	r5.MatchAny = append(r5.MatchAny,
-		MatchAnyElem{
-			MatchFeatures: FeatureMatcher{
-				FeatureMatcherTerm{
+		nfdv1alpha1.MatchAnyElem{
+			MatchFeatures: nfdv1alpha1.FeatureMatcher{
+				nfdv1alpha1.FeatureMatcherTerm{
 					Feature: "domain-1.kf-1",
-					MatchExpressions: &MatchExpressionSet{
-						"key-1": newMatchExpression(MatchExists),
+					MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+						"key-1": newMatchExpression(nfdv1alpha1.MatchExists),
 					},
 				},
 			},
 		})
-	(*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(MatchIn, "val-1")
-	m, err = r5.Execute(f)
+	(*r5.MatchFeatures[0].MatchExpressions)["key-1"] = newMatchExpression(nfdv1alpha1.MatchIn, "val-1")
+	m, err = Execute(r5, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
 }
 
 func TestTemplating(t *testing.T) {
-	f := &Features{
-		Flags: map[string]FlagFeatureSet{
+	f := &nfdv1alpha1.Features{
+		Flags: map[string]nfdv1alpha1.FlagFeatureSet{
 			"domain_1.kf_1": {
-				Elements: map[string]Nil{
+				Elements: map[string]nfdv1alpha1.Nil{
 					"key-a": {},
 					"key-b": {},
 					"key-c": {},
 				},
 			},
 		},
-		Attributes: map[string]AttributeFeatureSet{
+		Attributes: map[string]nfdv1alpha1.AttributeFeatureSet{
 			"domain_1.vf_1": {
 				Elements: map[string]string{
 					"key-1": "val-1",
@@ -234,9 +236,9 @@ func TestTemplating(t *testing.T) {
 				},
 			},
 		},
-		Instances: map[string]InstanceFeatureSet{
+		Instances: map[string]nfdv1alpha1.InstanceFeatureSet{
 			"domain_1.if_1": {
-				Elements: []InstanceFeature{
+				Elements: []nfdv1alpha1.InstanceFeature{
 					{
 						Attributes: map[string]string{
 							"attr-1": "1",
@@ -267,7 +269,7 @@ func TestTemplating(t *testing.T) {
 		},
 	}
 
-	r1 := Rule{
+	r1 := &nfdv1alpha1.Rule{
 		Labels: map[string]string{"label-1": "label-val-1"},
 		LabelsTemplate: `
 label-1=will-be-overridden
@@ -284,34 +286,34 @@ var-1=value-will-be-overridden-by-vars
 var-2=
 {{range .domain_1.kf_1}}kf-{{.Name}}=true
 {{end}}`,
-		MatchFeatures: FeatureMatcher{
-			FeatureMatcherTerm{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain_1.kf_1",
-				MatchExpressions: &MatchExpressionSet{
-					"key-a": newMatchExpression(MatchExists),
-					"key-c": newMatchExpression(MatchExists),
-					"foo":   newMatchExpression(MatchDoesNotExist),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"key-a": newMatchExpression(nfdv1alpha1.MatchExists),
+					"key-c": newMatchExpression(nfdv1alpha1.MatchExists),
+					"foo":   newMatchExpression(nfdv1alpha1.MatchDoesNotExist),
 				},
 			},
-			FeatureMatcherTerm{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain_1.vf_1",
-				MatchExpressions: &MatchExpressionSet{
-					"key-1": newMatchExpression(MatchIn, "val-1", "val-2"),
-					"bar":   newMatchExpression(MatchDoesNotExist),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"key-1": newMatchExpression(nfdv1alpha1.MatchIn, "val-1", "val-2"),
+					"bar":   newMatchExpression(nfdv1alpha1.MatchDoesNotExist),
 				},
 			},
-			FeatureMatcherTerm{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain_1.if_1",
-				MatchExpressions: &MatchExpressionSet{
-					"attr-1": newMatchExpression(MatchLt, "100"),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"attr-1": newMatchExpression(nfdv1alpha1.MatchLt, "100"),
 				},
 			},
-			FeatureMatcherTerm{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain_1.if_1",
-				MatchExpressions: &MatchExpressionSet{
-					"attr-1": newMatchExpression(MatchExists),
-					"attr-2": newMatchExpression(MatchExists),
-					"attr-3": newMatchExpression(MatchExists),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"attr-1": newMatchExpression(nfdv1alpha1.MatchExists),
+					"attr-2": newMatchExpression(nfdv1alpha1.MatchExists),
+					"attr-3": newMatchExpression(nfdv1alpha1.MatchExists),
 				},
 			},
 		},
@@ -319,7 +321,7 @@ var-2=
 
 	// test with empty MatchFeatures, but with MatchAny
 	r3 := r1.DeepCopy()
-	r3.MatchAny = []MatchAnyElem{{MatchFeatures: r3.MatchFeatures}}
+	r3.MatchAny = []nfdv1alpha1.MatchAnyElem{{MatchFeatures: r3.MatchFeatures}}
 	r3.MatchFeatures = nil
 
 	expectedLabels := map[string]string{
@@ -346,12 +348,12 @@ var-2=
 		"kf-foo":   "true",
 	}
 
-	m, err := r1.Execute(f)
+	m, err := Execute(r1, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
 	assert.Equal(t, expectedVars, m.Vars, "instances should have matched")
 
-	m, err = r3.Execute(f)
+	m, err = Execute(r3, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
 	assert.Equal(t, expectedVars, m.Vars, "instances should have matched")
@@ -359,60 +361,60 @@ var-2=
 	//
 	// Test error cases
 	//
-	r2 := Rule{
-		MatchFeatures: FeatureMatcher{
+	r2 := &nfdv1alpha1.Rule{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
 			// We need at least one matcher to match to execute the template.
 			// Use a simple empty matchexpression set to match anything.
-			FeatureMatcherTerm{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain_1.kf_1",
-				MatchExpressions: &MatchExpressionSet{
-					"key-a": newMatchExpression(MatchExists),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"key-a": newMatchExpression(nfdv1alpha1.MatchExists),
 				},
 			},
 		},
 	}
 
 	r2.LabelsTemplate = "foo=bar"
-	m, err = r2.Execute(f)
+	m, err = Execute(r2, f)
 	assert.Nil(t, err)
 	assert.Equal(t, map[string]string{"foo": "bar"}, m.Labels, "instances should have matched")
 	assert.Empty(t, m.Vars)
 
 	r2.LabelsTemplate = "foo"
-	_, err = r2.Execute(f)
+	_, err = Execute(r2, f)
 	assert.Error(t, err)
 
 	r2.LabelsTemplate = "{{"
-	_, err = r2.Execute(f)
+	_, err = Execute(r2, f)
 	assert.Error(t, err)
 
 	r2.LabelsTemplate = ""
 	r2.VarsTemplate = "bar=baz"
-	m, err = r2.Execute(f)
+	m, err = Execute(r2, f)
 	assert.Nil(t, err)
 	assert.Empty(t, m.Labels)
 	assert.Equal(t, map[string]string{"bar": "baz"}, m.Vars, "instances should have matched")
 
 	r2.VarsTemplate = "bar"
-	_, err = r2.Execute(f)
+	_, err = Execute(r2, f)
 	assert.Error(t, err)
 
 	r2.VarsTemplate = "{{"
-	_, err = r2.Execute(f)
+	_, err = Execute(r2, f)
 	assert.Error(t, err)
 
 	//
 	// Test matchName
 	//
-	r4 := Rule{
+	r4 := &nfdv1alpha1.Rule{
 		LabelsTemplate: "{{range .domain_1.vf_1}}{{.Name}}={{.Value}}\n{{end}}",
-		MatchFeatures: FeatureMatcher{
-			FeatureMatcherTerm{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature: "domain_1.vf_1",
-				MatchExpressions: &MatchExpressionSet{
-					"key-5": newMatchExpression(MatchDoesNotExist),
+				MatchExpressions: &nfdv1alpha1.MatchExpressionSet{
+					"key-5": newMatchExpression(nfdv1alpha1.MatchDoesNotExist),
 				},
-				MatchName: newMatchExpression(MatchIn, "key-1", "key-4"),
+				MatchName: newMatchExpression(nfdv1alpha1.MatchIn, "key-1", "key-4"),
 			},
 		},
 	}
@@ -422,21 +424,21 @@ var-2=
 		"key-5": "",
 	}
 
-	m, err = r4.Execute(f)
+	m, err = Execute(r4, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, expectedLabels, m.Labels, "instances should have matched")
 
-	r4 = Rule{
+	r4 = &nfdv1alpha1.Rule{
 		Labels: map[string]string{"should-not-match": "true"},
-		MatchFeatures: FeatureMatcher{
-			FeatureMatcherTerm{
+		MatchFeatures: nfdv1alpha1.FeatureMatcher{
+			nfdv1alpha1.FeatureMatcherTerm{
 				Feature:   "domain_1.vf_1",
-				MatchName: newMatchExpression(MatchIn, "key-not-exists"),
+				MatchName: newMatchExpression(nfdv1alpha1.MatchIn, "key-not-exists"),
 			},
 		},
 	}
 
-	m, err = r4.Execute(f)
+	m, err = Execute(r4, f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, map[string]string(nil), m.Labels, "instances should have matched")
 }