Repository: kubernetes-sigs/node-feature-discovery
Pull request #925 (2022-10-18 15:43:34)

apis/nfd: flatten the structure of features data type 
Flatten the data structure that stores features, dropping the "domain"
level from the data model. That extra level of hierarchy brought little
benefit but just caused some extra complexity, instead. The new
structure nicely matches what we have in the NodeFeatureRule object (the
matchFeatures field of uses the same flat structure with the "feature"
field having a value `<domain>.<feature>`, e.g. `kernel.version`).

This is pre-work for introducing a new "node feature" CRD that contains
the raw feature data. It makes the life of both users and developers
easier when both CRDs, plus our internal code, handle feature data in a
similar flat structure.

Split out from #903


Patch:
diff --git a/deployment/base/nfd-crds/nfd-api-crds.yaml b/deployment/base/nfd-crds/nfd-api-crds.yaml
new file mode 100644
index 000000000..c59796b83
--- /dev/null
+++ b/deployment/base/nfd-crds/nfd-api-crds.yaml
@@ -0,0 +1,334 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  creationTimestamp: null
+  name: nodefeatures.nfd.k8s-sigs.io
+spec:
+  group: nfd.k8s-sigs.io
+  names:
+    kind: NodeFeature
+    listKind: NodeFeatureList
+    plural: nodefeatures
+    singular: nodefeature
+  scope: Cluster
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: NodeFeature resource holds the features discovered for one node
+          in the cluster.
+        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: NodeFeatureSpec describes a NodeFeature object.
+            properties:
+              domains:
+                description: Features is a list per-domain feature sets. This is the
+                  full "raw" features data that nfd-worker has discovered.
+                items:
+                  description: DomainFeatures is the collection of all discovered
+                    features of one domain.
+                  properties:
+                    attributes:
+                      additionalProperties:
+                        description: AttributeFeatureSet is a set of features having
+                          string value.
+                        properties:
+                          elements:
+                            additionalProperties:
+                              type: string
+                            type: object
+                        required:
+                        - elements
+                        type: object
+                      type: object
+                    flags:
+                      additionalProperties:
+                        description: FlagFeatureSet is a set of simple features only
+                          containing names without values.
+                        properties:
+                          elements:
+                            additionalProperties:
+                              description: Nil is a dummy empty struct for protobuf
+                                compatibility
+                              type: object
+                            type: object
+                        required:
+                        - elements
+                        type: object
+                      type: object
+                    instances:
+                      additionalProperties:
+                        description: InstanceFeatureSet is a set of features each
+                          of which is an instance having multiple attributes.
+                        properties:
+                          elements:
+                            items:
+                              description: InstanceFeature represents one instance
+                                of a complex features, e.g. a device.
+                              properties:
+                                attributes:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              required:
+                              - attributes
+                              type: object
+                            type: array
+                        required:
+                        - elements
+                        type: object
+                      type: object
+                    name:
+                      type: string
+                  required:
+                  - attributes
+                  - flags
+                  - instances
+                  - name
+                  type: object
+                type: array
+              labelsRequest:
+                additionalProperties:
+                  type: string
+                description: LabelsRequest is the set of node labels that are requested
+                  to be generatd.
+                type: object
+            required:
+            - domains
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  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
+                    vars:
+                      additionalProperties:
+                        type: string
+                      description: Vars is the variables to store if the rule matches.
+                        Variables do not directly inflict any changes in the node
+                        object. However, they can be referenced from other rules enabling
+                        more complex rule hierarchies, without exposing intermediary
+                        output values as labels.
+                      type: object
+                    varsTemplate:
+                      description: VarsTemplate specifies a template to expand for
+                        dynamically generating multiple variables. Data (after template
+                        expansion) must be keys with an optional value (<key>[=<value>])
+                        separated by newlines.
+                      type: string
+                  required:
+                  - name
+                  type: object
+                type: array
+            required:
+            - rules
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
diff --git a/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml
new file mode 100644
index 000000000..c59796b83
--- /dev/null
+++ b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml
@@ -0,0 +1,334 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  creationTimestamp: null
+  name: nodefeatures.nfd.k8s-sigs.io
+spec:
+  group: nfd.k8s-sigs.io
+  names:
+    kind: NodeFeature
+    listKind: NodeFeatureList
+    plural: nodefeatures
+    singular: nodefeature
+  scope: Cluster
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: NodeFeature resource holds the features discovered for one node
+          in the cluster.
+        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: NodeFeatureSpec describes a NodeFeature object.
+            properties:
+              domains:
+                description: Features is a list per-domain feature sets. This is the
+                  full "raw" features data that nfd-worker has discovered.
+                items:
+                  description: DomainFeatures is the collection of all discovered
+                    features of one domain.
+                  properties:
+                    attributes:
+                      additionalProperties:
+                        description: AttributeFeatureSet is a set of features having
+                          string value.
+                        properties:
+                          elements:
+                            additionalProperties:
+                              type: string
+                            type: object
+                        required:
+                        - elements
+                        type: object
+                      type: object
+                    flags:
+                      additionalProperties:
+                        description: FlagFeatureSet is a set of simple features only
+                          containing names without values.
+                        properties:
+                          elements:
+                            additionalProperties:
+                              description: Nil is a dummy empty struct for protobuf
+                                compatibility
+                              type: object
+                            type: object
+                        required:
+                        - elements
+                        type: object
+                      type: object
+                    instances:
+                      additionalProperties:
+                        description: InstanceFeatureSet is a set of features each
+                          of which is an instance having multiple attributes.
+                        properties:
+                          elements:
+                            items:
+                              description: InstanceFeature represents one instance
+                                of a complex features, e.g. a device.
+                              properties:
+                                attributes:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              required:
+                              - attributes
+                              type: object
+                            type: array
+                        required:
+                        - elements
+                        type: object
+                      type: object
+                    name:
+                      type: string
+                  required:
+                  - attributes
+                  - flags
+                  - instances
+                  - name
+                  type: object
+                type: array
+              labelsRequest:
+                additionalProperties:
+                  type: string
+                description: LabelsRequest is the set of node labels that are requested
+                  to be generatd.
+                type: object
+            required:
+            - domains
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  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
+                    vars:
+                      additionalProperties:
+                        type: string
+                      description: Vars is the variables to store if the rule matches.
+                        Variables do not directly inflict any changes in the node
+                        object. However, they can be referenced from other rules enabling
+                        more complex rule hierarchies, without exposing intermediary
+                        output values as labels.
+                      type: object
+                    varsTemplate:
+                      description: VarsTemplate specifies a template to expand for
+                        dynamically generating multiple variables. Data (after template
+                        expansion) must be keys with an optional value (<key>[=<value>])
+                        separated by newlines.
+                      type: string
+                  required:
+                  - name
+                  type: object
+                type: array
+            required:
+            - rules
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
diff --git a/pkg/apihelper/mock_APIHelpers.go b/pkg/apihelper/mock_APIHelpers.go
index 0f0fd895c..7e677339f 100644
--- a/pkg/apihelper/mock_APIHelpers.go
+++ b/pkg/apihelper/mock_APIHelpers.go
@@ -6,7 +6,7 @@ import (
 	mock "github.com/stretchr/testify/mock"
 	kubernetes "k8s.io/client-go/kubernetes"
 
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 
 	versioned "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
 )
@@ -40,15 +40,15 @@ func (_m *MockAPIHelpers) GetClient() (*kubernetes.Clientset, error) {
 }
 
 // GetNode provides a mock function with given fields: _a0, _a1
-func (_m *MockAPIHelpers) GetNode(_a0 *kubernetes.Clientset, _a1 string) (*corev1.Node, error) {
+func (_m *MockAPIHelpers) GetNode(_a0 *kubernetes.Clientset, _a1 string) (*v1.Node, error) {
 	ret := _m.Called(_a0, _a1)
 
-	var r0 *corev1.Node
-	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, string) *corev1.Node); ok {
+	var r0 *v1.Node
+	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, string) *v1.Node); ok {
 		r0 = rf(_a0, _a1)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*corev1.Node)
+			r0 = ret.Get(0).(*v1.Node)
 		}
 	}
 
@@ -63,15 +63,15 @@ func (_m *MockAPIHelpers) GetNode(_a0 *kubernetes.Clientset, _a1 string) (*corev
 }
 
 // GetNodes provides a mock function with given fields: _a0
-func (_m *MockAPIHelpers) GetNodes(_a0 *kubernetes.Clientset) (*corev1.NodeList, error) {
+func (_m *MockAPIHelpers) GetNodes(_a0 *kubernetes.Clientset) (*v1.NodeList, error) {
 	ret := _m.Called(_a0)
 
-	var r0 *corev1.NodeList
-	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset) *corev1.NodeList); ok {
+	var r0 *v1.NodeList
+	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset) *v1.NodeList); ok {
 		r0 = rf(_a0)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*corev1.NodeList)
+			r0 = ret.Get(0).(*v1.NodeList)
 		}
 	}
 
@@ -86,15 +86,15 @@ func (_m *MockAPIHelpers) GetNodes(_a0 *kubernetes.Clientset) (*corev1.NodeList,
 }
 
 // GetPod provides a mock function with given fields: _a0, _a1, _a2
-func (_m *MockAPIHelpers) GetPod(_a0 *kubernetes.Clientset, _a1 string, _a2 string) (*corev1.Pod, error) {
+func (_m *MockAPIHelpers) GetPod(_a0 *kubernetes.Clientset, _a1 string, _a2 string) (*v1.Pod, error) {
 	ret := _m.Called(_a0, _a1, _a2)
 
-	var r0 *corev1.Pod
-	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, string, string) *corev1.Pod); ok {
+	var r0 *v1.Pod
+	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, string, string) *v1.Pod); ok {
 		r0 = rf(_a0, _a1, _a2)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*corev1.Pod)
