Repository: kubernetes-sigs/node-feature-discovery
Pull request #1813 (2024-07-26 13:37:59)

nfd-gc: only fetch object metadata
Significantly reduce the apiserver and network load by only listing/getting the object metadata.

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/1813#" 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":[]} -->
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *kubernetes-sigs-nfd* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 50d2a2de43b53f16dfc50b1f98e24a492113d1bf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kubernetes-sigs-nfd/deploys/66a3a6b93c3e3e000851f857 |
marquiz: /assign @PiotrProkop @ArangoGutierrez 
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 | 50d2a2de43b53f16dfc50b1f98e24a492113d1bf | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1813/pull-node-feature-discovery-verify-master/1816830334969319424) | 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=1813). [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-sigs/prow](https://github.com/kubernetes-sigs/prow/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: @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 | 4165ebb47543b8f0ee6d4723c7ff6702b311e1eb | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1813/pull-node-feature-discovery-verify-master/1816831713054035968) | 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=1813). [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-sigs/prow](https://github.com/kubernetes-sigs/prow/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: @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 | 262b3f03865ff05dfa5a69d56acf62982969af48 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/1813/pull-node-feature-discovery-verify-master/1816880878089932800) | 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=1813). [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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/docs/reference/master-commandline-reference.md b/docs/reference/master-commandline-reference.md
index a4c2740bb..2dc8da30a 100644
--- a/docs/reference/master-commandline-reference.md
+++ b/docs/reference/master-commandline-reference.md
@@ -34,7 +34,7 @@ Print version and exit.
 
 The `-feature-gates` flag is used to enable or disable non GA features.
 The list of available feature gates can be found in the [feature gates
-documentation](feature-gates.md).
+documentation](../feature-gates.md).
 
 Example:
 
