Repository: kubernetes-sigs/node-feature-discovery
Pull request #653 (2021-11-16 16:21:08)

 specify CRD for custom labeling rules
Add a cluster-scoped Custom Resource Definition for specifying labeling
rules. Nodes (node features, node objects) are cluster-level objects and
thus the natural and encouraged setup is to only have one NFD deployment
per cluster - the set of underlying features of the node stays the same
independent of how many parallel NFD deployments you have. Our extension
points (hooks, feature files and now CRs) can be be used by multiple
actors (depending on us) simultaneously. Having the CRD cluster-scoped
hopefully drives deployments in this direction. It also should make
deployment of vendor-specific labeling rules easy as there is no need to
worry about the namespace.

This patch virtually replicates the source.custom.FeatureSpec in a CRD
API (located in the pkg/apis/nfd/v1alpha1 package) with the notable
exception that "MatchOn" legacy rules are not supported. Legacy rules
are left out in order to keep the CRD simple and clean.

The duplicate functionality in source/custom will be dropped by upcoming
patches.

This patch utilizes controller-gen (from sigs.k8s.io/controller-tools)
for generating the CRD and deepcopy methods. Code can be (re-)generated
with "make apigen". Install controller-gen with:

```
  go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.7.0
```

Update kustomize and helm deployments to deploy the CRD.


Patch:
diff --git a/Makefile b/Makefile
index 9e47f006e..0d4895d58 100644
--- a/Makefile
+++ b/Makefile
@@ -102,6 +102,8 @@ generate:
 	go mod vendor
 	go generate ./cmd/... ./pkg/... ./source/...
 	rm -rf vendor/
+	controller-gen object crd output:crd:stdout paths=./pkg/apis/... > deployment/base/nfd-crds/nodefeaturerule-crd.yaml
+	cp deployment/base/nfd-crds/nodefeaturerule-crd.yaml deployment/helm/node-feature-discovery/manifests/
 
 gofmt:
 	@$(GO_FMT) -w -l $$(find . -name '*.go')
diff --git a/deployment/base/nfd-crds/cr-sample.yaml b/deployment/base/nfd-crds/cr-sample.yaml
new file mode 100644
index 000000000..f9228140f
--- /dev/null
+++ b/deployment/base/nfd-crds/cr-sample.yaml
@@ -0,0 +1,71 @@
+apiVersion: nfd.k8s-sigs.io/v1alpha1
+kind: NodeFeatureRule
+metadata:
+  name: my-rule
+spec:
+  rules:
+    # The following feature demonstrates the capabilities of the matchFeatures and
+    # matchAny matchers.
+    - name: "my feature rule"
+      labels:
+        "my-complex-feature": "my-value"
+      # matchFeatures implements a logical AND over feature matchers.
+      matchFeatures:
+        - feature: cpu.cpuid
+          matchExpressions:
+            AVX512F: {op: Exists}
+        - feature: cpu.cstate
+          matchExpressions:
+            enabled: {op: IsTrue}
+        - feature: cpu.pstate
+          matchExpressions:
+            no_turbo: {op: IsFalse}
+            scaling_governor: {op: In, value: ["performance"]}
+        - feature: cpu.rdt
+          matchExpressions:
+            RDTL3CA: {op: Exists}
+        - feature: cpu.sst
+          matchExpressions:
+            bf.enabled: {op: IsTrue}
+        - feature: cpu.topology
+          matchExpressions:
+            hardware_multithreading: {op: IsFalse}
+
+        - feature: kernel.config
+          matchExpressions:
+            X86: {op: Exists}
+            LSM: {op: InRegexp, value: ["apparmor"]}
+        - feature: kernel.loadedmodule
+          matchExpressions:
+            e1000e: {op: Exists}
+        - feature: kernel.selinux
+          matchExpressions:
+            enabled: {op: IsFalse}
+        - feature: kernel.version
+          matchExpressions:
+            major: {op: In, value: ["5"]}
+            minor: {op: Gt, value: ["10"]}
+
+        - feature: system.osrelease
+          matchExpressions:
+            ID: {op: In, value: ["fedora", "centos"]}
+        - feature: system.name
+          matchExpressions:
+            nodename: {op: InRegexp, value: ["^worker-X"]}
+
+        - feature: local.label
+          matchExpressions:
+            custom-feature-knob: {op: Gt, value: ["100"]}
+
+      # matchAny implements a logical IF over all listed matchers (i.e. at
+      # least one must match)
+      matchAny:
+        - matchFeatures:
+          - feature: pci.device
+            matchExpressions:
+              vendor: {op: In, value: ["8086"]}
+              class: {op: In, value: ["0200"]}
+          - feature: usb.device
+            matchExpressions:
+              vendor: {op: In, value: ["8086"]}
+              class: {op: In, value: ["02"]}
diff --git a/deployment/base/nfd-crds/kustomization.yaml b/deployment/base/nfd-crds/kustomization.yaml
new file mode 100644
index 000000000..73fd8cc08
--- /dev/null
+++ b/deployment/base/nfd-crds/kustomization.yaml
@@ -0,0 +1,5 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- nodefeaturerule-crd.yaml
diff --git a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml b/deployment/base/nfd-crds/nodefeaturerule-crd.yaml
new file mode 100644
index 000000000..277a1508f
--- /dev/null
+++ b/deployment/base/nfd-crds/nodefeaturerule-crd.yaml
@@ -0,0 +1,208 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: nodefeaturerules.nfd.k8s-sigs.io
+spec:
+  group: nfd.k8s-sigs.io
+  names:
+    kind: NodeFeatureRule
+    listKind: NodeFeatureRuleList
+    plural: nodefeaturerules
+    singular: nodefeaturerule
+  scope: Cluster
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: NodeFeatureRule resource specifies a configuration for feature-based
+          customization of node objects, such as node labeling.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: NodeFeatureRuleSpec describes a NodeFeatureRule.
+            properties:
+              rules:
+                description: Rules is a list of node customization rules.
+                items:
+                  description: Rule defines a rule for node customization such as
+                    labeling.
+                  properties:
+                    labels:
+                      additionalProperties:
+                        type: string
+                      description: Labels to create if the rule matches.
+                      type: object
+                    labelsTemplate:
+                      description: LabelsTemplate specifies a template to expand for
+                        dynamically generating multiple labels. Data (after template
+                        expansion) must be keys with an optional value (<key>[=<value>])
+                        separated by newlines.
+                      type: string
+                    matchAny:
+                      description: MatchAny specifies a list of matchers one of which
+                        must match.
+                      items:
+                        description: MatchAnyElem specifies one sub-matcher of MatchAny.
+                        properties:
+                          matchFeatures:
+                            description: MatchFeatures specifies a set of matcher
+                              terms all of which must match.
+                            items:
+                              description: FeatureMatcherTerm defines requirements
+                                against one feature set. All requirements (specified
+                                as MatchExpressions) are evaluated against each element
+                                in the feature set.
+                              properties:
+                                feature:
+                                  type: string
+                                matchExpressions:
+                                  additionalProperties:
+                                    description: "MatchExpression specifies an expression
+                                      to evaluate against a set of input values. It
+                                      contains an operator that is applied when matching
+                                      the input and an array of values that the operator
+                                      evaluates the input against. \n NB: CreateMatchExpression
+                                      or MustCreateMatchExpression() should be used
+                                      for     creating new instances. NB: Validate()
+                                      must be called if Op or Value fields are modified
+                                      or if a new     instance is created from scratch
+                                      without using the helper functions."
+                                    properties:
+                                      op:
+                                        description: Op is the operator to be applied.
+                                        enum:
+                                        - In
+                                        - NotIn
+                                        - InRegexp
+                                        - Exists
+                                        - DoesNotExist
+                                        - Gt
+                                        - Lt
+                                        - GtLt
+                                        - IsTrue
+                                        - IsFalse
+                                        type: string
+                                      value:
+                                        description: Value is the list of values that
+                                          the operand evaluates the input against.
+                                          Value should be empty if the operator is
+                                          Exists, DoesNotExist, IsTrue or IsFalse.
+                                          Value should contain exactly one element
+                                          if the operator is Gt or Lt and exactly
+                                          two elements if the operator is GtLt. In
+                                          other cases Value should contain at least
+                                          one element.
+                                        items:
+                                          type: string
+                                        type: array
+                                    required:
+                                    - op
+                                    type: object
+                                  description: MatchExpressionSet contains a set of
+                                    MatchExpressions, each of which is evaluated against
+                                    a set of input values.
+                                  type: object
+                              required:
+                              - feature
+                              - matchExpressions
+                              type: object
+                            type: array
+                        required:
+                        - matchFeatures
+                        type: object
+                      type: array
+                    matchFeatures:
+                      description: MatchFeatures specifies a set of matcher terms
+                        all of which must match.
+                      items:
+                        description: FeatureMatcherTerm defines requirements against
+                          one feature set. All requirements (specified as MatchExpressions)
+                          are evaluated against each element in the feature set.
+                        properties:
+                          feature:
+                            type: string
+                          matchExpressions:
+                            additionalProperties:
+                              description: "MatchExpression specifies an expression
+                                to evaluate against a set of input values. It contains
+                                an operator that is applied when matching the input
+                                and an array of values that the operator evaluates
+                                the input against. \n NB: CreateMatchExpression or
+                                MustCreateMatchExpression() should be used for     creating
+                                new instances. NB: Validate() must be called if Op
+                                or Value fields are modified or if a new     instance
+                                is created from scratch without using the helper functions."
+                              properties:
+                                op:
+                                  description: Op is the operator to be applied.
+                                  enum:
+                                  - In
+                                  - NotIn
+                                  - InRegexp
+                                  - Exists
+                                  - DoesNotExist
+                                  - Gt
+                                  - Lt
+                                  - GtLt
+                                  - IsTrue
+                                  - IsFalse
+                                  type: string
+                                value:
+                                  description: Value is the list of values that the
+                                    operand evaluates the input against. Value should
+                                    be empty if the operator is Exists, DoesNotExist,
+                                    IsTrue or IsFalse. Value should contain exactly
+                                    one element if the operator is Gt or Lt and exactly
+                                    two elements if the operator is GtLt. In other
+                                    cases Value should contain at least one element.
+                                  items:
+                                    type: string
+                                  type: array
+                              required:
+                              - op
+                              type: object
+                            description: MatchExpressionSet contains a set of MatchExpressions,
+                              each of which is evaluated against a set of input values.
+                            type: object
+                        required:
+                        - feature
+                        - matchExpressions
+                        type: object
+                      type: array
+                    name:
+                      description: Name of the rule.
+                      type: string
+                  required:
+                  - name
+                  type: object
+                type: array
+            required:
+            - rules
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/deployment/helm/node-feature-discovery/manifests/nodefeaturerule-crd.yaml b/deployment/helm/node-feature-discovery/manifests/nodefeaturerule-crd.yaml
new file mode 100644
index 000000000..277a1508f
--- /dev/null
+++ b/deployment/helm/node-feature-discovery/manifests/nodefeaturerule-crd.yaml
@@ -0,0 +1,208 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: nodefeaturerules.nfd.k8s-sigs.io
+spec:
+  group: nfd.k8s-sigs.io
+  names:
+    kind: NodeFeatureRule
+    listKind: NodeFeatureRuleList
+    plural: nodefeaturerules
+    singular: nodefeaturerule
+  scope: Cluster
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: NodeFeatureRule resource specifies a configuration for feature-based
+          customization of node objects, such as node labeling.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: NodeFeatureRuleSpec describes a NodeFeatureRule.
+            properties:
+              rules:
+                description: Rules is a list of node customization rules.
+                items:
+                  description: Rule defines a rule for node customization such as
+                    labeling.
+                  properties:
+                    labels:
+                      additionalProperties:
+                        type: string
+                      description: Labels to create if the rule matches.
+                      type: object
+                    labelsTemplate:
+                      description: LabelsTemplate specifies a template to expand for
+                        dynamically generating multiple labels. Data (after template
+                        expansion) must be keys with an optional value (<key>[=<value>])
+                        separated by newlines.
+                      type: string
+                    matchAny:
+                      description: MatchAny specifies a list of matchers one of which
+                        must match.
+                      items:
+                        description: MatchAnyElem specifies one sub-matcher of MatchAny.
+                        properties:
+                          matchFeatures:
+                            description: MatchFeatures specifies a set of matcher
+                              terms all of which must match.
+                            items:
+                              description: FeatureMatcherTerm defines requirements
+                                against one feature set. All requirements (specified
+                                as MatchExpressions) are evaluated against each element
+                                in the feature set.
+                              properties:
+                                feature:
+                                  type: string
+                                matchExpressions:
+                                  additionalProperties:
+                                    description: "MatchExpression specifies an expression
+                                      to evaluate against a set of input values. It
+                                      contains an operator that is applied when matching
+                                      the input and an array of values that the operator
+                                      evaluates the input against. \n NB: CreateMatchExpression
+                                      or MustCreateMatchExpression() should be used
+                                      for     creating new instances. NB: Validate()
+                                      must be called if Op or Value fields are modified
+                                      or if a new     instance is created from scratch
+                                      without using the helper functions."
+                                    properties:
+                                      op:
+                                        description: Op is the operator to be applied.
+                                        enum:
+                                        - In
+                                        - NotIn
+                                        - InRegexp
+                                        - Exists
+                                        - DoesNotExist
+                                        - Gt
+                                        - Lt
+                                        - GtLt
+                                        - IsTrue
+                                        - IsFalse
+                                        type: string
+                                      value:
+                                        description: Value is the list of values that
+                                          the operand evaluates the input against.
+                                          Value should be empty if the operator is
+                                          Exists, DoesNotExist, IsTrue or IsFalse.
+                                          Value should contain exactly one element
+                                          if the operator is Gt or Lt and exactly
+                                          two elements if the operator is GtLt. In
+                                          other cases Value should contain at least
+                                          one element.
+                                        items:
+                                          type: string
+                                        type: array
+                                    required:
+                                    - op
+                                    type: object
+                                  description: MatchExpressionSet contains a set of
+                                    MatchExpressions, each of which is evaluated against
+                                    a set of input values.
+                                  type: object
+                              required:
+                              - feature
+                              - matchExpressions
+                              type: object
+                            type: array
+                        required:
+                        - matchFeatures
+                        type: object
+                      type: array
+                    matchFeatures:
+                      description: MatchFeatures specifies a set of matcher terms
+                        all of which must match.
+                      items:
+                        description: FeatureMatcherTerm defines requirements against
+                          one feature set. All requirements (specified as MatchExpressions)
+                          are evaluated against each element in the feature set.
+                        properties:
+                          feature:
+                            type: string
+                          matchExpressions:
+                            additionalProperties:
+                              description: "MatchExpression specifies an expression
+                                to evaluate against a set of input values. It contains
+                                an operator that is applied when matching the input
+                                and an array of values that the operator evaluates
+                                the input against. \n NB: CreateMatchExpression or
+                                MustCreateMatchExpression() should be used for     creating
+                                new instances. NB: Validate() must be called if Op
+                                or Value fields are modified or if a new     instance
+                                is created from scratch without using the helper functions."
+                              properties:
+                                op:
+                                  description: Op is the operator to be applied.
+                                  enum:
+                                  - In
+                                  - NotIn
+                                  - InRegexp
+                                  - Exists
+                                  - DoesNotExist
+                                  - Gt
+                                  - Lt
+                                  - GtLt
+                                  - IsTrue
+                                  - IsFalse
+                                  type: string
+                                value:
+                                  description: Value is the list of values that the
+                                    operand evaluates the input against. Value should
+                                    be empty if the operator is Exists, DoesNotExist,
+                                    IsTrue or IsFalse. Value should contain exactly
+                                    one element if the operator is Gt or Lt and exactly
+                                    two elements if the operator is GtLt. In other
+                                    cases Value should contain at least one element.
+                                  items:
+                                    type: string
+                                  type: array
+                              required:
+                              - op
+                              type: object
+                            description: MatchExpressionSet contains a set of MatchExpressions,
+                              each of which is evaluated against a set of input values.
+                            type: object
+                        required:
+                        - feature
+                        - matchExpressions
+                        type: object
+                      type: array
+                    name:
+                      description: Name of the rule.
+                      type: string
+                  required:
+                  - name
+                  type: object
+                type: array
+            required:
+            - rules
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/deployment/helm/node-feature-discovery/templates/nodefeaturerule-crd.yaml b/deployment/helm/node-feature-discovery/templates/nodefeaturerule-crd.yaml
new file mode 100644
index 000000000..f5d30850a
--- /dev/null
+++ b/deployment/helm/node-feature-discovery/templates/nodefeaturerule-crd.yaml
@@ -0,0 +1,3 @@
+{{- if .Values.nodeFeatureRule.createCRD }}
+{{ .Files.Get "manifests/nodefeaturerule-crd.yaml" }}
+{{- end}}
diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml
index d4516dcbc..1765d65e3 100644
--- a/deployment/helm/node-feature-discovery/values.yaml
+++ b/deployment/helm/node-feature-discovery/values.yaml
@@ -18,6 +18,9 @@ serviceAccount:
 nameOverride: ""
 fullnameOverride: ""
 
