Repository: kubernetes-sigs/node-feature-discovery
Pull request #986 (2022-12-09 16:21:17)

Introduce NodeFeature CRD
This PR is a subset of #903 

Implement a new CRD-based communication mechanism between nfd-worker and nfd-master. This PR introduces a new `NodeFeature` CRD API and adds an initial implementation of it in nfd-worker and nfd-master.

NFD-Workers create/update `NodeFeature` objects whereas nfd-master consumes them and creates node labels accordingly. The PR introduces new command-line flags in nfd-worker and nfd-master to enable the new CRD API which will automatically disable the gRPC interface. Default behavior of NFD is not changed in any way and support for `NodeFeature` objects must be explicitly enabled.

Having 3rd party extensions in mind, the new `NodeFeature` CRD is namespaced (instead of cluster-scoped like `NodeFeatureRule`). The goal is to make management of `NodeFeature` objects easier: no clashes of object names, easy cleanup as objects are deleted with namespaces etc... What is still missing is handling of multiple `NodeFeature` objects per node - this will be added by an upcoming PR. So far nfd-master only uses `NodeFeature` objects originating from nfd-workers.

The PR consists of three parts:
1. Define the new `NodeFeature` CRD (first two commits)
2. Change nfd-worker to create `NodeFeature` objects (3rd commit)
3. Change nfd-master to watch/process `NodeFeature` objects (4th commit)

An example `NodeFeature` object, created by nfd-worker would be:

```yaml
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeature
metadata:
  labels:
    nfd.node.kubernetes.io/node-name: node-1
  name: node-1
  namespace: default
spec:
  # Features for NodeFeatureRule matching
  features:
    flags:
      feature.domain-a:
        elements:
          feature-x: {}
    attributes:
      feature.domain-b:
        elements:
          feature-y: "foo"
    instances:
      feature.domain-c:
        elements:
        - attributes:
            name: "elem-1"
            vendor: "acme"
  # Labels to be created
  labels:
    vendor-feature.enabled: "true"
    vendor-setting.value: "100"
```

NOTES: the CRD contains fields for both "raw features" and request to create labels. The reason is that nfd-worker still handles the creation of "built-in" labels. One alternative could be to have only "raw features" in the CRD and turn all built-in labels into a `NodeFeatureRule` object but that's probably a separate discussion.

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


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

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

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

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

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

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

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: @marquiz: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-node-feature-discovery-verify-master | 772b80ff9f41e4a24025c47a7e5a21f7db68ee82 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/986/pull-node-feature-discovery-verify-master/1602786681197432832) | true | `/test pull-node-feature-discovery-verify-master`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=986). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Amarquiz). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/node-feature-discovery/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
marquiz: Needed to rebase on top of latest master.

Also, required a new fix to make ci-linter happy: #988 (don't want to skip errchecking, I overlooked that when reviewing #987). Let's merge that one before proceeding with this PR
/hold

Patch:
diff --git a/cmd/nfd-master/main.go b/cmd/nfd-master/main.go
index b18223563..40cafd7ae 100644
--- a/cmd/nfd-master/main.go
+++ b/cmd/nfd-master/main.go
@@ -94,6 +94,8 @@ func initFlags(flagset *flag.FlagSet) *master.Args {
 	flagset.Var(&args.LabelWhiteList, "label-whitelist",
 		"Regular expression to filter label names to publish to the Kubernetes API server. "+
 			"NB: the label namespace is omitted i.e. the filter is only applied to the name part after '/'.")
+	flagset.BoolVar(&args.EnableNodeFeatureApi, "-enable-nodefeature-api", false,
+		"Enable the NodeFeature CRD API for receiving node features. This will automatically disable the gRPC communication.")
 	flagset.BoolVar(&args.NoPublish, "no-publish", false,
 		"Do not publish feature labels")
 	flagset.BoolVar(&args.EnableTaints, "enable-taints", false,
diff --git a/cmd/nfd-worker/main.go b/cmd/nfd-worker/main.go
index 90892380f..30e3290d1 100644
--- a/cmd/nfd-worker/main.go
+++ b/cmd/nfd-worker/main.go
@@ -101,6 +101,10 @@ func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs)
 		"Config file to use.")
 	flagset.StringVar(&args.KeyFile, "key-file", "",
 		"Private key matching -cert-file")
+	flagset.BoolVar(&args.EnableNodeFeatureApi, "enable-nodefeature-api", false,
+		"Enable the NodeFeature CRD API for communicating with nfd-master. This will automatically disable the gRPC communication.")
+	flagset.StringVar(&args.Kubeconfig, "kubeconfig", "",
+		"Kubeconfig to use")
 	flagset.BoolVar(&args.Oneshot, "oneshot", false,
 		"Do not publish feature labels")
 	flagset.StringVar(&args.Options, "options", "",
@@ -119,7 +123,7 @@ func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs)
 		LabelSources:   &utils.StringSliceVal{},
 	}
 	overrides.NoPublish = flagset.Bool("no-publish", false,
-		"Do not publish discovered features, disable connection to nfd-master.")
+		"Do not publish discovered features, disable connection to nfd-master and don't create NodeFeature object.")
 	flagset.Var(overrides.FeatureSources, "feature-sources",
 		"Comma separated list of feature sources. Special value 'all' enables all sources. "+
 			"Prefix the source name with '-' to disable it.")
diff --git a/deployment/base/nfd-crds/kustomization.yaml b/deployment/base/nfd-crds/kustomization.yaml
index 73fd8cc08..b7f95447e 100644
--- a/deployment/base/nfd-crds/kustomization.yaml
+++ b/deployment/base/nfd-crds/kustomization.yaml
@@ -2,4 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
 resources:
-- nodefeaturerule-crd.yaml
+- nfd-api-crds.yaml
diff --git a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml b/deployment/base/nfd-crds/nfd-api-crds.yaml
similarity index 74%
rename from deployment/base/nfd-crds/nodefeaturerule-crd.yaml
rename to deployment/base/nfd-crds/nfd-api-crds.yaml
index 9a1fc53c8..ab10f15a9 100644
--- a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml
+++ b/deployment/base/nfd-crds/nfd-api-crds.yaml
@@ -1,6 +1,117 @@
 ---
 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: Namespaced
+  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:
+              features:
+                description: Features is the full "raw" features data that has been
+                  discovered.
+                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
+                required:
+                - attributes
+                - flags
+                - instances
+                type: object
+              labels:
+                additionalProperties:
+                  type: string
+                description: Labels is the set of node labels that are requested to
+                  be created.
+                type: object
+            required:
+            - features
+            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
diff --git a/deployment/base/rbac/kustomization.yaml b/deployment/base/rbac/kustomization.yaml
index fdadb5675..6eb2d8a8c 100644
--- a/deployment/base/rbac/kustomization.yaml
+++ b/deployment/base/rbac/kustomization.yaml
@@ -7,3 +7,6 @@ resources:
 - master-serviceaccount.yaml
 - master-clusterrole.yaml
 - master-clusterrolebinding.yaml
+- worker-serviceaccount.yaml
+- worker-role.yaml
+- worker-rolebinding.yaml
diff --git a/deployment/base/rbac/master-clusterrole.yaml b/deployment/base/rbac/master-clusterrole.yaml
index ca4b5705a..d464e546e 100644
--- a/deployment/base/rbac/master-clusterrole.yaml
+++ b/deployment/base/rbac/master-clusterrole.yaml
@@ -15,6 +15,7 @@ rules:
 - apiGroups:
   - nfd.k8s-sigs.io
   resources:
+  - nodefeatures
   - nodefeaturerules
   verbs:
   - get
diff --git a/deployment/base/rbac/worker-role.yaml b/deployment/base/rbac/worker-role.yaml
new file mode 100644
index 000000000..72f261e9e
--- /dev/null
+++ b/deployment/base/rbac/worker-role.yaml
@@ -0,0 +1,13 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: nfd-worker
+rules:
+- apiGroups:
+  - nfd.k8s-sigs.io
+  resources:
+  - nodefeatures
+  verbs:
+  - create
+  - get
+  - update
diff --git a/deployment/base/rbac/worker-rolebinding.yaml b/deployment/base/rbac/worker-rolebinding.yaml
new file mode 100644
index 000000000..707c75fbb
--- /dev/null
+++ b/deployment/base/rbac/worker-rolebinding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: nfd-worker
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: nfd-worker
+subjects:
+- kind: ServiceAccount
+  name: nfd-worker
+  namespace: default
diff --git a/deployment/base/rbac/worker-serviceaccount.yaml b/deployment/base/rbac/worker-serviceaccount.yaml
new file mode 100644
index 000000000..442b06c5e
--- /dev/null
+++ b/deployment/base/rbac/worker-serviceaccount.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: nfd-worker
diff --git a/deployment/base/worker-daemonset/worker-daemonset.yaml b/deployment/base/worker-daemonset/worker-daemonset.yaml
index 8f02fd0e1..f93bf175f 100644
--- a/deployment/base/worker-daemonset/worker-daemonset.yaml
+++ b/deployment/base/worker-daemonset/worker-daemonset.yaml
@@ -13,6 +13,7 @@ spec:
       labels:
         app: nfd-worker
     spec:
