Repository: kubernetes-sigs/kro

Issue:
High memory usage after applying RGDs in cluster with large CRDs
### Description


**Observed Behavior**:

After deploying the KRO controller the initial startup has a large memory footprint. In my cluster this is almost 2GB. After creating or updating and RGD additional memory spikes occur with each apply. Depending on the memory limit set on the deployment this may result in the PODs being evicted due to OOM.  

There is a high possibility that this is related to the size of the CRDs in the cluster. My cluster has about 200 CRDs including some very large ones like CAPI.

here is a graph of the memory usage at startup with no RGDs applied. Then after some time it reduces (assuming GC) and then the spike is when a simple RGD is applied. 

<img width="360" height="297" alt="Image" src="https://github.com/user-attachments/assets/ce1652f0-d212-4552-866a-24cb699f3e4d" />

Slack Thread: https://kubernetes.slack.com/archives/C081TMY9D6Y/p1760114567757019


**Expected Behavior**:

Memory usage is not as high at startup and does not spike and cause OOM when applying RGDs

**Reproduction Steps** (Please include `ResourceGraphDefinition` and `Instances` files):

1. Deploy KRO into a cluster with large CRDs and monitor the startup memory usage
2. apply any RGD and watch the memory spike.

**Versions**:
- kro version: `0.4.1`
- Kubernetes Version (`kubectl version`): `v1.31.6`


### Which option describes the most your issue?

_No response_

Hints from issue comments:
@jakobmoellerdev: @warroyo do you have a producer someone can do from 0 ? we will do our best for best effort fixes but it makes life much easier for us to verify a fix@warroyo: I will  reproduce the issue on a local cluster and write up the steps. @warroyo: I did some more testing and ruled out the high memory usage at startup. this was just some misleading info in the metrics I was looking at in the graphs. the memory issue is only happening when creating/updating RGDs@warroyo: pprof details after applying an RGD:

```
(pprof) top
Showing nodes accounting for 815.86MB, 99.51% of 819.88MB total
Dropped 21 nodes (cum <= 4.10MB)
Showing top 10 nodes out of 46
      flat  flat%   sum%        cum   cum%
  673.68MB 82.17% 82.17%   798.98MB 97.45%  github.com/kro-run/kro/pkg/graph/emulator.(*Emulator).generateObject
      54MB  6.59% 88.75%       54MB  6.59%  fmt.Sprintf
      43MB  5.24% 94.00%   798.98MB 97.45%  github.com/kro-run/kro/pkg/graph/emulator.(*Emulator).generateValue
   27.30MB  3.33% 97.33%   798.98MB 97.45%  github.com/kro-run/kro/pkg/graph/emulator.(*Emulator).generateArray
   10.50MB  1.28% 98.61%    10.50MB  1.28%  reflect.mapassign0
    4.37MB  0.53% 99.14%     4.37MB  0.53%  sigs.k8s.io/json/internal/golang/encoding/json.(*decodeState).literalStore
    1.50MB  0.18% 99.33%     8.01MB  0.98%  k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.(*JSONSchemaPropsOrArray).UnmarshalJSON
       1MB  0.12% 99.45%       55MB  6.71%  github.com/kro-run/kro/pkg/graph/emulator.(*Emulator).generateString
    0.50MB 0.061% 99.51%    18.88MB  2.30%  sigs.k8s.io/json/internal/golang/encoding/json.(*decodeState).object
         0     0% 99.51%   798.98MB 97.45%  github.com/kro-run/kro/pkg/controller/resourcegraphdefinition.(*ResourceGraphDefinitionReconciler).Reconcile
```@jakobmoellerdev: alrighty thanks for that, this is super helpful. We will look into getting rid of emulator or reducing memory overhead drastically. THanks for the insights!@jakobmoellerdev: fixed by https://github.com/kubernetes-sigs/kro/pull/756@a-hilaly: 0.6 should have lower mem usage (which is being released very soon). Stay tuned @warroyo !