+nodeFeatureRule:
+  createCRD: true
+
 master:
   instance:
   extraLabelNs: []
diff --git a/deployment/overlays/default-combined/kustomization.yaml b/deployment/overlays/default-combined/kustomization.yaml
index 89de02aaa..a7778fc83 100644
--- a/deployment/overlays/default-combined/kustomization.yaml
+++ b/deployment/overlays/default-combined/kustomization.yaml
@@ -5,6 +5,7 @@ namespace: node-feature-discovery
 
 bases:
 - ../../base/rbac
+- ../../base/nfd-crds
 - ../../base/master-worker-combined
 
 resources:
diff --git a/deployment/overlays/default-job/kustomization.yaml b/deployment/overlays/default-job/kustomization.yaml
index 45ef3b7d5..04bfd7688 100644
--- a/deployment/overlays/default-job/kustomization.yaml
+++ b/deployment/overlays/default-job/kustomization.yaml
@@ -5,6 +5,7 @@ namespace: node-feature-discovery
 
 bases:
 - ../../base/rbac
+- ../../base/nfd-crds
 - ../../base/master
 - ../../base/worker-job
 
diff --git a/deployment/overlays/default/kustomization.yaml b/deployment/overlays/default/kustomization.yaml
index 5230f2a54..317095e79 100644
--- a/deployment/overlays/default/kustomization.yaml
+++ b/deployment/overlays/default/kustomization.yaml
@@ -5,6 +5,7 @@ namespace: node-feature-discovery
 
 bases:
 - ../../base/rbac
+- ../../base/nfd-crds
 - ../../base/master
 - ../../base/worker-daemonset
 
diff --git a/deployment/overlays/master-worker-topologyupdater/kustomization.yaml b/deployment/overlays/master-worker-topologyupdater/kustomization.yaml
index 48845d8b9..0725d0c56 100644
--- a/deployment/overlays/master-worker-topologyupdater/kustomization.yaml
+++ b/deployment/overlays/master-worker-topologyupdater/kustomization.yaml
@@ -6,6 +6,7 @@ namespace: node-feature-discovery
 bases:
 - ../../base/rbac
 - ../../base/rbac-topologyupdater
+- ../../base/nfd-crds
 - ../../base/master
 - ../../base/worker-daemonset
 - ../../base/noderesourcetopologies-crd