+      serviceAccount: nfd-worker
       dnsPolicy: ClusterFirstWithHostNet
       containers:
         - name: nfd-worker
diff --git a/deployment/base/worker-job/worker-job.yaml b/deployment/base/worker-job/worker-job.yaml
index dd2f671dc..6cf2c9dbf 100644
--- a/deployment/base/worker-job/worker-job.yaml
+++ b/deployment/base/worker-job/worker-job.yaml
@@ -12,6 +12,7 @@ spec:
       labels:
         app: nfd-worker
     spec:
+      serviceAccount: nfd-worker
       dnsPolicy: ClusterFirstWithHostNet
       restartPolicy: Never
       affinity:
diff --git a/deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml
similarity index 74%
rename from deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml
rename to deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml
index 9a1fc53c8..ab10f15a9 100644
--- a/deployment/helm/node-feature-discovery/crds/nodefeaturerule-crd.yaml
+++ b/deployment/helm/node-feature-discovery/crds/nfd-api-crds.yaml
@@ -1,6 +1,117 @@
 ---
 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: Namespaced
+  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:
+              features:
+                description: Features is the full "raw" features data that has been
+                  discovered.
+                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
+                required:
+                - attributes
+                - flags
+                - instances
+                type: object
+              labels:
+                additionalProperties:
+                  type: string
+                description: Labels is the set of node labels that are requested to
+                  be created.
+                type: object
+            required:
+            - features
+            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
diff --git a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
index f1c5805a1..3dd6f6f3b 100644
--- a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
+++ b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
@@ -27,6 +27,7 @@ rules:
 - apiGroups:
   - nfd.k8s-sigs.io
   resources:
+  - nodefeatures
   - nodefeaturerules
   verbs:
   - get
diff --git a/deployment/helm/node-feature-discovery/templates/master.yaml b/deployment/helm/node-feature-discovery/templates/master.yaml
index 498964f8a..03e995934 100644
--- a/deployment/helm/node-feature-discovery/templates/master.yaml
+++ b/deployment/helm/node-feature-discovery/templates/master.yaml
@@ -78,6 +78,9 @@ spec:
             {{- if .Values.master.instance | empty | not }}
             - "--instance={{ .Values.master.instance }}"
             {{- end }}
+            {{- if .Values.enableNodeFeatureApi }}
+            - "-enable-nodefeature-api"
+            {{- end }}
             {{- if .Values.master.extraLabelNs | empty | not }}
             - "--extra-label-ns={{- join "," .Values.master.extraLabelNs }}"
             {{- end }}
diff --git a/deployment/helm/node-feature-discovery/templates/role.yaml b/deployment/helm/node-feature-discovery/templates/role.yaml
new file mode 100644
index 000000000..f63cb8ff4
--- /dev/null
+++ b/deployment/helm/node-feature-discovery/templates/role.yaml
@@ -0,0 +1,18 @@
+{{- if .Values.worker.rbac.create }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ include "node-feature-discovery.fullname" . }}-worker
+  labels:
+    {{- include "node-feature-discovery.labels" . | nindent 4 }}
+rules:
+- apiGroups:
+  - nfd.k8s-sigs.io
+  resources:
+  - nodefeatures
+  verbs:
+  - create
+  - get
+  - update
+{{- end }}
+
diff --git a/deployment/helm/node-feature-discovery/templates/rolebinding.yaml b/deployment/helm/node-feature-discovery/templates/rolebinding.yaml
new file mode 100644
index 000000000..30a00381f
--- /dev/null
+++ b/deployment/helm/node-feature-discovery/templates/rolebinding.yaml
@@ -0,0 +1,17 @@
+{{- if .Values.worker.rbac.create }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ include "node-feature-discovery.fullname" . }}-worker
+  labels:
+    {{- include "node-feature-discovery.labels" . | nindent 4 }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ include "node-feature-discovery.fullname" . }}-worker
+subjects:
+- kind: ServiceAccount
+  name: {{ include "node-feature-discovery.worker.serviceAccountName" . }}
+  namespace: {{ include "node-feature-discovery.namespace" .  }}
+{{- end }}
+
diff --git a/deployment/helm/node-feature-discovery/templates/worker.yaml b/deployment/helm/node-feature-discovery/templates/worker.yaml
index f48aade32..e723cc5cb 100644
--- a/deployment/helm/node-feature-discovery/templates/worker.yaml
+++ b/deployment/helm/node-feature-discovery/templates/worker.yaml
@@ -46,6 +46,9 @@ spec:
         - "nfd-worker"
         args:
         - "--server={{ include "node-feature-discovery.fullname" . }}-master:{{ .Values.master.service.port }}"
+        {{- if .Values.enableNodeFeatureApi }}
+        - "-enable-nodefeature-api"
+        {{- end }}
 {{- if .Values.tls.enable }}
         - "--ca-file=/etc/kubernetes/node-feature-discovery/certs/ca.crt"
         - "--key-file=/etc/kubernetes/node-feature-discovery/certs/tls.key"
diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml
index 2b3a5942a..85a737ce1 100644
--- a/deployment/helm/node-feature-discovery/values.yaml
+++ b/deployment/helm/node-feature-discovery/values.yaml
@@ -10,8 +10,11 @@ nameOverride: ""
 fullnameOverride: ""
 namespaceOverride: ""
 
+enableNodeFeatureApi: false
+
 master:
   instance:
+  featureApi:
   extraLabelNs: []
   resourceLabels: []
   featureRulesController: null
@@ -338,6 +341,9 @@ worker:
     # If not set and create is true, a name is generated using the fullname template
     name:
 
+  rbac:
+    create: true
+
   # Allow users to mount the hostPath /usr/src, useful for RHCOS on s390x
   # Does not work on systems without /usr/src AND a read-only /usr, such as Talos
   mountUsrSrc: false
