Repository: Kong/kubernetes-ingress-controller
Pull request #7116 (2025-02-13 03:40:03)

[Backport to release/3.4.x] fix(manager): Run AddToScheme only once in scheme.Get
Backport of #7105.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/7116?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) Report
Attention: Patch coverage is `95.83333%` with `1 line` in your changes missing coverage. Please review.
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`release/3.4.x@0c9c290`). [Learn more](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#section-missing-base-commit) about missing BASE report.

| [Files with missing lines](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/7116?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) | Patch % | Lines |
|---|---|---|
| [internal/store/cache\_stores.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/7116?src=pr&el=tree&filepath=internal%2Fstore%2Fcache_stores.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvc3RvcmUvY2FjaGVfc3RvcmVzLmdv) | 0.0% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/7116?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff               @@
##             release/3.4.x   #7116   +/-   ##
===============================================
  Coverage                 ?   76.6%           
===============================================
  Files                    ?     211           
  Lines                    ?   25215           
  Branches                 ?       0           
===============================================
  Hits                     ?   19325           
  Misses                   ?    4886           
  Partials                 ?    1004           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/7116?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a95bca006..aabef6ff2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -100,6 +100,14 @@ Adding a new version? You'll need three changes:
  - [0.0.5](#005)
  - [0.0.4 and prior](#004-and-prior)
 
+## Unreleased
+
+### Fixed
+
+- `AddToScheme` is only run in the initialization of scheme of the manager but
+  not called in `scheme.Get` to reduce the CPU usage.
+  [#7105](https://github.com/Kong/kubernetes-ingress-controller/pull/7105) 
+
 ## [3.4.1]
 
 > Release date: 2025-01-02
diff --git a/internal/controllers/reference/indexer.go b/internal/controllers/reference/indexer.go
index afc4ad1b3..ade56cd6e 100644
--- a/internal/controllers/reference/indexer.go
+++ b/internal/controllers/reference/indexer.go
@@ -40,11 +40,7 @@ func objectKeyFunc(obj client.Object) (string, error) {
 	if !ok {
 		return "", fmt.Errorf("could not convert %s/%s back to client Object", obj.GetNamespace(), obj.GetName())
 	}
-	s, err := scheme.Get()
-	if err != nil {
-		return "", fmt.Errorf("could not get scheme for %s/%s metadata: %w", obj.GetNamespace(), obj.GetName(), err)
-	}
-	err = util.PopulateTypeMeta(o, s)
+	err := util.PopulateTypeMeta(o, scheme.Get())
 	if err != nil {
 		return "", fmt.Errorf("could not populate %s/%s metadata: %w", obj.GetNamespace(), obj.GetName(), err)
 	}
diff --git a/internal/manager/run.go b/internal/manager/run.go
index 79159c441..4d66e3f8c 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -131,10 +131,7 @@ func Run(
 	}
 
 	setupLog.Info("Configuring and building the controller manager")
-	managerOpts, err := setupManagerOptions(ctx, setupLog, c, dbMode)
-	if err != nil {
-		return fmt.Errorf("unable to setup manager options: %w", err)
-	}
+	managerOpts := setupManagerOptions(ctx, setupLog, c, dbMode)
 
 	mgr, err := ctrl.NewManager(kubeconfig, managerOpts)
 	if err != nil {
diff --git a/internal/manager/scheme/scheme.go b/internal/manager/scheme/scheme.go
index cc4621cbb..d66a8c5d3 100644
--- a/internal/manager/scheme/scheme.go
+++ b/internal/manager/scheme/scheme.go
@@ -3,6 +3,7 @@ package scheme
 import (
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
@@ -15,46 +16,27 @@ import (
 	incubatorv1alpha1 "github.com/kong/kubernetes-configuration/api/incubator/v1alpha1"
 )
 
+var kicScheme *runtime.Scheme = initScheme()
+
+func initScheme() *runtime.Scheme {
+	s := runtime.NewScheme()
+	utilruntime.Must(apiextensionsv1.AddToScheme(s))
+	utilruntime.Must(clientgoscheme.AddToScheme(s))
+
+	utilruntime.Must(kongv1.AddToScheme(s))
+	utilruntime.Must(kongv1alpha1.AddToScheme(s))
+	utilruntime.Must(kongv1beta1.AddToScheme(s))
+	utilruntime.Must(incubatorv1alpha1.AddToScheme(s))
+
+	utilruntime.Must(gatewayv1.Install(s))
+	utilruntime.Must(gatewayv1beta1.Install(s))
+	utilruntime.Must(gatewayv1alpha2.Install(s))
+	utilruntime.Must(gatewayv1alpha3.Install(s))
+
+	return s
+}
+
 // Get returns a scheme aware of all types the manager can interact with.
-func Get() (*runtime.Scheme, error) {
-	scheme := runtime.NewScheme()
-
-	if err := apiextensionsv1.AddToScheme(scheme); err != nil {
-		return nil, err
-	}
-
-	if err := clientgoscheme.AddToScheme(scheme); err != nil {
-		return nil, err
-	}
-
-	if err := kongv1.AddToScheme(scheme); err != nil {
-		return nil, err
-	}
-	if err := kongv1alpha1.AddToScheme(scheme); err != nil {
-		return nil, err
-	}
-	if err := kongv1beta1.AddToScheme(scheme); err != nil {
-		return nil, err
-	}
-	if err := incubatorv1alpha1.AddToScheme(scheme); err != nil {
-		return nil, err
-	}
-
-	if err := gatewayv1alpha2.Install(scheme); err != nil {
-		return nil, err
-	}
-
-	if err := gatewayv1alpha3.Install(scheme); err != nil {
-		return nil, err
-	}
-
-	if err := gatewayv1beta1.Install(scheme); err != nil {
-		return nil, err
-	}
-
-	if err := gatewayv1.Install(scheme); err != nil {
-		return nil, err
-	}
-
-	return scheme, nil
+func Get() *runtime.Scheme {
+	return kicScheme
 }
diff --git a/internal/manager/setup.go b/internal/manager/setup.go
index 7c27756af..339c3407c 100644
--- a/internal/manager/setup.go
+++ b/internal/manager/setup.go
@@ -68,12 +68,8 @@ func SetupLoggers(c *Config, output io.Writer) (logr.Logger, error) {
 	return logger, nil
 }
 
-func setupManagerOptions(ctx context.Context, logger logr.Logger, c *Config, dbmode dpconf.DBMode) (ctrl.Options, error) {
+func setupManagerOptions(ctx context.Context, logger logr.Logger, c *Config, dbmode dpconf.DBMode) ctrl.Options {
 	logger.Info("Building the manager runtime scheme and loading apis into the scheme")
-	scheme, err := scheme.Get()
-	if err != nil {
-		return ctrl.Options{}, err
-	}
 
 	// configure the general manager options
 	managerOpts := ctrl.Options{
@@ -85,7 +81,7 @@ func setupManagerOptions(ctx context.Context, logger logr.Logger, c *Config, dbm
 			SkipNameValidation: lo.ToPtr(true),
 		},
 		GracefulShutdownTimeout: c.GracefulShutdownTimeout,
-		Scheme:                  scheme,
+		Scheme:                  scheme.Get(),
 		Metrics: metricsserver.Options{
 			BindAddress: c.MetricsAddr,
 		},
@@ -130,7 +126,7 @@ func setupManagerOptions(ctx context.Context, logger logr.Logger, c *Config, dbm
 		managerOpts.LeaderElectionNamespace = c.LeaderElectionNamespace
 	}
 
-	return managerOpts, nil
+	return managerOpts
 }
 
 const (
diff --git a/internal/store/cache_stores.go b/internal/store/cache_stores.go
index bf0b60f69..974adaca7 100644
--- a/internal/store/cache_stores.go
+++ b/internal/store/cache_stores.go
@@ -15,10 +15,6 @@ import (
 // NewCacheStoresFromObjYAML provides a new CacheStores object given any number of byte arrays containing
 // YAML Kubernetes objects. An error is returned if any provided YAML was not a valid Kubernetes object.
 func NewCacheStoresFromObjYAML(objs ...[]byte) (c CacheStores, err error) {
-	s, err := scheme.Get()
-	if err != nil {
-		return c, err
-	}
 	kobjs := make([]runtime.Object, 0, len(objs))
 	sr := serializer.NewYAMLSerializer(
 		yamlserializer.DefaultMetaFactory,
@@ -30,7 +26,7 @@ func NewCacheStoresFromObjYAML(objs ...[]byte) (c CacheStores, err error) {
 		if err = decodeErr; err != nil {
 			return
 		}
-		if err := util.PopulateTypeMeta(kobj, s); err != nil {
+		if err := util.PopulateTypeMeta(kobj, scheme.Get()); err != nil {
 			return c, err
 		}
 		kobjs = append(kobjs, kobj)


Test patch:
diff --git a/internal/admission/validation/gateway/httproute_test.go b/internal/admission/validation/gateway/httproute_test.go
index 88bcf6748..7e3d3f5d4 100644
--- a/internal/admission/validation/gateway/httproute_test.go
+++ b/internal/admission/validation/gateway/httproute_test.go
@@ -956,7 +956,7 @@ func TestValidateHTTPRoute(t *testing.T) {
 		t.Run(tt.msg, func(t *testing.T) {
 			fakeClient := fakeclient.
 				NewClientBuilder().
-				WithScheme(lo.Must(scheme.Get())).
+				WithScheme(scheme.Get()).
 				WithObjects(tt.cachedObjects...).
 				Build()
 
diff --git a/internal/admission/validator_test.go b/internal/admission/validator_test.go
index 009049f37..d4689092a 100644
--- a/internal/admission/validator_test.go
+++ b/internal/admission/validator_test.go
@@ -1249,7 +1249,7 @@ func TestValidator_ValidateIngress(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			storer := lo.Must(store.NewFakeStore(tc.storerObjects))
 			validator := KongHTTPValidator{
-				ManagerClient: fake.NewClientBuilder().WithScheme(lo.Must(managerscheme.Get())).WithObjects(tc.clientObjects...).Build(),
+				ManagerClient: fake.NewClientBuilder().WithScheme(managerscheme.Get()).WithObjects(tc.clientObjects...).Build(),
 				Storer:        storer,
 				AdminAPIServicesProvider: fakeServicesProvider{
 					routeSvc: &fakeRouteSvc{
diff --git a/internal/controllers/configuration/kongupstreampolicy_utils_test.go b/internal/controllers/configuration/kongupstreampolicy_utils_test.go
index 8712363fa..22cd2b5d8 100644
--- a/internal/controllers/configuration/kongupstreampolicy_utils_test.go
+++ b/internal/controllers/configuration/kongupstreampolicy_utils_test.go
@@ -727,7 +727,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) {
 			tc.inputObjects = append(tc.inputObjects, &tc.kongUpstreamPolicy)
 			fakeClient := fakectrlruntimeclient.
 				NewClientBuilder().
-				WithScheme(lo.Must(scheme.Get())).
+				WithScheme(scheme.Get()).
 				WithObjects(tc.inputObjects...).
 				WithStatusSubresource(tc.inputObjects...).
 				WithIndex(&corev1.Service{}, upstreamPolicyIndexKey, indexServicesOnUpstreamPolicyAnnotation).
diff --git a/internal/controllers/gateway/route_predicate_test.go b/internal/controllers/gateway/route_predicate_test.go
index c8610ac2b..52fcc9225 100644
--- a/internal/controllers/gateway/route_predicate_test.go
+++ b/internal/controllers/gateway/route_predicate_test.go
@@ -201,7 +201,7 @@ func TestIsRouteAttachedToReconciledGateway(t *testing.T) {
 	}
 
 	for _, tc := range httpRouteTestCases {
-		cl := fakeclient.NewClientBuilder().WithScheme(lo.Must(scheme.Get())).WithObjects(tc.objects...).Build()
+		cl := fakeclient.NewClientBuilder().WithScheme(scheme.Get()).WithObjects(tc.objects...).Build()
 		t.Run(tc.name, func(t *testing.T) {
 			logger := logr.Discard()
 			result := IsRouteAttachedToReconciledGateway[*gatewayapi.HTTPRoute](cl, logger, tc.gatewayNN, tc.route)
diff --git a/internal/dataplane/translator/translator_test.go b/internal/dataplane/translator/translator_test.go
index 4bb24a929..7370b0095 100644
--- a/internal/dataplane/translator/translator_test.go
+++ b/internal/dataplane/translator/translator_test.go
@@ -1096,9 +1096,7 @@ func TestServiceClientCertificate(t *testing.T) {
 			},
 		}
 		for _, service := range services {
-			scheme, err := scheme.Get()
-			require.NoError(t, err)
-			err = util.PopulateTypeMeta(service, scheme)
+			err := util.PopulateTypeMeta(service, scheme.Get())
 			require.NoError(t, err)
 		}
 		store, err := store.NewFakeStore(store.FakeObjects{
diff --git a/internal/util/types_test.go b/internal/util/types_test.go
index 68c20cf7e..658867658 100644
--- a/internal/util/types_test.go
+++ b/internal/util/types_test.go
@@ -3,7 +3,6 @@ package util
 import (
 	"testing"
 
-	"github.com/samber/lo"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -28,7 +27,7 @@ func TestPopulateTypeMeta(t *testing.T) {
 
 	require.Empty(t, credential.GetObjectKind().GroupVersionKind().Kind)
 
-	err := PopulateTypeMeta(credential, lo.Must(scheme.Get()))
+	err := PopulateTypeMeta(credential, scheme.Get())
 
 	require.NoError(t, err)
 	require.NotEmpty(t, credential.GetObjectKind().GroupVersionKind().Kind)
diff --git a/test/helpers/k8s_meta.go b/test/helpers/k8s_meta.go
index 04cdb0125..b15ca7301 100644
--- a/test/helpers/k8s_meta.go
+++ b/test/helpers/k8s_meta.go
@@ -12,9 +12,7 @@ import (
 
 // WithTypeMeta adds type meta to the given object based on its Go type.
 func WithTypeMeta[T runtime.Object](t *testing.T, obj T) T {
-	s, err := scheme.Get()
-	require.NoError(t, err)
-	err = util.PopulateTypeMeta(obj, s)
+	err := util.PopulateTypeMeta(obj, scheme.Get())
 	require.NoError(t, err)
 	return obj
 }
diff --git a/test/mocks/events_recorder.go b/test/mocks/events_recorder.go
index 9819ad3b2..dd5fa335f 100644
--- a/test/mocks/events_recorder.go
+++ b/test/mocks/events_recorder.go
@@ -44,8 +44,7 @@ func (r *EventRecorder) writeEvent(o runtime.Object, eventtype, reason, messageF
 	r.l.Lock()
 	defer r.l.Unlock()
 
-	s, _ := scheme.Get()
-	_ = util.PopulateTypeMeta(o, s)
+	_ = util.PopulateTypeMeta(o, scheme.Get())
 	fmtString := fmt.Sprintf("%s: %s %s %s", o.GetObjectKind().GroupVersionKind().Kind, eventtype, reason, messageFmt)
 	r.events = append(r.events, fmt.Sprintf(fmtString, args...))
 }