Repository: kubernetes-sigs/node-feature-discovery
Pull request #1024 (2023-01-04 11:27:12)

Add NRT garbage collector
Fixes: https://github.com/kubernetes-sigs/node-feature-discovery/issues/928

I am still missing RBAC configuration. Also I am wondering whether this garbage collector should only be started when some flag is set to `true`. Let me know what you think.


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


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 1da2b8529dffda18535d8525d607fd2e551e67cf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kubernetes-sigs-nfd/deploys/63b56292b410ad000b661cd3 |
k8s-ci-robot: Welcome @PiotrProkop! <br><br>It looks like this is your first PR to <a href='https://github.com/kubernetes-sigs/node-feature-discovery'>kubernetes-sigs/node-feature-discovery</a> 🎉. Please refer to our [pull request process documentation](https://git.k8s.io/community/contributors/guide/pull-requests.md) to help your PR have a smooth ride to approval. <br><br>You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. [Here is the bot commands documentation](https://go.k8s.io/bot-commands). <br><br>You can also check if kubernetes-sigs/node-feature-discovery has [its own contribution guidelines](https://github.com/kubernetes-sigs/node-feature-discovery/tree/master/CONTRIBUTING.md). <br><br>You may want to refer to our [testing guide](https://git.k8s.io/community/contributors/devel/sig-testing/testing.md) if you run into trouble with your tests not passing. <br><br>If you are having difficulty getting your pull request seen, please follow the [recommended escalation practices](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#why-is-my-pull-request-not-getting-reviewed). Also, for tips and tricks in the contribution process you may want to read the [Kubernetes contributor cheat sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md). We want to make sure your contribution gets all the attention it needs! <br><br>Thank you, and welcome to Kubernetes. :smiley:
k8s-ci-robot: Hi @PiotrProkop. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fnode-feature-discovery).

<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.
</details>

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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/node-feature-discovery/pull/1024#" title="Author self-approved">PiotrProkop</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign marquiz for approval by writing `/assign @marquiz` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["marquiz"]} -->
eero-t: >     * I concur the GC should be enabled only if a flag is given - note: if we move the GC to its own tiny server we get this for free
>     * and probably the resync loop interval as well should be tunable using an option

Another option would be running GC only e.g. at NFD master startup & exit.
 
>     * we may want to add, perhaps in the future, a check if a NRT is orphaned, meaning there is no daemon (as in daemonset) for a given node. So even if we node is still here, we may want to delete that NRT (this may be governed by another option)


fromanirh: > > ```
> > * I concur the GC should be enabled only if a flag is given - note: if we move the GC to its own tiny server we get this for free
> > * and probably the resync loop interval as well should be tunable using an option
> > ```
> 
> Another option would be running GC only e.g. at NFD master startup & exit.

Designwise I have no objections, but in practical terms AFAIU nfd-master is supposed to stay running for very long periods, possibly forever (bar bugs/upgrades), and in this case the actual GC will happen very seldomly.
fromanirh: > Thanks for implementing this! very initial pass, only shallow review.
> 
> Some open questions for discussion, I don't have strong opinions here yet
> 
>     * should this be (yet another) separate tiny component or do we want to merge this on nfd-master?

Or, we can add this option to nfd-topology-updater, and add manifests to reuse this binary as GC. It can work like this:

1. we add code to handle GC, but instead of calling it into nfd-master, we call into nfd-topology-updater
2. by default, the GC code is disabled
3. nfd-topology-updater becomes EITHER updater (= like today) or does GC
4. we add new manifests to run nfd-topology-updater as GC agent

benefits:

1. all the NRT handling logic is neatly included into nfd-topology-updater, no spillover on nfd-master
2. very clear when GC is enabled: only if you run `nfd-topology-updater --gc-enabled --gc-period 5m`
3. very clear separation of concerns
4. no new images (but this is also true for nfd-master integration)

@PiotrProkop @marquiz WDYT?
**PLEASE NOTE** I don't really have anything against the current direction and use nfd-master, so I'm not against it. I feel the proposed approach is a little cleaner though.
eero-t: I'm also in favor of moving this to `nfd-topology-updater`.

> > Another option would be running GC only e.g. at NFD master startup & exit.
> 
> Designwise I have no objections, but in practical terms AFAIU nfd-master is supposed to stay running for very long periods, possibly forever (bar bugs/upgrades), and in this case the actual GC will happen very seldomly.