Solution patch:
diff --git a/cmd/controller/main.go b/cmd/controller/main.go
index d22decf..d595a00 100644
--- a/cmd/controller/main.go
+++ b/cmd/controller/main.go
@@ -25,6 +25,7 @@ import (
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/healthz"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
@@ -34,7 +35,7 @@ import (
 	resourcegraphdefinitionctrl "github.com/kubernetes-sigs/kro/pkg/controller/resourcegraphdefinition"
 	"github.com/kubernetes-sigs/kro/pkg/dynamiccontroller"
 	"github.com/kubernetes-sigs/kro/pkg/graph"
-	//+kubebuilder:scaffold:imports
+	// +kubebuilder:scaffold:imports
 )
 
 var (
@@ -47,7 +48,7 @@ func init() {
 
 	utilruntime.Must(xv1alpha1.AddToScheme(scheme))
 	utilruntime.Must(extv1.AddToScheme(scheme))
-	//+kubebuilder:scaffold:scheme
+	// +kubebuilder:scaffold:scheme
 }
 
 type customLevelEnabler struct {
@@ -151,6 +152,9 @@ func main() {
 		Metrics: metricsserver.Options{
 			BindAddress: metricsAddr,
 		},
+		Client: client.Options{
+			HTTPClient: set.HTTPClient(),
+		},
 		GracefulShutdownTimeout: &gracefulShutdownTimeout,
 		HealthProbeBindAddress:  probeAddr,
 		LeaderElection:          enableLeaderElection,
@@ -185,7 +189,7 @@ func main() {
 	}, set.Dynamic())
 
 	resourceGraphDefinitionGraphBuilder, err := graph.NewBuilder(
-		restConfig,
+		restConfig, set.HTTPClient(),
 	)
 	if err != nil {
 		setupLog.Error(err, "unable to create resource graph definition graph builder")
@@ -209,7 +213,7 @@ func main() {
 		os.Exit(1)
 	}
 
-	//+kubebuilder:scaffold:builder
+	// +kubebuilder:scaffold:builder
 
 	if err = mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
 		setupLog.Error(err, "unable to set up health check")
diff --git a/cmd/kro/commands/generate/generate_utils.go b/cmd/kro/commands/generate/generate_utils.go
index e899b69..209221a 100644
--- a/cmd/kro/commands/generate/generate_utils.go
+++ b/cmd/kro/commands/generate/generate_utils.go
@@ -33,7 +33,7 @@ func createGraphBuilder(rgd *v1alpha1.ResourceGraphDefinition) (*graph.Graph, er
 
 	restConfig := set.RESTConfig()
 
-	builder, err := graph.NewBuilder(restConfig)
+	builder, err := graph.NewBuilder(restConfig, set.HTTPClient())
 	if err != nil {
 		return nil, fmt.Errorf("failed to create graph builder: %w", err)
 	}
diff --git a/cmd/kro/commands/validate/validate.go b/cmd/kro/commands/validate/validate.go
index 9a08bec..4555491 100644
--- a/cmd/kro/commands/validate/validate.go
+++ b/cmd/kro/commands/validate/validate.go
@@ -72,9 +72,7 @@ func validateRGD(rgd *v1alpha1.ResourceGraphDefinition) error {
 		return fmt.Errorf("failed to create client set: %w", err)
 	}
 
-	restConfig := set.RESTConfig()
-
-	builder, err := graph.NewBuilder(restConfig)
+	builder, err := graph.NewBuilder(set.RESTConfig(), set.HTTPClient())
 	if err != nil {
 		return fmt.Errorf("failed to create graph builder: %w", err)
 	}
diff --git a/pkg/client/crd.go b/pkg/client/crd.go
index 04a3a6c..dc203a6 100644
--- a/pkg/client/crd.go
+++ b/pkg/client/crd.go
@@ -206,7 +206,7 @@ func (w *CRDWrapper) waitForReady(ctx context.Context, name string) error {
 // where multiple ResourceGraphDefinitions try to manage the same CRD, or where a
 // CRD created outside of KRO is accidentally overwritten.
 func (w *CRDWrapper) verifyOwnership(existingCRD *v1.CustomResourceDefinition, newCRD v1.CustomResourceDefinition) error {
-	if !metadata.IsKROOwned(existingCRD.ObjectMeta) {
+	if !metadata.IsKROOwned(&existingCRD.ObjectMeta) {
 		return fmt.Errorf("conflict detected: CRD %s already exists and is not owned by KRO", existingCRD.Name)
 	}
 
diff --git a/pkg/client/fake/fake.go b/pkg/client/fake/fake.go
index 02e2276..a4ad989 100644
--- a/pkg/client/fake/fake.go
+++ b/pkg/client/fake/fake.go
@@ -16,6 +16,7 @@ package fake
 
 import (
 	"context"
+	"net/http"
 
 	"github.com/kubernetes-sigs/kro/pkg/client"
 	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -33,6 +34,7 @@ type FakeSet struct {
 	ApiExtensionsClient apiextensionsv1.ApiextensionsV1Interface
 	Config              *rest.Config
 	restMapper          meta.RESTMapper
+	HTTP                *http.Client
 }
 
 var _ client.SetInterface = (*FakeSet)(nil)
@@ -45,6 +47,10 @@ func NewFakeSet(dynamicClient dynamic.Interface) *FakeSet {
 	}
 }
 
+func (f *FakeSet) HTTPClient() *http.Client {
+	return f.HTTP
+}
+
 // Kubernetes returns the standard Kubernetes clientset
 func (f *FakeSet) Kubernetes() kubernetes.Interface {
 	return f.KubernetesClient
diff --git a/pkg/client/set.go b/pkg/client/set.go
index 319ee2e..2d5aaf4 100644
--- a/pkg/client/set.go
+++ b/pkg/client/set.go
@@ -16,6 +16,7 @@ package client
 
 import (
 	"fmt"
+	"net/http"
 
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -28,6 +29,7 @@ import (
 
 // SetInterface provides a unified interface for different Kubernetes clients
 type SetInterface interface {
+	HTTPClient() *http.Client
 
 	// Kubernetes returns the standard Kubernetes clientset
 	Kubernetes() kubernetes.Interface
@@ -59,6 +61,7 @@ type Set struct {
 	apiExtensionsV1 *apiextensionsv1.ApiextensionsV1Client
 	// restMapper is a REST mapper for the Kubernetes API server
 	restMapper meta.RESTMapper
+	httpClient *http.Client
 }
 
 var _ SetInterface = (*Set)(nil)
@@ -111,22 +114,22 @@ func (c *Set) init() error {
 	var err error
 
 	// share http client between all k8s clients
-	sharedHttpClient, err := rest.HTTPClientFor(c.config)
+	c.httpClient, err = rest.HTTPClientFor(c.config)
 	if err != nil {
 		return fmt.Errorf("failed to create HTTP client: %w", err)
 	}
 
-	c.kubernetes, err = kubernetes.NewForConfigAndClient(c.config, sharedHttpClient)
+	c.kubernetes, err = kubernetes.NewForConfigAndClient(c.config, c.httpClient)
 	if err != nil {
 		return err
 	}
 
-	c.dynamic, err = dynamic.NewForConfigAndClient(c.config, sharedHttpClient)
+	c.dynamic, err = dynamic.NewForConfigAndClient(c.config, c.httpClient)
 	if err != nil {
 		return err
 	}
 
-	c.apiExtensionsV1, err = apiextensionsv1.NewForConfigAndClient(c.config, sharedHttpClient)
+	c.apiExtensionsV1, err = apiextensionsv1.NewForConfigAndClient(c.config, c.httpClient)
 	if err != nil {
 		return err
 	}
@@ -134,6 +137,10 @@ func (c *Set) init() error {
 	return nil
 }
 
+func (c *Set) HTTPClient() *http.Client {
+	return c.httpClient
+}
+
 // Kubernetes returns the standard Kubernetes clientset
 func (c *Set) Kubernetes() kubernetes.Interface {
 	return c.kubernetes
diff --git a/pkg/controller/resourcegraphdefinition/controller.go b/pkg/controller/resourcegraphdefinition/controller.go
index 74200a5..33767d5 100644
--- a/pkg/controller/resourcegraphdefinition/controller.go
+++ b/pkg/controller/resourcegraphdefinition/controller.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/go-logr/logr"
 	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -37,9 +38,9 @@ import (
 	"github.com/kubernetes-sigs/kro/pkg/metadata"
 )
 
-//+kubebuilder:rbac:groups=kro.run,resources=resourcegraphdefinitions,verbs=get;list;watch;create;update;patch;delete
-//+kubebuilder:rbac:groups=kro.run,resources=resourcegraphdefinitions/status,verbs=get;update;patch
-//+kubebuilder:rbac:groups=kro.run,resources=resourcegraphdefinitions/finalizers,verbs=update
+// +kubebuilder:rbac:groups=kro.run,resources=resourcegraphdefinitions,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=kro.run,resources=resourcegraphdefinitions/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=kro.run,resources=resourcegraphdefinitions/finalizers,verbs=update
 
 // ResourceGraphDefinitionReconciler reconciles a ResourceGraphDefinition object
 type ResourceGraphDefinitionReconciler struct {
@@ -108,7 +109,7 @@ func (r *ResourceGraphDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) e
 				MaxConcurrentReconciles: r.maxConcurrentReconciles,
 			},
 		).
-		Watches(
+		WatchesMetadata(
 			&extv1.CustomResourceDefinition{},
 			handler.EnqueueRequestsFromMapFunc(r.findRGDsForCRD),
 			builder.WithPredicates(predicate.Funcs{
@@ -129,17 +130,17 @@ func (r *ResourceGraphDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) e
 // findRGDsForCRD returns a list of reconcile requests for the ResourceGraphDefinition
 // that owns the given CRD. It is used to trigger reconciliation when a CRD is updated.
 func (r *ResourceGraphDefinitionReconciler) findRGDsForCRD(ctx context.Context, obj client.Object) []reconcile.Request {
-	crd, ok := obj.(*extv1.CustomResourceDefinition)
-	if !ok {
+	mobj, err := meta.Accessor(obj)
+	if err != nil {
 		return nil
 	}
 
 	// Check if the CRD is owned by a ResourceGraphDefinition
-	if !metadata.IsKROOwned(crd.ObjectMeta) {
+	if !metadata.IsKROOwned(mobj) {
 		return nil
 	}
 
-	rgdName, ok := crd.Labels[metadata.ResourceGraphDefinitionNameLabel]
+	rgdName, ok := mobj.GetLabels()[metadata.ResourceGraphDefinitionNameLabel]
 	if !ok {
 		return nil
 	}
diff --git a/pkg/graph/builder.go b/pkg/graph/builder.go
index ee66bf5..91f0be4 100644
--- a/pkg/graph/builder.go
+++ b/pkg/graph/builder.go
@@ -16,18 +16,19 @@ package graph
 
 import (
 	"fmt"
+	"net/http"
 	"slices"
 
 	"github.com/google/cel-go/cel"
 	"github.com/google/cel-go/common/types/ref"
 	"golang.org/x/exp/maps"
 	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	k8sschema "k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/yaml"
 	"k8s.io/apiserver/pkg/cel/openapi/resolver"
-	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 
 	"github.com/kubernetes-sigs/kro/api/v1alpha1"
 	krocel "github.com/kubernetes-sigs/kro/pkg/cel"
@@ -45,19 +46,24 @@ import (
 
 // NewBuilder creates a new GraphBuilder instance.
 func NewBuilder(
-	clientConfig *rest.Config,
+	clientConfig *rest.Config, httpClient *http.Client,
 ) (*Builder, error) {
-	schemaResolver, dc, err := schemaresolver.NewCombinedResolver(clientConfig)
+	schemaResolver, err := schemaresolver.NewCombinedResolver(clientConfig, httpClient)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create schema resolver: %w", err)
 	}
 
+	rm, err := apiutil.NewDynamicRESTMapper(clientConfig, httpClient)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create dynamic REST mapper: %w", err)
+	}
+
 	resourceEmulator := emulator.NewEmulator()
 
 	rgBuilder := &Builder{
 		resourceEmulator: resourceEmulator,
 		schemaResolver:   schemaResolver,
-		discoveryClient:  dc,
+		restMapper:       rm,
 	}
 	return rgBuilder, nil
 }
@@ -95,7 +101,7 @@ type Builder struct {
 	// Maybe there is a better way, if anything probably there is a better way to
 	// validate the CEL expressions. To revisit.
 	resourceEmulator *emulator.Emulator
-	discoveryClient  discovery.DiscoveryInterface
+	restMapper       meta.RESTMapper
 }
 
 // NewResourceGraphDefinition creates a new ResourceGraphDefinition object from the given ResourceGraphDefinition
@@ -130,24 +136,12 @@ func (b *Builder) NewResourceGraphDefinition(originalCR *v1alpha1.ResourceGraphD
 	//    CEL expressions.
 	// 4. Extract the CEL expressions from the resource + validate them.
 
-	namespacedResources := map[k8sschema.GroupKind]bool{}
-	apiResourceList, err := b.discoveryClient.ServerPreferredNamespacedResources()
-	if err != nil {
-		return nil, fmt.Errorf("failed to retrieve Kubernetes namespaced resources: %w", err)
-	}
-	for _, resourceList := range apiResourceList {
-		for _, r := range resourceList.APIResources {
-			gvk := k8sschema.FromAPIVersionAndKind(resourceList.GroupVersion, r.Kind)
-			namespacedResources[gvk.GroupKind()] = r.Namespaced
-		}
-	}
-
 	// we'll also store the resources in a map for easy access later.
 	resources := make(map[string]*Resource)
 	for i, rgResource := range rgd.Spec.Resources {
 		id := rgResource.ID
 		order := i
-		r, err := b.buildRGResource(rgResource, namespacedResources, order)
+		r, err := b.buildRGResource(rgResource, order)
 		if err != nil {
 			return nil, fmt.Errorf("failed to build resource %q: %w", id, err)
 		}
@@ -213,7 +207,7 @@ func (b *Builder) NewResourceGraphDefinition(originalCR *v1alpha1.ResourceGraphD
 	// in the instance resource.
 	// To do that, we need to isolate each resource
 	// and evaluate the CEL expressions in the context of the resource graph definition.
-	//This is done
+	// This is done
 	// by dry-running the CEL expressions against the emulated resources.
 	err = validateResourceCELExpressions(resources, instance)
 	if err != nil {
@@ -273,7 +267,6 @@ func (b *Builder) buildExternalRefResource(
 // from the schema.
 func (b *Builder) buildRGResource(
 	rgResource *v1alpha1.Resource,
-	namespacedResources map[k8sschema.GroupKind]bool,
 	order int,
 ) (*Resource, error) {
 	// 1. We need to unmarshal the resource into a map[string]interface{} to
@@ -356,19 +349,22 @@ func (b *Builder) buildRGResource(
 		return nil, fmt.Errorf("failed to parse includeWhen expressions: %v", err)
 	}
 
-	_, isNamespaced := namespacedResources[gvk.GroupKind()]
+	mapping, err := b.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get REST mapping for resource %s: %w", rgResource.ID, err)
+	}
 
 	// Note that at this point we don't inject the dependencies into the resource.
 	return &Resource{
 		id:                     rgResource.ID,
-		gvr:                    metadata.GVKtoGVR(gvk),
+		gvr:                    mapping.Resource,
 		schema:                 resourceSchema,
 		emulatedObject:         emulatedResource,
 		originalObject:         &unstructured.Unstructured{Object: resourceObject},
 		variables:              resourceVariables,
 		readyWhenExpressions:   readyWhen,
 		includeWhenExpressions: includeWhen,
-		namespaced:             isNamespaced,
+		namespaced:             mapping.Scope.Name() == meta.RESTScopeNameNamespace,
 		order:                  order,
 		isExternalRef:          rgResource.ExternalRef != nil,
 	}, nil
@@ -388,9 +384,7 @@ func (b *Builder) buildRGResource(
 func (b *Builder) buildDependencyGraph(
 	resources map[string]*Resource,
 ) (
-	// directed acyclic graph
-	*dag.DirectedAcyclicGraph[string],
-	// map of runtime variables per resource
+	*dag.DirectedAcyclicGraph[string], // directed acyclic graph
 	error,
 ) {
 
diff --git a/pkg/graph/schema/resolver/resolver.go b/pkg/graph/schema/resolver/resolver.go
index acef30c..401e388 100644
--- a/pkg/graph/schema/resolver/resolver.go
+++ b/pkg/graph/schema/resolver/resolver.go
@@ -15,6 +15,7 @@
 package resolver
 
 import (
+	"net/http"
 	"time"
 
 	"k8s.io/apiextensions-apiserver/pkg/generated/openapi"
@@ -25,11 +26,11 @@ import (
 )
 
 // NewCombinedResolver creates a new schema resolver that can resolve both core and client types.
-func NewCombinedResolver(clientConfig *rest.Config) (resolver.SchemaResolver, discovery.DiscoveryInterface, error) {
+func NewCombinedResolver(clientConfig *rest.Config, httpClient *http.Client) (resolver.SchemaResolver, error) {
 	// Create a regular discovery client first
-	discoveryClient, err := discovery.NewDiscoveryClientForConfig(clientConfig)
+	discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(clientConfig, httpClient)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
 	// ClientResolver is a resolver that uses the discovery client to resolve
@@ -54,5 +55,5 @@ func NewCombinedResolver(clientConfig *rest.Config) (resolver.SchemaResolver, di
 	)
 
 	combinedResolver := coreResolver.Combine(cachedResolver)
-	return combinedResolver, discoveryClient, nil
+	return combinedResolver, nil
 }
diff --git a/pkg/metadata/labels.go b/pkg/metadata/labels.go
index ffe9996..9293301 100644
--- a/pkg/metadata/labels.go
+++ b/pkg/metadata/labels.go
@@ -48,8 +48,8 @@ const (
 )
 
 // IsKROOwned returns true if the resource is owned by KRO.
-func IsKROOwned(meta metav1.ObjectMeta) bool {
-	v, ok := meta.Labels[OwnedLabel]
+func IsKROOwned(meta metav1.Object) bool {
+	v, ok := meta.GetLabels()[OwnedLabel]
 	return ok && booleanFromString(v)
 }
 



Test patch:
diff --git a/pkg/graph/builder_test.go b/pkg/graph/builder_test.go
index 8b41f5f..8d18287 100644
--- a/pkg/graph/builder_test.go
+++ b/pkg/graph/builder_test.go
@@ -15,11 +15,14 @@
 package graph
 
 import (
+	"net/http"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	memory2 "k8s.io/client-go/discovery/cached/memory"
 	"k8s.io/client-go/rest"
+	"k8s.io/client-go/restmapper"
 
 	"github.com/kubernetes-sigs/kro/pkg/graph/emulator"
 	"github.com/kubernetes-sigs/kro/pkg/graph/variable"
@@ -30,9 +33,10 @@ import (
 
 func TestGraphBuilder_Validation(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
+	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
 		schemaResolver:   fakeResolver,
-		discoveryClient:  fakeDiscovery,
+		restMapper:       restMapper,
 		resourceEmulator: emulator.NewEmulator(),
 	}
 
@@ -466,9 +470,10 @@ func TestGraphBuilder_Validation(t *testing.T) {
 
 func TestGraphBuilder_DependencyValidation(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
+	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
 		schemaResolver:   fakeResolver,
-		discoveryClient:  fakeDiscovery,
+		restMapper:       restMapper,
 		resourceEmulator: emulator.NewEmulator(),
 	}
 
@@ -1049,9 +1054,10 @@ func TestGraphBuilder_DependencyValidation(t *testing.T) {
 
 func TestGraphBuilder_ExpressionParsing(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
+	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
 		schemaResolver:   fakeResolver,
-		discoveryClient:  fakeDiscovery,
+		restMapper:       restMapper,
 		resourceEmulator: emulator.NewEmulator(),
 	}
 
@@ -1398,16 +1404,17 @@ func validateVariables(t *testing.T, actual []*variable.ResourceField, expected
 }
 
 func TestNewBuilder(t *testing.T) {
-	builder, err := NewBuilder(&rest.Config{})
+	builder, err := NewBuilder(&rest.Config{}, &http.Client{})
 	assert.Nil(t, err)
 	assert.NotNil(t, builder)
 }
 
 func Test_ValidateOpenAPISchema(t *testing.T) {
 	fakeResolver, fakeDiscovery := k8s.NewFakeResolver()
+	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(memory2.NewMemCacheClient(fakeDiscovery))
 	builder := &Builder{
 		schemaResolver:   fakeResolver,
-		discoveryClient:  fakeDiscovery,
+		restMapper:       restMapper,
 		resourceEmulator: emulator.NewEmulator(),
 	}
 
diff --git a/pkg/metadata/labels_test.go b/pkg/metadata/labels_test.go
index b8ccc81..2082757 100644
--- a/pkg/metadata/labels_test.go
+++ b/pkg/metadata/labels_test.go
@@ -59,7 +59,7 @@ func TestIsKROOwned(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			meta := metav1.ObjectMeta{Labels: tc.labels}
-			result := IsKROOwned(meta)
+			result := IsKROOwned(&meta)
 			assert.Equal(t, tc.expected, result)
 		})
 	}
diff --git a/test/integration/environment/setup.go b/test/integration/environment/setup.go
index 23892b2..ab01e9b 100644
--- a/test/integration/environment/setup.go
+++ b/test/integration/environment/setup.go
@@ -131,7 +131,7 @@ func (e *Environment) initializeClients() error {
 	e.CRDManager = e.ClientSet.CRD(kroclient.CRDWrapperConfig{})
 
 	restConfig := e.ClientSet.RESTConfig()
-	e.GraphBuilder, err = graph.NewBuilder(restConfig)
+	e.GraphBuilder, err = graph.NewBuilder(restConfig, e.ClientSet.HTTPClient())
 	if err != nil {
 		return fmt.Errorf("creating graph builder: %w", err)
 	}
diff --git a/test/integration/suites/core/crd_test.go b/test/integration/suites/core/crd_test.go
index 7605c7e..6ac5ec1 100644
--- a/test/integration/suites/core/crd_test.go
+++ b/test/integration/suites/core/crd_test.go
@@ -213,7 +213,7 @@ var _ = Describe("CRD", func() {
 			Eventually(func(g Gomega, ctx SpecContext) {
 				err := env.Client.Get(ctx, types.NamespacedName{Name: crdName}, crd)
 				g.Expect(err).ToNot(HaveOccurred())
-				g.Expect(metadata.IsKROOwned(crd.ObjectMeta)).To(BeTrue())
+				g.Expect(metadata.IsKROOwned(&crd.ObjectMeta)).To(BeTrue())
 				g.Expect(crd.Labels[metadata.ResourceGraphDefinitionNameLabel]).To(Equal("test-crd-owner-1"))
 			}, 10*time.Second, time.Second).WithContext(ctx).Should(Succeed())
 
@@ -271,7 +271,7 @@ var _ = Describe("CRD", func() {
 				}, crd)
 				g.Expect(err).ToNot(HaveOccurred())
 
-				g.Expect(metadata.IsKROOwned(crd.ObjectMeta)).To(BeTrue())
+				g.Expect(metadata.IsKROOwned(&crd.ObjectMeta)).To(BeTrue())
 				g.Expect(crd.Labels[metadata.ResourceGraphDefinitionNameLabel]).To(Equal(rgdName))
 
 				// store the original schema for later comparison