diff --git a/docs/deployment/helm.md b/docs/deployment/helm.md
index e2f0435cd..6e032026e 100644
--- a/docs/deployment/helm.md
+++ b/docs/deployment/helm.md
@@ -97,6 +97,7 @@ We have introduced the following Chart parameters.
 | `fullnameOverride` | string |  | Override a default fully qualified app name |
 | `tls.enable` | bool | false | Specifies whether to use TLS for communications between components |
 | `tls.certManager` | bool | false | If enabled, requires [cert-manager](https://cert-manager.io/docs/) to be installed and will automatically create the required TLS certificates |
+| `enableNodeFeatureApi` | bool  | false | Enable the NodeFeature CRD API for communicating node features. This will automatically disable the gRPC communication.
 
 ### Master pod parameters
 
@@ -134,6 +135,7 @@ We have introduced the following Chart parameters.
 | `worker.serviceAccount.create`      | bool   | true | Specifies whether a service account for nfd-worker should be created
 | `worker.serviceAccount.annotations` | dict   | {}   | Annotations to add to the service account for nfd-worker
 | `worker.serviceAccount.name`        | string |      | The name of the service account to use for nfd-worker. If not set and create is true, a name is generated using the fullname template (suffixed with `-worker`)
+| `worker.rbac.create`  | bool | true | Specifies whether to create [RBAC][rbac] configuration for nfd-worker
 | `worker.mountUsrSrc` | bool | false | Specifies whether to allow users to mount the hostpath /user/src. Does not work on systems without /usr/src AND a read-only /usr |
 | `worker.resources` | dict | {} | NFD worker pod [resources management](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) |
 | `worker.nodeSelector` | dict | {} | NFD worker pod [node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) |
diff --git a/docs/reference/master-commandline-reference.md b/docs/reference/master-commandline-reference.md
index 8542742d1..c7dae1d60 100644
--- a/docs/reference/master-commandline-reference.md
+++ b/docs/reference/master-commandline-reference.md
@@ -99,18 +99,6 @@ Example:
 nfd-master -cert-file=/opt/nfd/master.crt -key-file=/opt/nfd/master.key -ca-file=/opt/nfd/ca.crt
 ```
 
-### -enable-taints
-
-The `-enable-taints` flag enables/disables node tainting feature of NFD.
-
-Default: *false*
-
-Example:
-
-```bash
-nfd-master -enable-taints=true
-```
-
 ### -key-file
 
 The `-key-file` is one of the three flags (together with `-ca-file` and
@@ -149,6 +137,32 @@ nfd-master -verify-node-name -ca-file=/opt/nfd/ca.crt \
     -cert-file=/opt/nfd/master.crt -key-file=/opt/nfd/master.key
 ```
 
+### -enable-nodefeature-api
+
+The `-enable-nodefeature-api` flag enables the NodeFeature CRD API for
+receiving feature requests. This will also automatically disable the gRPC
+interface.
+
+Default: false
+
+Example:
+
+```bash
+nfd-master -enable-nodefeature-api
+```
+
+### -enable-taints
+
+The `-enable-taints` flag enables/disables node tainting feature of NFD.
+
+Default: *false*
+
+Example:
+
+```bash
+nfd-master -enable-taints=true
+```
+
 ### -no-publish
 
 The `-no-publish` flag disables updates to the Node objects in the Kubernetes
diff --git a/docs/reference/worker-commandline-reference.md b/docs/reference/worker-commandline-reference.md
index 886faf4c7..cf6651b8f 100644
--- a/docs/reference/worker-commandline-reference.md
+++ b/docs/reference/worker-commandline-reference.md
@@ -122,6 +122,22 @@ Example:
 nfd-worker -key-file=/opt/nfd/worker.key -cert-file=/opt/nfd/worker.crt -ca-file=/opt/nfd/ca.crt
 ```
 
+### -kubeconfig
+
+The `-kubeconfig` flag specifies the kubeconfig to use for connecting to the
+Kubernetes API server. It is only needed for manipulating NodeFeature
+objects, and thus the flag only takes effect when
+[`-enable-nodefeature-api`](#-enable-nodefeature-api)) is specified. An empty
+value (which is also the default) implies in-cluster kubeconfig.
+
+Default: *empty*
+
+Example:
+
+```bash
+nfd-worker -kubeconfig ${HOME}/.kube/config
+```
+
 ### -server-name-override
 
 The `-server-name-override` flag specifies the common name (CN) which to
@@ -178,11 +194,33 @@ Example:
 nfd-worker -label-sources=kernel,system,local
 ```
 
+### -enable-nodefeature-api
+
+The `-enable-nodefeature-api` flag enables the experimental NodeFeature CRD API
+for communicating with nfd-master. This will also automatically disable the
+gRPC communication to nfd-master. When enabled, nfd-worker will create per-node
+NodeFeature objects the contain all discovered node features and the set of
+feature labels to be created.
+
+Default: false
+
+Example:
+
+```bash
+nfd-worker -enable-nodefeature-api
+```
+
 ### -no-publish
 
-The `-no-publish` flag disables all communication with the nfd-master, making
-it a "dry-run" flag for nfd-worker. NFD-Worker runs feature detection normally,
-but no labeling requests are sent to nfd-master.
+The `-no-publish` flag disables all communication with the nfd-master and the
+Kubernetes API server. It is effectively a "dry-run" flag for nfd-worker.
+NFD-Worker runs feature detection normally, but no labeling requests are sent
+to nfd-master and no NodeFeature objects are created or updated in the API
+server.
+
+Note: This flag takes precedence over the
+[`core.noPublish`](worker-configuration-reference#corenopublish)
+configuration file option.
 
 Default: *false*
 
diff --git a/docs/reference/worker-configuration-reference.md b/docs/reference/worker-configuration-reference.md
index 5dd239e81..48b13307c 100644
--- a/docs/reference/worker-configuration-reference.md
+++ b/docs/reference/worker-configuration-reference.md
@@ -131,10 +131,14 @@ core:
 ### core.noPublish
 
 Setting `core.noPublish` to `true` disables all communication with the
-nfd-master. It is effectively a "dry-run" flag: nfd-worker runs feature
-detection normally, but no labeling requests are sent to nfd-master.
-
-Note: Overridden by the `-no-publish` command line flag (if specified).
+nfd-master and the Kubernetes API server. It is effectively a "dry-run" option.
+NFD-Worker runs feature detection normally, but no labeling requests are sent
+to nfd-master and no NodeFeature objects are created or updated in the API
+server.
+
+Note: Overridden by the
+[`-no-publish`](worker-commandline-reference#-no-publish) command line flag (if
+specified).
 
 Default: `false`
 
diff --git a/hack/generate.sh b/hack/generate.sh
index 9b3b5ca22..0d5c165d6 100755
--- a/hack/generate.sh
+++ b/hack/generate.sh
@@ -10,10 +10,10 @@ go generate ./cmd/... ./pkg/... ./source/...
 
 rm -rf vendor/
 
-controller-gen object crd output:crd:stdout paths=./pkg/apis/... > deployment/base/nfd-crds/nodefeaturerule-crd.yaml
+controller-gen object crd output:crd:stdout paths=./pkg/apis/... > deployment/base/nfd-crds/nfd-api-crds.yaml
 
 mkdir -p deployment/helm/node-feature-discovery/crds
-cp deployment/base/nfd-crds/nodefeaturerule-crd.yaml deployment/helm/node-feature-discovery/crds/
+cp deployment/base/nfd-crds/nfd-api-crds.yaml deployment/helm/node-feature-discovery/crds
 
 rm -rf sigs.k8s.io
 
diff --git a/pkg/apis/nfd/v1alpha1/annotations_labels.go b/pkg/apis/nfd/v1alpha1/annotations_labels.go
index 7e4cbee25..978ba853c 100644
--- a/pkg/apis/nfd/v1alpha1/annotations_labels.go
+++ b/pkg/apis/nfd/v1alpha1/annotations_labels.go
@@ -46,4 +46,10 @@ const (
 
 	// NodeTaintsAnnotation is the annotation that holds the taints that nfd-master set on the node
 	NodeTaintsAnnotation = AnnotationNs + "/taints"
+
+	// NodeFeatureObjNodeNameLabel is the label that specifies which node the
+	// NodeFeature object is targeting. Creators of NodeFeature objects must
+	// set this label and consumers of the objects are supposed to use the
+	// label for filtering features designated for a certain node.
+	NodeFeatureObjNodeNameLabel = "nfd.node.kubernetes.io/node-name"
 )
diff --git a/pkg/apis/nfd/v1alpha1/register.go b/pkg/apis/nfd/v1alpha1/register.go
index c87a4852a..8c1d41f5b 100644
--- a/pkg/apis/nfd/v1alpha1/register.go
+++ b/pkg/apis/nfd/v1alpha1/register.go
@@ -40,6 +40,7 @@ func Resource(resource string) schema.GroupResource {
 
 func addKnownTypes(scheme *runtime.Scheme) error {
 	scheme.AddKnownTypes(SchemeGroupVersion,
+		&NodeFeature{},
 		&NodeFeatureRule{},
 	)
 	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go
index c057cddf7..3565db1cd 100644
--- a/pkg/apis/nfd/v1alpha1/types.go
+++ b/pkg/apis/nfd/v1alpha1/types.go
@@ -21,6 +21,37 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+// NodeFeatureList contains a list of NodeFeature objects.
+// +kubebuilder:object:root=true
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type NodeFeatureList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []NodeFeature `json:"items"`
+}
+
+// NodeFeature resource holds the features discovered for one node in the
+// cluster.
+// +kubebuilder:object:root=true
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +genclient
+type NodeFeature struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec NodeFeatureSpec `json:"spec"`
+}
+
+// NodeFeatureSpec describes a NodeFeature object.
+type NodeFeatureSpec struct {
+	// Features is the full "raw" features data that has been discovered.
+	Features Features `json:"features"`
+	// Labels is the set of node labels that are requested to be created.
+	// +optional
+	Labels map[string]string `json:"labels"`
+}
+
 // Features is the collection of all discovered features.
 //
 // +protobuf=true
diff --git a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
index 47513f133..07073afac 100644
--- a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
@@ -342,6 +342,64 @@ func (in *Nil) DeepCopy() *Nil {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeFeature) DeepCopyInto(out *NodeFeature) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeature.
+func (in *NodeFeature) DeepCopy() *NodeFeature {
+	if in == nil {
+		return nil
+	}
+	out := new(NodeFeature)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeFeature) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeFeatureList) DeepCopyInto(out *NodeFeatureList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]NodeFeature, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureList.
+func (in *NodeFeatureList) DeepCopy() *NodeFeatureList {
+	if in == nil {
+		return nil
+	}
+	out := new(NodeFeatureList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeFeatureList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NodeFeatureRule) DeepCopyInto(out *NodeFeatureRule) {
 	*out = *in
@@ -422,6 +480,29 @@ func (in *NodeFeatureRuleSpec) DeepCopy() *NodeFeatureRuleSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeFeatureSpec) DeepCopyInto(out *NodeFeatureSpec) {
+	*out = *in
+	in.Features.DeepCopyInto(&out.Features)
+	if in.Labels != nil {
+		in, out := &in.Labels, &out.Labels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeFeatureSpec.
+func (in *NodeFeatureSpec) DeepCopy() *NodeFeatureSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(NodeFeatureSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Rule) DeepCopyInto(out *Rule) {
 	*out = *in
diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go
index 713326833..4022cd369 100644
--- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go
+++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nfd_client.go
@@ -28,6 +28,10 @@ type FakeNfdV1alpha1 struct {
 	*testing.Fake
 }
 
+func (c *FakeNfdV1alpha1) NodeFeatures(namespace string) v1alpha1.NodeFeatureInterface {
+	return &FakeNodeFeatures{c, namespace}
+}
+
 func (c *FakeNfdV1alpha1) NodeFeatureRules() v1alpha1.NodeFeatureRuleInterface {
 	return &FakeNodeFeatureRules{c}
 }
diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeature.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeature.go
new file mode 100644
index 000000000..1986bf618
--- /dev/null
+++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeature.go
@@ -0,0 +1,130 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+	v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+)
+
+// FakeNodeFeatures implements NodeFeatureInterface
+type FakeNodeFeatures struct {
+	Fake *FakeNfdV1alpha1
+	ns   string
+}
+
+var nodefeaturesResource = schema.GroupVersionResource{Group: "nfd.k8s-sigs.io", Version: "v1alpha1", Resource: "nodefeatures"}
+
+var nodefeaturesKind = schema.GroupVersionKind{Group: "nfd.k8s-sigs.io", Version: "v1alpha1", Kind: "NodeFeature"}
+
+// Get takes name of the nodeFeature, and returns the corresponding nodeFeature object, and an error if there is any.
+func (c *FakeNodeFeatures) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.NodeFeature, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(nodefeaturesResource, c.ns, name), &v1alpha1.NodeFeature{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.NodeFeature), err
+}
+
+// List takes label and field selectors, and returns the list of NodeFeatures that match those selectors.
+func (c *FakeNodeFeatures) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.NodeFeatureList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(nodefeaturesResource, nodefeaturesKind, c.ns, opts), &v1alpha1.NodeFeatureList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.NodeFeatureList{ListMeta: obj.(*v1alpha1.NodeFeatureList).ListMeta}
+	for _, item := range obj.(*v1alpha1.NodeFeatureList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested nodeFeatures.
+func (c *FakeNodeFeatures) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(nodefeaturesResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a nodeFeature and creates it.  Returns the server's representation of the nodeFeature, and an error, if there is any.
+func (c *FakeNodeFeatures) Create(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.CreateOptions) (result *v1alpha1.NodeFeature, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(nodefeaturesResource, c.ns, nodeFeature), &v1alpha1.NodeFeature{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.NodeFeature), err
+}
+
+// Update takes the representation of a nodeFeature and updates it. Returns the server's representation of the nodeFeature, and an error, if there is any.
+func (c *FakeNodeFeatures) Update(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.UpdateOptions) (result *v1alpha1.NodeFeature, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(nodefeaturesResource, c.ns, nodeFeature), &v1alpha1.NodeFeature{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.NodeFeature), err
+}
+
+// Delete takes name of the nodeFeature and deletes it. Returns an error if one occurs.
+func (c *FakeNodeFeatures) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteActionWithOptions(nodefeaturesResource, c.ns, name, opts), &v1alpha1.NodeFeature{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeNodeFeatures) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(nodefeaturesResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.NodeFeatureList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched nodeFeature.
+func (c *FakeNodeFeatures) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeature, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(nodefeaturesResource, c.ns, name, pt, data, subresources...), &v1alpha1.NodeFeature{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.NodeFeature), err
+}
diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go
index 8807cd64d..cf392dd10 100644
--- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go
+++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/generated_expansion.go
@@ -18,4 +18,6 @@ limitations under the License.
 
 package v1alpha1
 
+type NodeFeatureExpansion interface{}
+
 type NodeFeatureRuleExpansion interface{}
diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go
index 034d5cbe7..8ecaf1998 100644
--- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go
+++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go
@@ -28,6 +28,7 @@ import (
 
 type NfdV1alpha1Interface interface {
 	RESTClient() rest.Interface
+	NodeFeaturesGetter
 	NodeFeatureRulesGetter
 }
 
@@ -36,6 +37,10 @@ type NfdV1alpha1Client struct {
 	restClient rest.Interface
 }
 
+func (c *NfdV1alpha1Client) NodeFeatures(namespace string) NodeFeatureInterface {
+	return newNodeFeatures(c, namespace)
+}
+
 func (c *NfdV1alpha1Client) NodeFeatureRules() NodeFeatureRuleInterface {
 	return newNodeFeatureRules(c)
 }
diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nodefeature.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nodefeature.go
new file mode 100644
index 000000000..bf6d9b175
--- /dev/null
+++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nodefeature.go
@@ -0,0 +1,178 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+	v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+	scheme "sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned/scheme"
+)
+
+// NodeFeaturesGetter has a method to return a NodeFeatureInterface.
+// A group's client should implement this interface.
+type NodeFeaturesGetter interface {
+	NodeFeatures(namespace string) NodeFeatureInterface
+}
+
+// NodeFeatureInterface has methods to work with NodeFeature resources.
+type NodeFeatureInterface interface {
+	Create(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.CreateOptions) (*v1alpha1.NodeFeature, error)
+	Update(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.UpdateOptions) (*v1alpha1.NodeFeature, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.NodeFeature, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.NodeFeatureList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeature, err error)
+	NodeFeatureExpansion
+}
+
+// nodeFeatures implements NodeFeatureInterface
+type nodeFeatures struct {
+	client rest.Interface
+	ns     string
+}
+
+// newNodeFeatures returns a NodeFeatures
+func newNodeFeatures(c *NfdV1alpha1Client, namespace string) *nodeFeatures {
+	return &nodeFeatures{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the nodeFeature, and returns the corresponding nodeFeature object, and an error if there is any.
+func (c *nodeFeatures) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.NodeFeature, err error) {
+	result = &v1alpha1.NodeFeature{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of NodeFeatures that match those selectors.
+func (c *nodeFeatures) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.NodeFeatureList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.NodeFeatureList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested nodeFeatures.
+func (c *nodeFeatures) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a nodeFeature and creates it.  Returns the server's representation of the nodeFeature, and an error, if there is any.
+func (c *nodeFeatures) Create(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.CreateOptions) (result *v1alpha1.NodeFeature, err error) {
+	result = &v1alpha1.NodeFeature{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(nodeFeature).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a nodeFeature and updates it. Returns the server's representation of the nodeFeature, and an error, if there is any.
+func (c *nodeFeatures) Update(ctx context.Context, nodeFeature *v1alpha1.NodeFeature, opts v1.UpdateOptions) (result *v1alpha1.NodeFeature, err error) {
+	result = &v1alpha1.NodeFeature{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		Name(nodeFeature.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(nodeFeature).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the nodeFeature and deletes it. Returns an error if one occurs.
+func (c *nodeFeatures) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *nodeFeatures) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched nodeFeature.
+func (c *nodeFeatures) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.NodeFeature, err error) {
+	result = &v1alpha1.NodeFeature{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("nodefeatures").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go
index 453de7f15..5c2b9eda3 100644
--- a/pkg/generated/informers/externalversions/generic.go
+++ b/pkg/generated/informers/externalversions/generic.go
@@ -53,6 +53,8 @@ func (f *genericInformer) Lister() cache.GenericLister {
 func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
 	switch resource {
 	// Group=nfd.k8s-sigs.io, Version=v1alpha1
+	case v1alpha1.SchemeGroupVersion.WithResource("nodefeatures"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatures().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("nodefeaturerules"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Nfd().V1alpha1().NodeFeatureRules().Informer()}, nil
 
diff --git a/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go b/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go
index 5f476d887..1cae0cfc2 100644
--- a/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go
+++ b/pkg/generated/informers/externalversions/nfd/v1alpha1/interface.go
@@ -24,6 +24,8 @@ import (
 
 // Interface provides access to all the informers in this group version.
 type Interface interface {
+	// NodeFeatures returns a NodeFeatureInformer.
+	NodeFeatures() NodeFeatureInformer
 	// NodeFeatureRules returns a NodeFeatureRuleInformer.
 	NodeFeatureRules() NodeFeatureRuleInformer
 }
@@ -39,6 +41,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
 }
 
+// NodeFeatures returns a NodeFeatureInformer.
+func (v *version) NodeFeatures() NodeFeatureInformer {
+	return &nodeFeatureInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
 // NodeFeatureRules returns a NodeFeatureRuleInformer.
 func (v *version) NodeFeatureRules() NodeFeatureRuleInformer {
 	return &nodeFeatureRuleInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/generated/informers/externalversions/nfd/v1alpha1/nodefeature.go b/pkg/generated/informers/externalversions/nfd/v1alpha1/nodefeature.go
new file mode 100644
index 000000000..20ea65f56
--- /dev/null
+++ b/pkg/generated/informers/externalversions/nfd/v1alpha1/nodefeature.go
@@ -0,0 +1,90 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	time "time"
+
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+	versioned "sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned"
+	internalinterfaces "sigs.k8s.io/node-feature-discovery/pkg/generated/informers/externalversions/internalinterfaces"
+	v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/generated/listers/nfd/v1alpha1"
+)
+
+// NodeFeatureInformer provides access to a shared informer and lister for
+// NodeFeatures.
+type NodeFeatureInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.NodeFeatureLister
+}
+
+type nodeFeatureInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewNodeFeatureInformer constructs a new informer for NodeFeature type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewNodeFeatureInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredNodeFeatureInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredNodeFeatureInformer constructs a new informer for NodeFeature type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredNodeFeatureInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.NfdV1alpha1().NodeFeatures(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.NfdV1alpha1().NodeFeatures(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&nfdv1alpha1.NodeFeature{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *nodeFeatureInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredNodeFeatureInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *nodeFeatureInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&nfdv1alpha1.NodeFeature{}, f.defaultInformer)
+}
+
+func (f *nodeFeatureInformer) Lister() v1alpha1.NodeFeatureLister {
+	return v1alpha1.NewNodeFeatureLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go b/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go
index a072a4f4a..5e6fc01e4 100644
--- a/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go
+++ b/pkg/generated/listers/nfd/v1alpha1/expansion_generated.go
@@ -18,6 +18,14 @@ limitations under the License.
 
 package v1alpha1
 
+// NodeFeatureListerExpansion allows custom methods to be added to
+// NodeFeatureLister.
+type NodeFeatureListerExpansion interface{}
+
+// NodeFeatureNamespaceListerExpansion allows custom methods to be added to
+// NodeFeatureNamespaceLister.
+type NodeFeatureNamespaceListerExpansion interface{}
+
 // NodeFeatureRuleListerExpansion allows custom methods to be added to
 // NodeFeatureRuleLister.
 type NodeFeatureRuleListerExpansion interface{}
diff --git a/pkg/generated/listers/nfd/v1alpha1/nodefeature.go b/pkg/generated/listers/nfd/v1alpha1/nodefeature.go
new file mode 100644
index 000000000..011dffd55
--- /dev/null
+++ b/pkg/generated/listers/nfd/v1alpha1/nodefeature.go
@@ -0,0 +1,99 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+	v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+)
+
+// NodeFeatureLister helps list NodeFeatures.
+// All objects returned here must be treated as read-only.
+type NodeFeatureLister interface {
+	// List lists all NodeFeatures in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error)
+	// NodeFeatures returns an object that can list and get NodeFeatures.
+	NodeFeatures(namespace string) NodeFeatureNamespaceLister
+	NodeFeatureListerExpansion
+}
+
+// nodeFeatureLister implements the NodeFeatureLister interface.
+type nodeFeatureLister struct {
+	indexer cache.Indexer
+}
+
+// NewNodeFeatureLister returns a new NodeFeatureLister.
+func NewNodeFeatureLister(indexer cache.Indexer) NodeFeatureLister {
+	return &nodeFeatureLister{indexer: indexer}
+}
+
+// List lists all NodeFeatures in the indexer.
+func (s *nodeFeatureLister) List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.NodeFeature))
+	})
+	return ret, err
+}
+
+// NodeFeatures returns an object that can list and get NodeFeatures.
+func (s *nodeFeatureLister) NodeFeatures(namespace string) NodeFeatureNamespaceLister {
+	return nodeFeatureNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// NodeFeatureNamespaceLister helps list and get NodeFeatures.
+// All objects returned here must be treated as read-only.
+type NodeFeatureNamespaceLister interface {
+	// List lists all NodeFeatures in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error)
+	// Get retrieves the NodeFeature from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*v1alpha1.NodeFeature, error)
+	NodeFeatureNamespaceListerExpansion
+}
+
+// nodeFeatureNamespaceLister implements the NodeFeatureNamespaceLister
+// interface.
+type nodeFeatureNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all NodeFeatures in the indexer for a given namespace.
+func (s nodeFeatureNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.NodeFeature, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.NodeFeature))
+	})
+	return ret, err
+}
+
+// Get retrieves the NodeFeature from the indexer for a given namespace and name.
+func (s nodeFeatureNamespaceLister) Get(name string) (*v1alpha1.NodeFeature, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("nodefeature"), name)
+	}
+	return obj.(*v1alpha1.NodeFeature), nil
+}
diff --git a/pkg/nfd-client/base.go b/pkg/nfd-client/base.go
index fa5e0b01a..c96553542 100644
--- a/pkg/nfd-client/base.go
+++ b/pkg/nfd-client/base.go
@@ -49,6 +49,7 @@ type Args struct {
 	CaFile             string
 	CertFile           string
 	KeyFile            string
+	Kubeconfig         string
 	Server             string
 	ServerNameOverride string
 
diff --git a/pkg/nfd-client/worker/nfd-worker.go b/pkg/nfd-client/worker/nfd-worker.go
index a7fdeb1f6..abc0fdb4a 100644
--- a/pkg/nfd-client/worker/nfd-worker.go
+++ b/pkg/nfd-client/worker/nfd-worker.go
@@ -27,10 +27,16 @@ import (
 	"time"
 
 	"golang.org/x/net/context"
+	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/util/validation"
 	"k8s.io/klog/v2"
 	"sigs.k8s.io/yaml"
 
+	apiequality "k8s.io/apimachinery/pkg/api/equality"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
+	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
+	nfdclient "sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned"
 	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"
@@ -76,9 +82,10 @@ type Labels map[string]string
 type Args struct {
 	clientcommon.Args
 
-	ConfigFile string
-	Oneshot    bool
-	Options    string
+	ConfigFile           string
+	EnableNodeFeatureApi bool
+	Oneshot              bool
+	Options              string
 
 	Klog      map[string]*utils.KlogFlagVal
 	Overrides ConfigOverrideArgs
@@ -101,6 +108,7 @@ type nfdWorker struct {
 	config              *NFDConfig
 	kubernetesNamespace string
 	grpcClient          pb.LabelerClient
+	nfdClient           *nfdclient.Clientset
 	stop                chan struct{} // channel for signaling stop
 	featureSources      []source.FeatureSource
 	labelSources        []source.LabelSource
@@ -150,6 +158,7 @@ func newDefaultConfig() *NFDConfig {
 func (w *nfdWorker) Run() error {
 	klog.Infof("Node Feature Discovery Worker %s", version.Get())
 	klog.Infof("NodeName: '%s'", clientcommon.NodeName())
+	klog.Infof("Kubernetes namespace: '%s'", w.kubernetesNamespace)
 
 	// Create watcher for config file and read initial configuration
 	configWatch, err := utils.CreateFsWatcher(time.Second, w.configFilePath)
@@ -185,9 +194,8 @@ func (w *nfdWorker) Run() error {
 
 			// Update the node with the feature labels.
 			if !w.config.Core.NoPublish {
-				err := w.advertiseFeatureLabels(labels)
-				if err != nil {
-					return fmt.Errorf("failed to advertise labels: %s", err.Error())
+				if err := w.advertiseFeatures(labels); err != nil {
+					return err
 				}
 			}
 
@@ -205,7 +213,7 @@ func (w *nfdWorker) Run() error {
 				return err
 			}
 			// Manage connection to master
-			if w.config.Core.NoPublish {
+			if w.config.Core.NoPublish || !w.args.EnableNodeFeatureApi {
 				w.GrpcDisconnect()
 			}
 
@@ -524,6 +532,22 @@ func getFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) (
 	return labels, nil
 }
 
+// advertiseFeatures advertises the features of a Kubernetes node
+func (w *nfdWorker) advertiseFeatures(labels Labels) error {
+	if w.args.EnableNodeFeatureApi {
+		// Create/update NodeFeature CR object
+		if err := w.updateNodeFeatureObject(labels); err != nil {
+			return fmt.Errorf("failed to advertise features (via CRD API): %w", err)
+		}
+	} else {
+		// Create/update feature labels through gRPC connection to nfd-master
+		if err := w.advertiseFeatureLabels(labels); err != nil {
+			return fmt.Errorf("failed to advertise features (via gRPC): %w", err)
+		}
+	}
+	return nil
+}
+
 // advertiseFeatureLabels advertises the feature labels to a Kubernetes node
 // via the NFD server.
 func (w *nfdWorker) advertiseFeatureLabels(labels Labels) error {
@@ -551,6 +575,85 @@ func (w *nfdWorker) advertiseFeatureLabels(labels Labels) error {
 	return nil
 }
 
+// updateNodeFeatureObject creates/updates the node-specific NodeFeature custom resource.
+func (m *nfdWorker) updateNodeFeatureObject(labels Labels) error {
+	cli, err := m.getNfdClient()
+	if err != nil {
+		return err
+	}
+	nodename := clientcommon.NodeName()
+	namespace := m.kubernetesNamespace
+
+	features := source.GetAllFeatures()
+
+	// TODO: we could implement some simple caching of the object, only get it
+	// every 10 minutes or so because nobody else should really be modifying it
+	if nfr, err := cli.NfdV1alpha1().NodeFeatures(namespace).Get(context.TODO(), nodename, metav1.GetOptions{}); errors.IsNotFound(err) {
+		klog.Infof("creating NodeFeature object %q", nodename)
+		nfr = &nfdv1alpha1.NodeFeature{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:        nodename,
+				Annotations: map[string]string{nfdv1alpha1.WorkerVersionAnnotation: version.Get()},
+				Labels:      map[string]string{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodename},
+			},
+			Spec: nfdv1alpha1.NodeFeatureSpec{
+				Features: *features,
+				Labels:   labels,
+			},
+		}
+
+		nfrCreated, err := cli.NfdV1alpha1().NodeFeatures(namespace).Create(context.TODO(), nfr, metav1.CreateOptions{})
+		if err != nil {
+			return fmt.Errorf("failed to create NodeFeature object %q: %w", nfr.Name, err)
+		}
+
+		utils.KlogDump(4, "NodeFeature object created:", "  ", nfrCreated)
+	} else if err != nil {
+		return fmt.Errorf("failed to get NodeFeature object: %w", err)
+	} else {
+
+		nfrUpdated := nfr.DeepCopy()
+		nfrUpdated.Annotations = map[string]string{nfdv1alpha1.WorkerVersionAnnotation: version.Get()}
+		nfrUpdated.Labels = map[string]string{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodename}
+		nfrUpdated.Spec = nfdv1alpha1.NodeFeatureSpec{
+			Features: *features,
+			Labels:   labels,
+		}
+
+		if !apiequality.Semantic.DeepEqual(nfr, nfrUpdated) {
+			klog.Infof("updating NodeFeature object %q", nodename)
+			nfrUpdated, err = cli.NfdV1alpha1().NodeFeatures(namespace).Update(context.TODO(), nfrUpdated, metav1.UpdateOptions{})
+			if err != nil {
+				return fmt.Errorf("failed to update NodeFeature object %q: %w", nfr.Name, err)
+			}
+			utils.KlogDump(4, "NodeFeature object updated:", "  ", nfrUpdated)
+		} else {
+			klog.V(1).Info("no changes in NodeFeature object, not updating")
+		}
+	}
+	return nil
+}
+
+// getNfdClient returns the clientset for using the nfd CRD api
+func (m *nfdWorker) getNfdClient() (*nfdclient.Clientset, error) {
+	if m.nfdClient != nil {
+		return m.nfdClient, nil
+	}
+
+	kubeconfig, err := apihelper.GetKubeconfig(m.args.Kubeconfig)
+	if err != nil {
+		return nil, err
+	}
+
+	c, err := nfdclient.NewForConfig(kubeconfig)
+	if err != nil {
+		return nil, err
+	}
+
+	m.nfdClient = c
+	return c, nil
+}
+
 // UnmarshalJSON implements the Unmarshaler interface from "encoding/json"
 func (d *duration) UnmarshalJSON(data []byte) error {
 	var v interface{}
diff --git a/pkg/nfd-master/nfd-api-controller.go b/pkg/nfd-master/nfd-api-controller.go
index 4a2acc46a..84fd7d524 100644
--- a/pkg/nfd-master/nfd-api-controller.go
+++ b/pkg/nfd-master/nfd-api-controller.go
@@ -32,42 +32,88 @@ import (
 )
 
 type nfdController struct {
-	ruleLister nfdlisters.NodeFeatureRuleLister
+	featureLister nfdlisters.NodeFeatureLister
+	ruleLister    nfdlisters.NodeFeatureRuleLister
 
 	stopChan chan struct{}
+
+	updateAllNodesChan chan struct{}
+	updateOneNodeChan  chan string
 }
 
-func newNfdController(config *restclient.Config) (*nfdController, error) {
+func newNfdController(config *restclient.Config, disableNodeFeature bool) (*nfdController, error) {
 	c := &nfdController{
-		stopChan: make(chan struct{}, 1),
+		stopChan:           make(chan struct{}, 1),
+		updateAllNodesChan: make(chan struct{}, 1),
+		updateOneNodeChan:  make(chan string),
 	}
 
 	nfdClient := nfdclientset.NewForConfigOrDie(config)
 
 	informerFactory := nfdinformers.NewSharedInformerFactory(nfdClient, 5*time.Minute)
+
+	// Add informer for NodeFeature objects
+	if !disableNodeFeature {
+		featureInformer := informerFactory.Nfd().V1alpha1().NodeFeatures()
+		if _, err := featureInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+			AddFunc: func(obj interface{}) {
+				key, _ := cache.MetaNamespaceKeyFunc(obj)
+				klog.V(2).Infof("NodeFeature %v added", key)
+				c.updateOneNode(obj)
+			},
+			UpdateFunc: func(oldObj, newObj interface{}) {
+				key, _ := cache.MetaNamespaceKeyFunc(newObj)
+				klog.V(2).Infof("NodeFeature %v updated", key)
+				c.updateOneNode(newObj)
+			},
+			DeleteFunc: func(obj interface{}) {
+				key, _ := cache.MetaNamespaceKeyFunc(obj)
+				klog.V(2).Infof("NodeFeature %v deleted", key)
+				c.updateOneNode(obj)
+			},
+		}); err != nil {
+			return nil, err
+		}
+		c.featureLister = featureInformer.Lister()
+	}
+
+	// Add informer for NodeFeatureRule objects
 	ruleInformer := informerFactory.Nfd().V1alpha1().NodeFeatureRules()
 	if _, err := ruleInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 		AddFunc: func(object interface{}) {
 			key, _ := cache.MetaNamespaceKeyFunc(object)
 			klog.V(2).Infof("NodeFeatureRule %v added", key)
+			if !disableNodeFeature {
+				c.updateAllNodes()
+			}
+			// else: rules will be processed only when gRPC requests are received
 		},
 		UpdateFunc: func(oldObject, newObject interface{}) {
 			key, _ := cache.MetaNamespaceKeyFunc(newObject)
 			klog.V(2).Infof("NodeFeatureRule %v updated", key)
+			if !disableNodeFeature {
+				c.updateAllNodes()
+			}
+			// else: rules will be processed only when gRPC requests are received
 		},
 		DeleteFunc: func(object interface{}) {
 			key, _ := cache.MetaNamespaceKeyFunc(object)
 			klog.V(2).Infof("NodeFeatureRule %v deleted", key)
+			if !disableNodeFeature {
+				c.updateAllNodes()
+			}
+			// else: rules will be processed only when gRPC requests are received
 		},
 	}); err != nil {
 		return nil, err
 	}
+	c.ruleLister = ruleInformer.Lister()
+
+	// Start informers
 	informerFactory.Start(c.stopChan)
 
 	utilruntime.Must(nfdv1alpha1.AddToScheme(nfdscheme.Scheme))
 
-	c.ruleLister = ruleInformer.Lister()
-
 	return c, nil
 }
 
@@ -77,3 +123,32 @@ func (c *nfdController) stop() {
 	default:
 	}
 }
+
+func (c *nfdController) updateOneNode(obj interface{}) {
+	o, ok := obj.(*nfdv1alpha1.NodeFeature)
+	if !ok {
+		klog.Errorf("not a NodeFeature object (but of type %T): %v", obj, obj)
+		return
+	}
+
+	nodeName, ok := o.Labels[nfdv1alpha1.NodeFeatureObjNodeNameLabel]
+	if !ok {
+		klog.Errorf("no node name for NodeFeature object %s/%s: %q label is missing",
+			o.Namespace, o.Name, nfdv1alpha1.NodeFeatureObjNodeNameLabel)
+		return
+	}
+	if nodeName == "" {
+		klog.Errorf("no node name for NodeFeature object %s/%s: %q label is empty",
+			o.Namespace, o.Name, nfdv1alpha1.NodeFeatureObjNodeNameLabel)
+		return
+	}
+
+	c.updateOneNodeChan <- nodeName
+}
+
+func (c *nfdController) updateAllNodes() {
+	select {
+	case c.updateAllNodesChan <- struct{}{}:
+	default:
+	}
+}
diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go
index db4f9f38e..afc9719e3 100644
--- a/pkg/nfd-master/nfd-master.go
+++ b/pkg/nfd-master/nfd-master.go
@@ -36,6 +36,7 @@ import (
 	"google.golang.org/grpc/health/grpc_health_v1"
 	"google.golang.org/grpc/peer"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	label "k8s.io/apimachinery/pkg/labels"
 	"k8s.io/client-go/kubernetes"
 	restclient "k8s.io/client-go/rest"
@@ -69,6 +70,7 @@ type Args struct {
 	Kubeconfig             string
 	LabelWhiteList         utils.RegexpVal
 	FeatureRulesController bool
+	EnableNodeFeatureApi   bool
 	NoPublish              bool
 	EnableTaints           bool
 	Port                   int
@@ -87,6 +89,7 @@ type nfdMaster struct {
 	*nfdController
 
 	args       Args
+	namespace  string
 	nodeName   string
 	server     *grpc.Server
 	stop       chan struct{}
@@ -98,9 +101,10 @@ type nfdMaster struct {
 // NewNfdMaster creates a new NfdMaster server instance.
 func NewNfdMaster(args *Args) (NfdMaster, error) {
 	nfd := &nfdMaster{args: *args,
-		nodeName: os.Getenv("NODE_NAME"),
-		ready:    make(chan bool, 1),
-		stop:     make(chan struct{}, 1),
+		nodeName:  os.Getenv("NODE_NAME"),
+		namespace: utils.GetKubernetesNamespace(),
+		ready:     make(chan bool, 1),
+		stop:      make(chan struct{}, 1),
 	}
 
 	if args.Instance != "" {
@@ -144,6 +148,7 @@ func (m *nfdMaster) Run() error {
 		klog.Infof("Master instance: %q", m.args.Instance)
 	}
 	klog.Infof("NodeName: %q", m.nodeName)
+	klog.Infof("Kubernetes namespace: %q", m.namespace)
 
 	if m.args.Prune {
 		return m.prune()
@@ -155,7 +160,7 @@ func (m *nfdMaster) Run() error {
 			return err
 		}
 		klog.Info("starting nfd api controller")
-		m.nfdController, err = newNfdController(kubeconfig)
+		m.nfdController, err = newNfdController(kubeconfig, !m.args.EnableNodeFeatureApi)
 		if err != nil {
 			return fmt.Errorf("failed to initialize CRD controller: %w", err)
 		}
@@ -170,7 +175,14 @@ func (m *nfdMaster) Run() error {
 
 	// Run gRPC server
 	grpcErr := make(chan error, 1)
-	go m.runGrpcServer(grpcErr)
+	if !m.args.EnableNodeFeatureApi {
+		go m.runGrpcServer(grpcErr)
+	}
+
+	// Run updater that handles events from the nfd CRD API.
+	if m.nfdController != nil {
+		go m.nfdAPIUpdateHandler()
+	}
 
 	// Notify that we're ready to accept connections
 	m.ready <- true
@@ -245,6 +257,22 @@ func (m *nfdMaster) runGrpcServer(errChan chan<- error) {
 	}
 }
 
+// nfdAPIUpdateHandler handles events from the nfd API controller.
+func (m *nfdMaster) nfdAPIUpdateHandler() {
+	for {
+		select {
+		case <-m.nfdController.updateAllNodesChan:
+			if err := m.nfdAPIUpdateAllNodes(); err != nil {
+				klog.Error(err)
+			}
+		case nodeName := <-m.nfdController.updateOneNodeChan:
+			if err := m.nfdAPIUpdateOneNode(nodeName); err != nil {
+				klog.Error(err)
+			}
+		}
+	}
+}
+
 // Stop NfdMaster
 func (m *nfdMaster) Stop() {
 	m.server.GracefulStop()
@@ -290,16 +318,9 @@ func (m *nfdMaster) prune() error {
 		klog.Infof("pruning node %q...", node.Name)
 
 		// Prune labels and extended resources
-		err := m.updateNodeFeatures(cli, node.Name, Labels{}, Annotations{}, ExtendedResources{})
+		err := m.updateNodeObject(cli, node.Name, Labels{}, Annotations{}, ExtendedResources{}, []corev1.Taint{})
 		if err != nil {
-			return fmt.Errorf("failed to prune labels from node %q: %v", node.Name, err)
-		}
-
-		// Prune taints
-		err = m.setTaints(cli, []corev1.Taint{}, node.Name)
-
-		if err != nil {
-			return fmt.Errorf("failed to prune taints from node %q: %v", node.Name, err)
+			return fmt.Errorf("failed to prune node %q: %v", node.Name, err)
 		}
 
 		// Prune annotations
@@ -421,20 +442,6 @@ func (m *nfdMaster) SetLabels(c context.Context, r *pb.SetLabelsRequest) (*pb.Se
 		klog.Infof("received labeling request for node %q", r.NodeName)
 	}
 
-	// Mix in CR-originated labels
-	rawLabels := make(map[string]string)
-	if r.Labels != nil {
-		// NOTE: we effectively mangle the request struct by not creating a deep copy of the map
-		rawLabels = r.Labels
-	}
-	crLabels, crTaints := m.processNodeFeatureRule(r)
-
-	for k, v := range crLabels {
-		rawLabels[k] = v
-	}
-
-	labels, extendedResources := filterFeatureLabels(rawLabels, m.args.ExtraLabelNs, m.args.LabelWhiteList.Regexp, m.args.ResourceLabels)
-
 	if !m.args.NoPublish {
 		cli, err := m.apihelper.GetClient()
 		if err != nil {
@@ -444,27 +451,124 @@ func (m *nfdMaster) SetLabels(c context.Context, r *pb.SetLabelsRequest) (*pb.Se
 		// Advertise NFD worker version as an annotation
 		annotations := Annotations{m.instanceAnnotation(nfdv1alpha1.WorkerVersionAnnotation): r.NfdVersion}
 
-		err = m.updateNodeFeatures(cli, r.NodeName, labels, annotations, extendedResources)
-		if err != nil {
-			klog.Errorf("failed to advertise labels: %v", err)
+		// Create labels et al
+		if err := m.refreshNodeFeatures(cli, r.NodeName, annotations, r.GetLabels(), r.GetFeatures()); err != nil {
 			return &pb.SetLabelsReply{}, err
 		}
+	}
+	return &pb.SetLabelsReply{}, nil
+}
+
+func (m *nfdMaster) nfdAPIUpdateAllNodes() error {
+	klog.Info("will process all nodes in the cluster")
+
+	cli, err := m.apihelper.GetClient()
+	if err != nil {
+		return err
+	}
 
-		// set taints
-		var taints []corev1.Taint
-		if m.args.EnableTaints {
-			taints = crTaints
+	nodes, err := m.apihelper.GetNodes(cli)
+	if err != nil {
+		return err
+	}
+
+	for _, node := range nodes.Items {
+		if err := m.nfdAPIUpdateOneNode(node.Name); err != nil {
+			return err
 		}
+	}
 
-		// Call setTaints even though the feature flag is disabled. This
-		// ensures that we delete NFD owned stale taints when flag got
-		// turned off.
-		err = m.setTaints(cli, taints, r.NodeName)
-		if err != nil {
-			return &pb.SetLabelsReply{}, err
+	return nil
+}
+
+func (m *nfdMaster) nfdAPIUpdateOneNode(nodeName string) error {
+	sel := labels.SelectorFromSet(labels.Set{nfdv1alpha1.NodeFeatureObjNodeNameLabel: nodeName})
+	objs, err := m.nfdController.featureLister.List(sel)
+	if len(objs) == 0 {
+		klog.Infof("no NodeFeature object exists for node %q, skipping...", nodeName)
+		return nil
+	} else if err != nil {
+		return fmt.Errorf("failed to get NodeFeature resources for node %q: %w", nodeName, err)
+	}
+
+	// Sort our objects
+	sort.Slice(objs, func(i, j int) bool {
+		// Objects in our nfd namespace gets into the beginning of the list
+		if objs[i].Namespace == m.namespace && objs[j].Namespace != m.namespace {
+			return true
 		}
+		if objs[i].Namespace != m.namespace && objs[j].Namespace == m.namespace {
+			return false
+		}
+		// After the nfd namespace, sort objects by their name
+		if objs[i].Name != objs[j].Name {
+			return objs[i].Name < objs[j].Name
+		}
+		// Objects with the same name are sorted by their namespace
+		return objs[i].Namespace < objs[j].Namespace
+	})
+
+	if m.args.NoPublish {
+		return nil
 	}
-	return &pb.SetLabelsReply{}, nil
+
+	klog.V(1).Infof("processing node %q, initiated by NodeFeature API", nodeName)
+
+	// Merge in features
+	//
+	// TODO: support multiple NodeFeature objects. There are two obvious options to implement this:
+	// 1. Merge features of all objects into one joint object
+	// 2. Change the rule api to support handle multiple objects
+	// Of these #2 would probably perform better with lot less data to copy. We
+	// could probably even get rid of the DeepCopy in this scenario.
+	features := objs[0].DeepCopy()
+
+	annotations := Annotations{}
+	if objs[0].Namespace == m.namespace && objs[0].Name == nodeName {
+		// This is the one created by nfd-worker
+		if v := objs[0].Annotations[nfdv1alpha1.WorkerVersionAnnotation]; v != "" {
+			annotations[nfdv1alpha1.WorkerVersionAnnotation] = v
+		}
+	}
+
+	// Create labels et al
+	cli, err := m.apihelper.GetClient()
+	if err != nil {
+		return err
+	}
+	if err := m.refreshNodeFeatures(cli, nodeName, annotations, features.Spec.Labels, &features.Spec.Features); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *nfdMaster) refreshNodeFeatures(cli *kubernetes.Clientset, nodeName string, annotations, labels map[string]string, features *nfdv1alpha1.Features) error {
+	if labels == nil {
+		labels = make(map[string]string)
+	}
+
+	crLabels, crTaints := m.processNodeFeatureRule(features)
+
+	// Mix in CR-originated labels
+	for k, v := range crLabels {
+		labels[k] = v
+	}
+
+	labels, extendedResources := filterFeatureLabels(labels, m.args.ExtraLabelNs, m.args.LabelWhiteList.Regexp, m.args.ResourceLabels)
+
+	var taints []corev1.Taint
+	if m.args.EnableTaints {
+		taints = crTaints
+	}
+
+	err := m.updateNodeObject(cli, nodeName, labels, annotations, extendedResources, taints)
+	if err != nil {
+		klog.Errorf("failed to update node %q: %v", nodeName, err)
+		return err
+	}
+
+	return nil
 }
 
 // setTaints sets node taints and annotations based on the taints passed via
@@ -572,7 +676,7 @@ func authorizeClient(c context.Context, checkNodeName bool, nodeName string) err
 	return nil
 }
 
-func (m *nfdMaster) processNodeFeatureRule(r *pb.SetLabelsRequest) (map[string]string, []corev1.Taint) {
+func (m *nfdMaster) processNodeFeatureRule(features *nfdv1alpha1.Features) (map[string]string, []corev1.Taint) {
 	if m.nfdController == nil {
 		return nil, nil
 	}
@@ -589,9 +693,6 @@ func (m *nfdMaster) processNodeFeatureRule(r *pb.SetLabelsRequest) (map[string]s
 		return nil, nil
 	}
 
-	// Helper struct for rule processing
-	features := r.GetFeatures()
-
 	// Process all rule CRs
 	for _, spec := range ruleSpecs {
 		switch {
@@ -621,10 +722,10 @@ func (m *nfdMaster) processNodeFeatureRule(r *pb.SetLabelsRequest) (map[string]s
 	return labels, taints
 }
 
-// updateNodeFeatures ensures the Kubernetes node object is up to date,
+// updateNodeObject ensures the Kubernetes node object is up to date,
 // creating new labels and extended resources where necessary and removing
 // outdated ones. Also updates the corresponding annotations.
-func (m *nfdMaster) updateNodeFeatures(cli *kubernetes.Clientset, nodeName string, labels Labels, annotations Annotations, extendedResources ExtendedResources) error {
+func (m *nfdMaster) updateNodeObject(cli *kubernetes.Clientset, nodeName string, labels Labels, annotations Annotations, extendedResources ExtendedResources, taints []corev1.Taint) error {
 	if cli == nil {
 		return fmt.Errorf("no client is passed, client:  %v", cli)
 	}
@@ -677,6 +778,12 @@ func (m *nfdMaster) updateNodeFeatures(cli *kubernetes.Clientset, nodeName strin
 		klog.V(1).Infof("no updates to node %q", nodeName)
 	}
 
+	// Set taints
+	err = m.setTaints(cli, taints, node.Name)
+	if err != nil {
+		return err
+	}
+
 	return err
 }
 


Test patch:
diff --git a/pkg/nfd-master/nfd-master-internal_test.go b/pkg/nfd-master/nfd-master-internal_test.go
index fc522daec..cf50e4906 100644
--- a/pkg/nfd-master/nfd-master-internal_test.go
+++ b/pkg/nfd-master/nfd-master-internal_test.go
@@ -61,7 +61,7 @@ func newMockMaster(apihelper apihelper.APIHelpers) *nfdMaster {
 	}
 }
 
-func TestUpdateNodeFeatures(t *testing.T) {
+func TestUpdateNodeObject(t *testing.T) {
 	Convey("When I update the node using fake client", t, func() {
 		fakeFeatureLabels := map[string]string{
 			nfdv1alpha1.FeatureLabelNs + "/source-feature.1": "1",
@@ -112,10 +112,10 @@ func TestUpdateNodeFeatures(t *testing.T) {
 			}
 
 			mockAPIHelper.On("GetClient").Return(mockClient, nil)
-			mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Once()
+			mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Twice()
 			mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(metadataPatches))).Return(nil)
 			mockAPIHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(statusPatches))).Return(nil)
-			err := mockMaster.updateNodeFeatures(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources)
+			err := mockMaster.updateNodeObject(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil)
 
 			Convey("Error is nil", func() {
 				So(err, ShouldBeNil)
@@ -125,7 +125,7 @@ func TestUpdateNodeFeatures(t *testing.T) {
 		Convey("When I fail to update the node with feature labels", func() {
 			expectedError := fmt.Errorf("no client is passed, client:  <nil>")
 			mockAPIHelper.On("GetClient").Return(nil, expectedError)
-			err := mockMaster.updateNodeFeatures(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources)
+			err := mockMaster.updateNodeObject(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil)
 
 			Convey("Error is produced", func() {
 				So(err, ShouldResemble, expectedError)
@@ -135,7 +135,7 @@ func TestUpdateNodeFeatures(t *testing.T) {
 		Convey("When I fail to get a mock client while updating feature labels", func() {
 			expectedError := fmt.Errorf("no client is passed, client:  <nil>")
 			mockAPIHelper.On("GetClient").Return(nil, expectedError)
-			err := mockMaster.updateNodeFeatures(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources)
+			err := mockMaster.updateNodeObject(nil, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil)
 
 			Convey("Error is produced", func() {
 				So(err, ShouldResemble, expectedError)
@@ -145,8 +145,8 @@ func TestUpdateNodeFeatures(t *testing.T) {
 		Convey("When I fail to get a mock node while updating feature labels", func() {
 			expectedError := errors.New("fake error")
 			mockAPIHelper.On("GetClient").Return(mockClient, nil)
-			mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(nil, expectedError).Once()
-			err := mockMaster.updateNodeFeatures(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources)
+			mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(nil, expectedError).Twice()
+			err := mockMaster.updateNodeObject(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil)
 
 			Convey("Error is produced", func() {
 				So(err, ShouldEqual, expectedError)
@@ -156,9 +156,9 @@ func TestUpdateNodeFeatures(t *testing.T) {
 		Convey("When I fail to update a mock node while updating feature labels", func() {
 			expectedError := errors.New("fake error")
 			mockAPIHelper.On("GetClient").Return(mockClient, nil)
-			mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Once()
-			mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.Anything).Return(expectedError).Once()
-			err := mockMaster.updateNodeFeatures(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources)
+			mockAPIHelper.On("GetNode", mockClient, mockNodeName).Return(mockNode, nil).Twice()
+			mockAPIHelper.On("PatchNode", mockClient, mockNodeName, mock.Anything).Return(expectedError).Twice()
+			err := mockMaster.updateNodeObject(mockClient, mockNodeName, fakeFeatureLabels, fakeAnnotations, fakeExtResources, nil)
 
 			Convey("Error is produced", func() {
 				So(err.Error(), ShouldEndWith, expectedError.Error())
@@ -294,7 +294,7 @@ func TestRemovingExtResources(t *testing.T) {
 
 func TestSetLabels(t *testing.T) {
 	Convey("When servicing SetLabels request", t, func() {
-		const workerName = "mock-worker"
+		const workerName = mockNodeName
 		const workerVer = "0.1-test"
 		mockHelper := &apihelper.MockAPIHelpers{}
 		mockMaster := newMockMaster(mockHelper)
@@ -324,7 +324,7 @@ func TestSetLabels(t *testing.T) {
 			}
 
 			mockHelper.On("GetClient").Return(mockClient, nil)
-			mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
+			mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil).Twice()
 			mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
 			mockHelper.On("PatchNodeStatus", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedStatusPatches))).Return(nil)
 			_, err := mockMaster.SetLabels(mockCtx, mockReq)