+			r0 = ret.Get(0).(*v1.Pod)
 		}
 	}
 
@@ -160,11 +160,11 @@ func (_m *MockAPIHelpers) PatchNodeStatus(_a0 *kubernetes.Clientset, _a1 string,
 }
 
 // UpdateNode provides a mock function with given fields: _a0, _a1
-func (_m *MockAPIHelpers) UpdateNode(_a0 *kubernetes.Clientset, _a1 *corev1.Node) error {
+func (_m *MockAPIHelpers) UpdateNode(_a0 *kubernetes.Clientset, _a1 *v1.Node) error {
 	ret := _m.Called(_a0, _a1)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, *corev1.Node) error); ok {
+	if rf, ok := ret.Get(0).(func(*kubernetes.Clientset, *v1.Node) error); ok {
 		r0 = rf(_a0, _a1)
 	} else {
 		r0 = ret.Error(0)
diff --git a/pkg/apis/nfd/v1alpha1/feature.go b/pkg/apis/nfd/v1alpha1/feature.go
index b54117cfe..17948aaa9 100644
--- a/pkg/apis/nfd/v1alpha1/feature.go
+++ b/pkg/apis/nfd/v1alpha1/feature.go
@@ -16,10 +16,10 @@ limitations under the License.
 
 package v1alpha1
 
-// NewDomainFeatures creates a new instance of Features, initializing specified
-// features to empty values
-func NewDomainFeatures() *DomainFeatures {
-	return &DomainFeatures{
+// NewFeatures creates a new instance of Features, initializing all feature
+// types (flags, attributes and instances) to empty values.
+func NewFeatures() *Features {
+	return &Features{
 		Flags:      make(map[string]FlagFeatureSet),
 		Attributes: make(map[string]AttributeFeatureSet),
 		Instances:  make(map[string]InstanceFeatureSet)}
@@ -56,16 +56,29 @@ func NewInstanceFeature(attrs map[string]string) *InstanceFeature {
 }
 
 // InsertAttributeFeatures inserts new values into a specific feature.
-func InsertAttributeFeatures(f Features, domain, feature string, values map[string]string) {
-	if _, ok := f[domain]; !ok {
-		f[domain] = NewDomainFeatures()
-	}
-	if _, ok := f[domain].Attributes[feature]; !ok {
-		f[domain].Attributes[feature] = NewAttributeFeatures(values)
+func (f *Features) InsertAttributeFeatures(domain, feature string, values map[string]string) {
+	key := domain + "." + feature
+	if _, ok := f.Attributes[key]; !ok {
+		f.Attributes[key] = NewAttributeFeatures(values)
 		return
 	}
 
 	for k, v := range values {
-		f[domain].Attributes[feature].Elements[k] = v
+		f.Attributes[key].Elements[k] = v
+	}
+}
+
+// Exists returns a non-empty string if a feature exists. The return value is
+// the type of the feautre, i.e. "flag", "attribute" or "instance".
+func (f *Features) Exists(name string) string {
+	if _, ok := f.Flags[name]; ok {
+		return "flag"
+	}
+	if _, ok := f.Attributes[name]; ok {
+		return "attribute"
+	}
+	if _, ok := f.Instances[name]; ok {
+		return "instance"
 	}
+	return ""
 }
diff --git a/pkg/apis/nfd/v1alpha1/generated.pb.go b/pkg/apis/nfd/v1alpha1/generated.pb.go
index f9f41ac6a..4593b9269 100644
--- a/pkg/apis/nfd/v1alpha1/generated.pb.go
+++ b/pkg/apis/nfd/v1alpha1/generated.pb.go
@@ -71,15 +71,15 @@ func (m *AttributeFeatureSet) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_AttributeFeatureSet proto.InternalMessageInfo
 
-func (m *DomainFeatures) Reset()      { *m = DomainFeatures{} }
-func (*DomainFeatures) ProtoMessage() {}
-func (*DomainFeatures) Descriptor() ([]byte, []int) {
+func (m *Features) Reset()      { *m = Features{} }
+func (*Features) ProtoMessage() {}
+func (*Features) Descriptor() ([]byte, []int) {
 	return fileDescriptor_6f67d44e41cfe439, []int{1}
 }
-func (m *DomainFeatures) XXX_Unmarshal(b []byte) error {
+func (m *Features) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
 }
-func (m *DomainFeatures) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+func (m *Features) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	b = b[:cap(b)]
 	n, err := m.MarshalToSizedBuffer(b)
 	if err != nil {
@@ -87,17 +87,17 @@ func (m *DomainFeatures) XXX_Marshal(b []byte, deterministic bool) ([]byte, erro
 	}
 	return b[:n], nil
 }
-func (m *DomainFeatures) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_DomainFeatures.Merge(m, src)
+func (m *Features) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Features.Merge(m, src)
 }
-func (m *DomainFeatures) XXX_Size() int {
+func (m *Features) XXX_Size() int {
 	return m.Size()
 }
-func (m *DomainFeatures) XXX_DiscardUnknown() {
-	xxx_messageInfo_DomainFeatures.DiscardUnknown(m)
+func (m *Features) XXX_DiscardUnknown() {
+	xxx_messageInfo_Features.DiscardUnknown(m)
 }
 
-var xxx_messageInfo_DomainFeatures proto.InternalMessageInfo
+var xxx_messageInfo_Features proto.InternalMessageInfo
 
 func (m *FlagFeatureSet) Reset()      { *m = FlagFeatureSet{} }
 func (*FlagFeatureSet) ProtoMessage() {}
@@ -214,10 +214,10 @@ var xxx_messageInfo_Nil proto.InternalMessageInfo
 func init() {
 	proto.RegisterType((*AttributeFeatureSet)(nil), "v1alpha1.AttributeFeatureSet")
 	proto.RegisterMapType((map[string]string)(nil), "v1alpha1.AttributeFeatureSet.ElementsEntry")
-	proto.RegisterType((*DomainFeatures)(nil), "v1alpha1.DomainFeatures")
-	proto.RegisterMapType((map[string]FlagFeatureSet)(nil), "v1alpha1.DomainFeatures.FlagsEntry")
-	proto.RegisterMapType((map[string]InstanceFeatureSet)(nil), "v1alpha1.DomainFeatures.InstancesEntry")
-	proto.RegisterMapType((map[string]AttributeFeatureSet)(nil), "v1alpha1.DomainFeatures.VattributesEntry")
+	proto.RegisterType((*Features)(nil), "v1alpha1.Features")
+	proto.RegisterMapType((map[string]FlagFeatureSet)(nil), "v1alpha1.Features.FlagsEntry")
+	proto.RegisterMapType((map[string]InstanceFeatureSet)(nil), "v1alpha1.Features.InstancesEntry")
+	proto.RegisterMapType((map[string]AttributeFeatureSet)(nil), "v1alpha1.Features.VattributesEntry")
 	proto.RegisterType((*FlagFeatureSet)(nil), "v1alpha1.FlagFeatureSet")
 	proto.RegisterMapType((map[string]Nil)(nil), "v1alpha1.FlagFeatureSet.ElementsEntry")
 	proto.RegisterType((*InstanceFeature)(nil), "v1alpha1.InstanceFeature")
@@ -231,42 +231,41 @@ func init() {
 }
 
 var fileDescriptor_6f67d44e41cfe439 = []byte{
-	// 546 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xcf, 0x6e, 0xd3, 0x40,
-	0x10, 0xc6, 0xb3, 0x4d, 0x23, 0x25, 0x13, 0x25, 0x0d, 0x0b, 0x07, 0x13, 0x81, 0x5b, 0xa5, 0x12,
-	0x14, 0xa1, 0xd8, 0x6a, 0xb8, 0x44, 0x20, 0x0e, 0xad, 0x68, 0x29, 0x1c, 0x7a, 0x30, 0x08, 0x41,
-	0xa5, 0x80, 0x36, 0xc9, 0xc6, 0x5d, 0xc5, 0xb1, 0x23, 0xef, 0x3a, 0x52, 0x6e, 0x3c, 0x02, 0x0f,
-	0xc1, 0x15, 0x21, 0xf1, 0x14, 0x39, 0xf6, 0xd8, 0x53, 0x45, 0xcc, 0x8b, 0xa0, 0xf8, 0x4f, 0x6c,
-	0x27, 0x76, 0x2a, 0xb8, 0xed, 0x4e, 0xbe, 0xf9, 0xcd, 0xcc, 0xb7, 0x13, 0xc3, 0x19, 0x67, 0x3a,
-	0x57, 0x86, 0x6d, 0xae, 0x30, 0x4b, 0x35, 0xad, 0x3e, 0x6d, 0x0e, 0x28, 0x11, 0x8e, 0x4d, 0x9b,
-	0x7d, 0xc6, 0x7b, 0xd6, 0x84, 0xda, 0x53, 0x75, 0x3c, 0xd4, 0x55, 0x32, 0x66, 0x5c, 0x35, 0x07,
-	0x7d, 0x75, 0x72, 0x48, 0x8c, 0xf1, 0x25, 0x39, 0x54, 0x75, 0x6a, 0x52, 0x9b, 0x08, 0xda, 0x57,
-	0xc6, 0xb6, 0x25, 0x2c, 0x5c, 0x0c, 0x7f, 0xa9, 0x37, 0x75, 0x26, 0x2e, 0x9d, 0xae, 0xd2, 0xb3,
-	0x46, 0xaa, 0x6e, 0xe9, 0x96, 0xea, 0x09, 0xba, 0xce, 0xc0, 0xbb, 0x79, 0x17, 0xef, 0xe4, 0x27,
-	0x36, 0x7e, 0x20, 0xb8, 0x7b, 0x24, 0x84, 0xcd, 0xba, 0x8e, 0xa0, 0xa7, 0x7e, 0xf5, 0x77, 0x54,
-	0xe0, 0x4f, 0x50, 0xa4, 0x06, 0x1d, 0x51, 0x53, 0x70, 0x09, 0xed, 0xe5, 0x0f, 0xca, 0xad, 0xa7,
-	0x4a, 0x58, 0x43, 0x49, 0x49, 0x50, 0x4e, 0x02, 0xf5, 0x89, 0x29, 0xec, 0xe9, 0x71, 0x6d, 0x76,
-	0xb3, 0x9b, 0x73, 0x6f, 0x76, 0x8b, 0x61, 0x58, 0x5b, 0xe2, 0xea, 0x2f, 0xa0, 0x92, 0x10, 0xe3,
-	0x1a, 0xe4, 0x87, 0x74, 0x2a, 0xa1, 0x3d, 0x74, 0x50, 0xd2, 0x16, 0x47, 0x7c, 0x0f, 0x0a, 0x13,
-	0x62, 0x38, 0x54, 0xda, 0xf2, 0x62, 0xfe, 0xe5, 0xf9, 0x56, 0x1b, 0x35, 0xbe, 0x6f, 0x43, 0xf5,
-	0x95, 0x35, 0x22, 0xcc, 0x0c, 0x6a, 0x73, 0x7c, 0x06, 0x85, 0x81, 0x41, 0x74, 0x2e, 0x6d, 0x79,
-	0x7d, 0xee, 0x47, 0x7d, 0x26, 0x85, 0xca, 0xe9, 0x42, 0xe5, 0xf7, 0x57, 0x09, 0xfa, 0x2b, 0x78,
-	0x31, 0xcd, 0x07, 0xe0, 0x2f, 0x50, 0x9e, 0x90, 0x70, 0x36, 0x2e, 0xe5, 0x3d, 0xde, 0x93, 0x4c,
-	0xde, 0x87, 0x48, 0xeb, 0x53, 0x71, 0x40, 0x85, 0xa5, 0x41, 0x5c, 0x8b, 0x13, 0xf1, 0x47, 0x28,
-	0x31, 0x93, 0x0b, 0x62, 0xf6, 0x28, 0x97, 0xb6, 0x3d, 0xfc, 0xe3, 0x4c, 0xfc, 0x9b, 0x50, 0xe9,
-	0xc3, 0xef, 0x04, 0xf0, 0xd2, 0x32, 0xae, 0x45, 0xb0, 0xba, 0x06, 0x10, 0x8d, 0x97, 0xe2, 0xa8,
-	0x12, 0x77, 0xb4, 0xdc, 0x92, 0xa2, 0xaa, 0x8b, 0xb4, 0xe8, 0x1d, 0x63, 0x5e, 0xd7, 0x3b, 0x50,
-	0x5b, 0x1d, 0x31, 0x85, 0xfc, 0x2c, 0x49, 0x7e, 0xb8, 0x71, 0x4d, 0xe2, 0xf8, 0x0b, 0xa8, 0x26,
-	0x47, 0x4c, 0x81, 0xb7, 0x92, 0xf0, 0x07, 0x11, 0x3c, 0x4c, 0x4d, 0x65, 0x37, 0x7e, 0x21, 0xa8,
-	0x26, 0x07, 0xc3, 0xef, 0xd7, 0x36, 0xfa, 0x51, 0x96, 0x09, 0xff, 0xb0, 0xcc, 0x6f, 0x6f, 0x5f,
-	0xe6, 0xfd, 0xe4, 0x0c, 0x95, 0xa8, 0xea, 0x39, 0x33, 0xe2, 0x4d, 0xff, 0x44, 0xb0, 0xb3, 0x32,
-	0x16, 0xee, 0x00, 0xc4, 0x36, 0x12, 0xad, 0x6e, 0xe4, 0x8a, 0x3c, 0xb2, 0x7c, 0xc3, 0x46, 0xc6,
-	0x80, 0xf5, 0x97, 0xb0, 0x73, 0x74, 0xeb, 0x0b, 0x67, 0xff, 0x1b, 0x3b, 0x80, 0xd7, 0xdf, 0x01,
-	0xbf, 0x5e, 0x73, 0xfa, 0x7e, 0x66, 0xc7, 0x9b, 0xcc, 0x6d, 0x14, 0x20, 0x7f, 0xce, 0x8c, 0xe3,
-	0xcf, 0xb3, 0xb9, 0x9c, 0xbb, 0x9a, 0xcb, 0xb9, 0xeb, 0xb9, 0x9c, 0xfb, 0xea, 0xca, 0x68, 0xe6,
-	0xca, 0xe8, 0xca, 0x95, 0xd1, 0xb5, 0x2b, 0xa3, 0xdf, 0xae, 0x8c, 0xbe, 0xfd, 0x91, 0x73, 0x17,
-	0xed, 0xff, 0xfd, 0x98, 0xfe, 0x0d, 0x00, 0x00, 0xff, 0xff, 0x75, 0x9e, 0xef, 0x0b, 0x87, 0x05,
-	0x00, 0x00,
+	// 539 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xcd, 0x6e, 0xda, 0x40,
+	0x10, 0xc7, 0xbd, 0x50, 0x24, 0x18, 0x44, 0x42, 0xb7, 0x3d, 0xb8, 0x56, 0xe3, 0xa4, 0x44, 0xaa,
+	0x52, 0x55, 0xd8, 0x0a, 0xbd, 0xa0, 0x56, 0x3d, 0x04, 0x29, 0xe9, 0xc7, 0x21, 0x07, 0xb7, 0xaa,
+	0x94, 0x48, 0x54, 0x5a, 0x60, 0x71, 0x56, 0x38, 0x36, 0xf2, 0xae, 0x91, 0xb8, 0xf5, 0x01, 0x7a,
+	0xe8, 0x8b, 0x54, 0x95, 0xfa, 0x14, 0x1c, 0x73, 0xcc, 0x29, 0x2a, 0xee, 0x8b, 0x54, 0xf1, 0x07,
+	0xb6, 0x83, 0x01, 0x35, 0x37, 0xef, 0xec, 0xcc, 0x6f, 0xe6, 0xff, 0xdf, 0x01, 0x78, 0xcf, 0x99,
+	0xc9, 0xb5, 0x51, 0x9b, 0x6b, 0xcc, 0xd1, 0x6d, 0x67, 0x40, 0x9b, 0x43, 0x4a, 0x84, 0xe7, 0xd2,
+	0xe6, 0x80, 0xf1, 0xbe, 0x33, 0xa1, 0xee, 0x54, 0x1f, 0x8f, 0x4c, 0x9d, 0x8c, 0x19, 0xd7, 0xed,
+	0xe1, 0x40, 0x9f, 0x1c, 0x12, 0x6b, 0x7c, 0x41, 0x0e, 0x75, 0x93, 0xda, 0xd4, 0x25, 0x82, 0x0e,
+	0xb4, 0xb1, 0xeb, 0x08, 0x07, 0x97, 0xe3, 0x1b, 0xa5, 0x69, 0x32, 0x71, 0xe1, 0xf5, 0xb4, 0xbe,
+	0x73, 0xa9, 0x9b, 0x8e, 0xe9, 0xe8, 0x41, 0x42, 0xcf, 0x1b, 0x06, 0xa7, 0xe0, 0x10, 0x7c, 0x85,
+	0x85, 0x8d, 0x9f, 0x08, 0x1e, 0x1d, 0x09, 0xe1, 0xb2, 0x9e, 0x27, 0xe8, 0x49, 0xd8, 0xfd, 0x13,
+	0x15, 0xf8, 0x0c, 0xca, 0xd4, 0xa2, 0x97, 0xd4, 0x16, 0x5c, 0x46, 0x7b, 0xc5, 0x83, 0x6a, 0xeb,
+	0xa5, 0x16, 0xf7, 0xd0, 0x72, 0x0a, 0xb4, 0xe3, 0x28, 0xfb, 0xd8, 0x16, 0xee, 0xb4, 0x53, 0x9f,
+	0xdd, 0xec, 0x4a, 0xfe, 0xcd, 0x6e, 0x39, 0x0e, 0x1b, 0x0b, 0x9c, 0xf2, 0x06, 0x6a, 0x99, 0x64,
+	0x5c, 0x87, 0xe2, 0x88, 0x4e, 0x65, 0xb4, 0x87, 0x0e, 0x2a, 0xc6, 0xed, 0x27, 0x7e, 0x0c, 0xa5,
+	0x09, 0xb1, 0x3c, 0x2a, 0x17, 0x82, 0x58, 0x78, 0x78, 0x5d, 0x68, 0xa3, 0xc6, 0xf7, 0x07, 0x50,
+	0x8e, 0xba, 0x72, 0xdc, 0x81, 0xd2, 0xd0, 0x22, 0x66, 0x3c, 0xe1, 0x4e, 0x32, 0x61, 0x9c, 0xa2,
+	0x9d, 0xdc, 0xde, 0x87, 0x33, 0xd5, 0xa2, 0x99, 0x4a, 0x41, 0xcc, 0x08, 0x4b, 0xf1, 0x19, 0x54,
+	0x27, 0x24, 0xd6, 0xc3, 0xe5, 0x42, 0x40, 0xda, 0xcf, 0x21, 0x7d, 0x49, 0xb2, 0x42, 0x1e, 0x8e,
+	0x78, 0xb0, 0xb0, 0x83, 0x1b, 0x69, 0x16, 0x36, 0xa0, 0xc2, 0x6c, 0x2e, 0x88, 0xdd, 0xa7, 0x5c,
+	0x2e, 0x06, 0xe0, 0x67, 0x39, 0xe0, 0x0f, 0x71, 0x4e, 0x88, 0x7d, 0x18, 0x61, 0x2b, 0x8b, 0xb8,
+	0x91, 0x60, 0x14, 0x03, 0x20, 0x91, 0x94, 0xe3, 0x9c, 0x96, 0x76, 0xae, 0xda, 0x92, 0x53, 0xfd,
+	0x2c, 0x62, 0x26, 0xef, 0x95, 0xf2, 0x54, 0xe9, 0x42, 0xfd, 0xae, 0xb8, 0x1c, 0xf2, 0xab, 0x2c,
+	0x79, 0x67, 0xed, 0x3a, 0xa4, 0xf1, 0xe7, 0xb0, 0x95, 0x95, 0x98, 0x03, 0x6f, 0x65, 0xe1, 0x4f,
+	0x13, 0x78, 0x5c, 0x9a, 0xcb, 0x6e, 0xfc, 0x46, 0xb0, 0x95, 0x15, 0x86, 0x3f, 0x2f, 0x6d, 0xee,
+	0xf3, 0x55, 0x26, 0xfc, 0xc7, 0xd2, 0x7e, 0xdc, 0xbc, 0xb4, 0xfb, 0x59, 0x0d, 0xb5, 0xa4, 0xeb,
+	0x29, 0xb3, 0xd2, 0x43, 0xff, 0x42, 0xb0, 0x7d, 0x47, 0x16, 0xee, 0x02, 0xa4, 0xb6, 0x30, 0x9c,
+	0xfb, 0xc5, 0x4a, 0x17, 0x12, 0xcb, 0xd7, 0xec, 0x62, 0x0a, 0xa8, 0xbc, 0x85, 0xed, 0xa3, 0x8d,
+	0x2f, 0xbc, 0xfa, 0x57, 0xd7, 0x05, 0xbc, 0xfc, 0x0e, 0xf8, 0xdd, 0x92, 0xd3, 0x4f, 0x56, 0x4e,
+	0xbc, 0xce, 0xdc, 0x46, 0x09, 0x8a, 0xa7, 0xcc, 0xea, 0x7c, 0x9d, 0xcd, 0x55, 0xe9, 0x6a, 0xae,
+	0x4a, 0xd7, 0x73, 0x55, 0xfa, 0xe6, 0xab, 0x68, 0xe6, 0xab, 0xe8, 0xca, 0x57, 0xd1, 0xb5, 0xaf,
+	0xa2, 0x3f, 0xbe, 0x8a, 0x7e, 0xfc, 0x55, 0xa5, 0xf3, 0xf6, 0x7d, 0xff, 0x34, 0xff, 0x05, 0x00,
+	0x00, 0xff, 0xff, 0xa9, 0x81, 0xcf, 0x6d, 0x6f, 0x05, 0x00, 0x00,
 }
 
 func (m *AttributeFeatureSet) Marshal() (dAtA []byte, err error) {
@@ -316,7 +315,7 @@ func (m *AttributeFeatureSet) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
-func (m *DomainFeatures) Marshal() (dAtA []byte, err error) {
+func (m *Features) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
 	n, err := m.MarshalToSizedBuffer(dAtA[:size])
@@ -326,12 +325,12 @@ func (m *DomainFeatures) Marshal() (dAtA []byte, err error) {
 	return dAtA[:n], nil
 }
 
-func (m *DomainFeatures) MarshalTo(dAtA []byte) (int, error) {
+func (m *Features) MarshalTo(dAtA []byte) (int, error) {
 	size := m.Size()
 	return m.MarshalToSizedBuffer(dAtA[:size])
 }
 
-func (m *DomainFeatures) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+func (m *Features) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	i := len(dAtA)
 	_ = i
 	var l int
@@ -362,7 +361,7 @@ func (m *DomainFeatures) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 			dAtA[i] = 0xa
 			i = encodeVarintGenerated(dAtA, i, uint64(baseI-i))
 			i--
-			dAtA[i] = 0x22
+			dAtA[i] = 0x1a
 		}
 	}
 	if len(m.Attributes) > 0 {
@@ -391,7 +390,7 @@ func (m *DomainFeatures) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 			dAtA[i] = 0xa
 			i = encodeVarintGenerated(dAtA, i, uint64(baseI-i))
 			i--
-			dAtA[i] = 0x1a
+			dAtA[i] = 0x12
 		}
 	}
 	if len(m.Flags) > 0 {
@@ -420,7 +419,7 @@ func (m *DomainFeatures) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 			dAtA[i] = 0xa
 			i = encodeVarintGenerated(dAtA, i, uint64(baseI-i))
 			i--
-			dAtA[i] = 0x12
+			dAtA[i] = 0xa
 		}
 	}
 	return len(dAtA) - i, nil
@@ -613,7 +612,7 @@ func (m *AttributeFeatureSet) Size() (n int) {
 	return n
 }
 
-func (m *DomainFeatures) Size() (n int) {
+func (m *Features) Size() (n int) {
 	if m == nil {
 		return 0
 	}
@@ -734,7 +733,7 @@ func (this *AttributeFeatureSet) String() string {
 	}, "")
 	return s
 }
-func (this *DomainFeatures) String() string {
+func (this *Features) String() string {
 	if this == nil {
 		return "nil"
 	}
@@ -768,7 +767,7 @@ func (this *DomainFeatures) String() string {
 		mapStringForInstances += fmt.Sprintf("%v: %v,", k, this.Instances[k])
 	}
 	mapStringForInstances += "}"
-	s := strings.Join([]string{`&DomainFeatures{`,
+	s := strings.Join([]string{`&Features{`,
 		`Flags:` + mapStringForFlags + `,`,
 		`Attributes:` + mapStringForAttributes + `,`,
 		`Instances:` + mapStringForInstances + `,`,
@@ -1025,7 +1024,7 @@ func (m *AttributeFeatureSet) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *DomainFeatures) Unmarshal(dAtA []byte) error {
+func (m *Features) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -1048,13 +1047,13 @@ func (m *DomainFeatures) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: DomainFeatures: wiretype end group for non-group")
+			return fmt.Errorf("proto: Features: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: DomainFeatures: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: Features: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
-		case 2:
+		case 1:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Flags", wireType)
 			}
@@ -1183,7 +1182,7 @@ func (m *DomainFeatures) Unmarshal(dAtA []byte) error {
 			}
 			m.Flags[mapkey] = *mapvalue
 			iNdEx = postIndex
-		case 3:
+		case 2:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Attributes", wireType)
 			}
@@ -1312,7 +1311,7 @@ func (m *DomainFeatures) Unmarshal(dAtA []byte) error {
 			}
 			m.Attributes[mapkey] = *mapvalue
 			iNdEx = postIndex
-		case 4:
+		case 3:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Instances", wireType)
 			}
diff --git a/pkg/apis/nfd/v1alpha1/generated.proto b/pkg/apis/nfd/v1alpha1/generated.proto
index 6cb1fc8e0..09f408ee7 100644
--- a/pkg/apis/nfd/v1alpha1/generated.proto
+++ b/pkg/apis/nfd/v1alpha1/generated.proto
@@ -31,15 +31,15 @@ message AttributeFeatureSet {
   map<string, string> elements = 1;
 }
 
-// DomainFeatures is the collection of all discovered features of one domain.
+// Features is the collection of all discovered features.
 //
 // +protobuf=true
-message DomainFeatures {
-  map<string, FlagFeatureSet> flags = 2;
+message Features {
+  map<string, FlagFeatureSet> flags = 1;
 
-  map<string, AttributeFeatureSet> vattributes = 3;
+  map<string, AttributeFeatureSet> vattributes = 2;
 
-  map<string, InstanceFeatureSet> instances = 4;
+  map<string, InstanceFeatureSet> instances = 3;
 }
 
 // FlagFeatureSet is a set of simple features only containing names without values.
diff --git a/pkg/apis/nfd/v1alpha1/rule.go b/pkg/apis/nfd/v1alpha1/rule.go
index ab0f6f913..b14bfdf1f 100644
--- a/pkg/apis/nfd/v1alpha1/rule.go
+++ b/pkg/apis/nfd/v1alpha1/rule.go
@@ -35,7 +35,7 @@ type RuleOutput struct {
 }
 
 // Execute the rule against a set of input features.
-func (r *Rule) Execute(features Features) (RuleOutput, error) {
+func (r *Rule) Execute(features *Features) (RuleOutput, error) {
 	labels := make(map[string]string)
 	vars := make(map[string]string)
 
@@ -145,55 +145,39 @@ func (r *Rule) executeVarsTemplate(in matchedFeatures, out map[string]string) er
 	return nil
 }
 
-type matchedFeatures map[string]domainMatchedFeatures
+type matchedFeatures map[string]interface{}
 
-type domainMatchedFeatures map[string]interface{}
-
-func (e *MatchAnyElem) match(features map[string]*DomainFeatures) (bool, matchedFeatures, error) {
+func (e *MatchAnyElem) match(features *Features) (bool, matchedFeatures, error) {
 	return e.MatchFeatures.match(features)
 }
 
-func (m *FeatureMatcher) match(features map[string]*DomainFeatures) (bool, matchedFeatures, error) {
+func (m *FeatureMatcher) match(features *Features) (bool, matchedFeatures, error) {
 	matches := make(matchedFeatures, len(*m))
 
 	// Logical AND over the terms
 	for _, term := range *m {
-		split := strings.SplitN(term.Feature, ".", 2)
-		if len(split) != 2 {
-			return false, nil, fmt.Errorf("invalid feature %q: must be <domain>.<feature>", term.Feature)
-		}
-		domain := split[0]
 		// Ignore case
-		featureName := strings.ToLower(split[1])
-
-		domainFeatures, ok := features[domain]
-		if !ok {
-			return false, nil, fmt.Errorf("unknown feature source/domain %q", domain)
-		}
-
-		if _, ok := matches[domain]; !ok {
-			matches[domain] = make(domainMatchedFeatures)
-		}
+		featureName := strings.ToLower(term.Feature)
 
 		var isMatch bool
 		var err error
-		if f, ok := domainFeatures.Flags[featureName]; ok {
+		if f, ok := features.Flags[featureName]; ok {
 			m, v, e := term.MatchExpressions.MatchGetKeys(f.Elements)
 			isMatch = m
 			err = e
-			matches[domain][featureName] = v
-		} else if f, ok := domainFeatures.Attributes[featureName]; ok {
+			matches[featureName] = v
+		} else if f, ok := features.Attributes[featureName]; ok {
 			m, v, e := term.MatchExpressions.MatchGetValues(f.Elements)
 			isMatch = m
 			err = e
-			matches[domain][featureName] = v
-		} else if f, ok := domainFeatures.Instances[featureName]; ok {
+			matches[featureName] = v
+		} else if f, ok := features.Instances[featureName]; ok {
 			v, e := term.MatchExpressions.MatchGetInstances(f.Elements)
 			isMatch = len(v) > 0
 			err = e
-			matches[domain][featureName] = v
+			matches[featureName] = v
 		} else {
-			return false, nil, fmt.Errorf("%q feature of source/domain %q not available", featureName, domain)
+			return false, nil, fmt.Errorf("feature %q not available", featureName)
 		}
 
 		if err != nil {
diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go
index 4c3606b2f..b3e4bd602 100644
--- a/pkg/apis/nfd/v1alpha1/types.go
+++ b/pkg/apis/nfd/v1alpha1/types.go
@@ -20,18 +20,13 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// Features is a collection of all features of the system, arranged by domain.
+// Features is the collection of all discovered features.
 //
 // +protobuf=true
-type Features map[string]*DomainFeatures
-
-// DomainFeatures is the collection of all discovered features of one domain.
-//
-// +protobuf=true
-type DomainFeatures struct {
-	Flags      map[string]FlagFeatureSet      `json:"flags" protobuf:"bytes,2,rep,name=flags"`
-	Attributes map[string]AttributeFeatureSet `json:"attributes" protobuf:"bytes,3,rep,name=vattributes"`
-	Instances  map[string]InstanceFeatureSet  `json:"instances" protobuf:"bytes,4,rep,name=instances"`
+type Features struct {
+	Flags      map[string]FlagFeatureSet      `json:"flags" protobuf:"bytes,1,rep,name=flags"`
+	Attributes map[string]AttributeFeatureSet `json:"attributes" protobuf:"bytes,2,rep,name=vattributes"`
+	Instances  map[string]InstanceFeatureSet  `json:"instances" protobuf:"bytes,3,rep,name=instances"`
 }
 
 // FlagFeatureSet is a set of simple features only containing names without values.
diff --git a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
index fb932ba67..1196b4139 100644
--- a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
@@ -31,42 +31,6 @@ func (in *AttributeFeatureSet) DeepCopy() *AttributeFeatureSet {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *DomainFeatures) DeepCopyInto(out *DomainFeatures) {
-	*out = *in
-	if in.Flags != nil {
-		in, out := &in.Flags, &out.Flags
-		*out = make(map[string]FlagFeatureSet, len(*in))
-		for key, val := range *in {
-			(*out)[key] = *val.DeepCopy()
-		}
-	}
-	if in.Attributes != nil {
-		in, out := &in.Attributes, &out.Attributes
-		*out = make(map[string]AttributeFeatureSet, len(*in))
-		for key, val := range *in {
-			(*out)[key] = *val.DeepCopy()
-		}
-	}
-	if in.Instances != nil {
-		in, out := &in.Instances, &out.Instances
-		*out = make(map[string]InstanceFeatureSet, len(*in))
-		for key, val := range *in {
-			(*out)[key] = *val.DeepCopy()
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DomainFeatures.
-func (in *DomainFeatures) DeepCopy() *DomainFeatures {
-	if in == nil {
-		return nil
-	}
-	out := new(DomainFeatures)
-	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) {
 	{
@@ -119,32 +83,39 @@ func (in *FeatureMatcherTerm) DeepCopy() *FeatureMatcherTerm {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in Features) DeepCopyInto(out *Features) {
-	{
-		in := &in
-		*out = make(Features, len(*in))
+func (in *Features) DeepCopyInto(out *Features) {
+	*out = *in
+	if in.Flags != nil {
+		in, out := &in.Flags, &out.Flags
+		*out = make(map[string]FlagFeatureSet, len(*in))
 		for key, val := range *in {
-			var outVal *DomainFeatures
-			if val == nil {
-				(*out)[key] = nil
-			} else {
-				in, out := &val, &outVal
-				*out = new(DomainFeatures)
-				(*in).DeepCopyInto(*out)
-			}
-			(*out)[key] = outVal
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+	if in.Attributes != nil {
+		in, out := &in.Attributes, &out.Attributes
+		*out = make(map[string]AttributeFeatureSet, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+	if in.Instances != nil {
+		in, out := &in.Instances, &out.Instances
+		*out = make(map[string]InstanceFeatureSet, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
 		}
 	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Features.
-func (in Features) DeepCopy() Features {
+func (in *Features) DeepCopy() *Features {
 	if in == nil {
 		return nil
 	}
 	out := new(Features)
 	in.DeepCopyInto(out)
-	return *out
+	return out
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
diff --git a/pkg/labeler/labeler.pb.go b/pkg/labeler/labeler.pb.go
index 68b2d8141..0a8707370 100644
--- a/pkg/labeler/labeler.pb.go
+++ b/pkg/labeler/labeler.pb.go
@@ -50,10 +50,10 @@ type SetLabelsRequest struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	NfdVersion string                              `protobuf:"bytes,1,opt,name=nfd_version,json=nfdVersion,proto3" json:"nfd_version,omitempty"`
-	NodeName   string                              `protobuf:"bytes,2,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"`
-	Labels     map[string]string                   `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
-	Features   map[string]*v1alpha1.DomainFeatures `protobuf:"bytes,4,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	NfdVersion string             `protobuf:"bytes,1,opt,name=nfd_version,json=nfdVersion,proto3" json:"nfd_version,omitempty"`
+	NodeName   string             `protobuf:"bytes,2,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"`
+	Labels     map[string]string  `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	Features   *v1alpha1.Features `protobuf:"bytes,4,opt,name=features,proto3" json:"features,omitempty"`
 }
 
 func (x *SetLabelsRequest) Reset() {
@@ -109,7 +109,7 @@ func (x *SetLabelsRequest) GetLabels() map[string]string {
 	return nil
 }
 
-func (x *SetLabelsRequest) GetFeatures() map[string]*v1alpha1.DomainFeatures {
+func (x *SetLabelsRequest) GetFeatures() *v1alpha1.Features {
 	if x != nil {
 		return x.Features
 	}
@@ -161,7 +161,7 @@ var file_labeler_proto_rawDesc = []byte{
 	0x07, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x1a, 0x25, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70,
 	0x69, 0x73, 0x2f, 0x6e, 0x66, 0x64, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f,
 	0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
-	0xe6, 0x02, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71,
+	0xfa, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71,
 	0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x66, 0x64, 0x5f, 0x76, 0x65, 0x72, 0x73,
 	0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x66, 0x64, 0x56, 0x65,
 	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61,
@@ -170,30 +170,23 @@ var file_labeler_proto_rawDesc = []byte{
 	0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x74,
 	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61,
 	0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c,
-	0x73, 0x12, 0x43, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x65,
-	0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46,
-	0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x66, 0x65,
-	0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73,
-	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
-	0x01, 0x1a, 0x55, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74,
-	0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x03, 0x6b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x44,
-	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x05, 0x76,
-	0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x10, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x4c,
-	0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x32, 0x4c, 0x0a, 0x07, 0x4c, 0x61,
-	0x62, 0x65, 0x6c, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65,
-	0x6c, 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x74,
-	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
-	0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c,
-	0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x30, 0x5a, 0x2e, 0x73, 0x69, 0x67, 0x73,
-	0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x69, 0x6f, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2d, 0x66, 0x65, 0x61,
-	0x74, 0x75, 0x72, 0x65, 0x2d, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x2f, 0x70,
-	0x6b, 0x67, 0x2f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x33,
+	0x73, 0x12, 0x2e, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x46,
+	0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65,
+	0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
+	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
+	0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x10, 0x0a, 0x0e,
+	0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x32, 0x4c,
+	0x0a, 0x07, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x09, 0x53, 0x65, 0x74,
+	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x19, 0x2e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72,
+	0x2e, 0x53, 0x65, 0x74, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x1a, 0x17, 0x2e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x74, 0x4c,
+	0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x30, 0x5a, 0x2e,
+	0x73, 0x69, 0x67, 0x73, 0x2e, 0x6b, 0x38, 0x73, 0x2e, 0x69, 0x6f, 0x2f, 0x6e, 0x6f, 0x64, 0x65,
+	0x2d, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x2d, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65,
+	0x72, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x65, 0x72, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -208,25 +201,23 @@ func file_labeler_proto_rawDescGZIP() []byte {
 	return file_labeler_proto_rawDescData
 }
 
-var file_labeler_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_labeler_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
 var file_labeler_proto_goTypes = []interface{}{
-	(*SetLabelsRequest)(nil),        // 0: labeler.SetLabelsRequest
-	(*SetLabelsReply)(nil),          // 1: labeler.SetLabelsReply
-	nil,                             // 2: labeler.SetLabelsRequest.LabelsEntry
-	nil,                             // 3: labeler.SetLabelsRequest.FeaturesEntry
-	(*v1alpha1.DomainFeatures)(nil), // 4: v1alpha1.DomainFeatures
+	(*SetLabelsRequest)(nil),  // 0: labeler.SetLabelsRequest
+	(*SetLabelsReply)(nil),    // 1: labeler.SetLabelsReply
+	nil,                       // 2: labeler.SetLabelsRequest.LabelsEntry
+	(*v1alpha1.Features)(nil), // 3: v1alpha1.Features
 }
 var file_labeler_proto_depIdxs = []int32{
 	2, // 0: labeler.SetLabelsRequest.labels:type_name -> labeler.SetLabelsRequest.LabelsEntry
-	3, // 1: labeler.SetLabelsRequest.features:type_name -> labeler.SetLabelsRequest.FeaturesEntry
-	4, // 2: labeler.SetLabelsRequest.FeaturesEntry.value:type_name -> v1alpha1.DomainFeatures
-	0, // 3: labeler.Labeler.SetLabels:input_type -> labeler.SetLabelsRequest
-	1, // 4: labeler.Labeler.SetLabels:output_type -> labeler.SetLabelsReply
-	4, // [4:5] is the sub-list for method output_type
-	3, // [3:4] is the sub-list for method input_type
-	3, // [3:3] is the sub-list for extension type_name
-	3, // [3:3] is the sub-list for extension extendee
-	0, // [0:3] is the sub-list for field type_name
+	3, // 1: labeler.SetLabelsRequest.features:type_name -> v1alpha1.Features
+	0, // 2: labeler.Labeler.SetLabels:input_type -> labeler.SetLabelsRequest
+	1, // 3: labeler.Labeler.SetLabels:output_type -> labeler.SetLabelsReply
+	3, // [3:4] is the sub-list for method output_type
+	2, // [2:3] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
 }
 
 func init() { file_labeler_proto_init() }
@@ -266,7 +257,7 @@ func file_labeler_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_labeler_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   4,
+			NumMessages:   3,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/pkg/labeler/labeler.proto b/pkg/labeler/labeler.proto
index e0119103d..10630d229 100644
--- a/pkg/labeler/labeler.proto
+++ b/pkg/labeler/labeler.proto
@@ -30,7 +30,7 @@ message SetLabelsRequest {
     string nfd_version = 1;
     string node_name = 2;
     map<string, string> labels = 3;
-    map<string, v1alpha1.DomainFeatures> features = 4;
+    v1alpha1.Features features = 4;
 }
 
 message SetLabelsReply {
diff --git a/pkg/nfd-client/worker/nfd-worker.go b/pkg/nfd-client/worker/nfd-worker.go
index f97fade42..ba3a5aad2 100644
--- a/pkg/nfd-client/worker/nfd-worker.go
+++ b/pkg/nfd-client/worker/nfd-worker.go
@@ -31,7 +31,6 @@ import (
 	"k8s.io/klog/v2"
 	"sigs.k8s.io/yaml"
 
-	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
 	clientcommon "sigs.k8s.io/node-feature-discovery/pkg/nfd-client"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
@@ -532,17 +531,6 @@ func getFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) (
 	return labels, nil
 }
 
-// getFeatures returns raw features from all feature sources
-func getFeatures() map[string]*nfdv1alpha1.DomainFeatures {
-	features := make(map[string]*nfdv1alpha1.DomainFeatures)
-
-	for name, src := range source.GetAllFeatureSources() {
-		features[name] = src.GetFeatures()
-	}
-
-	return features
-}
-
 // advertiseFeatureLabels advertises the feature labels to a Kubernetes node
 // via the NFD server.
 func (w *nfdWorker) advertiseFeatureLabels(labels Labels) error {
@@ -552,7 +540,7 @@ func (w *nfdWorker) advertiseFeatureLabels(labels Labels) error {
 	klog.Infof("sending labeling request to nfd-master")
 
 	labelReq := pb.SetLabelsRequest{Labels: labels,
-		Features:   getFeatures(),
+		Features:   source.GetAllFeatures(),
 		NfdVersion: version.Get(),
 		NodeName:   clientcommon.NodeName()}
 
diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go
index 812aa3baf..ff419e6e6 100644
--- a/pkg/nfd-master/nfd-master.go
+++ b/pkg/nfd-master/nfd-master.go
@@ -501,6 +501,9 @@ func (m *nfdMaster) crLabels(r *pb.SetLabelsRequest) map[string]string {
 		return nil
 	}
 
+	// Helper struct for rule processing
+	features := r.GetFeatures()
+
 	// Process all rule CRs
 	for _, spec := range ruleSpecs {
 		switch {
@@ -511,7 +514,7 @@ func (m *nfdMaster) crLabels(r *pb.SetLabelsRequest) map[string]string {
 			klog.Infof("executing NodeFeatureRule %q", spec.ObjectMeta.Name)
 		}
 		for _, rule := range spec.Spec.Rules {
-			ruleOut, err := rule.Execute(r.Features)
+			ruleOut, err := rule.Execute(features)
 			if err != nil {
 				klog.Errorf("failed to process Rule %q: %v", rule.Name, err)
 				continue
@@ -522,8 +525,8 @@ func (m *nfdMaster) crLabels(r *pb.SetLabelsRequest) map[string]string {
 			}
 
 			// Feed back rule output to features map for subsequent rules to match
-			nfdv1alpha1.InsertAttributeFeatures(r.Features, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Labels)
-			nfdv1alpha1.InsertAttributeFeatures(r.Features, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Vars)
+			features.InsertAttributeFeatures(nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Labels)
+			features.InsertAttributeFeatures(nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Vars)
 		}
 	}
 
diff --git a/source/cpu/cpu.go b/source/cpu/cpu.go
index b2db7d8e0..d9b77f8e3 100644
--- a/source/cpu/cpu.go
+++ b/source/cpu/cpu.go
@@ -102,7 +102,7 @@ type keyFilter struct {
 type cpuSource struct {
 	config      *Config
 	cpuidFilter *keyFilter
-	features    *nfdv1alpha1.DomainFeatures
+	features    *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -197,7 +197,7 @@ func (s *cpuSource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource Interface
 func (s *cpuSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	// Detect CPUID
 	s.features.Flags[CpuidFeature] = nfdv1alpha1.NewFlagFeatures(getCpuidFlags()...)
@@ -252,9 +252,9 @@ func (s *cpuSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface
-func (s *cpuSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *cpuSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/custom/custom.go b/source/custom/custom.go
index 80e374060..c9f3c69c8 100644
--- a/source/custom/custom.go
+++ b/source/custom/custom.go
@@ -107,10 +107,7 @@ func (s *customSource) Priority() int { return 10 }
 // GetLabels method of the LabelSource interface
 func (s *customSource) GetLabels() (source.FeatureLabels, error) {
 	// Get raw features from all sources
-	domainFeatures := make(map[string]*nfdv1alpha1.DomainFeatures)
-	for n, s := range source.GetAllFeatureSources() {
-		domainFeatures[n] = s.GetFeatures()
-	}
+	features := source.GetAllFeatures()
 
 	labels := source.FeatureLabels{}
 	allFeatureConfig := append(getStaticFeatureConfig(), *s.config...)
@@ -118,7 +115,7 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
 	utils.KlogDump(2, "custom features configuration:", "  ", allFeatureConfig)
 	// Iterate over features
 	for _, rule := range allFeatureConfig {
-		ruleOut, err := rule.execute(domainFeatures)
+		ruleOut, err := rule.execute(features)
 		if err != nil {
 			klog.Error(err)
 			continue
@@ -128,15 +125,15 @@ func (s *customSource) GetLabels() (source.FeatureLabels, error) {
 			labels[n] = v
 		}
 		// Feed back rule output to features map for subsequent rules to match
-		nfdv1alpha1.InsertAttributeFeatures(domainFeatures, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Labels)
-		nfdv1alpha1.InsertAttributeFeatures(domainFeatures, nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Vars)
+		features.InsertAttributeFeatures(nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Labels)
+		features.InsertAttributeFeatures(nfdv1alpha1.RuleBackrefDomain, nfdv1alpha1.RuleBackrefFeature, ruleOut.Vars)
 	}
 	return labels, nil
 }
 
-func (r *CustomRule) execute(features map[string]*nfdv1alpha1.DomainFeatures) (nfdv1alpha1.RuleOutput, error) {
+func (r *CustomRule) execute(features *nfdv1alpha1.Features) (nfdv1alpha1.RuleOutput, error) {
 	if r.LegacyRule != nil {
-		ruleOut, err := r.LegacyRule.execute(features)
+		ruleOut, err := r.LegacyRule.execute()
 		if err != nil {
 			return nfdv1alpha1.RuleOutput{}, fmt.Errorf("failed to execute legacy rule %s: %w", r.LegacyRule.Name, err)
 		}
@@ -154,7 +151,7 @@ func (r *CustomRule) execute(features map[string]*nfdv1alpha1.DomainFeatures) (n
 	return nfdv1alpha1.RuleOutput{}, fmt.Errorf("BUG: an empty rule, this really should not happen")
 }
 
-func (r *LegacyRule) execute(features map[string]*nfdv1alpha1.DomainFeatures) (map[string]string, error) {
+func (r *LegacyRule) execute() (map[string]string, error) {
 	if len(r.MatchOn) > 0 {
 		// Logical OR over the legacy rules
 		matched := false
diff --git a/source/fake/fake.go b/source/fake/fake.go
index 9a155f6aa..d377ac6f6 100644
--- a/source/fake/fake.go
+++ b/source/fake/fake.go
@@ -84,7 +84,7 @@ func newDefaultConfig() *Config {
 // fakeSource implements the FeatureSource, LabelSource and ConfigurableSource interfaces.
 type fakeSource struct {
 	config   *Config
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -116,7 +116,7 @@ func (s *fakeSource) SetConfig(conf source.Config) {
 
 // Discover method of the FeatureSource interface
 func (s *fakeSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	s.features.Flags[FlagFeature] = nfdv1alpha1.NewFlagFeatures(s.config.FlagFeatures...)
 	s.features.Attributes[AttributeFeature] = nfdv1alpha1.NewAttributeFeatures(s.config.AttributeFeatures)
@@ -133,9 +133,9 @@ func (s *fakeSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface.
-func (s *fakeSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *fakeSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/kernel/kernel.go b/source/kernel/kernel.go
index a96041104..5c57b5153 100644
--- a/source/kernel/kernel.go
+++ b/source/kernel/kernel.go
@@ -58,7 +58,7 @@ func newDefaultConfig() *Config {
 // kernelSource implements the FeatureSource, LabelSource and ConfigurableSource interfaces.
 type kernelSource struct {
 	config   *Config
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 	// legacyKconfig contains mangled kconfig values used for
 	// kernel.config-<flag> labels and legacy kConfig custom rules.
 	legacyKconfig map[string]string
@@ -117,7 +117,7 @@ func (s *kernelSource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource interface
 func (s *kernelSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	// Read kernel version
 	if version, err := parseVersion(); err != nil {
@@ -153,9 +153,9 @@ func (s *kernelSource) Discover() error {
 	return nil
 }
 
-func (s *kernelSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *kernelSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/local/local.go b/source/local/local.go
index 2187d0b27..2bebfb0d0 100644
--- a/source/local/local.go
+++ b/source/local/local.go
@@ -45,7 +45,7 @@ var (
 
 // localSource implements the FeatureSource and LabelSource interfaces.
 type localSource struct {
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 	config   *Config
 }
 
@@ -103,7 +103,7 @@ func newDefaultConfig() *Config {
 
 // Discover method of the FeatureSource interface
 func (s *localSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	featuresFromFiles, err := getFeaturesFromFiles()
 	if err != nil {
@@ -137,9 +137,9 @@ func (s *localSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface
-func (s *localSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *localSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/memory/memory.go b/source/memory/memory.go
index c9390fd7c..a26abc122 100644
--- a/source/memory/memory.go
+++ b/source/memory/memory.go
@@ -42,7 +42,7 @@ const NumaFeature = "numa"
 
 // memorySource implements the FeatureSource and LabelSource interfaces.
 type memorySource struct {
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -84,7 +84,7 @@ func (s *memorySource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource interface
 func (s *memorySource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	// Detect NUMA
 	if numa, err := detectNuma(); err != nil {
@@ -106,9 +106,9 @@ func (s *memorySource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface.
-func (s *memorySource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *memorySource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/network/network.go b/source/network/network.go
index c906c1d61..4b9d3b2dc 100644
--- a/source/network/network.go
+++ b/source/network/network.go
@@ -40,7 +40,7 @@ const sysfsBaseDir = "class/net"
 
 // networkSource implements the FeatureSource and LabelSource interfaces.
 type networkSource struct {
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -91,7 +91,7 @@ func (s *networkSource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource interface.
 func (s *networkSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	devs, err := detectNetDevices()
 	if err != nil {
@@ -105,9 +105,9 @@ func (s *networkSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface.
-func (s *networkSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *networkSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/pci/pci.go b/source/pci/pci.go
index 93fd0bdcd..d09850ff8 100644
--- a/source/pci/pci.go
+++ b/source/pci/pci.go
@@ -50,7 +50,7 @@ func newDefaultConfig() *Config {
 // pciSource implements the FeatureSource, LabelSource and ConfigurableSource interfaces.
 type pciSource struct {
 	config   *Config
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -140,7 +140,7 @@ func (s *pciSource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource interface
 func (s *pciSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	devs, err := detectPci()
 	if err != nil {
@@ -154,9 +154,9 @@ func (s *pciSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface
-func (s *pciSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *pciSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/source.go b/source/source.go
index 2f0eb6dc8..413aca719 100644
--- a/source/source.go
+++ b/source/source.go
@@ -21,6 +21,8 @@ package source
 import (
 	"fmt"
 
+	"k8s.io/klog/v2"
+
 	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 )
 
@@ -38,7 +40,7 @@ type FeatureSource interface {
 	Discover() error
 
 	// GetFeatures returns discovered features in raw form
-	GetFeatures() *nfdv1alpha1.DomainFeatures
+	GetFeatures() *nfdv1alpha1.Features
 }
 
 // LabelSource represents a source of node feature labels
@@ -154,3 +156,37 @@ func GetAllConfigurableSources() map[string]ConfigurableSource {
 	}
 	return all
 }
+
+// GetAllFeatures returns a combined set of all features from all feature
+// sources.
+func GetAllFeatures() *nfdv1alpha1.Features {
+	features := nfdv1alpha1.NewFeatures()
+	for n, s := range GetAllFeatureSources() {
+		f := s.GetFeatures()
+		for k, v := range f.Flags {
+			// Prefix feature with the name of the source
+			k = n + "." + k
+			if typ := features.Exists(k); typ != "" {
+				klog.Exitf("feature source %q returned flag feature %q which already exists (type %q)", n, k, typ)
+			}
+			features.Flags[k] = v
+		}
+		for k, v := range f.Attributes {
+			// Prefix feature with the name of the source
+			k = n + "." + k
+			if typ := features.Exists(k); typ != "" {
+				klog.Exitf("feature source %q returned attribute feature %q which already exists (type %q)", n, k, typ)
+			}
+			features.Attributes[k] = v
+		}
+		for k, v := range f.Instances {
+			// Prefix feature with the name of the source
+			k = n + "." + k
+			if typ := features.Exists(k); typ != "" {
+				klog.Exitf("feature source %q returned instance feature %q which already exists (type %q)", n, k, typ)
+			}
+			features.Instances[k] = v
+		}
+	}
+	return features
+}
diff --git a/source/storage/storage.go b/source/storage/storage.go
index e0b107b43..64db926f8 100644
--- a/source/storage/storage.go
+++ b/source/storage/storage.go
@@ -37,7 +37,7 @@ const BlockFeature = "block"
 
 // storageSource implements the FeatureSource and LabelSource interfaces.
 type storageSource struct {
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -73,7 +73,7 @@ func (s *storageSource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource interface
 func (s *storageSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	devs, err := detectBlock()
 	if err != nil {
@@ -87,9 +87,9 @@ func (s *storageSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface.
-func (s *storageSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *storageSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/system/system.go b/source/system/system.go
index 304c9ed74..6945ef24d 100644
--- a/source/system/system.go
+++ b/source/system/system.go
@@ -47,7 +47,7 @@ const (
 
 // systemSource implements the FeatureSource and LabelSource interfaces.
 type systemSource struct {
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -78,7 +78,7 @@ func (s *systemSource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource interface
 func (s *systemSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	// Get node name
 	s.features.Attributes[NameFeature] = nfdv1alpha1.NewAttributeFeatures(nil)
@@ -107,9 +107,9 @@ func (s *systemSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface
-func (s *systemSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *systemSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }
diff --git a/source/usb/usb.go b/source/usb/usb.go
index fd8a10dbc..a2fce7d2f 100644
--- a/source/usb/usb.go
+++ b/source/usb/usb.go
@@ -53,7 +53,7 @@ func defaultDeviceLabelFields() []string { return []string{"class", "vendor", "d
 // usbSource implements the LabelSource and ConfigurableSource interfaces.
 type usbSource struct {
 	config   *Config
-	features *nfdv1alpha1.DomainFeatures
+	features *nfdv1alpha1.Features
 }
 
 // Singleton source instance
@@ -139,7 +139,7 @@ func (s *usbSource) GetLabels() (source.FeatureLabels, error) {
 
 // Discover method of the FeatureSource interface
 func (s *usbSource) Discover() error {
-	s.features = nfdv1alpha1.NewDomainFeatures()
+	s.features = nfdv1alpha1.NewFeatures()
 
 	devs, err := detectUsb()
 	if err != nil {
@@ -153,9 +153,9 @@ func (s *usbSource) Discover() error {
 }
 
 // GetFeatures method of the FeatureSource Interface
-func (s *usbSource) GetFeatures() *nfdv1alpha1.DomainFeatures {
+func (s *usbSource) GetFeatures() *nfdv1alpha1.Features {
 	if s.features == nil {
-		s.features = nfdv1alpha1.NewDomainFeatures()
+		s.features = nfdv1alpha1.NewFeatures()
 	}
 	return s.features
 }


Test patch:
diff --git a/pkg/apis/nfd/v1alpha1/rule_test.go b/pkg/apis/nfd/v1alpha1/rule_test.go
index b19c2b633..3ad012a76 100644
--- a/pkg/apis/nfd/v1alpha1/rule_test.go
+++ b/pkg/apis/nfd/v1alpha1/rule_test.go
@@ -23,14 +23,14 @@ import (
 )
 
 func TestRule(t *testing.T) {
-	f := map[string]*DomainFeatures{}
+	f := &Features{}
 	r1 := Rule{Labels: map[string]string{"label-1": "", "label-2": "true"}}
 	r2 := Rule{
 		Labels: map[string]string{"label-1": "label-val-1"},
 		Vars:   map[string]string{"var-1": "var-val-1"},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature: "domain-1.kf-1",
+				Feature: "kf-1",
 				MatchExpressions: MatchExpressionSet{
 					"key-1": MustCreateMatchExpression(MatchExists),
 				},
@@ -44,11 +44,10 @@ func TestRule(t *testing.T) {
 	assert.Equal(t, r1.Labels, m.Labels, "empty matcher should have matched empty features")
 
 	_, err = r2.Execute(f)
-	assert.Error(t, err, "matching against a missing domain should have returned an error")
+	assert.Error(t, err, "matching against a missing feature should have returned an error")
 
-	// Test empty domain
-	d := NewDomainFeatures()
-	f["domain-1"] = d
+	// Test properly initialized empty features
+	f = NewFeatures()
 
 	m, err = r1.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
@@ -59,9 +58,9 @@ func TestRule(t *testing.T) {
 	assert.Error(t, err, "matching against a missing feature type should have returned an error")
 
 	// Test empty feature sets
-	d.Flags["kf-1"] = NewFlagFeatures()
-	d.Attributes["vf-1"] = NewAttributeFeatures(nil)
-	d.Instances["if-1"] = NewInstanceFeatures(nil)
+	f.Flags["kf-1"] = NewFlagFeatures()
+	f.Attributes["vf-1"] = NewAttributeFeatures(nil)
+	f.Instances["if-1"] = NewInstanceFeatures(nil)
 
 	m, err = r1.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
@@ -72,9 +71,9 @@ func TestRule(t *testing.T) {
 	assert.Nil(t, m.Labels, "unexpected match")
 
 	// Test non-empty feature sets
-	d.Flags["kf-1"].Elements["key-x"] = Nil{}
-	d.Attributes["vf-1"].Elements["key-1"] = "val-x"
-	d.Instances["if-1"] = NewInstanceFeatures([]InstanceFeature{
+	f.Flags["kf-1"].Elements["key-x"] = Nil{}
+	f.Attributes["vf-1"].Elements["key-1"] = "val-x"
+	f.Instances["if-1"] = NewInstanceFeatures([]InstanceFeature{
 		*NewInstanceFeature(map[string]string{"attr-1": "val-x"})})
 
 	m, err = r1.Execute(f)
@@ -84,7 +83,7 @@ func TestRule(t *testing.T) {
 	// Test empty MatchExpressions
 	r1.MatchFeatures = FeatureMatcher{
 		FeatureMatcherTerm{
-			Feature:          "domain-1.kf-1",
+			Feature:          "kf-1",
 			MatchExpressions: MatchExpressionSet{},
 		},
 	}
@@ -97,7 +96,7 @@ func TestRule(t *testing.T) {
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "keys should not have matched")
 
-	d.Flags["kf-1"].Elements["key-1"] = Nil{}
+	f.Flags["kf-1"].Elements["key-1"] = Nil{}
 	m, err = r2.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r2.Labels, m.Labels, "keys should have matched")
@@ -108,7 +107,7 @@ func TestRule(t *testing.T) {
 		Labels: map[string]string{"label-3": "label-val-3", "empty": ""},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature: "domain-1.vf-1",
+				Feature: "vf-1",
 				MatchExpressions: MatchExpressionSet{
 					"key-1": MustCreateMatchExpression(MatchIn, "val-1"),
 				},
@@ -119,7 +118,7 @@ func TestRule(t *testing.T) {
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "values should not have matched")
 
-	d.Attributes["vf-1"].Elements["key-1"] = "val-1"
+	f.Attributes["vf-1"].Elements["key-1"] = "val-1"
 	m, err = r3.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r3.Labels, m.Labels, "values should have matched")
@@ -129,7 +128,7 @@ func TestRule(t *testing.T) {
 		Labels: map[string]string{"label-4": "label-val-4"},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature: "domain-1.if-1",
+				Feature: "if-1",
 				MatchExpressions: MatchExpressionSet{
 					"attr-1": MustCreateMatchExpression(MatchIn, "val-1"),
 				},
@@ -140,7 +139,7 @@ func TestRule(t *testing.T) {
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "instances should not have matched")
 
-	d.Instances["if-1"].Elements[0].Attributes["attr-1"] = "val-1"
+	f.Instances["if-1"].Elements[0].Attributes["attr-1"] = "val-1"
 	m, err = r4.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r4.Labels, m.Labels, "instances should have matched")
@@ -150,13 +149,13 @@ func TestRule(t *testing.T) {
 		Labels: map[string]string{"label-5": "label-val-5"},
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature: "domain-1.vf-1",
+				Feature: "vf-1",
 				MatchExpressions: MatchExpressionSet{
 					"key-1": MustCreateMatchExpression(MatchIn, "val-x"),
 				},
 			},
 			FeatureMatcherTerm{
-				Feature: "domain-1.if-1",
+				Feature: "if-1",
 				MatchExpressions: MatchExpressionSet{
 					"attr-1": MustCreateMatchExpression(MatchIn, "val-1"),
 				},
@@ -177,7 +176,7 @@ func TestRule(t *testing.T) {
 		{
 			MatchFeatures: FeatureMatcher{
 				FeatureMatcherTerm{
-					Feature: "domain-1.kf-1",
+					Feature: "kf-1",
 					MatchExpressions: MatchExpressionSet{
 						"key-na": MustCreateMatchExpression(MatchExists),
 					},
@@ -193,7 +192,7 @@ func TestRule(t *testing.T) {
 		MatchAnyElem{
 			MatchFeatures: FeatureMatcher{
 				FeatureMatcherTerm{
-					Feature: "domain-1.kf-1",
+					Feature: "kf-1",
 					MatchExpressions: MatchExpressionSet{
 						"key-1": MustCreateMatchExpression(MatchExists),
 					},
@@ -207,46 +206,44 @@ func TestRule(t *testing.T) {
 }
 
 func TestTemplating(t *testing.T) {
-	f := map[string]*DomainFeatures{
-		"domain_1": &DomainFeatures{
-			Flags: map[string]FlagFeatureSet{
-				"kf_1": FlagFeatureSet{
-					Elements: map[string]Nil{
-						"key-a": {},
-						"key-b": {},
-						"key-c": {},
-					},
+	f := &Features{
+		Flags: map[string]FlagFeatureSet{
+			"kf_1": {
+				Elements: map[string]Nil{
+					"key-a": {},
+					"key-b": {},
+					"key-c": {},
 				},
 			},
-			Attributes: map[string]AttributeFeatureSet{
-				"vf_1": AttributeFeatureSet{
-					Elements: map[string]string{
-						"key-1": "val-1",
-						"keu-2": "val-2",
-						"key-3": "val-3",
-					},
+		},
+		Attributes: map[string]AttributeFeatureSet{
+			"vf_1": {
+				Elements: map[string]string{
+					"key-1": "val-1",
+					"keu-2": "val-2",
+					"key-3": "val-3",
 				},
 			},
-			Instances: map[string]InstanceFeatureSet{
-				"if_1": InstanceFeatureSet{
-					Elements: []InstanceFeature{
-						{
-							Attributes: map[string]string{
-								"attr-1": "1",
-								"attr-2": "val-2",
-							},
+		},
+		Instances: map[string]InstanceFeatureSet{
+			"if_1": {
+				Elements: []InstanceFeature{
+					{
+						Attributes: map[string]string{
+							"attr-1": "1",
+							"attr-2": "val-2",
 						},
-						{
-							Attributes: map[string]string{
-								"attr-1": "10",
-								"attr-2": "val-20",
-							},
+					},
+					{
+						Attributes: map[string]string{
+							"attr-1": "10",
+							"attr-2": "val-20",
 						},
-						{
-							Attributes: map[string]string{
-								"attr-1": "100",
-								"attr-2": "val-200",
-							},
+					},
+					{
+						Attributes: map[string]string{
+							"attr-1": "100",
+							"attr-2": "val-200",
 						},
 					},
 				},
@@ -259,21 +256,21 @@ func TestTemplating(t *testing.T) {
 		LabelsTemplate: `
 label-1=will-be-overridden
 label-2=
-{{range .domain_1.kf_1}}kf-{{.Name}}=present
+{{range .kf_1}}kf-{{.Name}}=present
 {{end}}
-{{range .domain_1.vf_1}}vf-{{.Name}}=vf-{{.Value}}
+{{range .vf_1}}vf-{{.Name}}=vf-{{.Value}}
 {{end}}
-{{range .domain_1.if_1}}if-{{index . "attr-1"}}_{{index . "attr-2"}}=present
+{{range .if_1}}if-{{index . "attr-1"}}_{{index . "attr-2"}}=present
 {{end}}`,
 		Vars: map[string]string{"var-1": "var-val-1"},
 		VarsTemplate: `
 var-1=value-will-be-overridden-by-vars
 var-2=
-{{range .domain_1.kf_1}}kf-{{.Name}}=true
+{{range .kf_1}}kf-{{.Name}}=true
 {{end}}`,
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
-				Feature: "domain_1.kf_1",
+				Feature: "kf_1",
 				MatchExpressions: MatchExpressionSet{
 					"key-a": MustCreateMatchExpression(MatchExists),
 					"key-c": MustCreateMatchExpression(MatchExists),
@@ -281,14 +278,14 @@ var-2=
 				},
 			},
 			FeatureMatcherTerm{
-				Feature: "domain_1.vf_1",
+				Feature: "vf_1",
 				MatchExpressions: MatchExpressionSet{
 					"key-1": MustCreateMatchExpression(MatchIn, "val-1", "val-2"),
 					"bar":   MustCreateMatchExpression(MatchDoesNotExist),
 				},
 			},
 			FeatureMatcherTerm{
-				Feature: "domain_1.if_1",
+				Feature: "if_1",
 				MatchExpressions: MatchExpressionSet{
 					"attr-1": MustCreateMatchExpression(MatchLt, "100"),
 				},
@@ -342,7 +339,7 @@ var-2=
 			// We need at least one matcher to match to execute the template.
 			// Use a simple empty matchexpression set to match anything.
 			FeatureMatcherTerm{
-				Feature: "domain_1.kf_1",
+				Feature: "kf_1",
 				MatchExpressions: MatchExpressionSet{
 					"key-a": MustCreateMatchExpression(MatchExists),
 				},