One more option would have been doing GC only when notified, but to my amazement, k8s still does not have an API for that, despite 6 years of discussions:
* https://github.com/kubernetes/kubernetes/issues/24957
* https://github.com/kubernetes/enhancements/issues/1977
* https://github.com/kubernetes/enhancements/pull/1995
PiotrProkop: Ok, I'll move `nrtGC` to `nfd-topology-updater`, it can be enabled when started with:
```
$ nfd-topology-updater --gc-enabled
```
I think we should also expose the informer's `defaultResync` period but I can' think of a good name for it `gc-period` will be misleading, because removing old NRTs happens on startup and then `nrtGC` is listening for `Node` deletion events and removes corresponding NRT object. 
k8s-ci-robot: @PiotrProkop: 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-build-image-generic | bf964212a883687dc9ba182ae1c87a05a77e1c66 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-build-image-generic/1610947931437797376) | true | `/test pull-node-feature-discovery-build-image-generic`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=1024). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3APiotrProkop). 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 -->
k8s-ci-robot: @PiotrProkop: The following tests **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-build-image-generic | bf964212a883687dc9ba182ae1c87a05a77e1c66 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-build-image-generic/1610947931437797376) | true | `/test pull-node-feature-discovery-build-image-generic`
pull-node-feature-discovery-build-image-cross-generic | bf964212a883687dc9ba182ae1c87a05a77e1c66 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-build-image-cross-generic/1610947931483934720) | true | `/test pull-node-feature-discovery-build-image-cross-generic`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=1024). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3APiotrProkop). 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 -->
k8s-ci-robot: @PiotrProkop: The following tests **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-build-image-generic | bf964212a883687dc9ba182ae1c87a05a77e1c66 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-build-image-generic/1610947931437797376) | true | `/test pull-node-feature-discovery-build-image-generic`
pull-node-feature-discovery-build-image-cross-generic | bf964212a883687dc9ba182ae1c87a05a77e1c66 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-build-image-cross-generic/1610947931483934720) | true | `/test pull-node-feature-discovery-build-image-cross-generic`
pull-node-feature-discovery-verify-master | bf964212a883687dc9ba182ae1c87a05a77e1c66 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-verify-master/1610947931542654976) | 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=1024). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3APiotrProkop). 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 -->
PiotrProkop: @marquiz Thanks for the review! For now I have move this GC to `nfd-topology-updater` and have it enabled via `gc-enabled` flag(I have also added proper yaml manifest to deploy it). I can move it to seperate binary if necessary. 
I have also added triggering `gc` based on `gc-period` flag. In my testing on kind cluster when I removed node by just simply running:
```
$ kubect delete node worker`
```
`topology-updater` was still alive for some period of time and recreated NRT custom resources, so this periodic trigger should mitigate this in real clusters 😄 

k8s-ci-robot: @PiotrProkop: The following tests **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-build-image-cross-generic | bf964212a883687dc9ba182ae1c87a05a77e1c66 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-build-image-cross-generic/1610947931483934720) | true | `/test pull-node-feature-discovery-build-image-cross-generic`
pull-node-feature-discovery-verify-master | 59d50c1c4a84057a2a9c8fef08ee23bb7c9a187c | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-verify-master/1610952031147659264) | 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=1024). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3APiotrProkop). 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: One more note @PiotrProkop, we need to add Helm deployment files, too, before merging this
PiotrProkop: @marquiz no problem, I'll try to do it early next week.
marquiz: > @marquiz no problem, I'll try to do it early next week.

Sounds good.

Oh, and I think we want to have minima documentation for that, too. We could have it as just a separate sub-section under topology-updater in `docs/get-started/introduction.md` and `docs/usage/nfd-topology-updater.md`. For completeness, also cmdline reference would be nice (copy-paste-edit `docs/reference/topology-updater-commandline-reference.md` should be easy enough)
PiotrProkop: @marquiz @ffromani I have move `nrtGC` to standalone binary, modified kustomize and helm. I am only missing docs but will add them after review 😄 
ffromani: > @marquiz @ffromani I have move `nrtGC` to standalone binary, modified kustomize and helm. I am only missing docs but will add them after review smile

sounds good to me. This work is high on my review list, will get to this shortly.
marquiz: One question of the deployment. Do we want to have `topology-gc` as a separate overlay in kustomize (and an independent setting in the Helm chart) or should it be always deployed with topology-updater (with possible opt-out setting in Helm? Just asking, you know better @PiotrProkop and @ffromani 😊
ffromani: > One question of the deployment. Do we want to have `topology-gc` as a separate overlay in kustomize (and an independent setting in the Helm chart) or should it be always deployed with topology-updater (with possible opt-out setting in Helm? Just asking, you know better @PiotrProkop and @ffromani blush

regarding deployment, I'm still gathering thoughts, but for the sake of the UX I think it's slightly better to always deploy the GC and let the users opt-out. This is because this seems the best default configuration, the opposite variant seems a bit less friendly. Im open for discussion though.
PiotrProkop: @marquiz and @ffromani thanks for review. I tried to address all comments. As for deployment I agree that bundling `topology-gc` with `topology-updater` is more friendly for end users, so I have implemented that in the newest patch.
PiotrProkop: @marquiz all comments resolved. Thanks!
k8s-ci-robot: LGTM label has been added.  <details>Git tree hash: a6249fa59f772901471a95774e535d7700e0f786</details>
marquiz: We still need the docs 😅

And one more thing I started to ponder about the Helm deployment. It would be nice for the Helm to also deploy topology-gc by default (if topology-updater is enabled). Probably the nicest/easiest way to do this would be to simply deploy topology-gc only if topology-updater is enabled, too. That is, deploy if `topologyUpdater.enabled==true && topologyGC.enabled==true`. This would prevent stand-alone deployment of topology-gc (i.e. without topology-updater) but I don't think that would be an interesting use case in any practical scenario. I think otherwise it gets a bit hairier with the `{{ if }}` statements without much  practical benefit. WDYT?
PiotrProkop: @marquiz I remember about docs 😄 I think it is a sane default to deploy `topologyGC` only if `topologyUpdater` is enabled. We can always change it in the future to something more sophisticated if someone raise issue about that. I'll change helm chart, add docs and apply @ffromani review right now. 
k8s-ci-robot: New changes are detected. LGTM label has been removed.
k8s-ci-robot: @PiotrProkop: 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-docs-master | daf84403e89d86536ecac19af06280903daad4be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1024/pull-node-feature-discovery-verify-docs-master/1612854561821691904) | true | `/test pull-node-feature-discovery-verify-docs-master`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=1024). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3APiotrProkop). 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 -->
PiotrProkop: @marquiz @ffromani docs added and review applied.

Patch:
diff --git a/cmd/nfd-topology-gc/main.go b/cmd/nfd-topology-gc/main.go
new file mode 100644
index 000000000..ac66bb3b0
--- /dev/null
+++ b/cmd/nfd-topology-gc/main.go
@@ -0,0 +1,88 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"time"
+
+	"k8s.io/klog/v2"
+
+	nfdtopologygarbagecollector "sigs.k8s.io/node-feature-discovery/pkg/nfd-topology-gc"
+	"sigs.k8s.io/node-feature-discovery/pkg/version"
+)
+
+const (
+	// ProgramName is the canonical name of this program
+	ProgramName = "nfd-topology-gc"
+)
+
+func main() {
+	flags := flag.NewFlagSet(ProgramName, flag.ExitOnError)
+
+	printVersion := flags.Bool("version", false, "Print version and exit.")
+
+	args := parseArgs(flags, os.Args[1:]...)
+
+	if *printVersion {
+		fmt.Println(ProgramName, version.Get())
+		os.Exit(0)
+	}
+
+	// Assert that the version is known
+	if version.Undefined() {
+		klog.Warningf("version not set! Set -ldflags \"-X sigs.k8s.io/node-feature-discovery/pkg/version.version=`git describe --tags --dirty --always`\" during build or run.")
+	}
+
+	// Get new TopologyGC instance
+	gc, err := nfdtopologygarbagecollector.New(args)
+	if err != nil {
+		klog.Exit(err)
+	}
+
+	if err = gc.Run(); err != nil {
+		klog.Exit(err)
+	}
+}
+
+func parseArgs(flags *flag.FlagSet, osArgs ...string) *nfdtopologygarbagecollector.Args {
+	args := initFlags(flags)
+
+	_ = flags.Parse(osArgs)
+	if len(flags.Args()) > 0 {
+		fmt.Fprintf(flags.Output(), "unknown command line argument: %s\n", flags.Args()[0])
+		flags.Usage()
+		os.Exit(2)
+	}
+
+	return args
+}
+
+func initFlags(flagset *flag.FlagSet) *nfdtopologygarbagecollector.Args {
+	args := &nfdtopologygarbagecollector.Args{}
+
+	flagset.DurationVar(&args.GCPeriod, "gc-interval", time.Duration(1)*time.Hour,
+		"Interval between which Garbage Collector will try to cleanup any missed but already obsolete NodeResourceTopology. [Default: 1h]")
+	flagset.StringVar(&args.Kubeconfig, "kubeconfig", "",
+		"Kubeconfig to use")
+
+	klog.InitFlags(flagset)
+
+	return args
+}
diff --git a/deployment/base/rbac-topology-gc/kustomization.yaml b/deployment/base/rbac-topology-gc/kustomization.yaml
new file mode 100644
index 000000000..d0105ebc0
--- /dev/null
+++ b/deployment/base/rbac-topology-gc/kustomization.yaml
@@ -0,0 +1,9 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+namespace: node-feature-discovery
+
+resources:
+- topology-gc-clusterrole.yaml
+- topology-gc-clusterrolebinding.yaml
+- topology-gc-serviceaccount.yaml
diff --git a/deployment/base/rbac-topology-gc/topology-gc-clusterrole.yaml b/deployment/base/rbac-topology-gc/topology-gc-clusterrole.yaml
new file mode 100644
index 000000000..c0f431444
--- /dev/null
+++ b/deployment/base/rbac-topology-gc/topology-gc-clusterrole.yaml
@@ -0,0 +1,25 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: nfd-topology-gc
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - nodes
+  verbs:
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - nodes/proxy
+  verbs:
+  - get
+- apiGroups:
+  - topology.node.k8s.io
+  resources:
+  - noderesourcetopologies
+  verbs:
+  - delete
+  - list
diff --git a/deployment/base/rbac-topology-gc/topology-gc-clusterrolebinding.yaml b/deployment/base/rbac-topology-gc/topology-gc-clusterrolebinding.yaml
new file mode 100644
index 000000000..b8615d63c
--- /dev/null
+++ b/deployment/base/rbac-topology-gc/topology-gc-clusterrolebinding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: nfd-topology-gc
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: nfd-topology-gc
+subjects:
+- kind: ServiceAccount
+  name: nfd-topology-gc
+  namespace: default
diff --git a/deployment/base/rbac-topology-gc/topology-gc-serviceaccount.yaml b/deployment/base/rbac-topology-gc/topology-gc-serviceaccount.yaml
new file mode 100644
index 000000000..e56f7bbef
--- /dev/null
+++ b/deployment/base/rbac-topology-gc/topology-gc-serviceaccount.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: nfd-topology-gc
diff --git a/deployment/base/topology-gc/kustomization.yaml b/deployment/base/topology-gc/kustomization.yaml
new file mode 100644
index 000000000..3d8da69b6
--- /dev/null
+++ b/deployment/base/topology-gc/kustomization.yaml
@@ -0,0 +1,7 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+namespace: node-feature-discovery
+
+resources:
+- topology-gc.yaml
diff --git a/deployment/base/topology-gc/topology-gc.yaml b/deployment/base/topology-gc/topology-gc.yaml
new file mode 100644
index 000000000..07565e3a0
--- /dev/null
+++ b/deployment/base/topology-gc/topology-gc.yaml
@@ -0,0 +1,23 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: nfd
+  name: nfd-topology-gc
+spec:
+  selector:
+    matchLabels:
+      app: nfd-topology-gc
+  template:
+    metadata:
+      labels:
+        app: nfd-topology-gc
+    spec:
+      dnsPolicy: ClusterFirstWithHostNet
+      serviceAccount: nfd-topology-gc
+      containers:
+        - name: nfd-topology-gc
+          image: gcr.io/k8s-staging-nfd/node-feature-discovery:master
+          imagePullPolicy: Always
+          command:
+            - "nfd-topology-gc"
diff --git a/deployment/helm/node-feature-discovery/templates/_helpers.tpl b/deployment/helm/node-feature-discovery/templates/_helpers.tpl
index 39c1e3df7..5a0a5c97f 100644
--- a/deployment/helm/node-feature-discovery/templates/_helpers.tpl
+++ b/deployment/helm/node-feature-discovery/templates/_helpers.tpl
@@ -94,3 +94,14 @@ Create the name of the service account which topologyUpdater will use
     {{ default "default" .Values.topologyUpdater.serviceAccount.name }}
 {{- end -}}
 {{- end -}}
+
+{{/*
+Create the name of the service account which topologyGC will use
+*/}}
+{{- define "node-feature-discovery.topologyGC.serviceAccountName" -}}
+{{- if .Values.topologyGC.serviceAccount.create -}}
+    {{ default (printf "%s-topology-gc" (include "node-feature-discovery.fullname" .)) .Values.topologyGC.serviceAccount.name }}
+{{- else -}}
+    {{ default "default" .Values.topologyGC.serviceAccount.name }}
+{{- end -}}
+{{- end -}}
diff --git a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
index 3dd6f6f3b..a282df376 100644
--- a/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
+++ b/deployment/helm/node-feature-discovery/templates/clusterrole.yaml
@@ -66,3 +66,34 @@ rules:
   - get
   - update
 {{- end }}
+
+---
+{{- if and .Values.topologyGC.enable .Values.topologyGC.rbac.create .Values.topologyUpdater.enable }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: {{ include "node-feature-discovery.fullname" . }}-topology-gc
+  labels:
+    {{- include "node-feature-discovery.labels" . | nindent 4 }}
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - nodes
+  verbs:
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - nodes/proxy
+  verbs:
+  - get
+- apiGroups:
+  - topology.node.k8s.io
+  resources:
+  - noderesourcetopologies
+  verbs:
+  - delete
+  - list
+{{- end }}
diff --git a/deployment/helm/node-feature-discovery/templates/clusterrolebinding.yaml b/deployment/helm/node-feature-discovery/templates/clusterrolebinding.yaml
index 5bceb41e7..227bce5e5 100644
--- a/deployment/helm/node-feature-discovery/templates/clusterrolebinding.yaml
+++ b/deployment/helm/node-feature-discovery/templates/clusterrolebinding.yaml
@@ -32,3 +32,21 @@ subjects:
   name: {{ include "node-feature-discovery.topologyUpdater.serviceAccountName" . }}
   namespace: {{ include "node-feature-discovery.namespace" .  }}
 {{- end }}
+
+---
+{{- if and .Values.topologyGC.enable .Values.topologyGC.rbac.create .Values.topologyUpdater.enable }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: {{ include "node-feature-discovery.fullname" . }}-topology-gc
+  labels:
+    {{- include "node-feature-discovery.labels" . | nindent 4 }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: {{ include "node-feature-discovery.fullname" . }}-topology-gc
+subjects:
+- kind: ServiceAccount
+  name: {{ .Values.topologyGC.serviceAccount.name | default "nfd-topology-gc" }}
+  namespace: {{ include "node-feature-discovery.namespace" .  }}
+{{- end }}
diff --git a/deployment/helm/node-feature-discovery/templates/serviceaccount.yaml b/deployment/helm/node-feature-discovery/templates/serviceaccount.yaml
index 883e5daab..022961e45 100644
--- a/deployment/helm/node-feature-discovery/templates/serviceaccount.yaml
+++ b/deployment/helm/node-feature-discovery/templates/serviceaccount.yaml
@@ -27,6 +27,21 @@ metadata:
   {{- end }}
 {{- end }}
 
+---
+{{- if and .Values.topologyGC.enable .Values.topologyGC.serviceAccount.create .Values.topologyUpdater.enable }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ .Values.topologyGC.serviceAccount.name | default "nfd-topology-gc" }}
+  namespace: {{ include "node-feature-discovery.namespace" . }}
+  labels:
+    {{- include "node-feature-discovery.labels" . | nindent 4 }}
+  {{- with .Values.topologyUpdater.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+{{- end }}
+
 ---
 {{- if .Values.worker.serviceAccount.create }}
 apiVersion: v1
diff --git a/deployment/helm/node-feature-discovery/templates/topology-gc.yaml b/deployment/helm/node-feature-discovery/templates/topology-gc.yaml
new file mode 100644
index 000000000..642fec455
--- /dev/null
+++ b/deployment/helm/node-feature-discovery/templates/topology-gc.yaml
@@ -0,0 +1,64 @@
+{{- if and .Values.topologyGC.enable .Values.topologyUpdater.enable -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "node-feature-discovery.fullname" . }}-topology-gc
+  namespace: {{ include "node-feature-discovery.namespace" . }}
+  labels:
+    {{- include "node-feature-discovery.labels" . | nindent 4 }}
+    role: topology-gc
+spec:
+  replicas: {{ .Values.topologyGC.replicaCount | default 1 }}
+  selector:
+    matchLabels:
+      {{- include "node-feature-discovery.selectorLabels" . | nindent 6 }}
+      role: topology-gc
+  template:
+    metadata:
+      labels:
+        {{- include "node-feature-discovery.selectorLabels" . | nindent 8 }}
+        role: topology-gc
+      annotations:
+        {{- toYaml .Values.topologyGC.annotations | nindent 8 }}
+    spec:
+      serviceAccountName: {{ .Values.topologyGC.serviceAccountName | default "nfd-topology-gc" }}
+      dnsPolicy: ClusterFirstWithHostNet
+    {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+    {{- end }}
+      securityContext:
+        {{- toYaml .Values.topologyGC.podSecurityContext | nindent 8 }}
+      containers:
+      - name: topology-gc
+        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+        imagePullPolicy: "{{ .Values.image.pullPolicy }}"
+        env:
+        - name: NODE_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: spec.nodeName
+        command:
+          - "nfd-topology-gc"
+        args:
+          {{- if .Values.topologyGC.interval | empty | not }}
+          - "-gc-interval={{ .Values.topologyGC.interval }}"
+          {{- end }}
+        resources:
+      {{- toYaml .Values.topologyGC.resources | nindent 12 }}
+        securityContext:
+      {{- toYaml .Values.topologyGC.securityContext | nindent 12 }}
+
+    {{- with .Values.topologyGC.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    {{- with .Values.topologyGC.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+    {{- end }}
+    {{- with .Values.topologyGC.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+    {{- end }}
+{{- end }}
diff --git a/deployment/helm/node-feature-discovery/values.yaml b/deployment/helm/node-feature-discovery/values.yaml
index 8531e963a..a30c42faa 100644
--- a/deployment/helm/node-feature-discovery/values.yaml
+++ b/deployment/helm/node-feature-discovery/values.yaml
@@ -422,6 +422,44 @@ topologyUpdater:
   annotations: {}
   affinity: {}
 
+topologyGC:
+  enable: true
+  replicaCount: 1
+
+  serviceAccount:
+    create: true
+    annotations: {}
+    name:
+  rbac:
+    create: true
+
+  interval: 1h
+
+  podSecurityContext: {}
+  securityContext:
+    allowPrivilegeEscalation: false
+    capabilities:
+      drop: [ "ALL" ]
+    readOnlyRootFilesystem: true
+    runAsNonRoot: true
+
+  resources: {}
+    # We usually recommend not to specify default resources and to leave this as a conscious
+    # choice for the user. This also increases chances charts run on environments with little
+    # resources, such as Minikube. If you do want to specify resources, uncomment the following
+    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+    # limits:
+    #   cpu: 100m
+    #   memory: 128Mi
+    # requests:
+    #   cpu: 100m
+    #   memory: 128Mi
+
+  nodeSelector: {}
+  tolerations: []
+  annotations: {}
+  affinity: {}
+
 # Optionally use encryption for worker <--> master comms
 # TODO: verify hostname is not yet supported
 #
diff --git a/deployment/overlays/master-worker-topologyupdater/kustomization.yaml b/deployment/overlays/master-worker-topologyupdater/kustomization.yaml
index ffcbab658..ebc3c1fc0 100644
--- a/deployment/overlays/master-worker-topologyupdater/kustomization.yaml
+++ b/deployment/overlays/master-worker-topologyupdater/kustomization.yaml
@@ -6,11 +6,13 @@ namespace: node-feature-discovery
 bases:
 - ../../base/rbac
 - ../../base/rbac-topologyupdater
+- ../../base/rbac-topology-gc
 - ../../base/nfd-crds
 - ../../base/master
 - ../../base/worker-daemonset
 - ../../base/noderesourcetopologies-crd
 - ../../base/topologyupdater-daemonset
+- ../../base/topology-gc
 
 resources:
 - namespace.yaml
diff --git a/deployment/overlays/topologyupdater/kustomization.yaml b/deployment/overlays/topologyupdater/kustomization.yaml
index b344d993b..964f990d0 100644
--- a/deployment/overlays/topologyupdater/kustomization.yaml
+++ b/deployment/overlays/topologyupdater/kustomization.yaml
@@ -5,8 +5,10 @@ namespace: node-feature-discovery
 
 bases:
 - ../../base/rbac-topologyupdater
+- ../../base/rbac-topology-gc
 - ../../base/noderesourcetopologies-crd
 - ../../base/topologyupdater-daemonset
+- ../../base/topology-gc
 
 resources:
 - namespace.yaml
diff --git a/docs/deployment/helm.md b/docs/deployment/helm.md
index f618d7615..17b2ad7e8 100644
--- a/docs/deployment/helm.md
+++ b/docs/deployment/helm.md
@@ -173,5 +173,24 @@ We have introduced the following Chart parameters.
 | `topologyUpdater.affinity`                    | dict   | {}      | Topology updater pod [affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/)                                                                      |
 | `topologyUpdater.config`                      | dict   |         | [configuration](../reference/topology-updater-configuration-reference)                                                                                                                                |
 
+### Topology garbage collector parameters
+
+| Name                                          | Type   | Default | description                                                                                                                                                                                           |
+|-----------------------------------------------|--------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `topologyGC.*`                                | dict   |         | NFD Topology Garbage Collector configuration                                                                                                                                                          |
+| `topologyGC.enable`                           | bool   | true    | Specifies whether the NFD Topology Garbage Collector should be created                                                                                                                                |
+| `topologyGC.serviceAccount.create`            | bool   | true    | Specifies whether the service account for topology garbage collector should be created                                                                                                                |
+| `topologyGC.serviceAccount.annotations`       | dict   | {}      | Annotations to add to the service account for topology garbage collector                                                                                                                              |
+| `topologyGC.serviceAccount.name`              | string |         | The name of the service account for topology garbage collector to use. If not set and create is true, a name is generated using the fullname template and `-topology-gc` suffix                       |
+| `topologyGC.rbac.create`                      | bool   | false   | Specifies whether to create [RBAC][rbac] configuration for topology garbage collector                                                                                                                 |
+| `topologyGC.interval`                         | string | 1h      | Time between periodic garbage collector runs                                                                                                                                                          |
+| `topologyGC.podSecurityContext`               | dict   | {}      | [PodSecurityContext](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod) holds pod-level security attributes and common container settings |
+| `topologyGC.securityContext`                  | dict   | {}      | Container [security settings](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container)                                                    |
+| `topologyGC.resources`                        | dict   | {}      | Topology garbage collector pod [resources management](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)                                                                 |
+| `topologyGC.nodeSelector`                     | dict   | {}      | Topology garbage collector pod [node selector](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector)                                                                 |
+| `topologyGC.tolerations`                      | dict   | {}      | Topology garbage collector pod [node tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)                                                                      |
+| `topologyGC.annotations`                      | dict   | {}      | Topology garbage collector pod [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)                                                                          |
+| `topologyGC.affinity`                         | dict   | {}      | Topology garbage collector pod [affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/)                                                            |
+
 <!-- Links -->
 [rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/
diff --git a/docs/get-started/introduction.md b/docs/get-started/introduction.md
index 21211a934..b70ec2193 100644
--- a/docs/get-started/introduction.md
+++ b/docs/get-started/introduction.md
@@ -47,6 +47,14 @@ creates or updates a
 resource object specific to this node. One instance of nfd-topology-updater is
 supposed to be running on each node of the cluster.
 
+## NFD-Topology-Garbage-Collector
+
+NFD-Topology-Garbage-Collector is a daemon responsible for cleaning obsolete
+[NodeResourceTopology](../usage/custom-resources#noderesourcetopology) objects,
+obsolete means that there is no corresponding worker node.
+
+One instance of nfd-topology-gc is supposed to be running in the cluster.
+
 ## Feature Discovery
 
 Feature discovery is divided into domain-specific feature sources:
diff --git a/docs/reference/topology-gc-commandline-reference.md b/docs/reference/topology-gc-commandline-reference.md
new file mode 100644
index 000000000..bfabb1f19
--- /dev/null
+++ b/docs/reference/topology-gc-commandline-reference.md
@@ -0,0 +1,46 @@
+---
+title: "Topology Garbage Collector Cmdline Reference"
+layout: default
+sort: 6
+---
+
+# NFD-Topology-Garbage-Collector Commandline Flags
+
+{: .no_toc }
+
+## Table of Contents
+
+{: .no_toc .text-delta }
+
+1. TOC
+{:toc}
+
+---
+
+To quickly view available command line flags execute `nfd-topology-gc -help`.
+In a docker container:
+
+```bash
+docker run {{ site.container_image }} \
+nfd-topology-gc -help
+```
+
+### -h, -help
+
+Print usage and exit.
+
+### -version
+
+Print version and exit.
+
+### -gc-interval
+
+The `-gc-interval` specifies the interval between periodic garbage collector runs.
+
+Default: 1h
+
+Example:
+
+```bash
+nfd-topology-gc -gc-interval=1h
+```
diff --git a/docs/usage/custom-resources.md b/docs/usage/custom-resources.md
index 1fd64d0ea..10e75769b 100644
--- a/docs/usage/custom-resources.md
+++ b/docs/usage/custom-resources.md
@@ -1,7 +1,7 @@
 ---
 title: "CRDs"
 layout: default
-sort: 6
+sort: 7
 ---
 
 # Custom Resources
diff --git a/docs/usage/customization-guide.md b/docs/usage/customization-guide.md
index a73c20d9b..8356b8b21 100644
--- a/docs/usage/customization-guide.md
+++ b/docs/usage/customization-guide.md
@@ -1,7 +1,7 @@
 ---
 title: "Customization guide"
 layout: default
-sort: 7
+sort: 8
 ---
 
 # Customization guide
diff --git a/docs/usage/examples-and-demos.md b/docs/usage/examples-and-demos.md
index 7d6492629..36936bf64 100644
--- a/docs/usage/examples-and-demos.md
+++ b/docs/usage/examples-and-demos.md
@@ -1,7 +1,7 @@
 ---
 title: "Examples and demos"
 layout: default
-sort: 8
+sort: 9
 ---
 
 # Examples and demos
diff --git a/docs/usage/nfd-topology-gc.md b/docs/usage/nfd-topology-gc.md
new file mode 100644
index 000000000..3af95f06f
--- /dev/null
+++ b/docs/usage/nfd-topology-gc.md
@@ -0,0 +1,29 @@
+---
+title: "NFD-Topology-Garbage-Collector"
+layout: default
+sort: 6
+---
+
+# NFD-Topology-Garbage-Collector
+{: .no_toc}
+
+---
+
+NFD-Topology-Garbage-Collector is preferably run as a Kubernetes deployment
+with one replica. It makes sure that all
+[NodeResourceTopology](custom-resources#noderesourcetopology)
+have corresponding worker nodes and removes stale objects for worker nodes
+which are no longer part of Kubernetes cluster.
+
+This service watches for Node deletion events and removes NodeResourceTopology
+objects upon them. It is also running periodically to make sure no event was
+missed or NodeResourceTopology object was created without corresponding worker
+node. The default garbage collector interval is set to 1h which is the value
+when no -gc-interval is specified.
+
+## Topology-Garbage-Collector Configuration
+
+In Helm deployments,
+(see [Topology Garbage Collector](../deployment/helm.md#topology-garbage-collector-parameters)
+for parameters). NFD-Topology-Garbage-Collector will only be deployed when
+topologyUpdater.enable is set to true.
diff --git a/go.mod b/go.mod
index d9a714f8b..fb1e6d0bc 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/golang/protobuf v1.5.2
 	github.com/google/go-cmp v0.5.9
 	github.com/jaypipes/ghw v0.8.1-0.20210827132705-c7224150a17e
-	github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.0.13
+	github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.0.12
 	github.com/klauspost/cpuid/v2 v2.2.3
 	github.com/onsi/ginkgo/v2 v2.4.0
 	github.com/onsi/gomega v1.23.0
diff --git a/go.sum b/go.sum
index 928bf981c..c7b6635fd 100644
--- a/go.sum
+++ b/go.sum
@@ -420,8 +420,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.0.13 h1:Y1RjPskyGMkVtNL8lq75bEdjqgq8gi+JJ1oWaz/mIJE=
-github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.0.13/go.mod h1:AkACMQGiTgCt0lQw3m7TTU8PLH9lYKNK5e9DqFf5VuM=
+github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.0.12 h1:NhXbOjO1st8hIcVpegr3zw/AGG12vs3z//tCDDcfPpE=
+github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.0.12/go.mod h1:AkACMQGiTgCt0lQw3m7TTU8PLH9lYKNK5e9DqFf5VuM=
 github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI=
 github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
diff --git a/pkg/nfd-topology-gc/nfd-nrt-gc.go b/pkg/nfd-topology-gc/nfd-nrt-gc.go
new file mode 100644
index 000000000..e59f597bd
--- /dev/null
+++ b/pkg/nfd-topology-gc/nfd-nrt-gc.go
@@ -0,0 +1,194 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package nfdtopologygarbagecollector
+
+import (
+	"context"
+	"time"
+
+	topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes"
+	restclient "k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/klog/v2"
+
+	"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
+)
+
+// Args are the command line arguments
+type Args struct {
+	GCPeriod time.Duration
+
+	Kubeconfig string
+}
+
+type TopologyGC interface {
+	Run() error
+	Stop()
+}
+
+type topologyGC struct {
+	stopChan   chan struct{}
+	topoClient topologyclientset.Interface
+	gcPeriod   time.Duration
+	factory    informers.SharedInformerFactory
+}
+
+func New(args *Args) (TopologyGC, error) {
+	kubeconfig, err := apihelper.GetKubeconfig(args.Kubeconfig)
+	if err != nil {
+		return nil, err
+	}
+
+	stop := make(chan struct{})
+
+	return newTopologyGC(kubeconfig, stop, args.GCPeriod)
+}
+
+func newTopologyGC(config *restclient.Config, stop chan struct{}, gcPeriod time.Duration) (*topologyGC, error) {
+	helper := apihelper.K8sHelpers{Kubeconfig: config}
+	cli, err := helper.GetTopologyClient()
+	if err != nil {
+		return nil, err
+	}
+
+	clientset := kubernetes.NewForConfigOrDie(config)
+	factory := informers.NewSharedInformerFactory(clientset, 5*time.Minute)
+
+	return &topologyGC{
+		topoClient: cli,
+		stopChan:   stop,
+		gcPeriod:   gcPeriod,
+		factory:    factory,
+	}, nil
+}
+
+func (n *topologyGC) deleteNRT(nodeName string) {
+	if err := n.topoClient.TopologyV1alpha1().NodeResourceTopologies().Delete(context.TODO(), nodeName, metav1.DeleteOptions{}); err != nil {
+		if errors.IsNotFound(err) {
+			klog.V(2).Infof("NodeResourceTopology for node %s not found, omitting deletion", nodeName)
+			return
+		} else {
+			klog.Warningf("failed to delete NodeResourceTopology for node %s: %s", nodeName, err.Error())
+			return
+		}
+	}
+	klog.Infof("NodeResourceTopology for node %s has been deleted", nodeName)
+}
+
+func (n *topologyGC) deleteNodeHandler(object interface{}) {
+	// handle a case when we are starting up and need to clear stale NRT resources
+	obj := object
+	if deletedFinalStateUnknown, ok := object.(cache.DeletedFinalStateUnknown); ok {
+		klog.V(2).Infof("found stale NodeResourceTopology for node: %s ", object)
+		obj = deletedFinalStateUnknown.Obj
+	}
+
+	node, ok := obj.(*corev1.Node)
+	if !ok {
+		klog.Errorf("cannot convert %v to v1.Node", object)
+		return
+	}
+
+	n.deleteNRT(node.GetName())
+}
+
+func (n *topologyGC) runGC() {
+	klog.Infof("Running GC")
+	objects := n.factory.Core().V1().Nodes().Informer().GetIndexer().List()
+	nodes := sets.NewString()
+	for _, object := range objects {
+		key, err := cache.MetaNamespaceKeyFunc(object)
+		if err != nil {
+			klog.Warningf("cannot create key for %v: %s", object, err.Error())
+			continue
+		}
+		nodes.Insert(key)
+	}
+
+	nrts, err := n.topoClient.TopologyV1alpha1().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+	if err != nil {
+		klog.Warningf("cannot list NRTs %s", err.Error())
+		return
+	}
+
+	for _, nrt := range nrts.Items {
+		key, err := cache.MetaNamespaceKeyFunc(&nrt)
+		if err != nil {
+			klog.Warningf("cannot create key for %v: %s", nrt, err.Error())
+			continue
+		}
+		if !nodes.Has(key) {
+			n.deleteNRT(key)
+		}
+	}
+}
+
+// periodicGC runs garbage collector at every gcPeriod to make sure we haven't missed any node
+func (n *topologyGC) periodicGC(gcPeriod time.Duration) {
+	gcTrigger := time.NewTicker(gcPeriod)
+	for {
+		select {
+		case <-gcTrigger.C:
+			n.runGC()
+		case <-n.stopChan:
+			klog.Infof("shutting down periodic Garbage Collector")
+			return
+		}
+	}
+}
+
+func (n *topologyGC) run() error {
+	nodeInformer := n.factory.Core().V1().Nodes().Informer()
+
+	if _, err := nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+		DeleteFunc: n.deleteNodeHandler,
+	}); err != nil {
+		return err
+	}
+
+	// start informers
+	n.factory.Start(n.stopChan)
+	n.factory.WaitForCacheSync(n.stopChan)
+
+	n.runGC()
+
+	return nil
+}
+
+// Run is a blocking function that removes stale NRT objects when Node is deleted and runs periodic GC to make sure any obsolete objects are removed
+func (n *topologyGC) Run() error {
+	if err := n.run(); err != nil {
+		return err
+	}
+	// run periodic GC
+	n.periodicGC(n.gcPeriod)
+
+	return nil
+}
+
+func (n *topologyGC) Stop() {
+	select {
+	case n.stopChan <- struct{}{}:
+	default:
+	}
+}
diff --git a/pkg/topologypolicy/topology-policy.go b/pkg/topologypolicy/topology-policy.go
index 75331f24e..3a7365346 100644
--- a/pkg/topologypolicy/topology-policy.go
+++ b/pkg/topologypolicy/topology-policy.go
@@ -24,39 +24,20 @@ import (
 // DetectTopologyPolicy returns string type which present
 // both Topology manager policy and scope
 func DetectTopologyPolicy(policy string, scope string) v1alpha1.TopologyManagerPolicy {
-	switch scope {
-	case config.PodTopologyManagerScope:
-		return detectPolicyPodScope(policy)
-	case config.ContainerTopologyManagerScope:
-		return detectPolicyContainerScope(policy)
-	default:
-		return v1alpha1.None
-	}
-}
-
-func detectPolicyPodScope(policy string) v1alpha1.TopologyManagerPolicy {
-	switch policy {
-	case config.SingleNumaNodeTopologyManagerPolicy:
-		return v1alpha1.SingleNUMANodePodLevel
-	case config.RestrictedTopologyManagerPolicy:
-		return v1alpha1.RestrictedPodLevel
-	case config.BestEffortTopologyManagerPolicy:
-		return v1alpha1.BestEffortPodLevel
-	case config.NoneTopologyManagerPolicy:
-		return v1alpha1.None
-	default:
-		return v1alpha1.None
-	}
-}
-
-func detectPolicyContainerScope(policy string) v1alpha1.TopologyManagerPolicy {
 	switch policy {
 	case config.SingleNumaNodeTopologyManagerPolicy:
-		return v1alpha1.SingleNUMANodeContainerLevel
+		if scope == config.PodTopologyManagerScope {
+			return v1alpha1.SingleNUMANodePodLevel
+		} else if scope == config.ContainerTopologyManagerScope {
+			return v1alpha1.SingleNUMANodeContainerLevel
+		} else {
+			// default scope for single-numa-node
+			return v1alpha1.SingleNUMANodeContainerLevel
+		}
 	case config.RestrictedTopologyManagerPolicy:
-		return v1alpha1.RestrictedContainerLevel
+		return v1alpha1.Restricted
 	case config.BestEffortTopologyManagerPolicy:
-		return v1alpha1.BestEffortContainerLevel
+		return v1alpha1.BestEffort
 	case config.NoneTopologyManagerPolicy:
 		return v1alpha1.None
 	default:


Test patch:
diff --git a/cmd/nfd-topology-gc/main_test.go b/cmd/nfd-topology-gc/main_test.go
new file mode 100644
index 000000000..541efa048
--- /dev/null
+++ b/cmd/nfd-topology-gc/main_test.go
@@ -0,0 +1,41 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"flag"
+	"testing"
+	"time"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestArgsParse(t *testing.T) {
+	Convey("When parsing command line arguments", t, func() {
+		flags := flag.NewFlagSet(ProgramName, flag.ExitOnError)
+
+		Convey("When valid -gc-interval is specified", func() {
+			args := parseArgs(flags,
+				"-gc-interval=30s")
+
+			Convey("args.GCPeriod is set to appropriate values", func() {
+				So(args.GCPeriod, ShouldEqual, 30*time.Second)
+			})
+		})
+
+	})
+}
diff --git a/pkg/nfd-topology-gc/nfd-nrt-gc_test.go b/pkg/nfd-topology-gc/nfd-nrt-gc_test.go
new file mode 100644
index 000000000..c343fab37
--- /dev/null
+++ b/pkg/nfd-topology-gc/nfd-nrt-gc_test.go
@@ -0,0 +1,234 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package nfdtopologygarbagecollector
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	nrtapi "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha1"
+	v1alpha1 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha1"
+	faketopologyv1alpha1 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned/fake"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/informers"
+	fakek8sclientset "k8s.io/client-go/kubernetes/fake"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestNRTGC(t *testing.T) {
+	Convey("When theres is old NRT ", t, func() {
+		k8sClient := fakek8sclientset.NewSimpleClientset()
+
+		fakeClient := faketopologyv1alpha1.NewSimpleClientset(&nrtapi.NodeResourceTopology{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "node1",
+			},
+		})
+		factory := informers.NewSharedInformerFactory(k8sClient, 5*time.Minute)
+
+		stopChan := make(chan struct{}, 1)
+
+		gc := &topologyGC{
+			factory:    factory,
+			topoClient: fakeClient,
+			stopChan:   stopChan,
+			gcPeriod:   10 * time.Minute,
+		}
+
+		err := gc.run()
+		So(err, ShouldBeNil)
+
+		nrts, err := fakeClient.TopologyV1alpha1().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+		So(err, ShouldBeNil)
+		So(nrts.Items, ShouldHaveLength, 0)
+
+		gc.Stop()
+	})
+	Convey("When theres is one old NRT and one up to date", t, func() {
+		k8sClient := fakek8sclientset.NewSimpleClientset(&corev1.Node{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "node1",
+			},
+		})
+
+		fakeClient := faketopologyv1alpha1.NewSimpleClientset(&nrtapi.NodeResourceTopology{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "node1",
+			},
+		},
+			&nrtapi.NodeResourceTopology{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node2",
+				},
+			},
+		)
+
+		stopChan := make(chan struct{}, 1)
+
+		factory := informers.NewSharedInformerFactory(k8sClient, 5*time.Minute)
+
+		gc := &topologyGC{
+			factory:    factory,
+			topoClient: fakeClient,
+			stopChan:   stopChan,
+			gcPeriod:   10 * time.Minute,
+		}
+
+		err := gc.run()
+		So(err, ShouldBeNil)
+
+		nrts, err := fakeClient.TopologyV1alpha1().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+		So(err, ShouldBeNil)
+		So(nrts.Items, ShouldHaveLength, 1)
+		So(nrts.Items[0].GetName(), ShouldEqual, "node1")
+
+	})
+	Convey("Should react to delete event", t, func() {
+		k8sClient := fakek8sclientset.NewSimpleClientset(
+			&corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node1",
+				},
+			},
+			&corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node2",
+				},
+			},
+		)
+
+		fakeClient := faketopologyv1alpha1.NewSimpleClientset(
+			&nrtapi.NodeResourceTopology{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node1",
+				},
+			},
+			&nrtapi.NodeResourceTopology{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node2",
+				},
+			},
+		)
+
+		stopChan := make(chan struct{}, 1)
+
+		factory := informers.NewSharedInformerFactory(k8sClient, 5*time.Minute)
+		gc := &topologyGC{
+			factory:    factory,
+			topoClient: fakeClient,
+			stopChan:   stopChan,
+			gcPeriod:   10 * time.Minute,
+		}
+
+		err := gc.run()
+		So(err, ShouldBeNil)
+
+		nrts, err := fakeClient.TopologyV1alpha1().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+		So(err, ShouldBeNil)
+
+		So(nrts.Items, ShouldHaveLength, 2)
+
+		err = k8sClient.CoreV1().Nodes().Delete(context.TODO(), "node1", metav1.DeleteOptions{})
+		So(err, ShouldBeNil)
+		// simple sleep with retry loop to make sure indexer will pick up event and trigger deleteNode Function
+		deleted := false
+		for i := 0; i < 5; i++ {
+			nrts, err := fakeClient.TopologyV1alpha1().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+			So(err, ShouldBeNil)
+
+			if len(nrts.Items) == 1 {
+				deleted = true
+				break
+			}
+			time.Sleep(time.Second)
+		}
+		So(deleted, ShouldBeTrue)
+	})
+	Convey("periodic GC should remove obsolete NRT", t, func() {
+		k8sClient := fakek8sclientset.NewSimpleClientset(
+			&corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node1",
+				},
+			},
+			&corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node2",
+				},
+			},
+		)
+
+		fakeClient := faketopologyv1alpha1.NewSimpleClientset(
+			&nrtapi.NodeResourceTopology{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node1",
+				},
+			},
+			&nrtapi.NodeResourceTopology{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "node2",
+				},
+			},
+		)
+
+		stopChan := make(chan struct{}, 1)
+
+		factory := informers.NewSharedInformerFactory(k8sClient, 5*time.Minute)
+		gc := &topologyGC{
+			factory:    factory,
+			topoClient: fakeClient,
+			stopChan:   stopChan,
+			gcPeriod:   time.Second,
+		}
+
+		err := gc.run()
+		So(err, ShouldBeNil)
+
+		nrts, err := fakeClient.TopologyV1alpha1().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+		So(err, ShouldBeNil)
+
+		So(nrts.Items, ShouldHaveLength, 2)
+
+		nrt := v1alpha1.NodeResourceTopology{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "not-existing",
+			},
+		}
+
+		go gc.periodicGC(time.Second)
+
+		_, err = fakeClient.TopologyV1alpha1().NodeResourceTopologies().Create(context.TODO(), &nrt, metav1.CreateOptions{})
+		So(err, ShouldBeNil)
+		// simple sleep with retry loop to make sure GC was triggered
+		deleted := false
+		for i := 0; i < 5; i++ {
+			nrts, err := fakeClient.TopologyV1alpha1().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+			So(err, ShouldBeNil)
+
+			if len(nrts.Items) == 2 {
+				deleted = true
+				break
+			}
+			time.Sleep(2 * time.Second)
+		}
+		So(deleted, ShouldBeTrue)
+	})
+
+}
diff --git a/pkg/topologypolicy/topology-policy_test.go b/pkg/topologypolicy/topology-policy_test.go
deleted file mode 100644
index b766814a8..000000000
--- a/pkg/topologypolicy/topology-policy_test.go
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package topologypolicy
-
-import (
-	"testing"
-
-	v1alpha1 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha1"
-)
-
-func TestDetectTopologyPolicy(t *testing.T) {
-	testCases := []struct {
-		scope    string
-		policy   string
-		expected v1alpha1.TopologyManagerPolicy
-	}{
-		{
-			policy:   "best-effort",
-			scope:    "pod",
-			expected: v1alpha1.BestEffortPodLevel,
-		},
-		{
-			policy:   "best-effort",
-			scope:    "container",
-			expected: v1alpha1.BestEffortContainerLevel,
-		},
-		{
-			policy:   "restricted",
-			scope:    "container",
-			expected: v1alpha1.RestrictedContainerLevel,
-		},
-		{
-			policy:   "restricted",
-			scope:    "pod",
-			expected: v1alpha1.RestrictedPodLevel,
-		},
-		{
-			policy:   "single-numa-node",
-			scope:    "pod",
-			expected: v1alpha1.SingleNUMANodePodLevel,
-		},
-		{
-			policy:   "single-numa-node",
-			scope:    "container",
-			expected: v1alpha1.SingleNUMANodeContainerLevel,
-		},
-		{
-			policy:   "none",
-			scope:    "container",
-			expected: v1alpha1.None,
-		},
-		{
-			policy:   "none",
-			scope:    "pod",
-			expected: v1alpha1.None,
-		},
-		{
-			policy:   "non-existent",
-			scope:    "pod",
-			expected: v1alpha1.None,
-		},
-		{
-			policy:   "non-existent",
-			scope:    "container",
-			expected: v1alpha1.None,
-		},
-		{
-			policy:   "single-numa-node",
-			scope:    "non-existent",
-			expected: v1alpha1.None,
-		},
-		{
-			policy:   "single-numa-node",
-			scope:    "non-existent",
-			expected: v1alpha1.None,
-		},
-	}
-
-	for _, tc := range testCases {
-		actual := DetectTopologyPolicy(tc.policy, tc.scope)
-		if actual != tc.expected {
-			t.Errorf("Expected TopologyPolicy to equal: %s not: %s", tc.expected, actual)
-		}
-	}
-}
diff --git a/test/e2e/data/nodefeature-1.yaml b/test/e2e/data/nodefeature-1.yaml
deleted file mode 100644
index 4b3b96523..000000000
--- a/test/e2e/data/nodefeature-1.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-apiVersion: nfd.k8s-sigs.io/v1alpha1
-kind: NodeFeature
-metadata:
-  # This name should ensure that it's processed later than that from nfd-worker
-  name: zzz-e2e-features-1
-spec:
-  # Features for NodeFeatureRule matching
-  features:
-    flags:
-      e2e.flags:
-        elements:
-          flag_1: {}
-          flag_2: {}
-    attributes:
-      # Override features from the fake sources
-      fake.attribute:
-        elements:
-          attr_2: "true"
-    instances:
-      # Append to features from the fake sources
-      fake.instance:
-        elements:
-        - attributes:
-            attr_1: "true"
-            attr_2: "9"
-  # Labels to be created
-  labels:
-    e2e-nodefeature-test-1: "obj-1"
-    e2e-nodefeature-test-2: "obj-1"
-    # Override feature from nfd-worker
-    fake-fakefeature3: "overridden"
diff --git a/test/e2e/data/nodefeature-2.yaml b/test/e2e/data/nodefeature-2.yaml
deleted file mode 100644
index 0696902d3..000000000
--- a/test/e2e/data/nodefeature-2.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: nfd.k8s-sigs.io/v1alpha1
-kind: NodeFeature
-metadata:
-  name: zzz-e2e-features-2
-spec:
-  labels:
-    e2e-nodefeature-test-1: "overridden-from-obj-2"
-    e2e-nodefeature-test-3: "obj-2"
diff --git a/test/e2e/node_feature_discovery.go b/test/e2e/node_feature_discovery.go
index 2553e30ec..5ba26404a 100644
--- a/test/e2e/node_feature_discovery.go
+++ b/test/e2e/node_feature_discovery.go
@@ -494,94 +494,6 @@ var _ = SIGDescribe("Node Feature Discovery", func() {
 				})
 			})
 
-			//
-			// Test NodeFeature
-			//
-			Context("and NodeFeature objects deployed", func() {
-				It("labels from the NodeFeature objects should be created", func() {
-					if !useNodeFeatureApi {
-						Skip("NodeFeature API not enabled")
-					}
-
-					// We pick one node targeted for our NodeFeature objects
-					nodes, err := getNonControlPlaneNodes(f.ClientSet)
-					Expect(err).NotTo(HaveOccurred())
-
-					targetNodeName := nodes[0].Name
-					Expect(targetNodeName).ToNot(BeEmpty(), "No suitable worker node found")
-
-					By("Creating NodeFeature object")
-					nodeFeatures, err := testutils.CreateOrUpdateNodeFeaturesFromFile(nfdClient, "nodefeature-1.yaml", f.Namespace.Name, targetNodeName)
-					Expect(err).NotTo(HaveOccurred())
-
-					By("Verifying node labels from NodeFeature object #1")
-					expectedLabels := map[string]k8sLabels{
-						targetNodeName: {
-							nfdv1alpha1.FeatureLabelNs + "/e2e-nodefeature-test-1": "obj-1",
-							nfdv1alpha1.FeatureLabelNs + "/e2e-nodefeature-test-2": "obj-1",
-							nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3":      "overridden",
-						},
-					}
-					Expect(waitForNfdNodeLabels(f.ClientSet, expectedLabels)).NotTo(HaveOccurred())
-
-					By("Deleting NodeFeature object")
-					err = nfdClient.NfdV1alpha1().NodeFeatures(f.Namespace.Name).Delete(context.TODO(), nodeFeatures[0], metav1.DeleteOptions{})
-					Expect(err).NotTo(HaveOccurred())
-
-					By("Verifying node labels from NodeFeature object were removed")
-					Expect(waitForNfdNodeLabels(f.ClientSet, nil)).NotTo(HaveOccurred())
-
-					By("Creating nfd-worker daemonset")
-					podSpecOpts := createPodSpecOpts(
-						testpod.SpecWithContainerImage(dockerImage),
-						testpod.SpecWithContainerExtraArgs("-label-sources=fake"),
-					)
-					workerDS := testds.NFDWorker(podSpecOpts...)
-					workerDS, err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Create(context.TODO(), workerDS, metav1.CreateOptions{})
-					Expect(err).NotTo(HaveOccurred())
-
-					By("Waiting for worker daemonset pods to be ready")
-					Expect(testpod.WaitForReady(f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 5)).NotTo(HaveOccurred())
-
-					By("Verifying node labels from nfd-worker")
-					expectedLabels = map[string]k8sLabels{
-						"*": {
-							nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature1": "true",
-							nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature2": "true",
-							nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3": "true",
-						},
-					}
-					Expect(waitForNfdNodeLabels(f.ClientSet, expectedLabels)).NotTo(HaveOccurred())
-
-					By("Re-creating NodeFeature object")
-					_, err = testutils.CreateOrUpdateNodeFeaturesFromFile(nfdClient, "nodefeature-1.yaml", f.Namespace.Name, targetNodeName)
-					Expect(err).NotTo(HaveOccurred())
-
-					By("Verifying node labels from NodeFeature object #1 are created")
-					expectedLabels[targetNodeName] = k8sLabels{
-						nfdv1alpha1.FeatureLabelNs + "/e2e-nodefeature-test-1": "obj-1",
-						nfdv1alpha1.FeatureLabelNs + "/e2e-nodefeature-test-2": "obj-1",
-						nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature1":      "true",
-						nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature2":      "true",
-						nfdv1alpha1.FeatureLabelNs + "/fake-fakefeature3":      "overridden",
-					}
-					Expect(waitForNfdNodeLabels(f.ClientSet, expectedLabels)).NotTo(HaveOccurred())
-
-					By("Creating extra namespace")
-					extraNs, err := f.CreateNamespace("node-feature-discvery-extra-ns", nil)
-					Expect(err).NotTo(HaveOccurred())
-
-					By("Create NodeFeature object in the extra namespace")
-					_, err = testutils.CreateOrUpdateNodeFeaturesFromFile(nfdClient, "nodefeature-2.yaml", extraNs.Name, targetNodeName)
-					Expect(err).NotTo(HaveOccurred())
-
-					By("Verifying node labels from NodeFeature object #2 are created")
-					expectedLabels[targetNodeName][nfdv1alpha1.FeatureLabelNs+"/e2e-nodefeature-test-1"] = "overridden-from-obj-2"
-					expectedLabels[targetNodeName][nfdv1alpha1.FeatureLabelNs+"/e2e-nodefeature-test-3"] = "obj-2"
-					Expect(waitForNfdNodeLabels(f.ClientSet, expectedLabels)).NotTo(HaveOccurred())
-				})
-			})
-
 			//
 			// Test NodeFeatureRule
 			//
@@ -610,13 +522,10 @@ core:
 					By("Waiting for daemonset pods to be ready")
 					Expect(testpod.WaitForReady(f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 5)).NotTo(HaveOccurred())
 
-					expected := map[string]k8sLabels{
-						"*": {
-							nfdv1alpha1.FeatureLabelNs + "/e2e-flag-test-1":      "true",
-							nfdv1alpha1.FeatureLabelNs + "/e2e-attribute-test-1": "true",
-							nfdv1alpha1.FeatureLabelNs + "/e2e-instance-test-1":  "true",
-						},
-					}
+					expected := map[string]string{
+						"feature.node.kubernetes.io/e2e-flag-test-1":      "true",
+						"feature.node.kubernetes.io/e2e-attribute-test-1": "true",
+						"feature.node.kubernetes.io/e2e-instance-test-1":  "true"}
 
 					By("Creating NodeFeatureRules #1")
 					Expect(testutils.CreateNodeFeatureRulesFromFile(nfdClient, "nodefeaturerule-1.yaml")).NotTo(HaveOccurred())
@@ -628,9 +537,9 @@ core:
 					Expect(testutils.CreateNodeFeatureRulesFromFile(nfdClient, "nodefeaturerule-2.yaml")).NotTo(HaveOccurred())
 
 					// Add features from NodeFeatureRule #2
-					expected["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-matchany-test-1"] = "true"
-					expected["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_1"] = "found"
-					expected["*"][nfdv1alpha1.FeatureLabelNs+"/e2e-template-test-1-instance_2"] = "found"
+					expected["feature.node.kubernetes.io/e2e-matchany-test-1"] = "true"
+					expected["feature.node.kubernetes.io/e2e-template-test-1-instance_1"] = "found"
+					expected["feature.node.kubernetes.io/e2e-template-test-1-instance_2"] = "found"
 
 					By("Verifying node labels from NodeFeatureRules #1 and #2")
 					Expect(waitForNfdNodeLabels(f.ClientSet, expected)).NotTo(HaveOccurred())
@@ -727,10 +636,8 @@ func waitForNfdNodeAnnotations(cli clientset.Interface, expected map[string]stri
 	return err
 }
 
-type k8sLabels map[string]string
-
 // waitForNfdNodeLabels waits for node to be labeled as expected.
-func waitForNfdNodeLabels(cli clientset.Interface, expected map[string]k8sLabels) error {
+func waitForNfdNodeLabels(cli clientset.Interface, expected map[string]string) error {
 	poll := func() error {
 		nodes, err := getNonControlPlaneNodes(cli)
 		if err != nil {
@@ -738,15 +645,8 @@ func waitForNfdNodeLabels(cli clientset.Interface, expected map[string]k8sLabels
 		}
 		for _, node := range nodes {
 			labels := nfdLabels(node.Labels)
-			nodeExpected, ok := expected[node.Name]
-			if !ok {
-				nodeExpected = k8sLabels{}
-				if defaultExpected, ok := expected["*"]; ok {
-					nodeExpected = defaultExpected
-				}
-			}
-			if !cmp.Equal(nodeExpected, labels) {
-				return fmt.Errorf("node %q labels do not match expected, diff (expected vs. received): %s", node.Name, cmp.Diff(nodeExpected, labels))
+			if !cmp.Equal(expected, labels) {
+				return fmt.Errorf("node %q labels do not match expected, diff (expected vs. received): %s", node.Name, cmp.Diff(expected, labels))
 			}
 		}
 		return nil
@@ -833,8 +733,8 @@ func getNonControlPlaneNodes(cli clientset.Interface) ([]corev1.Node, error) {
 }
 
 // nfdLabels gets labels that are in the nfd label namespace.
-func nfdLabels(labels map[string]string) k8sLabels {
-	ret := k8sLabels{}
+func nfdLabels(labels map[string]string) map[string]string {
+	ret := map[string]string{}
 
 	for key, val := range labels {
 		if strings.HasPrefix(key, nfdv1alpha1.FeatureLabelNs) {
diff --git a/test/e2e/utils/crd.go b/test/e2e/utils/crd.go
index 6546f312a..6d95d9066 100644
--- a/test/e2e/utils/crd.go
+++ b/test/e2e/utils/crd.go
@@ -59,38 +59,6 @@ func CreateNfdCRDs(cli extclient.Interface) ([]*apiextensionsv1.CustomResourceDe
 	return newCRDs, nil
 }
 
-// CreateOrUpdateNodeFeaturesFromFile creates/updates a NodeFeature object from a given file located under test data directory.
-func CreateOrUpdateNodeFeaturesFromFile(cli nfdclientset.Interface, filename, namespace, nodename string) ([]string, error) {
-	objs, err := nodeFeaturesFromFile(filepath.Join(packagePath, "..", "data", filename))
-	if err != nil {
-		return nil, err
-	}
-
-	names := make([]string, len(objs))
-	for i, obj := range objs {
-		obj.Namespace = namespace
-		if obj.Labels == nil {
-			obj.Labels = map[string]string{}
-		}
-		obj.Labels[nfdv1alpha1.NodeFeatureObjNodeNameLabel] = nodename
-
-		if oldObj, err := cli.NfdV1alpha1().NodeFeatures(namespace).Get(context.TODO(), obj.Name, metav1.GetOptions{}); errors.IsNotFound(err) {
-			if _, err := cli.NfdV1alpha1().NodeFeatures(namespace).Create(context.TODO(), obj, metav1.CreateOptions{}); err != nil {
-				return names, fmt.Errorf("failed to create NodeFeature %w", err)
-			}
-		} else if err == nil {
-			obj.SetResourceVersion(oldObj.GetResourceVersion())
-			if _, err = cli.NfdV1alpha1().NodeFeatures(namespace).Update(context.TODO(), obj, metav1.UpdateOptions{}); err != nil {
-				return names, fmt.Errorf("failed to update NodeFeature object: %w", err)
-			}
-		} else {
-			return names, fmt.Errorf("failed to get NodeFeature %w", err)
-		}
-		names[i] = obj.Name
-	}
-	return names, nil
-}
-
 // CreateNodeFeatureRuleFromFile creates a NodeFeatureRule object from a given file located under test data directory.
 func CreateNodeFeatureRulesFromFile(cli nfdclientset.Interface, filename string) error {
 	objs, err := nodeFeatureRulesFromFile(filepath.Join(packagePath, "..", "data", filename))
@@ -171,25 +139,6 @@ func crdsFromFile(path string) ([]*apiextensionsv1.CustomResourceDefinition, err
 	return crds, nil
 }
 
-func nodeFeaturesFromFile(path string) ([]*nfdv1alpha1.NodeFeature, error) {
-	objs, err := apiObjsFromFile(path, nfdscheme.Codecs.UniversalDeserializer())
-	if err != nil {
-		return nil, err
-	}
-
-	crs := make([]*nfdv1alpha1.NodeFeature, len(objs))
-
-	for i, obj := range objs {
-		var ok bool
-		crs[i], ok = obj.(*nfdv1alpha1.NodeFeature)
-		if !ok {
-			return nil, fmt.Errorf("unexpected type %t when reading %q", obj, path)
-		}
-	}
-
-	return crs, nil
-}
-
 func nodeFeatureRulesFromFile(path string) ([]*nfdv1alpha1.NodeFeatureRule, error) {
 	objs, err := apiObjsFromFile(path, nfdscheme.Codecs.UniversalDeserializer())
 	if err != nil {