diff --git a/go.mod b/go.mod
index 328675002..bf44b3797 100644
--- a/go.mod
+++ b/go.mod
@@ -11,8 +11,8 @@ require (
 	github.com/k8stopologyawareschedwg/noderesourcetopology-api v0.1.2
 	github.com/k8stopologyawareschedwg/podfingerprint v0.2.2
 	github.com/klauspost/cpuid/v2 v2.2.8
-	github.com/onsi/ginkgo/v2 v2.19.1
-	github.com/onsi/gomega v1.34.0
+	github.com/onsi/ginkgo/v2 v2.19.0
+	github.com/onsi/gomega v1.33.1
 	github.com/opencontainers/runc v1.1.13
 	github.com/prometheus/client_golang v1.18.0
 	github.com/smartystreets/goconvey v1.8.1
diff --git a/go.sum b/go.sum
index 57c3ec177..9561cc3d6 100644
--- a/go.sum
+++ b/go.sum
@@ -190,10 +190,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
-github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0=
-github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA=
-github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os=
-github.com/onsi/gomega v1.34.0/go.mod h1:MIKI8c+f+QLWk+hxbePD4i0LMJSExPaZOVfkoex4cAo=
+github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs=
diff --git a/pkg/nfd-gc/nfd-gc.go b/pkg/nfd-gc/nfd-gc.go
index a092aa1b0..194ae8a8e 100644
--- a/pkg/nfd-gc/nfd-gc.go
+++ b/pkg/nfd-gc/nfd-gc.go
@@ -21,23 +21,28 @@ import (
 	"fmt"
 	"time"
 
-	topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
+	topologyv1alpha2 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha2"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/sets"
-	"k8s.io/client-go/informers"
-	"k8s.io/client-go/kubernetes"
+	metadataclient "k8s.io/client-go/metadata"
+	"k8s.io/client-go/metadata/metadatainformer"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/klog/v2"
 
-	nfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned"
 	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/pkg/version"
 )
 
+var (
+	gvrNF   = nfdv1alpha1.SchemeGroupVersion.WithResource("nodefeatures")
+	gvrNRT  = topologyv1alpha2.SchemeGroupVersion.WithResource("noderesourcetopologies")
+	gvrNode = corev1.SchemeGroupVersion.WithResource("nodes")
+)
+
 // Args are the command line arguments
 type Args struct {
 	GCPeriod    time.Duration
@@ -51,11 +56,10 @@ type NfdGarbageCollector interface {
 }
 
 type nfdGarbageCollector struct {
-	args       *Args
-	stopChan   chan struct{}
-	nfdClient  nfdclientset.Interface
-	topoClient topologyclientset.Interface
-	factory    informers.SharedInformerFactory
+	args     *Args
+	stopChan chan struct{}
+	client   metadataclient.Interface
+	factory  metadatainformer.SharedInformerFactory
 }
 
 func New(args *Args) (NfdGarbageCollector, error) {
@@ -64,20 +68,19 @@ func New(args *Args) (NfdGarbageCollector, error) {
 		return nil, err
 	}
 
-	clientset := kubernetes.NewForConfigOrDie(kubeconfig)
+	cli := metadataclient.NewForConfigOrDie(kubeconfig)
 
 	return &nfdGarbageCollector{
-		args:       args,
-		stopChan:   make(chan struct{}),
-		topoClient: topologyclientset.NewForConfigOrDie(kubeconfig),
-		nfdClient:  nfdclientset.NewForConfigOrDie(kubeconfig),
-		factory:    informers.NewSharedInformerFactory(clientset, 5*time.Minute),
+		args:     args,
+		stopChan: make(chan struct{}),
+		client:   cli,
+		factory:  metadatainformer.NewSharedInformerFactory(cli, 0),
 	}, nil
 }
 
 func (n *nfdGarbageCollector) deleteNodeFeature(namespace, name string) {
 	kind := "NodeFeature"
-	if err := n.nfdClient.NfdV1alpha1().NodeFeatures(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
+	if err := n.client.Resource(gvrNF).Namespace(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
 		if errors.IsNotFound(err) {
 			klog.V(2).InfoS("NodeFeature not found, omitting deletion", "nodefeature", klog.KRef(namespace, name))
 			return
@@ -93,7 +96,7 @@ func (n *nfdGarbageCollector) deleteNodeFeature(namespace, name string) {
 
 func (n *nfdGarbageCollector) deleteNRT(nodeName string) {
 	kind := "NodeResourceTopology"
-	if err := n.topoClient.TopologyV1alpha2().NodeResourceTopologies().Delete(context.TODO(), nodeName, metav1.DeleteOptions{}); err != nil {
+	if err := n.client.Resource(gvrNRT).Delete(context.TODO(), nodeName, metav1.DeleteOptions{}); err != nil {
 		if errors.IsNotFound(err) {
 			klog.V(2).InfoS("NodeResourceTopology not found, omitting deletion", "nodeName", nodeName)
 			return
@@ -115,17 +118,18 @@ func (n *nfdGarbageCollector) deleteNodeHandler(object interface{}) {
 		obj = deletedFinalStateUnknown.Obj
 	}
 
-	node, ok := obj.(*corev1.Node)
+	meta, ok := obj.(*metav1.PartialObjectMetadata)
 	if !ok {
-		klog.InfoS("cannot convert object to v1.Node", "object", object)
+		klog.InfoS("cannot convert object to metav1.ObjectMeta", "object", object)
 		return
 	}
+	nodeName := meta.ObjectMeta.GetName()
 
-	n.deleteNRT(node.GetName())
+	n.deleteNRT(nodeName)
 
 	// Delete all NodeFeature objects (from all namespaces) targeting the deleted node
-	nfListOptions := metav1.ListOptions{LabelSelector: nfdv1alpha1.NodeFeatureObjNodeNameLabel + "=" + node.GetName()}
-	if nfs, err := n.nfdClient.NfdV1alpha1().NodeFeatures("").List(context.TODO(), nfListOptions); err != nil {
+	nfListOptions := metav1.ListOptions{LabelSelector: nfdv1alpha1.NodeFeatureObjNodeNameLabel + "=" + nodeName}
+	if nfs, err := n.client.Resource(gvrNF).List(context.TODO(), nfListOptions); err != nil {
 		klog.ErrorS(err, "failed to list NodeFeature objects")
 	} else {
 		for _, nf := range nfs.Items {
@@ -137,24 +141,25 @@ func (n *nfdGarbageCollector) deleteNodeHandler(object interface{}) {
 // garbageCollect removes all stale API objects
 func (n *nfdGarbageCollector) garbageCollect() {
 	klog.InfoS("performing garbage collection")
-	nodes, err := n.factory.Core().V1().Nodes().Lister().List(labels.Everything())
+	objs, err := n.factory.ForResource(gvrNode).Lister().List(labels.Everything())
 	if err != nil {
 		klog.ErrorS(err, "failed to list Node objects")
 		return
 	}
 	nodeNames := sets.NewString()
-	for _, node := range nodes {
-		nodeNames.Insert(node.Name)
+	for _, obj := range objs {
+		meta := obj.(*metav1.PartialObjectMetadata).ObjectMeta
+		nodeNames.Insert(meta.Name)
 	}
 
 	// Handle NodeFeature objects
-	nfs, err := n.nfdClient.NfdV1alpha1().NodeFeatures("").List(context.TODO(), metav1.ListOptions{})
+	objMetas, err := n.client.Resource(gvrNF).List(context.TODO(), metav1.ListOptions{})
 	if errors.IsNotFound(err) {
 		klog.V(2).InfoS("NodeFeature CRD does not exist")
 	} else if err != nil {
 		klog.ErrorS(err, "failed to list NodeFeature objects")
 	} else {
-		for _, nf := range nfs.Items {
+		for _, nf := range objMetas.Items {
 			nodeName, ok := nf.GetLabels()[nfdv1alpha1.NodeFeatureObjNodeNameLabel]
 			if !ok {
 				klog.InfoS("node name label missing from NodeFeature object", "nodefeature", klog.KObj(&nf))
@@ -166,13 +171,13 @@ func (n *nfdGarbageCollector) garbageCollect() {
 	}
 
 	// Handle NodeResourceTopology objects
-	nrts, err := n.topoClient.TopologyV1alpha2().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+	objMetas, err = n.client.Resource(gvrNRT).List(context.TODO(), metav1.ListOptions{})
 	if errors.IsNotFound(err) {
 		klog.V(2).InfoS("NodeResourceTopology CRD does not exist")
 	} else if err != nil {
 		klog.ErrorS(err, "failed to list NodeResourceTopology objects")
 	} else {
-		for _, nrt := range nrts.Items {
+		for _, nrt := range objMetas.Items {
 			if !nodeNames.Has(nrt.Name) {
 				n.deleteNRT(nrt.Name)
 			}
@@ -199,7 +204,7 @@ func (n *nfdGarbageCollector) periodicGC(gcPeriod time.Duration) {
 }
 
 func (n *nfdGarbageCollector) startNodeInformer() error {
-	nodeInformer := n.factory.Core().V1().Nodes().Informer()
+	nodeInformer := n.factory.ForResource(gvrNode).Informer()
 
 	if _, err := nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
 		DeleteFunc: n.deleteNodeHandler,
diff --git a/pkg/nfd-master/nfd-api-controller.go b/pkg/nfd-master/nfd-api-controller.go
index 81b5bd35e..fa9ac738f 100644
--- a/pkg/nfd-master/nfd-api-controller.go
+++ b/pkg/nfd-master/nfd-api-controller.go
@@ -29,7 +29,6 @@ import (
 	nfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned"
 	nfdscheme "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned/scheme"
 	nfdinformers "sigs.k8s.io/node-feature-discovery/api/generated/informers/externalversions"
-	nfdinformersv1alpha1 "sigs.k8s.io/node-feature-discovery/api/generated/informers/externalversions/nfd/v1alpha1"
 	nfdlisters "sigs.k8s.io/node-feature-discovery/api/generated/listers/nfd/v1alpha1"
 	nfdv1alpha1 "sigs.k8s.io/node-feature-discovery/api/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
@@ -68,23 +67,13 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC
 	}
 
 	nfdClient := nfdclientset.NewForConfigOrDie(config)
-
 	klog.V(2).InfoS("initializing new NFD API controller", "options", utils.DelayedDumper(nfdApiControllerOptions))
 
 	informerFactory := nfdinformers.NewSharedInformerFactory(nfdClient, nfdApiControllerOptions.ResyncPeriod)
 
 	// Add informer for NodeFeature objects
 	if !nfdApiControllerOptions.DisableNodeFeature {
-		tweakListOpts := func(opts *metav1.ListOptions) {
-			// Tweak list opts on initial sync to avoid timeouts on the apiserver.
-			// NodeFeature objects are huge and the Kubernetes apiserver
-			// (v1.30) experiences http handler timeouts when the resource
-			// version is set to some non-empty value (TODO: find out why).
-			if opts.ResourceVersion == "0" {
-				opts.ResourceVersion = ""
-			}
-		}
-		featureInformer := nfdinformersv1alpha1.New(informerFactory, "", tweakListOpts).NodeFeatures()
+		featureInformer := informerFactory.Nfd().V1alpha1().NodeFeatures()
 		if _, err := featureInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 			AddFunc: func(obj interface{}) {
 				nfr := obj.(*nfdv1alpha1.NodeFeature)


Test patch:
diff --git a/pkg/nfd-gc/nfd-gc_test.go b/pkg/nfd-gc/nfd-gc_test.go
index 02323aca8..d93c48748 100644
--- a/pkg/nfd-gc/nfd-gc_test.go
+++ b/pkg/nfd-gc/nfd-gc_test.go
@@ -21,17 +21,16 @@ import (
 	"testing"
 	"time"
 
-	"github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha2"
-	topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
-	faketopologyv1alpha2 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned/fake"
+	topologyv1alpha2 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha2"
+
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/sets"
-	"k8s.io/client-go/informers"
-	k8sclientset "k8s.io/client-go/kubernetes"
-	fakek8sclientset "k8s.io/client-go/kubernetes/fake"
-	fakenfdclientset "sigs.k8s.io/node-feature-discovery/api/generated/clientset/versioned/fake"
+	metadataclient "k8s.io/client-go/metadata"
+	"k8s.io/client-go/metadata/fake"
+	fakemetadataclient "k8s.io/client-go/metadata/fake"
+	"k8s.io/client-go/metadata/metadatainformer"
 
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -43,7 +42,7 @@ func TestNRTGC(t *testing.T) {
 		errChan := make(chan error)
 		go func() { errChan <- gc.Run() }()
 
-		So(waitForNRT(gc.topoClient), ShouldBeTrue)
+		So(waitForNRT(gc.client), ShouldBeTrue)
 
 		gc.Stop()
 		So(<-errChan, ShouldBeNil)
@@ -54,7 +53,7 @@ func TestNRTGC(t *testing.T) {
 		errChan := make(chan error)
 		go func() { errChan <- gc.Run() }()
 
-		So(waitForNRT(gc.topoClient, "node1"), ShouldBeTrue)
+		So(waitForNRT(gc.client, "node1"), ShouldBeTrue)
 
 		gc.Stop()
 		So(<-errChan, ShouldBeNil)
@@ -65,85 +64,85 @@ func TestNRTGC(t *testing.T) {
 		errChan := make(chan error)
 		go func() { errChan <- gc.Run() }()
 
-		err := gc.k8sClient.CoreV1().Nodes().Delete(context.TODO(), "node1", metav1.DeleteOptions{})
+		gvr := corev1.SchemeGroupVersion.WithResource("nodes")
+		err := gc.client.Resource(gvr).Delete(context.TODO(), "node1", metav1.DeleteOptions{})
 		So(err, ShouldBeNil)
 
-		So(waitForNRT(gc.topoClient, "node2"), ShouldBeTrue)
+		So(waitForNRT(gc.client, "node2"), ShouldBeTrue)
 	})
 	Convey("periodic GC should remove obsolete NRT", t, func() {
 		gc := newMockGC([]string{"node1", "node2"}, []string{"node1", "node2"})
 		// Override period to run fast
 		gc.args.GCPeriod = 100 * time.Millisecond
 
-		nrt := v1alpha2.NodeResourceTopology{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: "not-existing",
-			},
-		}
+		nrt := createPartialObjectMetadata("topology.node.k8s.io/v1alpha2", "NodeResourceTopology", "", "not-existing")
 
 		errChan := make(chan error)
 		go func() { errChan <- gc.Run() }()
 
-		_, err := gc.topoClient.TopologyV1alpha2().NodeResourceTopologies().Create(context.TODO(), &nrt, metav1.CreateOptions{})
+		gvr := topologyv1alpha2.SchemeGroupVersion.WithResource("noderesourcetopologies")
+		_, err := gc.client.Resource(gvr).(fake.MetadataClient).CreateFake(nrt, metav1.CreateOptions{})
 		So(err, ShouldBeNil)
 
-		So(waitForNRT(gc.topoClient, "node1", "node2"), ShouldBeTrue)
+		So(waitForNRT(gc.client, "node1", "node2"), ShouldBeTrue)
 	})
 }
 
 func newMockGC(nodes, nrts []string) *mockGC {
-	k8sClient := fakek8sclientset.NewSimpleClientset(createFakeNodes(nodes...)...)
+	// Create fake objects
+	objs := []runtime.Object{}
+	for _, name := range nodes {
+		objs = append(objs, createPartialObjectMetadata("v1", "Node", "", name))
+	}
+	for _, name := range nrts {
+		objs = append(objs, createPartialObjectMetadata("topology.node.k8s.io/v1alpha2", "NodeResourceTopology", "", name))
+	}
+
+	scheme := fake.NewTestScheme()
+	_ = metav1.AddMetaToScheme(scheme)
+	cli := fakemetadataclient.NewSimpleMetadataClient(scheme, objs...)
 	return &mockGC{
 		nfdGarbageCollector: nfdGarbageCollector{
-			factory:    informers.NewSharedInformerFactory(k8sClient, 5*time.Minute),
-			nfdClient:  fakenfdclientset.NewSimpleClientset(),
-			topoClient: faketopologyv1alpha2.NewSimpleClientset(createFakeNRTs(nrts...)...),
-			stopChan:   make(chan struct{}),
+			factory:  metadatainformer.NewSharedInformerFactory(cli, 0),
+			client:   cli,
+			stopChan: make(chan struct{}),
 			args: &Args{
 				GCPeriod: 10 * time.Minute,
 			},
 		},
-		k8sClient: k8sClient,
+		client: cli,
 	}
 }
 
-func createFakeNodes(names ...string) []runtime.Object {
-	nodes := make([]runtime.Object, len(names))
-	for i, n := range names {
-		nodes[i] = &corev1.Node{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: n,
-			}}
-	}
-	return nodes
-}
-
-func createFakeNRTs(names ...string) []runtime.Object {
-	nrts := make([]runtime.Object, len(names))
-	for i, n := range names {
-		nrts[i] = &v1alpha2.NodeResourceTopology{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: n,
-			}}
+func createPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata {
+	return &metav1.PartialObjectMetadata{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: apiVersion,
+			Kind:       kind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      name,
+		},
 	}
-	return nrts
 }
 
 type mockGC struct {
 	nfdGarbageCollector
 
-	k8sClient k8sclientset.Interface
+	client metadataclient.Interface
 }
 
-func waitForNRT(cli topologyclientset.Interface, names ...string) bool {
+func waitForNRT(cli metadataclient.Interface, names ...string) bool {
 	nameSet := sets.NewString(names...)
+	gvr := topologyv1alpha2.SchemeGroupVersion.WithResource("noderesourcetopologies")
 	for i := 0; i < 2; i++ {
-		nrts, err := cli.TopologyV1alpha2().NodeResourceTopologies().List(context.TODO(), metav1.ListOptions{})
+		rsp, err := cli.Resource(gvr).List(context.TODO(), metav1.ListOptions{})
 		So(err, ShouldBeNil)
 
 		nrtNames := sets.NewString()
-		for _, nrt := range nrts.Items {
-			nrtNames.Insert(nrt.Name)
+		for _, meta := range rsp.Items {
+			nrtNames.Insert(meta.Name)
 		}
 
 		if nrtNames.Equal(nameSet) {