diff --git a/docs/get-started/deployment-and-usage.md b/docs/get-started/deployment-and-usage.md
index b4f7dd081..b9035e040 100644
--- a/docs/get-started/deployment-and-usage.md
+++ b/docs/get-started/deployment-and-usage.md
@@ -290,6 +290,7 @@ We have introduced the following Chart parameters.
 | `rbac` | dict |  | RBAC [parameteres](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) |
 | `nameOverride` | string |  | Override the name of the chart |
 | `fullnameOverride` | string |  | Override a default fully qualified app name |
+| `nodeFeatureRule.createCRD` | bool | true | Specifies whether to create the NodeFeatureRule CRD |
 
 ##### Master pod parameters
 
diff --git a/go.mod b/go.mod
index 9952da201..b5667e2e6 100644
--- a/go.mod
+++ b/go.mod
@@ -87,7 +87,6 @@ require (
 	github.com/go-ozzo/ozzo-validation v3.5.0+incompatible // indirect
 	github.com/godbus/dbus/v5 v5.0.4 // indirect
 	github.com/gofrs/uuid v4.0.0+incompatible // indirect
-	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/mock v1.4.4 // indirect
 	github.com/google/cadvisor v0.39.2 // indirect
diff --git a/pkg/apis/nfd/v1alpha1/doc.go b/pkg/apis/nfd/v1alpha1/doc.go
new file mode 100644
index 000000000..ff1057c29
--- /dev/null
+++ b/pkg/apis/nfd/v1alpha1/doc.go
@@ -0,0 +1,20 @@
+/*
+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.
+*/
+
+// +k8s:deepcopy-gen=package
+// +kubebuilder:object:generate=true
+// +groupName=nfd.k8s-sigs.io
+package v1alpha1
diff --git a/source/custom/expression/expression.go b/pkg/apis/nfd/v1alpha1/expression.go
similarity index 75%
rename from source/custom/expression/expression.go
rename to pkg/apis/nfd/v1alpha1/expression.go
index f5595306b..1591b88d2 100644
--- a/source/custom/expression/expression.go
+++ b/pkg/apis/nfd/v1alpha1/expression.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package expression
+package v1alpha1
 
 import (
 	"encoding/json"
@@ -29,80 +29,6 @@ import (
 	"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
 )
 
-// MatchExpressionSet contains a set of MatchExpressions, each of which is
-// evaluated against a set of input values.
-type MatchExpressionSet map[string]*MatchExpression
-
-// MatchExpression specifies an expression to evaluate against a set of input
-// values. It contains an operator that is applied when matching the input and
-// an array of values that the operator evaluates the input against.
-// NB: CreateMatchExpression or MustCreateMatchExpression() should be used for
-//     creating new instances.
-// NB: Validate() must be called if Op or Value fields are modified or if a new
-//     instance is created from scratch without using the helper functions.
-type MatchExpression struct {
-	// Op is the operator to be applied.
-	Op MatchOp
-
-	// Value is the list of values that the operand evaluates the input
-	// against. Value should be empty if the operator is Exists, DoesNotExist,
-	// IsTrue or IsFalse. Value should contain exactly one element if the
-	// operator is Gt or Lt and exactly two elements if the operator is GtLt.
-	// In other cases Value should contain at least one element.
-	Value MatchValue `json:",omitempty"`
-
-	// valueRe caches compiled regexps for "InRegexp" operator
-	valueRe []*regexp.Regexp
-}
-
-// MatchOp is the match operator that is applied on values when evaluating a
-// MatchExpression.
-type MatchOp string
-
-// MatchValue is the list of values associated with a MatchExpression.
-type MatchValue []string
-
-const (
-	// MatchAny returns always true.
-	MatchAny MatchOp = ""
-	// MatchIn returns true if any of the values stored in the expression is
-	// equal to the input.
-	MatchIn MatchOp = "In"
-	// MatchIn returns true if none of the values in the expression are equal
-	// to the input.
-	MatchNotIn MatchOp = "NotIn"
-	// MatchInRegexp treats values of the expression as regular expressions and
-	// returns true if any of them matches the input.
-	MatchInRegexp MatchOp = "InRegexp"
-	// MatchExists returns true if the input is valid. The expression must not
-	// have any values.
-	MatchExists MatchOp = "Exists"
-	// MatchDoesNotExist returns true if the input is not valid. The expression
-	// must not have any values.
-	MatchDoesNotExist MatchOp = "DoesNotExist"
-	// MatchGt returns true if the input is greater than the value of the
-	// expression (number of values in the expression must be exactly one).
-	// Both the input and value must be integer numbers, otherwise an error is
-	// returned.
-	MatchGt MatchOp = "Gt"
-	// MatchLt returns true if the input is less  than the value of the
-	// expression (number of values in the expression must be exactly one).
-	// Both the input and value must be integer numbers, otherwise an error is
-	// returned.
-	MatchLt MatchOp = "Lt"
-	// MatchGtLt returns true if the input is between two values, i.e. greater
-	// than the first value and less than the second value of the expression
-	// (number of values in the expression must be exactly two). Both the input
-	// and values must be integer numbers, otherwise an error is returned.
-	MatchGtLt MatchOp = "GtLt"
-	// MatchIsTrue returns true if the input holds the value "true". The
-	// expression must not have any values.
-	MatchIsTrue MatchOp = "IsTrue"
-	// MatchIsTrue returns true if the input holds the value "false". The
-	// expression must not have any values.
-	MatchIsFalse MatchOp = "IsFalse"
-)
-
 var matchOps = map[MatchOp]struct{}{
 	MatchAny:          struct{}{},
 	MatchIn:           struct{}{},
@@ -117,6 +43,18 @@ var matchOps = map[MatchOp]struct{}{
 	MatchIsFalse:      struct{}{},
 }
 
+type valueRegexpCache []*regexp.Regexp
+
+// NewMatchExpressionSet returns a new MatchExpressionSet instance.
+func NewMatchExpressionSet() *MatchExpressionSet {
+	return &MatchExpressionSet{Expressions: make(Expressions)}
+}
+
+// Len returns the number of expressions.
+func (e *Expressions) Len() int {
+	return len(*e)
+}
+
 // CreateMatchExpression creates a new MatchExpression instance. Returns an
 // error if validation fails.
 func CreateMatchExpression(op MatchOp, values ...string) (*MatchExpression, error) {
@@ -376,7 +314,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
 
 // MatchKeys evaluates the MatchExpressionSet against a set of keys.
 func (m *MatchExpressionSet) MatchKeys(keys map[string]feature.Nil) (bool, error) {
-	for n, e := range *m {
+	for n, e := range (*m).Expressions {
 		match, err := e.MatchKeys(n, keys)
 		if err != nil {
 			return false, err
@@ -390,7 +328,7 @@ func (m *MatchExpressionSet) MatchKeys(keys map[string]feature.Nil) (bool, error
 
 // MatchValues evaluates the MatchExpressionSet against a set of key-value pairs.
 func (m *MatchExpressionSet) MatchValues(values map[string]string) (bool, error) {
-	for n, e := range *m {
+	for n, e := range (*m).Expressions {
 		match, err := e.MatchValues(n, values)
 		if err != nil {
 			return false, err
@@ -418,7 +356,7 @@ func (m *MatchExpressionSet) MatchInstances(instances []feature.InstanceFeature)
 
 // UnmarshalJSON implements the Unmarshaler interface of "encoding/json".
 func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
-	*m = make(MatchExpressionSet)
+	*m = *NewMatchExpressionSet()
 
 	names := make([]string, 0)
 	if err := json.Unmarshal(data, &names); err == nil {
@@ -426,9 +364,9 @@ func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
 		for _, name := range names {
 			split := strings.SplitN(name, "=", 2)
 			if len(split) == 1 {
-				(*m)[split[0]] = newMatchExpression(MatchExists)
+				(*m).Expressions[split[0]] = newMatchExpression(MatchExists)
 			} else {
-				(*m)[split[0]] = newMatchExpression(MatchIn, split[1])
+				(*m).Expressions[split[0]] = newMatchExpression(MatchIn, split[1])
 			}
 		}
 	} else {
@@ -439,9 +377,9 @@ func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
 		} else {
 			for k, v := range expressions {
 				if v != nil {
-					(*m)[k] = v
+					(*m).Expressions[k] = v
 				} else {
-					(*m)[k] = newMatchExpression(MatchExists)
+					(*m).Expressions[k] = newMatchExpression(MatchExists)
 				}
 			}
 		}
@@ -496,3 +434,22 @@ func (m *MatchValue) UnmarshalJSON(data []byte) error {
 
 	return nil
 }
+
+// DeepCopy supplements the auto-generated code
+func (in *valueRegexpCache) DeepCopy() *valueRegexpCache {
+	if in == nil {
+		return nil
+	}
+	out := new(valueRegexpCache)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is a stub to augment the auto-generated code
+//nolint:staticcheck  // re.Copy is deprecated but we want to use  it here
+func (in *valueRegexpCache) DeepCopyInto(out *valueRegexpCache) {
+	*out = make(valueRegexpCache, len(*in))
+	for i, re := range *in {
+		(*out)[i] = re.Copy()
+	}
+}
diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go
new file mode 100644
index 000000000..75cdadcef
--- /dev/null
+++ b/pkg/apis/nfd/v1alpha1/types.go
@@ -0,0 +1,173 @@
+/*
+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
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// NodeFeatureRuleList contains a list of NodeFeatureRule objects.
+// +kubebuilder:object:root=true
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type NodeFeatureRuleList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []NodeFeatureRule `json:"items"`
+}
+
+// NodeFeatureRule resource specifies a configuration for feature-based
+// customization of node objects, such as node labeling.
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:scope=Cluster
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type NodeFeatureRule struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec NodeFeatureRuleSpec `json:"spec"`
+}
+
+// NodeFeatureRuleSpec describes a NodeFeatureRule.
+type NodeFeatureRuleSpec struct {
+	// Rules is a list of node customization rules.
+	Rules []Rule `json:"rules"`
+}
+
+// Rule defines a rule for node customization such as labeling.
+type Rule struct {
+	// Name of the rule.
+	Name string `json:"name"`
+
+	// Labels to create if the rule matches.
+	// +optional
+	Labels map[string]string `json:"labels"`
+
+	// LabelsTemplate specifies a template to expand for dynamically generating
+	// multiple labels. Data (after template expansion) must be keys with an
+	// optional value (<key>[=<value>]) separated by newlines.
+	// +optional
+	LabelsTemplate string `json:"labelsTemplate"`
+
+	// MatchFeatures specifies a set of matcher terms all of which must match.
+	// +optional
+	MatchFeatures FeatureMatcher `json:"matchFeatures"`
+
+	// MatchAny specifies a list of matchers one of which must match.
+	// +optional
+	MatchAny []MatchAnyElem `json:"matchAny"`
+}
+
+// MatchAnyElem specifies one sub-matcher of MatchAny.
+type MatchAnyElem struct {
+	// MatchFeatures specifies a set of matcher terms all of which must match.
+	MatchFeatures FeatureMatcher `json:"matchFeatures"`
+}
+
+// FeatureMatcher specifies a set of feature matcher terms (i.e. per-feature
+// matchers), all of which must match.
+type FeatureMatcher []FeatureMatcherTerm
+
+// FeatureMatcherTerm defines requirements against one feature set. All
+// requirements (specified as MatchExpressions) are evaluated against each
+// element in the feature set.
+type FeatureMatcherTerm struct {
+	Feature          string             `json:"feature"`
+	MatchExpressions MatchExpressionSet `json:"matchExpressions"`
+}
+
+// MatchExpressionSet contains a set of MatchExpressions, each of which is
+// evaluated against a set of input values.
+type MatchExpressionSet struct {
+	Expressions `json:",inline"`
+}
+
+// Expressions is a helper type to work around issues with k8s deepcopy-gen
+type Expressions map[string]*MatchExpression
+
+// MatchExpression specifies an expression to evaluate against a set of input
+// values. It contains an operator that is applied when matching the input and
+// an array of values that the operator evaluates the input against.
+//
+// NB: CreateMatchExpression or MustCreateMatchExpression() should be used for
+//     creating new instances.
+// NB: Validate() must be called if Op or Value fields are modified or if a new
+//     instance is created from scratch without using the helper functions.
+type MatchExpression struct {
+	// Op is the operator to be applied.
+	Op MatchOp `json:"op"`
+
+	// Value is the list of values that the operand evaluates the input
+	// against. Value should be empty if the operator is Exists, DoesNotExist,
+	// IsTrue or IsFalse. Value should contain exactly one element if the
+	// operator is Gt or Lt and exactly two elements if the operator is GtLt.
+	// In other cases Value should contain at least one element.
+	// +optional
+	Value MatchValue `json:"value,omitempty"`
+
+	// valueRe caches compiled regexps for "InRegexp" operator
+	valueRe valueRegexpCache `json:"-"`
+}
+
+// MatchOp is the match operator that is applied on values when evaluating a
+// MatchExpression.
+// +kubebuilder:validation:Enum="In";"NotIn";"InRegexp";"Exists";"DoesNotExist";"Gt";"Lt";"GtLt";"IsTrue";"IsFalse"
+type MatchOp string
+
+// MatchValue is the list of values associated with a MatchExpression.
+type MatchValue []string
+
+const (
+	// MatchAny returns always true.
+	MatchAny MatchOp = ""
+	// MatchIn returns true if any of the values stored in the expression is
+	// equal to the input.
+	MatchIn MatchOp = "In"
+	// MatchIn returns true if none of the values in the expression are equal
+	// to the input.
+	MatchNotIn MatchOp = "NotIn"
+	// MatchInRegexp treats values of the expression as regular expressions and
+	// returns true if any of them matches the input.
+	MatchInRegexp MatchOp = "InRegexp"
+	// MatchExists returns true if the input is valid. The expression must not
+	// have any values.
+	MatchExists MatchOp = "Exists"
+	// MatchDoesNotExist returns true if the input is not valid. The expression
+	// must not have any values.
+	MatchDoesNotExist MatchOp = "DoesNotExist"
+	// MatchGt returns true if the input is greater than the value of the
+	// expression (number of values in the expression must be exactly one).
+	// Both the input and value must be integer numbers, otherwise an error is
+	// returned.
+	MatchGt MatchOp = "Gt"
+	// MatchLt returns true if the input is less  than the value of the
+	// expression (number of values in the expression must be exactly one).
+	// Both the input and value must be integer numbers, otherwise an error is
+	// returned.
+	MatchLt MatchOp = "Lt"
+	// MatchGtLt returns true if the input is between two values, i.e. greater
+	// than the first value and less than the second value of the expression
+	// (number of values in the expression must be exactly two). Both the input
+	// and values must be integer numbers, otherwise an error is returned.
+	MatchGtLt MatchOp = "GtLt"
+	// MatchIsTrue returns true if the input holds the value "true". The
+	// expression must not have any values.
+	MatchIsTrue MatchOp = "IsTrue"
+	// MatchIsTrue returns true if the input holds the value "false". The
+	// expression must not have any values.
+	MatchIsFalse MatchOp = "IsFalse"
+)
diff --git a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..f81515d0d
--- /dev/null
+++ b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,284 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in Expressions) DeepCopyInto(out *Expressions) {
+	{
+		in := &in
+		*out = make(Expressions, len(*in))
+		for key, val := range *in {
+			var outVal *MatchExpression
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = new(MatchExpression)
+				(*in).DeepCopyInto(*out)
+			}
+			(*out)[key] = outVal
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Expressions.
+func (in Expressions) DeepCopy() Expressions {
+	if in == nil {
+		return nil
+	}
+	out := new(Expressions)
+	in.DeepCopyInto(out)
+	return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in FeatureMatcher) DeepCopyInto(out *FeatureMatcher) {
+	{
+		in := &in
+		*out = make(FeatureMatcher, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureMatcher.
+func (in FeatureMatcher) DeepCopy() FeatureMatcher {
+	if in == nil {
+		return nil
+	}
+	out := new(FeatureMatcher)
+	in.DeepCopyInto(out)
+	return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FeatureMatcherTerm) DeepCopyInto(out *FeatureMatcherTerm) {
+	*out = *in
+	in.MatchExpressions.DeepCopyInto(&out.MatchExpressions)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureMatcherTerm.
+func (in *FeatureMatcherTerm) DeepCopy() *FeatureMatcherTerm {
+	if in == nil {
+		return nil
+	}
+	out := new(FeatureMatcherTerm)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MatchAnyElem) DeepCopyInto(out *MatchAnyElem) {
+	*out = *in
+	if in.MatchFeatures != nil {
+		in, out := &in.MatchFeatures, &out.MatchFeatures
+		*out = make(FeatureMatcher, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchAnyElem.
+func (in *MatchAnyElem) DeepCopy() *MatchAnyElem {
+	if in == nil {
+		return nil
+	}
+	out := new(MatchAnyElem)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MatchExpression) DeepCopyInto(out *MatchExpression) {
+	*out = *in
+	if in.Value != nil {
+		in, out := &in.Value, &out.Value
+		*out = make(MatchValue, len(*in))
+		copy(*out, *in)
+	}
+	in.valueRe.DeepCopyInto(&out.valueRe)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchExpression.
+func (in *MatchExpression) DeepCopy() *MatchExpression {
+	if in == nil {
+		return nil
+	}
+	out := new(MatchExpression)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MatchExpressionSet) DeepCopyInto(out *MatchExpressionSet) {
+	*out = *in
+	if in.Expressions != nil {
+		in, out := &in.Expressions, &out.Expressions
+		*out = make(Expressions, len(*in))
+		for key, val := range *in {
+			var outVal *MatchExpression
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = new(MatchExpression)
+				(*in).DeepCopyInto(*out)
+			}
+			(*out)[key] = outVal
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchExpressionSet.
+func (in *MatchExpressionSet) DeepCopy() *MatchExpressionSet {
+	if in == nil {
+		return nil
+	}
+	out := new(MatchExpressionSet)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in MatchValue) DeepCopyInto(out *MatchValue) {
+	{
+		in := &in
+		*out = make(MatchValue, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchValue.
+func (in MatchValue) DeepCopy() MatchValue {
+	if in == nil {
+		return nil
+	}
+	out := new(MatchValue)
+	in.DeepCopyInto(out)
+	return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeFeatureRule) DeepCopyInto(out *NodeFeatureRule) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureRule.
+func (in *NodeFeatureRule) DeepCopy() *NodeFeatureRule {
+	if in == nil {
+		return nil
+	}
+	out := new(NodeFeatureRule)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeFeatureRule) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeFeatureRuleList) DeepCopyInto(out *NodeFeatureRuleList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]NodeFeatureRule, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureRuleList.
+func (in *NodeFeatureRuleList) DeepCopy() *NodeFeatureRuleList {
+	if in == nil {
+		return nil
+	}
+	out := new(NodeFeatureRuleList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeFeatureRuleList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeFeatureRuleSpec) DeepCopyInto(out *NodeFeatureRuleSpec) {
+	*out = *in
+	if in.Rules != nil {
+		in, out := &in.Rules, &out.Rules
+		*out = make([]Rule, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureRuleSpec.
+func (in *NodeFeatureRuleSpec) DeepCopy() *NodeFeatureRuleSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(NodeFeatureRuleSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Rule) DeepCopyInto(out *Rule) {
+	*out = *in
+	if in.Labels != nil {
+		in, out := &in.Labels, &out.Labels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.MatchFeatures != nil {
+		in, out := &in.MatchFeatures, &out.MatchFeatures
+		*out = make(FeatureMatcher, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.MatchAny != nil {
+		in, out := &in.MatchAny, &out.MatchAny
+		*out = make([]MatchAnyElem, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rule.
+func (in *Rule) DeepCopy() *Rule {
+	if in == nil {
+		return nil
+	}
+	out := new(Rule)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/source/custom/custom.go b/source/custom/custom.go
index 4ccc5f85a..84b5a502f 100644
--- a/source/custom/custom.go
+++ b/source/custom/custom.go
@@ -26,9 +26,9 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/source"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
 	"sigs.k8s.io/node-feature-discovery/source/custom/rules"
 )
 
@@ -65,7 +65,7 @@ type FeatureMatcher []FeatureMatcherTerm
 
 type FeatureMatcherTerm struct {
 	Feature          string
-	MatchExpressions expression.MatchExpressionSet
+	MatchExpressions nfdv1alpha1.MatchExpressionSet
 }
 
 type config []CustomRule
diff --git a/source/custom/rules/cpuid_rule.go b/source/custom/rules/cpuid_rule.go
index fda326aa1..6f13c0709 100644
--- a/source/custom/rules/cpuid_rule.go
+++ b/source/custom/rules/cpuid_rule.go
@@ -19,14 +19,14 @@ package rules
 import (
 	"fmt"
 
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/source"
 	"sigs.k8s.io/node-feature-discovery/source/cpu"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
 )
 
 // CpuIDRule implements Rule for the custom source
 type CpuIDRule struct {
-	expression.MatchExpressionSet
+	nfdv1alpha1.MatchExpressionSet
 }
 
 func (r *CpuIDRule) Match() (bool, error) {
diff --git a/source/custom/rules/kconfig_rule.go b/source/custom/rules/kconfig_rule.go
index a8c79a823..efb7674b4 100644
--- a/source/custom/rules/kconfig_rule.go
+++ b/source/custom/rules/kconfig_rule.go
@@ -19,14 +19,14 @@ package rules
 import (
 	"fmt"
 
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/source"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
 	"sigs.k8s.io/node-feature-discovery/source/kernel"
 )
 
 // KconfigRule implements Rule for the custom source
 type KconfigRule struct {
-	expression.MatchExpressionSet
+	nfdv1alpha1.MatchExpressionSet
 }
 
 func (r *KconfigRule) Match() (bool, error) {
diff --git a/source/custom/rules/loaded_kmod_rule.go b/source/custom/rules/loaded_kmod_rule.go
index 38a3be060..54e616d4a 100644
--- a/source/custom/rules/loaded_kmod_rule.go
+++ b/source/custom/rules/loaded_kmod_rule.go
@@ -19,14 +19,14 @@ package rules
 import (
 	"fmt"
 
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/source"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
 	"sigs.k8s.io/node-feature-discovery/source/kernel"
 )
 
 // LoadedKModRule matches loaded kernel modules in the system
 type LoadedKModRule struct {
-	expression.MatchExpressionSet
+	nfdv1alpha1.MatchExpressionSet
 }
 
 // Match loaded kernel modules on provided list of kernel modules
diff --git a/source/custom/rules/nodename_rule.go b/source/custom/rules/nodename_rule.go
index 7f5724f93..69eab4dad 100644
--- a/source/custom/rules/nodename_rule.go
+++ b/source/custom/rules/nodename_rule.go
@@ -20,14 +20,14 @@ import (
 	"encoding/json"
 	"fmt"
 
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/source"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
 	"sigs.k8s.io/node-feature-discovery/source/system"
 )
 
 // NodenameRule matches on nodenames configured in a ConfigMap
 type NodenameRule struct {
-	expression.MatchExpression
+	nfdv1alpha1.MatchExpression
 }
 
 func (r *NodenameRule) Match() (bool, error) {
@@ -43,8 +43,8 @@ func (r *NodenameRule) UnmarshalJSON(data []byte) error {
 		return err
 	}
 	// Force regexp matching
-	if r.Op == expression.MatchIn {
-		r.Op = expression.MatchInRegexp
+	if r.Op == nfdv1alpha1.MatchIn {
+		r.Op = nfdv1alpha1.MatchInRegexp
 	}
 	// We need to run Validate() because operator forcing above
 	return r.Validate()
diff --git a/source/custom/rules/pci_id_rule.go b/source/custom/rules/pci_id_rule.go
index 28793e341..9efa4e810 100644
--- a/source/custom/rules/pci_id_rule.go
+++ b/source/custom/rules/pci_id_rule.go
@@ -19,13 +19,13 @@ package rules
 import (
 	"fmt"
 
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/source"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
 	"sigs.k8s.io/node-feature-discovery/source/pci"
 )
 
 type PciIDRule struct {
-	expression.MatchExpressionSet
+	nfdv1alpha1.MatchExpressionSet
 }
 
 // Match PCI devices on provided PCI device attributes
diff --git a/source/custom/rules/usb_id_rule.go b/source/custom/rules/usb_id_rule.go
index 23f53e602..d0c83c106 100644
--- a/source/custom/rules/usb_id_rule.go
+++ b/source/custom/rules/usb_id_rule.go
@@ -19,13 +19,13 @@ package rules
 import (
 	"fmt"
 
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/source"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
 	"sigs.k8s.io/node-feature-discovery/source/usb"
 )
 
 type UsbIDRule struct {
-	expression.MatchExpressionSet
+	nfdv1alpha1.MatchExpressionSet
 }
 
 // Match USB devices on provided USB device attributes
diff --git a/source/custom/static_features.go b/source/custom/static_features.go
index d0c4cd96e..db1856c9f 100644
--- a/source/custom/static_features.go
+++ b/source/custom/static_features.go
@@ -17,7 +17,7 @@ limitations under the License.
 package custom
 
 import (
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/source/custom/rules"
 )
 
@@ -31,8 +31,10 @@ func getStaticFeatureConfig() []CustomRule {
 				MatchOn: []LegacyMatcher{
 					{
 						PciID: &rules.PciIDRule{
-							MatchExpressionSet: expression.MatchExpressionSet{
-								"vendor": expression.MustCreateMatchExpression(expression.MatchIn, "15b3"),
+							MatchExpressionSet: nfdv1alpha1.MatchExpressionSet{
+								Expressions: nfdv1alpha1.Expressions{
+									"vendor": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "15b3"),
+								},
 							},
 						},
 					},
@@ -45,9 +47,11 @@ func getStaticFeatureConfig() []CustomRule {
 				MatchOn: []LegacyMatcher{
 					{
 						LoadedKMod: &rules.LoadedKModRule{
-							MatchExpressionSet: expression.MatchExpressionSet{
-								"ib_uverbs": expression.MustCreateMatchExpression(expression.MatchExists),
-								"rdma_ucm":  expression.MustCreateMatchExpression(expression.MatchExists),
+							MatchExpressionSet: nfdv1alpha1.MatchExpressionSet{
+								Expressions: nfdv1alpha1.Expressions{
+									"ib_uverbs": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+									"rdma_ucm":  nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+								},
 							},
 						},
 					},


Test patch:
diff --git a/pkg/apis/nfd/v1alpha1/expression_test.go b/pkg/apis/nfd/v1alpha1/expression_test.go
new file mode 100644
index 000000000..33682a3b1
--- /dev/null
+++ b/pkg/apis/nfd/v1alpha1/expression_test.go
@@ -0,0 +1,439 @@
+/*
+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"
+
+	"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
+	api "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+)
+
+type BoolAssertionFuncf func(assert.TestingT, bool, string, ...interface{}) bool
+
+type ValueAssertionFuncf func(assert.TestingT, interface{}, string, ...interface{}) bool
+
+func TestCreateMatchExpression(t *testing.T) {
+	type V = api.MatchValue
+	type TC struct {
+		op     api.MatchOp
+		values V
+		err    ValueAssertionFuncf
+	}
+
+	tcs := []TC{
+		{op: api.MatchAny, err: assert.Nilf}, // #0
+		{op: api.MatchAny, values: V{"1"}, err: assert.NotNilf},
+
+		{op: api.MatchIn, err: assert.NotNilf},
+		{op: api.MatchIn, values: V{"1"}, err: assert.Nilf},
+		{op: api.MatchIn, values: V{"1", "2", "3", "4"}, err: assert.Nilf},
+
+		{op: api.MatchNotIn, err: assert.NotNilf},
+		{op: api.MatchNotIn, values: V{"1"}, err: assert.Nilf},
+		{op: api.MatchNotIn, values: V{"1", "2"}, err: assert.Nilf},
+
+		{op: api.MatchInRegexp, err: assert.NotNilf},
+		{op: api.MatchInRegexp, values: V{"1"}, err: assert.Nilf},
+		{op: api.MatchInRegexp, values: V{"()", "2", "3"}, err: assert.Nilf},
+		{op: api.MatchInRegexp, values: V{"("}, err: assert.NotNilf},
+
+		{op: api.MatchExists, err: assert.Nilf},
+		{op: api.MatchExists, values: V{"1"}, err: assert.NotNilf},
+
+		{op: api.MatchDoesNotExist, err: assert.Nilf},
+		{op: api.MatchDoesNotExist, values: V{"1"}, err: assert.NotNilf},
+
+		{op: api.MatchGt, err: assert.NotNilf},
+		{op: api.MatchGt, values: V{"1"}, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"-10"}, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"1", "2"}, err: assert.NotNilf},
+		{op: api.MatchGt, values: V{""}, err: assert.NotNilf},
+
+		{op: api.MatchLt, err: assert.NotNilf},
+		{op: api.MatchLt, values: V{"1"}, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"-1"}, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"1", "2", "3"}, err: assert.NotNilf},
+		{op: api.MatchLt, values: V{"a"}, err: assert.NotNilf},
+
+		{op: api.MatchGtLt, err: assert.NotNilf},
+		{op: api.MatchGtLt, values: V{"1"}, err: assert.NotNilf},
+		{op: api.MatchGtLt, values: V{"1", "2"}, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"2", "1"}, err: assert.NotNilf},
+		{op: api.MatchGtLt, values: V{"1", "2", "3"}, err: assert.NotNilf},
+		{op: api.MatchGtLt, values: V{"a", "2"}, err: assert.NotNilf},
+
+		{op: api.MatchIsTrue, err: assert.Nilf},
+		{op: api.MatchIsTrue, values: V{"1"}, err: assert.NotNilf},
+
+		{op: api.MatchIsFalse, err: assert.Nilf},
+		{op: api.MatchIsFalse, values: V{"1", "2"}, err: assert.NotNilf},
+	}
+
+	for i, tc := range tcs {
+		_, err := api.CreateMatchExpression(tc.op, tc.values...)
+		tc.err(t, err, "test case #%d (%v) failed", i, tc)
+	}
+}
+
+func TestMatch(t *testing.T) {
+	type V = api.MatchValue
+	type TC struct {
+		op     api.MatchOp
+		values V
+		input  interface{}
+		valid  bool
+		result BoolAssertionFuncf
+		err    ValueAssertionFuncf
+	}
+
+	tcs := []TC{
+		{op: api.MatchAny, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchAny, input: "2", valid: false, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
+
+		{op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchExists, input: nil, valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchExists, input: nil, valid: true, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchDoesNotExist, input: false, valid: false, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchDoesNotExist, input: false, valid: true, result: assert.Falsef, err: assert.Nilf},
+
+		{op: api.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"2"}, input: "3a", valid: true, result: assert.Falsef, err: assert.NotNilf},
+
+		{op: api.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"2"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf},
+
+		{op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"1", "10"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf},
+
+		{op: api.MatchIsTrue, input: true, valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIsTrue, input: true, valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchIsTrue, input: false, valid: true, result: assert.Falsef, err: assert.Nilf},
+
+		{op: api.MatchIsFalse, input: "false", valid: false, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIsFalse, input: "false", valid: true, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchIsFalse, input: "true", valid: true, result: assert.Falsef, err: assert.Nilf},
+	}
+
+	for i, tc := range tcs {
+		me := api.MustCreateMatchExpression(tc.op, tc.values...)
+		res, err := me.Match(tc.valid, tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
+		tc.err(t, err, "test case #%d (%v) failed", i, tc)
+	}
+
+	// Check some special error cases separately because MustCreateMatch panics
+	tcs = []TC{
+
+		{op: api.MatchGt, values: V{"3.0"}, input: 1, valid: true},
+		{op: api.MatchLt, values: V{"0x2"}, input: 1, valid: true},
+		{op: api.MatchGtLt, values: V{"1", "str"}, input: 1, valid: true},
+		{op: "non-existent-op", values: V{"1"}, input: 1, valid: true},
+	}
+
+	for i, tc := range tcs {
+		me := api.MatchExpression{Op: tc.op, Value: tc.values}
+		res, err := me.Match(tc.valid, tc.input)
+		assert.Falsef(t, res, "err test case #%d (%v) failed", i, tc)
+		assert.NotNilf(t, err, "err test case #%d (%v) failed", i, tc)
+	}
+}
+
+func TestMatchKeys(t *testing.T) {
+	type V = api.MatchValue
+	type I = map[string]feature.Nil
+	type TC struct {
+		op     api.MatchOp
+		values V
+		name   string
+		input  I
+		result BoolAssertionFuncf
+		err    ValueAssertionFuncf
+	}
+
+	tcs := []TC{
+		{op: api.MatchAny, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchExists, name: "foo", input: nil, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchExists, name: "foo", input: I{"bar": {}}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchExists, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchDoesNotExist, name: "foo", input: I{}, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchDoesNotExist, name: "foo", input: I{"bar": {}}, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchDoesNotExist, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Falsef, err: assert.Nilf},
+
+		// All other ops should return an error
+		{op: api.MatchIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+		{op: api.MatchNotIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+		{op: api.MatchInRegexp, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+		{op: api.MatchGt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+		{op: api.MatchLt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+		{op: api.MatchGtLt, values: V{"1", "10"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+		{op: api.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+		{op: api.MatchIsFalse, name: "foo", result: assert.Falsef, err: assert.NotNilf},
+	}
+
+	for i, tc := range tcs {
+		me := api.MustCreateMatchExpression(tc.op, tc.values...)
+		res, err := me.MatchKeys(tc.name, tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
+		tc.err(t, err, "test case #%d (%v) failed", i, tc)
+	}
+}
+
+func TestMatchValues(t *testing.T) {
+	type V = []string
+	type I = map[string]string
+
+	type TC struct {
+		op     api.MatchOp
+		values V
+		name   string
+		input  I
+		result BoolAssertionFuncf
+		err    ValueAssertionFuncf
+	}
+
+	tcs := []TC{
+		{op: api.MatchAny, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Falsef, err: assert.Nilf},
+
+		{op: api.MatchInRegexp, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "9"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchExists, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchExists, name: "foo", input: I{"foo": "1"}, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchDoesNotExist, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf},
+
+		{op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
+
+		{op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
+
+		{op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf},
+		{op: api.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
+
+		{op: api.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIsTrue, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIsTrue, name: "foo", input: I{"foo": "true"}, result: assert.Truef, err: assert.Nilf},
+
+		{op: api.MatchIsFalse, name: "foo", input: I{"foo": "true"}, result: assert.Falsef, err: assert.Nilf},
+		{op: api.MatchIsFalse, name: "foo", input: I{"foo": "false"}, result: assert.Truef, err: assert.Nilf},
+	}
+
+	for i, tc := range tcs {
+		me := api.MustCreateMatchExpression(tc.op, tc.values...)
+		res, err := me.MatchValues(tc.name, tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
+		tc.err(t, err, "test case #%d (%v) failed", i, tc)
+	}
+}
+
+func TestMESMatchKeys(t *testing.T) {
+	type I = map[string]feature.Nil
+	type TC struct {
+		mes    string
+		input  I
+		result BoolAssertionFuncf
+		err    ValueAssertionFuncf
+	}
+
+	tcs := []TC{
+		{result: assert.Truef, err: assert.Nilf},
+
+		{input: I{"foo": {}}, result: assert.Truef, err: assert.Nilf},
+
+		{mes: `
+foo: { op: DoesNotExist }
+bar: { op: Exists }
+`,
+			input:  I{"bar": {}, "baz": {}},
+			result: assert.Truef, err: assert.Nilf},
+
+		{mes: `
+foo: { op: DoesNotExist }
+bar: { op: Exists }
+`,
+			input:  I{"foo": {}, "bar": {}, "baz": {}},
+			result: assert.Falsef, err: assert.Nilf},
+
+		{mes: `
+foo: { op: In, value: ["bar"] }
+bar: { op: Exists }
+`,
+			input:  I{"bar": {}, "baz": {}},
+			result: assert.Falsef, err: assert.NotNilf},
+	}
+
+	for i, tc := range tcs {
+		mes := &api.MatchExpressionSet{}
+		if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
+			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
+		}
+
+		res, err := mes.MatchKeys(tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
+		tc.err(t, err, "test case #%d (%v) failed", i, tc)
+	}
+}
+
+func TestMESMatchValues(t *testing.T) {
+	type I = map[string]string
+	type TC struct {
+		mes    string
+		input  I
+		result BoolAssertionFuncf
+		err    ValueAssertionFuncf
+	}
+
+	tcs := []TC{
+		{result: assert.Truef, err: assert.Nilf},
+
+		{input: I{"foo": "bar"}, result: assert.Truef, err: assert.Nilf},
+
+		{mes: `
+foo: { op: Exists }
+bar: { op: In, value: ["val", "wal"] }
+baz: { op: Gt, value: ["10"] }
+`,
+			input:  I{"bar": "val"},
+			result: assert.Falsef, err: assert.Nilf},
+
+		{mes: `
+foo: { op: Exists }
+bar: { op: In, value: ["val", "wal"] }
+baz: { op: Gt, value: ["10"] }
+`,
+			input:  I{"foo": "1", "bar": "val", "baz": "123"},
+			result: assert.Truef, err: assert.Nilf},
+
+		{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.Falsef, err: assert.NotNilf},
+	}
+
+	for i, tc := range tcs {
+		mes := &api.MatchExpressionSet{}
+		if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
+			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
+		}
+
+		res, err := mes.MatchValues(tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
+		tc.err(t, err, "test case #%d (%v) failed", i, tc)
+	}
+}
+
+func TestMESMatchInstances(t *testing.T) {
+	type I = feature.InstanceFeature
+	type A = map[string]string
+	type TC struct {
+		mes    string
+		input  []I
+		result BoolAssertionFuncf
+		err    ValueAssertionFuncf
+	}
+
+	tcs := []TC{
+		{result: assert.Falsef, err: assert.Nilf}, // nil instances -> false
+
+		{input: []I{}, result: assert.Falsef, err: assert.Nilf}, // zero instances -> false
+
+		{input: []I{I{Attributes: A{}}}, result: assert.Truef, err: assert.Nilf}, // one "empty" instance
+
+		{mes: `
+foo: { op: Exists }
+bar: { op: Lt, value: ["10"] }
+`,
+			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}},
+			result: assert.Falsef, err: assert.Nilf},
+
+		{mes: `
+foo: { op: Exists }
+bar: { op: Lt, value: ["10"] }
+`,
+			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}},
+			result: assert.Truef, err: assert.Nilf},
+
+		{mes: `
+bar: { op: Lt, value: ["10"] }
+`,
+			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}},
+			result: assert.Falsef, err: assert.NotNilf},
+	}
+
+	for i, tc := range tcs {
+		mes := &api.MatchExpressionSet{}
+		if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
+			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
+		}
+
+		res, err := mes.MatchInstances(tc.input)
+		tc.result(t, res, "test case #%d (%v) failed", i, tc)
+		tc.err(t, err, "test case #%d (%v) failed", i, tc)
+	}
+}
diff --git a/source/custom/custom_test.go b/source/custom/custom_test.go
index 968413260..7d4f6f8d7 100644
--- a/source/custom/custom_test.go
+++ b/source/custom/custom_test.go
@@ -21,7 +21,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
-	"sigs.k8s.io/node-feature-discovery/source/custom/expression"
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 )
 
 func TestRule(t *testing.T) {
@@ -31,8 +31,12 @@ func TestRule(t *testing.T) {
 		Labels: map[string]string{"label-1": "label-val-1"},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature:          "domain-1.kf-1",
-				MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchExists)},
+				Feature: "domain-1.kf-1",
+				MatchExpressions: nfdv1alpha1.MatchExpressionSet{
+					Expressions: nfdv1alpha1.Expressions{
+						"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+					},
+				},
 			},
 		},
 	}
@@ -94,8 +98,12 @@ func TestRule(t *testing.T) {
 		Labels: map[string]string{"label-3": "label-val-3", "empty": ""},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature:          "domain-1.vf-1",
-				MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-1")},
+				Feature: "domain-1.vf-1",
+				MatchExpressions: nfdv1alpha1.MatchExpressionSet{
+					Expressions: nfdv1alpha1.Expressions{
+						"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1"),
+					},
+				},
 			},
 		},
 	}
@@ -113,8 +121,12 @@ func TestRule(t *testing.T) {
 		Labels: map[string]string{"label-4": "label-val-4"},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature:          "domain-1.if-1",
-				MatchExpressions: expression.MatchExpressionSet{"attr-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-1")},
+				Feature: "domain-1.if-1",
+				MatchExpressions: nfdv1alpha1.MatchExpressionSet{
+					Expressions: nfdv1alpha1.Expressions{
+						"attr-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1"),
+					},
+				},
 			},
 		},
 	}
@@ -132,12 +144,20 @@ func TestRule(t *testing.T) {
 		Labels: map[string]string{"label-5": "label-val-5"},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature:          "domain-1.vf-1",
-				MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-x")},
+				Feature: "domain-1.vf-1",
+				MatchExpressions: nfdv1alpha1.MatchExpressionSet{
+					Expressions: nfdv1alpha1.Expressions{
+						"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-x"),
+					},
+				},
 			},
 			FeatureMatcherTerm{
-				Feature:          "domain-1.if-1",
-				MatchExpressions: expression.MatchExpressionSet{"attr-1": expression.MustCreateMatchExpression(expression.MatchIn, "val-1")},
+				Feature: "domain-1.if-1",
+				MatchExpressions: nfdv1alpha1.MatchExpressionSet{
+					Expressions: nfdv1alpha1.Expressions{
+						"attr-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1"),
+					},
+				},
 			},
 		},
 	}
@@ -145,7 +165,7 @@ func TestRule(t *testing.T) {
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m, "instances should not have matched")
 
-	r5.MatchFeatures[0].MatchExpressions["key-1"] = expression.MustCreateMatchExpression(expression.MatchIn, "val-1")
+	r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1")
 	m, err = r5.execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r5.Labels, m, "instances should have matched")
@@ -155,8 +175,12 @@ func TestRule(t *testing.T) {
 		MatchAnyElem{
 			MatchFeatures: FeatureMatcher{
 				FeatureMatcherTerm{
-					Feature:          "domain-1.kf-1",
-					MatchExpressions: expression.MatchExpressionSet{"key-na": expression.MustCreateMatchExpression(expression.MatchExists)},
+					Feature: "domain-1.kf-1",
+					MatchExpressions: nfdv1alpha1.MatchExpressionSet{
+						Expressions: nfdv1alpha1.Expressions{
+							"key-na": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+						},
+					},
 				},
 			},
 		},
@@ -169,12 +193,16 @@ func TestRule(t *testing.T) {
 		MatchAnyElem{
 			MatchFeatures: FeatureMatcher{
 				FeatureMatcherTerm{
-					Feature:          "domain-1.kf-1",
-					MatchExpressions: expression.MatchExpressionSet{"key-1": expression.MustCreateMatchExpression(expression.MatchExists)},
+					Feature: "domain-1.kf-1",
+					MatchExpressions: nfdv1alpha1.MatchExpressionSet{
+						Expressions: nfdv1alpha1.Expressions{
+							"key-1": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+						},
+					},
 				},
 			},
 		})
-	r5.MatchFeatures[0].MatchExpressions["key-1"] = expression.MustCreateMatchExpression(expression.MatchIn, "val-1")
+	r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "val-1")
 	m, err = r5.execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r5.Labels, m, "instances should have matched")
diff --git a/source/custom/expression/expression_test.go b/source/custom/expression/expression_test.go
deleted file mode 100644
index 94b1681e3..000000000
--- a/source/custom/expression/expression_test.go
+++ /dev/null
@@ -1,439 +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 expression_test
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"sigs.k8s.io/yaml"
-
-	"sigs.k8s.io/node-feature-discovery/pkg/api/feature"
-	e "sigs.k8s.io/node-feature-discovery/source/custom/expression"
-)
-
-type BoolAssertionFuncf func(assert.TestingT, bool, string, ...interface{}) bool
-
-type ValueAssertionFuncf func(assert.TestingT, interface{}, string, ...interface{}) bool
-
-func TestCreateMatchExpression(t *testing.T) {
-	type V = e.MatchValue
-	type TC struct {
-		op     e.MatchOp
-		values V
-		err    ValueAssertionFuncf
-	}
-
-	tcs := []TC{
-		{op: e.MatchAny, err: assert.Nilf}, // #0
-		{op: e.MatchAny, values: V{"1"}, err: assert.NotNilf},
-
-		{op: e.MatchIn, err: assert.NotNilf},
-		{op: e.MatchIn, values: V{"1"}, err: assert.Nilf},
-		{op: e.MatchIn, values: V{"1", "2", "3", "4"}, err: assert.Nilf},
-
-		{op: e.MatchNotIn, err: assert.NotNilf},
-		{op: e.MatchNotIn, values: V{"1"}, err: assert.Nilf},
-		{op: e.MatchNotIn, values: V{"1", "2"}, err: assert.Nilf},
-
-		{op: e.MatchInRegexp, err: assert.NotNilf},
-		{op: e.MatchInRegexp, values: V{"1"}, err: assert.Nilf},
-		{op: e.MatchInRegexp, values: V{"()", "2", "3"}, err: assert.Nilf},
-		{op: e.MatchInRegexp, values: V{"("}, err: assert.NotNilf},
-
-		{op: e.MatchExists, err: assert.Nilf},
-		{op: e.MatchExists, values: V{"1"}, err: assert.NotNilf},
-
-		{op: e.MatchDoesNotExist, err: assert.Nilf},
-		{op: e.MatchDoesNotExist, values: V{"1"}, err: assert.NotNilf},
-
-		{op: e.MatchGt, err: assert.NotNilf},
-		{op: e.MatchGt, values: V{"1"}, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"-10"}, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"1", "2"}, err: assert.NotNilf},
-		{op: e.MatchGt, values: V{""}, err: assert.NotNilf},
-
-		{op: e.MatchLt, err: assert.NotNilf},
-		{op: e.MatchLt, values: V{"1"}, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"-1"}, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"1", "2", "3"}, err: assert.NotNilf},
-		{op: e.MatchLt, values: V{"a"}, err: assert.NotNilf},
-
-		{op: e.MatchGtLt, err: assert.NotNilf},
-		{op: e.MatchGtLt, values: V{"1"}, err: assert.NotNilf},
-		{op: e.MatchGtLt, values: V{"1", "2"}, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"2", "1"}, err: assert.NotNilf},
-		{op: e.MatchGtLt, values: V{"1", "2", "3"}, err: assert.NotNilf},
-		{op: e.MatchGtLt, values: V{"a", "2"}, err: assert.NotNilf},
-
-		{op: e.MatchIsTrue, err: assert.Nilf},
-		{op: e.MatchIsTrue, values: V{"1"}, err: assert.NotNilf},
-
-		{op: e.MatchIsFalse, err: assert.Nilf},
-		{op: e.MatchIsFalse, values: V{"1", "2"}, err: assert.NotNilf},
-	}
-
-	for i, tc := range tcs {
-		_, err := e.CreateMatchExpression(tc.op, tc.values...)
-		tc.err(t, err, "test case #%d (%v) failed", i, tc)
-	}
-}
-
-func TestMatch(t *testing.T) {
-	type V = e.MatchValue
-	type TC struct {
-		op     e.MatchOp
-		values V
-		input  interface{}
-		valid  bool
-		result BoolAssertionFuncf
-		err    ValueAssertionFuncf
-	}
-
-	tcs := []TC{
-		{op: e.MatchAny, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchAny, input: "2", valid: false, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchIn, values: V{"1"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIn, values: V{"1"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchNotIn, values: V{"2"}, input: 2, valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchNotIn, values: V{"1"}, input: 2, valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchNotIn, values: V{"1", "2", "3"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
-
-		{op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-1", valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchInRegexp, values: V{"val-[0-9]$"}, input: "val-12", valid: true, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchInRegexp, values: V{"val-[0-9]$", "al-[1-9]"}, input: "val-12", valid: true, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchExists, input: nil, valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchExists, input: nil, valid: true, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchDoesNotExist, input: false, valid: false, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchDoesNotExist, input: false, valid: true, result: assert.Falsef, err: assert.Nilf},
-
-		{op: e.MatchGt, values: V{"2"}, input: 3, valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"2"}, input: 2, valid: true, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"2"}, input: 3, valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"-10"}, input: -3, valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"2"}, input: "3a", valid: true, result: assert.Falsef, err: assert.NotNilf},
-
-		{op: e.MatchLt, values: V{"2"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"2"}, input: "2", valid: true, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"-10"}, input: -3, valid: true, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"2"}, input: "1", valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"2"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf},
-
-		{op: e.MatchGtLt, values: V{"1", "10"}, input: "1", valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"1", "10"}, input: "1", valid: true, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"1", "10"}, input: "10", valid: true, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"1", "10"}, input: "2", valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"1", "10"}, input: "1.0", valid: true, result: assert.Falsef, err: assert.NotNilf},
-
-		{op: e.MatchIsTrue, input: true, valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIsTrue, input: true, valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchIsTrue, input: false, valid: true, result: assert.Falsef, err: assert.Nilf},
-
-		{op: e.MatchIsFalse, input: "false", valid: false, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIsFalse, input: "false", valid: true, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchIsFalse, input: "true", valid: true, result: assert.Falsef, err: assert.Nilf},
-	}
-
-	for i, tc := range tcs {
-		me := e.MustCreateMatchExpression(tc.op, tc.values...)
-		res, err := me.Match(tc.valid, tc.input)
-		tc.result(t, res, "test case #%d (%v) failed", i, tc)
-		tc.err(t, err, "test case #%d (%v) failed", i, tc)
-	}
-
-	// Check some special error cases separately because MustCreateMatch panics
-	tcs = []TC{
-
-		{op: e.MatchGt, values: V{"3.0"}, input: 1, valid: true},
-		{op: e.MatchLt, values: V{"0x2"}, input: 1, valid: true},
-		{op: e.MatchGtLt, values: V{"1", "str"}, input: 1, valid: true},
-		{op: "non-existent-op", values: V{"1"}, input: 1, valid: true},
-	}
-
-	for i, tc := range tcs {
-		me := e.MatchExpression{Op: tc.op, Value: tc.values}
-		res, err := me.Match(tc.valid, tc.input)
-		assert.Falsef(t, res, "err test case #%d (%v) failed", i, tc)
-		assert.NotNilf(t, err, "err test case #%d (%v) failed", i, tc)
-	}
-}
-
-func TestMatchKeys(t *testing.T) {
-	type V = e.MatchValue
-	type I = map[string]feature.Nil
-	type TC struct {
-		op     e.MatchOp
-		values V
-		name   string
-		input  I
-		result BoolAssertionFuncf
-		err    ValueAssertionFuncf
-	}
-
-	tcs := []TC{
-		{op: e.MatchAny, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchExists, name: "foo", input: nil, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchExists, name: "foo", input: I{"bar": {}}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchExists, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchDoesNotExist, name: "foo", input: I{}, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchDoesNotExist, name: "foo", input: I{"bar": {}}, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchDoesNotExist, name: "foo", input: I{"bar": {}, "foo": {}}, result: assert.Falsef, err: assert.Nilf},
-
-		// All other ops should return an error
-		{op: e.MatchIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-		{op: e.MatchNotIn, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-		{op: e.MatchInRegexp, values: V{"foo"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-		{op: e.MatchGt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-		{op: e.MatchLt, values: V{"1"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-		{op: e.MatchGtLt, values: V{"1", "10"}, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-		{op: e.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-		{op: e.MatchIsFalse, name: "foo", result: assert.Falsef, err: assert.NotNilf},
-	}
-
-	for i, tc := range tcs {
-		me := e.MustCreateMatchExpression(tc.op, tc.values...)
-		res, err := me.MatchKeys(tc.name, tc.input)
-		tc.result(t, res, "test case #%d (%v) failed", i, tc)
-		tc.err(t, err, "test case #%d (%v) failed", i, tc)
-	}
-}
-
-func TestMatchValues(t *testing.T) {
-	type V = []string
-	type I = map[string]string
-
-	type TC struct {
-		op     e.MatchOp
-		values V
-		name   string
-		input  I
-		result BoolAssertionFuncf
-		err    ValueAssertionFuncf
-	}
-
-	tcs := []TC{
-		{op: e.MatchAny, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "3"}, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchNotIn, values: V{"1", "2"}, name: "foo", input: I{"foo": "2"}, result: assert.Falsef, err: assert.Nilf},
-
-		{op: e.MatchInRegexp, values: V{"1", "2"}, name: "foo", input: I{"bar": "2"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "9"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchInRegexp, values: V{"1", "[0-8]"}, name: "foo", input: I{"foo": "2"}, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchExists, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchExists, name: "foo", input: I{"foo": "1"}, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchDoesNotExist, name: "foo", input: nil, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchDoesNotExist, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf},
-
-		{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "2"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "3", "foo": "3"}, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchGt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
-
-		{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "2"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchLt, values: V{"2"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
-
-		{op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "11"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "-11"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "1", "foo": "1"}, result: assert.Truef, err: assert.Nilf},
-		{op: e.MatchGtLt, values: V{"-10", "10"}, name: "foo", input: I{"bar": "str", "foo": "str"}, result: assert.Falsef, err: assert.NotNilf},
-
-		{op: e.MatchIsTrue, name: "foo", result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIsTrue, name: "foo", input: I{"foo": "1"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIsTrue, name: "foo", input: I{"foo": "true"}, result: assert.Truef, err: assert.Nilf},
-
-		{op: e.MatchIsFalse, name: "foo", input: I{"foo": "true"}, result: assert.Falsef, err: assert.Nilf},
-		{op: e.MatchIsFalse, name: "foo", input: I{"foo": "false"}, result: assert.Truef, err: assert.Nilf},
-	}
-
-	for i, tc := range tcs {
-		me := e.MustCreateMatchExpression(tc.op, tc.values...)
-		res, err := me.MatchValues(tc.name, tc.input)
-		tc.result(t, res, "test case #%d (%v) failed", i, tc)
-		tc.err(t, err, "test case #%d (%v) failed", i, tc)
-	}
-}
-
-func TestMESMatchKeys(t *testing.T) {
-	type I = map[string]feature.Nil
-	type TC struct {
-		mes    string
-		input  I
-		result BoolAssertionFuncf
-		err    ValueAssertionFuncf
-	}
-
-	tcs := []TC{
-		{result: assert.Truef, err: assert.Nilf},
-
-		{input: I{"foo": {}}, result: assert.Truef, err: assert.Nilf},
-
-		{mes: `
-foo: { op: DoesNotExist }
-bar: { op: Exists }
-`,
-			input:  I{"bar": {}, "baz": {}},
-			result: assert.Truef, err: assert.Nilf},
-
-		{mes: `
-foo: { op: DoesNotExist }
-bar: { op: Exists }
-`,
-			input:  I{"foo": {}, "bar": {}, "baz": {}},
-			result: assert.Falsef, err: assert.Nilf},
-
-		{mes: `
-foo: { op: In, value: ["bar"] }
-bar: { op: Exists }
-`,
-			input:  I{"bar": {}, "baz": {}},
-			result: assert.Falsef, err: assert.NotNilf},
-	}
-
-	for i, tc := range tcs {
-		mes := &e.MatchExpressionSet{}
-		if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
-			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
-		}
-
-		res, err := mes.MatchKeys(tc.input)
-		tc.result(t, res, "test case #%d (%v) failed", i, tc)
-		tc.err(t, err, "test case #%d (%v) failed", i, tc)
-	}
-}
-
-func TestMESMatchValues(t *testing.T) {
-	type I = map[string]string
-	type TC struct {
-		mes    string
-		input  I
-		result BoolAssertionFuncf
-		err    ValueAssertionFuncf
-	}
-
-	tcs := []TC{
-		{result: assert.Truef, err: assert.Nilf},
-
-		{input: I{"foo": "bar"}, result: assert.Truef, err: assert.Nilf},
-
-		{mes: `
-foo: { op: Exists }
-bar: { op: In, value: ["val", "wal"] }
-baz: { op: Gt, value: ["10"] }
-`,
-			input:  I{"bar": "val"},
-			result: assert.Falsef, err: assert.Nilf},
-
-		{mes: `
-foo: { op: Exists }
-bar: { op: In, value: ["val", "wal"] }
-baz: { op: Gt, value: ["10"] }
-`,
-			input:  I{"foo": "1", "bar": "val", "baz": "123"},
-			result: assert.Truef, err: assert.Nilf},
-
-		{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.Falsef, err: assert.NotNilf},
-	}
-
-	for i, tc := range tcs {
-		mes := &e.MatchExpressionSet{}
-		if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
-			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
-		}
-
-		res, err := mes.MatchValues(tc.input)
-		tc.result(t, res, "test case #%d (%v) failed", i, tc)
-		tc.err(t, err, "test case #%d (%v) failed", i, tc)
-	}
-}
-
-func TestMESMatchInstances(t *testing.T) {
-	type I = feature.InstanceFeature
-	type A = map[string]string
-	type TC struct {
-		mes    string
-		input  []I
-		result BoolAssertionFuncf
-		err    ValueAssertionFuncf
-	}
-
-	tcs := []TC{
-		{result: assert.Falsef, err: assert.Nilf}, // nil instances -> false
-
-		{input: []I{}, result: assert.Falsef, err: assert.Nilf}, // zero instances -> false
-
-		{input: []I{I{Attributes: A{}}}, result: assert.Truef, err: assert.Nilf}, // one "empty" instance
-
-		{mes: `
-foo: { op: Exists }
-bar: { op: Lt, value: ["10"] }
-`,
-			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "1"}}},
-			result: assert.Falsef, err: assert.Nilf},
-
-		{mes: `
-foo: { op: Exists }
-bar: { op: Lt, value: ["10"] }
-`,
-			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"foo": "2", "bar": "1"}}},
-			result: assert.Truef, err: assert.Nilf},
-
-		{mes: `
-bar: { op: Lt, value: ["10"] }
-`,
-			input:  []I{I{Attributes: A{"foo": "1"}}, I{Attributes: A{"bar": "0x1"}}},
-			result: assert.Falsef, err: assert.NotNilf},
-	}
-
-	for i, tc := range tcs {
-		mes := &e.MatchExpressionSet{}
-		if err := yaml.Unmarshal([]byte(tc.mes), mes); err != nil {
-			t.Fatalf("failed to parse data of test case #%d (%v): %v", i, tc, err)
-		}
-
-		res, err := mes.MatchInstances(tc.input)
-		tc.result(t, res, "test case #%d (%v) failed", i, tc)
-		tc.err(t, err, "test case #%d (%v) failed", i, tc)
-	}
-}