Repository: Kong/kubernetes-ingress-controller
Pull request #6285 (2024-07-03 14:47:30)

chore(deps): bump google.golang.org/grpc from 1.64.0 to 1.65.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.64.0 to 1.65.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's releases</a>.</em></p>
<blockquote>
<h2>Release 1.65.0</h2>
<h1>Dependencies</h1>
<ul>
<li>Change support policy to cover only the latest TWO releases of Go, matching the policy for Go itself.  See <a href="https://redirect.github.com/grpc/grpc-go/issues/7249">#7249</a> for more information. (<a href="https://redirect.github.com/grpc/grpc-go/issues/7250">#7250</a>)</li>
<li>Update x/net/http2 to address <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-45288">CVE-2023-45288</a> (<a href="https://redirect.github.com/grpc/grpc-go/issues/7282">#7282</a>)</li>
</ul>
<h1>Behavior Changes</h1>
<ul>
<li>credentials/tls: clients and servers will now reject connections that don't support ALPN when environment variable <code>GRPC_ENFORCE_ALPN_ENABLED</code> is set to &quot;true&quot; (case insensitive). (<a href="https://redirect.github.com/grpc/grpc-go/issues/7184">#7184</a>)
<ul>
<li>NOTE: this behavior will become the default in a future release.</li>
</ul>
</li>
<li>metadata: remove String method from MD to make printing more consistent (<a href="https://redirect.github.com/grpc/grpc-go/issues/7373">#7373</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>grpc: add <code>WithMaxCallAttempts</code> to configure gRPC's retry behavior per-channel. (<a href="https://redirect.github.com/grpc/grpc-go/issues/7229">#7229</a>)
<ul>
<li>Special Thanks: <a href="https://github.com/imoore76"><code>@​imoore76</code></a></li>
</ul>
</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>ringhash: properly apply endpoint weights instead of ignoring them (<a href="https://redirect.github.com/grpc/grpc-go/issues/7156">#7156</a>)</li>
<li>xds: fix a bug that could cause xds-enabled servers to stop accepting new connections after handshaking errors (<a href="https://redirect.github.com/grpc/grpc-go/issues/7128">#7128</a>)
<ul>
<li>Special Thanks: <a href="https://github.com/bozaro"><code>@​bozaro</code></a></li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/grpc/grpc-go/commit/2da976983bbb33feb3e25b7daaa8f60b9769adb5"><code>2da9769</code></a> Change version to 1.65.0 (<a href="https://redirect.github.com/grpc/grpc-go/issues/7306">#7306</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/ede96b776c9cea4ff2b06dfdedad841e53fb0c26"><code>ede96b7</code></a> metadata: remove String method (<a href="https://redirect.github.com/grpc/grpc-go/issues/7373">#7373</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/64be203f7828d03224aa44944a84deacac54b72b"><code>64be203</code></a> grpc: Readd pick first name (<a href="https://redirect.github.com/grpc/grpc-go/issues/7336">#7336</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7341">#7341</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/25e33a6c207464c6017d001255f9ac1297ba5d3e"><code>25e33a6</code></a> examples: Add CSM Observability example (<a href="https://redirect.github.com/grpc/grpc-go/issues/7302">#7302</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7318">#7318</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/04a5f4604baa9297b7168543834e531cfb8858dc"><code>04a5f46</code></a> xds/internal/xdsclient: Emit unknown for CSM Labels if not present in CDS (<a href="https://redirect.github.com/grpc/grpc-go/issues/7">#7</a>...</li>
<li><a href="https://github.com/grpc/grpc-go/commit/cff5c3e3d0f104459af4f52e77fc1c49ef4cb6aa"><code>cff5c3e</code></a> stats/opentelemetry: Add e2e testing for CSM Observability (<a href="https://redirect.github.com/grpc/grpc-go/issues/7279">#7279</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7316">#7316</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/9b970fdd173c1951a354630133273639e8acf00c"><code>9b970fd</code></a> dns: fix constant 30s backoff for re-resolution (<a href="https://redirect.github.com/grpc/grpc-go/issues/7262">#7262</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7311">#7311</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/6d236200ea68ea644d78a164c4ad9952a765aed3"><code>6d23620</code></a> documentation: on server, use FromIncomingContext  for retrieving context and...</li>
<li><a href="https://github.com/grpc/grpc-go/commit/7e5898e7c569b8cc32d6eee9bc8ad1393a8fcc61"><code>7e5898e</code></a> xds: unify xDS client creation APIs meant for testing (<a href="https://redirect.github.com/grpc/grpc-go/issues/7268">#7268</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/5d7bd7aacb0c35fc14bf144fa81ecbb61eec91de"><code>5d7bd7a</code></a> interop/xds: Interop client and server changes for CSM Observability (<a href="https://redirect.github.com/grpc/grpc-go/issues/7280">#7280</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/grpc/grpc-go/compare/v1.64.0...v1.65.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.64.0&new-version=1.65.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29c26acad..b4abd3c4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -95,11 +95,19 @@ Adding a new version? You'll need three changes:
 
 ## Unreleased
 
+### Added
+
+- `KongCustomEntity` is now supported by the `FallbackConfiguration` feature.
+  [#6286](https://github.com/Kong/kubernetes-ingress-controller/pull/6286)
+
 ### Fixed
 
 - Services using `Secret`s containing the same certificate as client certificates
   by annotation `konghq.com/client-cert` can be correctly translated.
   [#6228](https://github.com/Kong/kubernetes-ingress-controller/pull/6228)
+- Generate one entity for each attached foreign entity if a `KongCustomEntity`
+  resource is attached to multiple foreign Kong entities.
+  [#6280](https://github.com/Kong/kubernetes-ingress-controller/pull/6280)
 
 ## 3.2.2
 
diff --git a/Dockerfile b/Dockerfile
index 2cbd4a0e6..bfa123834 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 ### Standard binary
 # Build the manager binary
-FROM --platform=$BUILDPLATFORM golang:1.22.4 as builder
+FROM --platform=$BUILDPLATFORM golang:1.22.5 as builder
 
 ARG GOPATH
 ARG GOCACHE
diff --git a/Dockerfile.debug b/Dockerfile.debug
index e0fe65aed..813fdd88f 100644
--- a/Dockerfile.debug
+++ b/Dockerfile.debug
@@ -1,5 +1,5 @@
 # Build a manager binary with debug symbols and download Delve
-FROM --platform=$BUILDPLATFORM golang:1.22.4 as builder
+FROM --platform=$BUILDPLATFORM golang:1.22.5 as builder
 
 ARG GOPATH
 ARG GOCACHE
@@ -46,7 +46,7 @@ RUN --mount=type=cache,target=$GOPATH/pkg/mod \
 
 ### Debug
 # Create an image that runs a debug build with Delve installed
-FROM golang:1.22.4 AS debug
+FROM golang:1.22.5 AS debug
 RUN go install github.com/go-delve/delve/cmd/dlv@latest
 # We want all source so Delve file location operations work
 COPY --from=builder /workspace/bin/manager-debug /
diff --git a/go.mod b/go.mod
index 8f4cc13ab..60b7dc577 100644
--- a/go.mod
+++ b/go.mod
@@ -30,7 +30,7 @@ require (
 	github.com/google/go-cmp v0.6.0
 	github.com/google/uuid v1.6.0
 	github.com/jpillora/backoff v1.0.0
-	github.com/kong/go-database-reconciler v1.12.2
+	github.com/kong/go-database-reconciler v1.13.0
 	github.com/kong/go-kong v0.57.0
 	github.com/kong/kubernetes-telemetry v0.1.4
 	github.com/kong/kubernetes-testing-framework v0.47.1
@@ -215,7 +215,7 @@ require (
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect
-	google.golang.org/grpc v1.64.0
+	google.golang.org/grpc v1.65.0
 	google.golang.org/protobuf v1.34.2 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/go.sum b/go.sum
index 0393ac29e..8a33f453f 100644
--- a/go.sum
+++ b/go.sum
@@ -238,8 +238,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
 github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/kong/go-database-reconciler v1.12.2 h1:bnlvLCgP4OjgJOK5JYqq1MlIJ2F7erXERMj8n/LNU8M=
-github.com/kong/go-database-reconciler v1.12.2/go.mod h1:bUPJkoeW//x4hzNxewQMoIkeoDzJzunI0stDMYJ3BkU=
+github.com/kong/go-database-reconciler v1.13.0 h1:6gRGeLDep0mvzT/5fCQY5waQp0F/T9MsyZ9fJmJq+Cc=
+github.com/kong/go-database-reconciler v1.13.0/go.mod h1:bUPJkoeW//x4hzNxewQMoIkeoDzJzunI0stDMYJ3BkU=
 github.com/kong/go-kong v0.57.0 h1:e+4bHTzcO0xhFPVyGIUtlO+B4E4l14k55NH8Vjw6ORY=
 github.com/kong/go-kong v0.57.0/go.mod h1:gyNwyP1fzztT6sX/0/ygMQ30OiRMIQ51b2jSfstMrcU=
 github.com/kong/kubernetes-telemetry v0.1.4 h1:Yz7OlECxWKgNRG1wJ5imA4+H0dQEpdU9d86uhwUVpu4=
@@ -581,8 +581,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
-google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/internal/dataplane/fallback/graph_dependencies.go b/internal/dataplane/fallback/graph_dependencies.go
index dfb3cfc9c..c9b7a2668 100644
--- a/internal/dataplane/fallback/graph_dependencies.go
+++ b/internal/dataplane/fallback/graph_dependencies.go
@@ -52,6 +52,8 @@ func ResolveDependencies(cache store.CacheStores, obj client.Object) ([]client.O
 		return resolveTCPIngressDependencies(cache, obj), nil
 	case *incubatorv1alpha1.KongServiceFacade:
 		return resolveKongServiceFacadeDependencies(cache, obj), nil
+	case *kongv1alpha1.KongCustomEntity:
+		return resolveKongCustomEntityDependencies(cache, obj), nil
 	// Object types that have no dependencies.
 	case *netv1.IngressClass,
 		*corev1.Secret,
@@ -63,10 +65,6 @@ func ResolveDependencies(cache store.CacheStores, obj client.Object) ([]client.O
 		*kongv1alpha1.IngressClassParameters,
 		*kongv1alpha1.KongVault:
 		return nil, nil
-	case *kongv1alpha1.KongCustomEntity:
-		// TODO: KongCustomEnity is not supported in failure domain yet.
-		// https://github.com/Kong/kubernetes-ingress-controller/issues/6122
-		return nil, nil
 	default:
 		return nil, fmt.Errorf("unsupported object type: %T", obj)
 	}
diff --git a/internal/dataplane/fallback/graph_dependencies_kong.go b/internal/dataplane/fallback/graph_dependencies_kong.go
index 7f15ca3fb..d46821a35 100644
--- a/internal/dataplane/fallback/graph_dependencies_kong.go
+++ b/internal/dataplane/fallback/graph_dependencies_kong.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
 	incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1"
 )
@@ -140,3 +141,36 @@ func resolveTCPIngressDependencies(cache store.CacheStores, tcpIngress *kongv1be
 func resolveKongServiceFacadeDependencies(cache store.CacheStores, kongServiceFacade *incubatorv1alpha1.KongServiceFacade) []client.Object {
 	return resolveDependenciesForServiceLikeObj(cache, kongServiceFacade)
 }
+
+// resolveKongCustomEntityDependencies resolves potential dependencies for a KongCustomEntities object:
+// - KongPlugin
+// - KongClusterPlugin.
+func resolveKongCustomEntityDependencies(cache store.CacheStores, obj *kongv1alpha1.KongCustomEntity) []client.Object {
+	if obj.Spec.ParentRef == nil {
+		return nil
+	}
+
+	parentRef := *obj.Spec.ParentRef
+	groupMatches := parentRef.Group != nil && *parentRef.Group == kongv1.GroupVersion.Group
+
+	if isKongPlugin := parentRef.Kind != nil && *parentRef.Kind == "KongPlugin" && groupMatches; isKongPlugin {
+		// TODO: Cross-namespace references are not supported yet.
+		if parentRef.Namespace != nil && *parentRef.Namespace != "" &&
+			*parentRef.Namespace != obj.GetNamespace() {
+			return nil
+		}
+		if plugin, exists, err := cache.Plugin.GetByKey(
+			fmt.Sprintf("%s/%s", obj.GetNamespace(), parentRef.Name),
+		); err == nil && exists {
+			return []client.Object{plugin.(client.Object)}
+		}
+	}
+
+	if isKongClusterPlugin := parentRef.Kind != nil && *parentRef.Kind == "KongClusterPlugin" && groupMatches; isKongClusterPlugin {
+		if plugin, exists, err := cache.ClusterPlugin.GetByKey(parentRef.Name); err == nil && exists {
+			return []client.Object{plugin.(client.Object)}
+		}
+	}
+
+	return nil
+}
diff --git a/internal/dataplane/kongstate/customentity.go b/internal/dataplane/kongstate/customentity.go
index 9ded14097..3177daf05 100644
--- a/internal/dataplane/kongstate/customentity.go
+++ b/internal/dataplane/kongstate/customentity.go
@@ -2,11 +2,18 @@ package kongstate
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"sort"
 
+	"github.com/go-logr/logr"
 	"github.com/kong/go-kong/kong"
 	"github.com/kong/go-kong/kong/custom"
+	"github.com/samber/lo"
 
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/failures"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 )
 
@@ -130,6 +137,8 @@ type CustomEntity struct {
 	custom.Object
 	// K8sKongCustomEntity refers to the KongCustomEntity resource that translate to it.
 	K8sKongCustomEntity *kongv1alpha1.KongCustomEntity
+	// ForeignEntityIDs stores the IDs of the foreign Kong entities attached to the entity.
+	ForeignEntityIDs map[kong.EntityType]string
 }
 
 // SchemaGetter is the interface to fetch the schema of a Kong entity by its type.
@@ -138,6 +147,96 @@ type SchemaGetter interface {
 	Get(ctx context.Context, entityType string) (kong.Schema, error)
 }
 
+type entityForeignFieldValue struct {
+	fieldName         string
+	foreignEntityType kong.EntityType
+	foreignEntityID   string
+}
+
+// FillCustomEntities fills custom entities in KongState.
+func (ks *KongState) FillCustomEntities(
+	logger logr.Logger,
+	s store.Storer,
+	failuresCollector *failures.ResourceFailuresCollector,
+	schemaGetter SchemaGetter,
+	workspace string,
+) {
+	entities := s.ListKongCustomEntities()
+	if len(entities) == 0 {
+		return
+	}
+	logger = logger.WithName("fillCustomEntities")
+
+	if ks.CustomEntities == nil {
+		ks.CustomEntities = map[string]*KongCustomEntityCollection{}
+	}
+	// Fetch relations between plugins and services/routes/consumers and store the pointer to translated Kong entities.
+	// Used for fetching entity referred by a custom entity and fill the ID of referred entity.
+	pluginRels := ks.getPluginRelatedEntitiesRef(s, logger)
+
+	for _, entity := range entities {
+		// reject the custom entity if its type is in "known" entity types that are already processed.
+		if IsKnownEntityType(entity.Spec.EntityType) {
+			failuresCollector.PushResourceFailure(
+				fmt.Sprintf("cannot use known entity type %s in custom entity", entity.Spec.EntityType),
+				entity,
+			)
+			continue
+		}
+		// Fetch the entity schema.
+		schema, err := ks.fetchEntitySchema(schemaGetter, entity.Spec.EntityType)
+		if err != nil {
+			failuresCollector.PushResourceFailure(
+				fmt.Sprintf("failed to fetch entity schema for entity type %s: %v", entity.Spec.EntityType, err),
+				entity,
+			)
+			continue
+		}
+
+		// Fill the "foreign" fields if the entity has such fields referencing services/routes/consumers.
+		// First Find out possible foreign field combinations attached to the KCE resource.
+		foreignFieldCombinations := findCustomEntityForeignFields(logger, entity, schema, pluginRels, workspace)
+		// generate Kong entities from the fields in the KCE itself and attached foreign entities.
+		generatedEntities, err := generateCustomEntities(entity, foreignFieldCombinations)
+		if err != nil {
+			failuresCollector.PushResourceFailure(fmt.Sprintf("failed to generate entities from itself and attach foreign entities: %v", err), entity)
+			continue
+		}
+		for _, generatedEntity := range generatedEntities {
+			ks.addCustomEntity(entity.Spec.EntityType, schema, generatedEntity)
+		}
+	}
+
+	ks.sortCustomEntities()
+}
+
+// addCustomEntity adds a custom entity into the collection of its type.
+func (ks *KongState) addCustomEntity(entityType string, schema EntitySchema, e CustomEntity) {
+	// Put the entity into the custom collection to store the entities of its type.
+	if _, ok := ks.CustomEntities[entityType]; !ok {
+		ks.CustomEntities[entityType] = &KongCustomEntityCollection{
+			Schema: schema,
+		}
+	}
+	collection := ks.CustomEntities[entityType]
+	collection.Entities = append(collection.Entities, e)
+}
+
+// fetchEntitySchema fetches schema of an entity by its type and stores the schema in its custom entity collection
+// as a cache to avoid excessive calling of Kong admin APIs.
+func (ks *KongState) fetchEntitySchema(schemaGetter SchemaGetter, entityType string) (EntitySchema, error) {
+	collection, ok := ks.CustomEntities[entityType]
+	if ok {
+		return collection.Schema, nil
+	}
+	// Use `context.Background()` here because `BuildKongConfig` does not provide a context.
+	schema, err := schemaGetter.Get(context.Background(), entityType)
+	if err != nil {
+		return EntitySchema{}, err
+	}
+	return ExtractEntityFieldDefinitions(schema), nil
+}
+
 // sortCustomEntities sorts the custom entities of each type.
 // Since there may not be a consistent field to identify an entity, here we sort them by the k8s namespace/name.
 func (ks *KongState) sortCustomEntities() {
@@ -146,14 +245,192 @@ func (ks *KongState) sortCustomEntities() {
 			e1 := collection.Entities[i]
 			e2 := collection.Entities[j]
 			// Compare namespace first.
-			if e1.K8sKongCustomEntity.Namespace < e2.K8sKongCustomEntity.Namespace {
-				return true
+			if e1.K8sKongCustomEntity.Namespace != e2.K8sKongCustomEntity.Namespace {
+				return e1.K8sKongCustomEntity.Namespace < e2.K8sKongCustomEntity.Namespace
+			}
+			// If namespace are the same, compare names.
+			if e1.K8sKongCustomEntity.Name != e2.K8sKongCustomEntity.Name {
+				return e1.K8sKongCustomEntity.Name < e2.K8sKongCustomEntity.Name
 			}
-			if e1.K8sKongCustomEntity.Namespace > e2.K8sKongCustomEntity.Namespace {
-				return false
+			// Namespace and name are all the same.
+			// This means the two entities are generated from the same KCE resource but attached to different foreign entities.
+			// So we need to compare foreign entities.
+			if e1.ForeignEntityIDs != nil && e2.ForeignEntityIDs != nil {
+				// Compare IDs of attached entities in services, routes, consumers order.
+				foreignEntityTypeList := []kong.EntityType{
+					kong.EntityTypeServices,
+					kong.EntityTypeRoutes,
+					kong.EntityTypeConsumers,
+				}
+				for _, t := range foreignEntityTypeList {
+					if e1.ForeignEntityIDs[t] != e2.ForeignEntityIDs[t] {
+						return e1.ForeignEntityIDs[t] < e2.ForeignEntityIDs[t]
+					}
+				}
 			}
-			// If namespace are the same, compare name.
-			return e1.K8sKongCustomEntity.Name < e2.K8sKongCustomEntity.Name
+			// Should not reach here when k8s namespace/names are the same, and foreign entities are also the same.
+			// This means we generated two Kong entities from one KCE (and attached to the same foreign entities if any).
+			return true
 		})
 	}
 }
+
+func findCustomEntityRelatedPlugin(k8sEntity *kongv1alpha1.KongCustomEntity) (string, bool) {
+	// Find referred entity via the plugin in its spec.parentRef.
+	// Then we can fetch the referred service/route/consumer from the reference relations of the plugin.
+	parentRef := k8sEntity.Spec.ParentRef
+	var namespace string
+	// Abort if the parentRef is empty or does not refer to a plugin.
+	if parentRef == nil ||
+		(parentRef.Group == nil || *parentRef.Group != kongv1alpha1.GroupVersion.Group) {
+		return "", false
+	}
+	if parentRef.Kind == nil || (*parentRef.Kind != "KongPlugin" && *parentRef.Kind != "KongClusterPlugin") {
+		return "", false
+	}
+	// Extract the plugin key to get the plugin relations.
+	if parentRef.Namespace == nil || *parentRef.Namespace == "" {
+		namespace = k8sEntity.Namespace
+	} else {
+		namespace = *parentRef.Namespace
+	}
+	return namespace + ":" + parentRef.Name, true
+}
+
+func findCustomEntityForeignFields(
+	logger logr.Logger,
+	k8sEntity *kongv1alpha1.KongCustomEntity,
+	schema EntitySchema,
+	pluginRelEntities PluginRelatedEntitiesRefs,
+	workspace string,
+) [][]entityForeignFieldValue {
+	pluginKey, ok := findCustomEntityRelatedPlugin(k8sEntity)
+	if !ok {
+		return nil
+	}
+	// Get the relations with other entities of the plugin.
+	rels, ok := pluginRelEntities.RelatedEntities[pluginKey]
+	if !ok {
+		return nil
+	}
+
+	var (
+		foreignRelations      util.ForeignRelations
+		foreignServiceFields  []string
+		foreignRouteFields    []string
+		foreignConsumerFields []string
+	)
+
+	ret := [][]entityForeignFieldValue{}
+	for fieldName, field := range schema.Fields {
+		if field.Type != EntityFieldTypeForeign {
+			continue
+		}
+		switch field.Reference {
+		case string(kong.EntityTypeServices):
+			foreignServiceFields = append(foreignServiceFields, fieldName)
+			foreignRelations.Service = getServiceIDFromPluginRels(logger, rels, pluginRelEntities.RouteAttachedService, workspace)
+		case string(kong.EntityTypeRoutes):
+			foreignRouteFields = append(foreignRouteFields, fieldName)
+			foreignRelations.Route = lo.FilterMap(rels.Routes, func(r *Route, _ int) (string, bool) {
+				if err := r.FillID(workspace); err != nil {
+					return "", false
+				}
+				return *r.ID, true
+			})
+		case string(kong.EntityTypeConsumers):
+			foreignConsumerFields = append(foreignConsumerFields, fieldName)
+			foreignRelations.Consumer = lo.FilterMap(rels.Consumers, func(c *Consumer, _ int) (string, bool) {
+				if err := c.FillID(workspace); err != nil {
+					return "", false
+				}
+				return *c.ID, true
+			})
+		} // end of switch
+	}
+
+	// TODO: Here we inherited the logic of generating combinations of attached foreign entities for plugins.
+	// Actually there are no such case that a custom entity required multiple "foreign" fields in current Kong plugins.
+	// So it is still uncertain how to generate foreign field combinations for custom entities.
+	for _, combination := range foreignRelations.GetCombinations() {
+		foreignFieldValues := []entityForeignFieldValue{}
+		for _, fieldName := range foreignServiceFields {
+			foreignFieldValues = append(foreignFieldValues, entityForeignFieldValue{
+				fieldName:         fieldName,
+				foreignEntityType: kong.EntityTypeServices,
+				foreignEntityID:   combination.Service,
+			})
+		}
+		for _, fieldName := range foreignRouteFields {
+			foreignFieldValues = append(foreignFieldValues, entityForeignFieldValue{
+				fieldName:         fieldName,
+				foreignEntityType: kong.EntityTypeRoutes,
+				foreignEntityID:   combination.Route,
+			})
+		}
+		for _, fieldName := range foreignConsumerFields {
+			foreignFieldValues = append(foreignFieldValues, entityForeignFieldValue{
+				fieldName:         fieldName,
+				foreignEntityType: kong.EntityTypeConsumers,
+				foreignEntityID:   combination.Consumer,
+			})
+		}
+		ret = append(ret, foreignFieldValues)
+	}
+
+	return ret
+}
+
+// generateCustomEntities generates Kong entities from KongCustomEntity resource and combinations of attached foreign entities.
+// If the KCE is attached to any foreign entities, it generates one entity per combination of foreign entities.
+// If the KCE is not attached, generate one entity for itself.
+func generateCustomEntities(
+	entity *kongv1alpha1.KongCustomEntity,
+	foreignFieldCombinations [][]entityForeignFieldValue,
+) ([]CustomEntity, error) {
+	copyEntityFields := func() (map[string]any, error) {
+		// Unmarshal the fields of the entity to have a fresh copy for each combination as we may modify them.
+		fields := map[string]any{}
+		if err := json.Unmarshal(entity.Spec.Fields.Raw, &fields); err != nil {
+			return nil, fmt.Errorf("failed to unmarshal entity fields: %w", err)
+		}
+		return fields, nil
+	}
+	// If there are any foreign fields, generate one entity per each foreign entity combination.
+	if len(foreignFieldCombinations) > 0 {
+		var customEntities []CustomEntity
+		for _, combination := range foreignFieldCombinations {
+			entityFields, err := copyEntityFields()
+			if err != nil {
+				return nil, err
+			}
+			generatedEntity := CustomEntity{
+				K8sKongCustomEntity: entity,
+				ForeignEntityIDs:    make(map[kong.EntityType]string),
+				Object:              entityFields,
+			}
+			// Fill the fields referring to foreign entities.
+			for _, foreignField := range combination {
+				entityFields[foreignField.fieldName] = map[string]any{
+					"id": foreignField.foreignEntityID,
+				}
+				// Save the referred foreign entity IDs for sorting.
+				generatedEntity.ForeignEntityIDs[foreignField.foreignEntityType] = foreignField.foreignEntityID
+			}
+			customEntities = append(customEntities, generatedEntity)
+		}
+		return customEntities, nil
+	}
+
+	// Otherwise (no foreign fields), generate a single entity.
+	entityFields, err := copyEntityFields()
+	if err != nil {
+		return nil, err
+	}
+	return []CustomEntity{
+		{
+			K8sKongCustomEntity: entity,
+			Object:              entityFields,
+		},
+	}, nil
+}
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index 6f601a898..598cab97f 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -1,9 +1,7 @@
 package kongstate
 
 import (
-	"context"
 	"crypto/sha256"
-	"encoding/json"
 	"fmt"
 	"strconv"
 	"strings"
@@ -678,173 +676,6 @@ func maybeLogKongIngressDeprecationError(logger logr.Logger, services []*corev1.
 	}
 }
 
-// FillCustomEntities fills custom entities in KongState.
-func (ks *KongState) FillCustomEntities(
-	logger logr.Logger,
-	s store.Storer,
-	failuresCollector *failures.ResourceFailuresCollector,
-	schemaGetter SchemaGetter,
-	workspace string,
-) {
-	entities := s.ListKongCustomEntities()
-	if len(entities) == 0 {
-		return
-	}
-	logger = logger.WithName("fillCustomEntities")
-
-	if ks.CustomEntities == nil {
-		ks.CustomEntities = map[string]*KongCustomEntityCollection{}
-	}
-	// Fetch relations between plugins and services/routes/consumers and store the pointer to translated Kong entities.
-	// Used for fetching entity referred by a custom entity and fill the ID of referred entity.
-	pluginRels := ks.getPluginRelatedEntitiesRef(s, logger)
-
-	for _, entity := range entities {
-		// reject the custom entity if its type is in "known" entity types that are already processed.
-		if IsKnownEntityType(entity.Spec.EntityType) {
-			failuresCollector.PushResourceFailure(
-				fmt.Sprintf("cannot use known entity type %s in custom entity", entity.Spec.EntityType),
-				entity,
-			)
-			continue
-		}
-		// Fetch the entity schema.
-		schema, err := ks.fetchEntitySchema(schemaGetter, entity.Spec.EntityType)
-		if err != nil {
-			failuresCollector.PushResourceFailure(
-				fmt.Sprintf("failed to fetch entity schema for entity type %s: %v", entity.Spec.EntityType, err),
-				entity,
-			)
-			continue
-		}
-		// Unmarshal fields of the entity.
-		var parsedEntity map[string]interface{}
-		if err = json.Unmarshal(entity.Spec.Fields.Raw, &parsedEntity); err != nil {
-			failuresCollector.PushResourceFailure(fmt.Sprintf("failed to unmarshal fields of entity: %v", err), entity)
-			continue
-		}
-		// Fill the "foreign" fields if the entity has such fields referencing services/routes/consumers.
-		ks.fillCustomEntityForeignFields(logger, entity, schema, parsedEntity, pluginRels, workspace)
-		// Put the entity into the custom collection to store the entities of its type.
-		if _, ok := ks.CustomEntities[entity.Spec.EntityType]; !ok {
-			ks.CustomEntities[entity.Spec.EntityType] = &KongCustomEntityCollection{
-				Schema: schema,
-			}
-		}
-		collection := ks.CustomEntities[entity.Spec.EntityType]
-		collection.Entities = append(collection.Entities, CustomEntity{
-			Object:              parsedEntity,
-			K8sKongCustomEntity: entity,
-		})
-	}
-
-	ks.sortCustomEntities()
-}
-
-// fetchEntitySchema fetches schema of an entity by its type and stores the schema in its custom entity collection
-// as a cache to avoid excessive calling of Kong admin APIs.
-func (ks *KongState) fetchEntitySchema(schemaGetter SchemaGetter, entityType string) (EntitySchema, error) {
-	collection, ok := ks.CustomEntities[entityType]
-	if ok {
-		return collection.Schema, nil
-	}
-	// Use `context.Background()` here because `BuildKongConfig` does not provide a context.
-	schema, err := schemaGetter.Get(context.Background(), entityType)
-	if err != nil {
-		return EntitySchema{}, err
-	}
-	return ExtractEntityFieldDefinitions(schema), nil
-}
-
-// fillCustomEntityForeignFields fills the "foreign" fields of a custom Kong entity
-// if it refers to a service/route/consumer.
-// Because Kong gateway requires the "foreign" fields to use IDs of the referred entity,
-// it fills ID of the referred entity and fill the ID of the entity to the field.
-// So the function has the side effect that the referred entity will have a generated fixed ID.
-// It does not support fields referring to other types of entities.
-func (ks *KongState) fillCustomEntityForeignFields(
-	logger logr.Logger,
-	k8sEntity *kongv1alpha1.KongCustomEntity,
-	schema EntitySchema,
-	parsedEntity map[string]any,
-	pluginRelEntities PluginRelatedEntitiesRefs,
-	workspace string,
-) {
-	logger = logger.WithValues("entity_namespace", k8sEntity.Namespace, "entity_name", k8sEntity.Name)
-	// Find referred entity via the plugin in its spec.parentRef.
-	// Then we can fetch the referred service/route/consumer from the reference relations of the plugin.
-	parentRef := k8sEntity.Spec.ParentRef
-	var namespace string
-	// Abort if the parentRef is empty or does not refer to a plugin.
-	if parentRef == nil ||
-		(parentRef.Group == nil || *parentRef.Group != kongv1alpha1.GroupVersion.Group) {
-		return
-	}
-	if parentRef.Kind == nil || (*parentRef.Kind != "KongPlugin" && *parentRef.Kind != "KongClusterPlugin") {
-		return
-	}
-	// Extract the plugin key to get the plugin relations.
-	if parentRef.Namespace == nil || *parentRef.Namespace == "" {
-		namespace = k8sEntity.Namespace
-	} else {
-		namespace = *parentRef.Namespace
-	}
-	pluginKey := namespace + ":" + parentRef.Name
-	// Get the relations with other entities of the plugin.
-	rels, ok := pluginRelEntities.RelatedEntities[pluginKey]
-	if !ok {
-		return
-	}
-	logger.V(util.DebugLevel).Info("fetch references via plugin", "plugin_key", pluginKey)
-
-	// Traverse through the fields of the entity and fill the "foreign" fields with IDs of referring entities.
-	// Note: this procedure will make referred services'/routes'/consumers' ID to be filled.
-	// So it requires the `FillIDs` feature gate to be enabled.
-	for fieldName, field := range schema.Fields {
-		if field.Type != EntityFieldTypeForeign {
-			continue
-		}
-		switch field.Reference {
-		case string(kong.EntityTypeServices):
-			serviceIDs := getServiceIDFromPluginRels(logger, rels, pluginRelEntities.RouteAttachedService, workspace)
-			// TODO: we should generate multiple entities if the plugin is attached to multiple services/routes/consumers.
-			// https://github.com/Kong/kubernetes-ingress-controller/issues/6123
-			if len(serviceIDs) > 0 {
-				parsedEntity[fieldName] = map[string]interface{}{
-					"id": serviceIDs[0],
-				}
-				logger.V(util.DebugLevel).Info("added ref to service", "service_id", serviceIDs[0])
-			}
-		case string(kong.EntityTypeRoutes):
-			routeIDs := lo.FilterMap(rels.Routes, func(r *Route, _ int) (string, bool) {
-				if err := r.FillID(workspace); err != nil {
-					return "", false
-				}
-				return *r.ID, true
-			})
-			if len(routeIDs) > 0 {
-				parsedEntity[fieldName] = map[string]interface{}{
-					"id": routeIDs[0],
-				}
-				logger.V(util.DebugLevel).Info("added ref to route", "route_id", routeIDs[0])
-			}
-		case string(kong.EntityTypeConsumers):
-			consumerIDs := lo.FilterMap(rels.Consumers, func(c *Consumer, _ int) (string, bool) {
-				if err := c.FillID(workspace); err != nil {
-					return "", false
-				}
-				return *c.ID, true
-			})
-			if len(consumerIDs) > 0 {
-				parsedEntity[fieldName] = map[string]interface{}{
-					"id": consumerIDs[0],
-				}
-				logger.V(util.DebugLevel).Info("added ref to consumer", "consumer_id", consumerIDs[0])
-			}
-		}
-	}
-}
-
 // getServiceIDFromPluginRels returns the ID of the services which a plugin refers to in RelatedEntitiesRef.
 // It fills the IDs of services directly referred, and IDs of services where referred routes attaches to.
 func getServiceIDFromPluginRels(log logr.Logger, rels RelatedEntitiesRef, routeAttachedService map[string]*Service, workspace string) []string {
diff --git a/internal/dataplane/translator/subtranslator/ingress.go b/internal/dataplane/translator/subtranslator/ingress.go
index c713e93c9..272e23c6c 100644
--- a/internal/dataplane/translator/subtranslator/ingress.go
+++ b/internal/dataplane/translator/subtranslator/ingress.go
@@ -415,11 +415,7 @@ func (m *ingressTranslationMeta) translateIntoKongRoute() *kongstate.Route {
 	routeName := m.backend.intoKongRouteName(k8stypes.NamespacedName{Namespace: m.ingressNamespace, Name: m.ingressName}, ingressHost)
 
 	route := &kongstate.Route{
-		Ingress: util.K8sObjectInfo{
-			Namespace:   m.parentIngress.GetNamespace(),
-			Name:        m.parentIngress.GetName(),
-			Annotations: m.parentIngress.GetAnnotations(),
-		},
+		Ingress: util.FromK8sObject(m.parentIngress),
 		Route: kong.Route{
 			Name:              kong.String(routeName),
 			StripPath:         kong.Bool(false),
diff --git a/internal/dataplane/translator/subtranslator/ingress_atc.go b/internal/dataplane/translator/subtranslator/ingress_atc.go
index cc834d5a9..94f88b832 100644
--- a/internal/dataplane/translator/subtranslator/ingress_atc.go
+++ b/internal/dataplane/translator/subtranslator/ingress_atc.go
@@ -39,11 +39,7 @@ func (m *ingressTranslationMeta) translateIntoKongExpressionRoute() *kongstate.R
 	)
 
 	route := &kongstate.Route{
-		Ingress: util.K8sObjectInfo{
-			Namespace:   m.parentIngress.GetNamespace(),
-			Name:        m.parentIngress.GetName(),
-			Annotations: m.parentIngress.GetAnnotations(),
-		},
+		Ingress: util.FromK8sObject(m.parentIngress),
 		Route: kong.Route{
 			Name:              kong.String(routeName),
 			StripPath:         kong.Bool(false),
diff --git a/internal/store/store.go b/internal/store/store.go
index 5cddb63c0..638a29f34 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -799,6 +799,8 @@ func mkObjFromGVK(gvk schema.GroupVersionKind) (runtime.Object, error) {
 		return &kongv1alpha1.KongCustomEntity{}, nil
 	case kongv1alpha1.GroupVersion.WithKind("KongVault"):
 		return &kongv1alpha1.KongVault{}, nil
+	case kongv1alpha1.GroupVersion.WithKind("KongCustomEntity"):
+		return &kongv1alpha1.KongCustomEntity{}, nil
 	default:
 		return nil, fmt.Errorf("%s is not a supported runtime.Object", gvk)
 	}
diff --git a/staticcheck.conf b/staticcheck.conf
index 8bb9d7515..b0b65ae2d 100644
--- a/staticcheck.conf
+++ b/staticcheck.conf
@@ -1,2 +1,10 @@
 # Configuration options described in: https://staticcheck.io/docs/configuration/options/
-checks = ["all", "-ST1000", "-ST1005"]
+checks = [
+    "all",
+    "-ST1000",
+    "-ST1005",
+    # Importing the same package multiple times - gives false positives for:
+    # kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
+    # incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1"
+    "-ST1019"
+]


Test patch:
diff --git a/.github/test_dependencies.yaml b/.github/test_dependencies.yaml
index 072a8c734..98edd796a 100644
--- a/.github/test_dependencies.yaml
+++ b/.github/test_dependencies.yaml
@@ -39,7 +39,7 @@ e2e:
       istio: '1.18.7'
 
   # renovate: datasource=helm depName=kuma registryUrl=https://kumahq.github.io/charts versioning=helm
-  kuma: '2.7.4'
+  kuma: '2.7.5'
 
 integration:
   helm:
@@ -55,6 +55,8 @@ integration:
 kongintegration:
   # renovate: datasource=docker depName=kong versioning=docker
   kong-oss: '3.7.1'
+  # renovate: datasource=docker depName=kong/kong-gateway versioning=docker
+  kong-ee: '3.7.1.1'
 
 envtests:
   # renovate: datasource=docker depName=kong/kong-gateway versioning=docker
diff --git a/.github/workflows/_kongintegration_tests.yaml b/.github/workflows/_kongintegration_tests.yaml
index b03a20671..4e6abb8c5 100644
--- a/.github/workflows/_kongintegration_tests.yaml
+++ b/.github/workflows/_kongintegration_tests.yaml
@@ -7,6 +7,14 @@ jobs:
   kongintegration-tests:
     timeout-minutes: ${{ fromJSON(vars.GHA_EXTENDED_TIMEOUT_MINUTES || 60) }}
     runs-on: ubuntu-latest
+    name: ${{ matrix.name }}
+    strategy:
+      matrix:
+        include:
+          - name: enterprise
+            enterprise: true
+          - name: oss
+            enterprise: false
     steps:
       - name: checkout repository
         uses: actions/checkout@v4
@@ -16,11 +24,23 @@ jobs:
         with:
           go-version-file: go.mod
 
-      - name: set kong version
+      - uses: Kong/kong-license@master
+        id: license
+        with:
+          op-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+
+      - name: set kong oss version
+        if: ${{ !matrix.enterprise }}
         run: |
           echo "TEST_KONG_IMAGE=kong" >> $GITHUB_ENV
           echo "TEST_KONG_TAG=$(yq -ojson -r '.kongintegration.kong-oss' < .github/test_dependencies.yaml )" >> $GITHUB_ENV
 
+      - name: set kong ee version
+        if: ${{ matrix.enterprise }}
+        run: |
+          echo "TEST_KONG_IMAGE=kong/kong-gateway" >> $GITHUB_ENV
+          echo "TEST_KONG_TAG=$(yq -ojson -r '.kongintegration.kong-ee' < .github/test_dependencies.yaml )" >> $GITHUB_ENV
+
       - uses: jdx/mise-action@v2
         with:
           install: false
@@ -30,13 +50,15 @@ jobs:
         env:
           MISE_VERBOSE: 1
           MISE_DEBUG: 1
-          GOTESTSUM_JUNITFILE: kongintegration-tests.xml
+          GOTESTSUM_JUNITFILE: kongintegration-${{ matrix.name }}-tests.xml
           TEST_KONG_KONNECT_ACCESS_TOKEN: ${{ secrets.K8S_TEAM_KONNECT_ACCESS_TOKEN }}
+          KONG_LICENSE_DATA: ${{ steps.license.outputs.license }}
+          TEST_KONG_ENTERPRISE: ${{ matrix.enterprise }}
 
       - name: collect test coverage
         uses: actions/upload-artifact@v4
         with:
-          name: coverage-kongintegration
+          name: coverage-kongintegration-${{ matrix.name }}
           path: "coverage.*.out"
 
       - name: collect test report
diff --git a/internal/dataplane/fallback/cache_to_graph_test.go b/internal/dataplane/fallback/cache_to_graph_test.go
index 7fe85ff1d..e46f2e6aa 100644
--- a/internal/dataplane/fallback/cache_to_graph_test.go
+++ b/internal/dataplane/fallback/cache_to_graph_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/fallback"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1"
 )
 
@@ -332,6 +333,49 @@ func TestDefaultCacheGraphProvider_CacheToGraph(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "cache with KongCustomEntities and its dependencies",
+			cache: cacheStoresFromObjs(t,
+				&kongv1alpha1.KongCustomEntity{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-entity-kong-plugin",
+						Namespace: testNamespace,
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						ParentRef: &kongv1alpha1.ObjectReference{
+							Kind:  lo.ToPtr("KongPlugin"),
+							Group: lo.ToPtr(kongv1alpha1.GroupVersion.Group),
+							Name:  "test-plugin",
+						},
+					},
+				},
+				&kongv1alpha1.KongCustomEntity{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-entity-kong-cluster-plugin",
+						Namespace: testNamespace,
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						ParentRef: &kongv1alpha1.ObjectReference{
+							Kind:  lo.ToPtr("KongClusterPlugin"),
+							Group: lo.ToPtr(kongv1alpha1.GroupVersion.Group),
+							Name:  "test-cluster-plugin",
+						},
+					},
+				},
+				testKongPlugin(t, "test-plugin"),
+				testKongClusterPlugin(t, "test-cluster-plugin"),
+			),
+			expectedAdjacencyMap: map[string][]string{
+				"configuration.konghq.com/KongPlugin:test-namespace/test-plugin": {
+					"configuration.konghq.com/KongCustomEntity:test-namespace/test-entity-kong-plugin",
+				},
+				"configuration.konghq.com/KongClusterPlugin:test-namespace/test-cluster-plugin": {
+					"configuration.konghq.com/KongCustomEntity:test-namespace/test-entity-kong-cluster-plugin",
+				},
+				"configuration.konghq.com/KongCustomEntity:test-namespace/test-entity-kong-plugin":         {},
+				"configuration.konghq.com/KongCustomEntity:test-namespace/test-entity-kong-cluster-plugin": {},
+			},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/dataplane/fallback/graph_dependencies_kong_test.go b/internal/dataplane/fallback/graph_dependencies_kong_test.go
index fa71e59ca..3128e0c87 100644
--- a/internal/dataplane/fallback/graph_dependencies_kong_test.go
+++ b/internal/dataplane/fallback/graph_dependencies_kong_test.go
@@ -3,12 +3,14 @@ package fallback_test
 import (
 	"testing"
 
+	"github.com/samber/lo"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
 	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
+	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 	kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1"
 	incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1"
 )
@@ -735,3 +737,103 @@ func TestResolveDependencies_TCPIngress(t *testing.T) {
 		runResolveDependenciesTest(t, tc)
 	}
 }
+
+func TestResolveDependencies_KongCustomEntity(t *testing.T) {
+	testCases := []resolveDependenciesTestCase{
+		{
+			name: "no dependencies - parent reference is nil",
+			object: &kongv1alpha1.KongCustomEntity{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-custom-entity",
+					Namespace: testNamespace,
+				},
+				Spec: kongv1alpha1.KongCustomEntitySpec{
+					EntityType: "test-entity",
+					ParentRef:  nil,
+				},
+			},
+			cache: cacheStoresFromObjs(t,
+				testKongPlugin(t, "1"),
+				testKongClusterPlugin(t, "1"),
+			),
+			expected: []client.Object{},
+		},
+		{
+			name: "parent reference to KongPlugin",
+			object: &kongv1alpha1.KongCustomEntity{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-custom-entity",
+					Namespace: testNamespace,
+				},
+				Spec: kongv1alpha1.KongCustomEntitySpec{
+					EntityType: "test-entity",
+					ParentRef: &kongv1alpha1.ObjectReference{
+						Name:  "1",
+						Kind:  lo.ToPtr("KongPlugin"),
+						Group: lo.ToPtr(kongv1alpha1.GroupVersion.Group),
+					},
+				},
+			},
+			cache: cacheStoresFromObjs(t,
+				testKongPlugin(t, "1"),
+				testKongClusterPlugin(t, "1"),
+			),
+			expected: []client.Object{
+				testKongPlugin(t, "1"),
+			},
+		},
+		{
+			name: "parent reference to KongClusterPlugin",
+			object: &kongv1alpha1.KongCustomEntity{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-custom-entity",
+					Namespace: testNamespace,
+				},
+				Spec: kongv1alpha1.KongCustomEntitySpec{
+					EntityType: "test-entity",
+					ParentRef: &kongv1alpha1.ObjectReference{
+						Name:  "1",
+						Kind:  lo.ToPtr("KongClusterPlugin"),
+						Group: lo.ToPtr(kongv1alpha1.GroupVersion.Group),
+					},
+				},
+			},
+			cache: cacheStoresFromObjs(t,
+				testKongPlugin(t, "1"),
+				testKongClusterPlugin(t, "1"),
+			),
+			expected: []client.Object{
+				testKongClusterPlugin(t, "1"),
+			},
+		},
+		{
+			name: "parent reference to KongPlugin in a different namespace",
+			object: &kongv1alpha1.KongCustomEntity{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-custom-entity",
+					Namespace: testNamespace,
+				},
+				Spec: kongv1alpha1.KongCustomEntitySpec{
+					EntityType: "test-entity",
+					ParentRef: &kongv1alpha1.ObjectReference{
+						Name:      "1",
+						Namespace: lo.ToPtr("other-namespace"),
+						Kind:      lo.ToPtr("KongPlugin"),
+						Group:     lo.ToPtr(kongv1alpha1.GroupVersion.Group),
+					},
+				},
+			},
+			cache: cacheStoresFromObjs(t,
+				testKongPlugin(t, "1", func(p *kongv1.KongPlugin) {
+					p.Namespace = "other-namespace"
+				}),
+				testKongClusterPlugin(t, "1"),
+			),
+			expected: []client.Object{},
+		},
+	}
+
+	for _, tc := range testCases {
+		runResolveDependenciesTest(t, tc)
+	}
+}
diff --git a/internal/dataplane/fallback/helpers_test.go b/internal/dataplane/fallback/helpers_test.go
index a46a37095..84221205a 100644
--- a/internal/dataplane/fallback/helpers_test.go
+++ b/internal/dataplane/fallback/helpers_test.go
@@ -66,13 +66,17 @@ func testKongServiceFacade(t *testing.T, name string) *incubatorv1alpha1.KongSer
 	})
 }
 
-func testKongPlugin(t *testing.T, name string) *kongv1.KongPlugin {
-	return helpers.WithTypeMeta(t, &kongv1.KongPlugin{
+func testKongPlugin(t *testing.T, name string, modifiers ...func(p *kongv1.KongPlugin)) *kongv1.KongPlugin {
+	p := helpers.WithTypeMeta(t, &kongv1.KongPlugin{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
 			Namespace: testNamespace,
 		},
 	})
+	for _, mod := range modifiers {
+		mod(p)
+	}
+	return p
 }
 
 func testKongClusterPlugin(t *testing.T, name string) *kongv1.KongClusterPlugin {
diff --git a/internal/dataplane/kong_client_golden_test.go b/internal/dataplane/kong_client_golden_test.go
index a6c3dbacd..b82b3387a 100644
--- a/internal/dataplane/kong_client_golden_test.go
+++ b/internal/dataplane/kong_client_golden_test.go
@@ -48,6 +48,7 @@ var (
 			// Feature flags that are directly propagated from the feature gates get their defaults.
 			FillIDs:           defaults.Enabled(featuregates.FillIDsFeature),
 			KongServiceFacade: defaults.Enabled(featuregates.KongServiceFacade),
+			KongCustomEntity:  defaults.Enabled(featuregates.KongCustomEntity),
 		}
 	}
 )
@@ -370,7 +371,33 @@ func extractObjectsFromYAML(t *testing.T, filePath string) [][]byte {
 type fakeSchemaServiceProvier struct{}
 
 func (p fakeSchemaServiceProvier) GetSchemaService() kong.AbstractSchemaService {
-	return translator.UnavailableSchemaService{}
+	return fakeSchemaService{}
+}
+
+// fakeSchemaService is a stub implementation of the kong.AbstractSchemaService interface returning hardcoded schemas
+// for testing purposes.
+type fakeSchemaService struct{}
+
+func (f fakeSchemaService) Get(_ context.Context, entityType string) (kong.Schema, error) {
+	switch entityType {
+	case "degraphql_routes":
+		return kong.Schema{
+			"fields": []interface{}{
+				map[string]interface{}{
+					"service": map[string]interface{}{
+						"type":      "foreign",
+						"reference": "services",
+					},
+				},
+			},
+		}, nil
+	default:
+		return kong.Schema{}, nil
+	}
+}
+
+func (f fakeSchemaService) Validate(context.Context, kong.EntityType, any) (bool, string, error) {
+	return true, "", nil
 }
 
 func buildPostConfigErrorResponseWithBrokenObjects(brokenObjects []client.Object) []byte {
diff --git a/internal/dataplane/kongstate/customentity_test.go b/internal/dataplane/kongstate/customentity_test.go
index 64ff4d049..c0917cb2a 100644
--- a/internal/dataplane/kongstate/customentity_test.go
+++ b/internal/dataplane/kongstate/customentity_test.go
@@ -1,13 +1,26 @@
 package kongstate
 
 import (
+	"fmt"
+	"sort"
 	"testing"
 
+	"github.com/go-logr/logr"
 	"github.com/kong/go-kong/kong"
 	"github.com/kong/go-kong/kong/custom"
+	"github.com/samber/lo"
 	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	k8stypes "k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/failures"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
+	kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1"
 	kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1"
 )
 
@@ -208,3 +221,726 @@ func TestSortCustomEntities(t *testing.T) {
 		})
 	}
 }
+
+func TestFindCustomEntityForeignFields(t *testing.T) {
+	testCustomEntity := &kongv1alpha1.KongCustomEntity{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+			Name:      "fake-entity",
+		},
+		Spec: kongv1alpha1.KongCustomEntitySpec{
+			EntityType:     "fake_entities",
+			ControllerName: annotations.DefaultIngressClass,
+			Fields: apiextensionsv1.JSON{
+				Raw: []byte(`{"uri":"/api/me"}`),
+			},
+			ParentRef: &kongv1alpha1.ObjectReference{
+				Group: kong.String(kongv1.GroupVersion.Group),
+				Kind:  kong.String("KongPlugin"),
+				Name:  "fake-plugin",
+			},
+		},
+	}
+	kongService1 := kong.Service{
+		Name: kong.String("service1"),
+		ID:   kong.String("service1"),
+	}
+	kongService2 := kong.Service{
+		Name: kong.String("service2"),
+		ID:   kong.String("service2"),
+	}
+	kongRoute1 := kong.Route{
+		Name: kong.String("route1"),
+		ID:   kong.String("route1"),
+	}
+	kongRoute2 := kong.Route{
+		Name: kong.String("route2"),
+		ID:   kong.String("route2"),
+	}
+	kongConsumer1 := kong.Consumer{
+		Username: kong.String("consumer1"),
+		ID:       kong.String("consumer1"),
+	}
+	kongConsumer2 := kong.Consumer{
+		Username: kong.String("consumer2"),
+		ID:       kong.String("consumer2"),
+	}
+	testCases := []struct {
+		name                     string
+		customEntity             *kongv1alpha1.KongCustomEntity
+		schema                   EntitySchema
+		pluginRelEntities        PluginRelatedEntitiesRefs
+		foreignFieldCombinations [][]entityForeignFieldValue
+	}{
+		{
+			name:         "attached to single entity: service",
+			customEntity: testCustomEntity,
+			schema: EntitySchema{
+				Fields: map[string]EntityField{
+					"foo":     {Name: "foo", Type: EntityFieldTypeString, Required: true},
+					"service": {Name: "service", Type: EntityFieldTypeForeign, Reference: "services"},
+				},
+			},
+			pluginRelEntities: PluginRelatedEntitiesRefs{
+				RelatedEntities: map[string]RelatedEntitiesRef{
+					"default:fake-plugin": {
+						Services: []*Service{
+							{
+								Service: kongService1,
+							},
+							{
+								Service: kongService2,
+							},
+						},
+					},
+				},
+			},
+			foreignFieldCombinations: [][]entityForeignFieldValue{
+				{
+					{fieldName: "service", foreignEntityType: kong.EntityTypeServices, foreignEntityID: "service1"},
+				},
+				{
+					{fieldName: "service", foreignEntityType: kong.EntityTypeServices, foreignEntityID: "service2"},
+				},
+			},
+		},
+		{
+			name:         "attached to routes and consumers",
+			customEntity: testCustomEntity,
+			schema: EntitySchema{
+				Fields: map[string]EntityField{
+					"foo":      {Name: "foo", Type: EntityFieldTypeString, Required: true},
+					"route":    {Name: "route", Type: EntityFieldTypeForeign, Reference: "routes"},
+					"consumer": {Name: "consumer", Type: EntityFieldTypeForeign, Reference: "consumers"},
+				},
+			},
+			pluginRelEntities: PluginRelatedEntitiesRefs{
+				RelatedEntities: map[string]RelatedEntitiesRef{
+					"default:fake-plugin": {
+						Routes: []*Route{
+							{
+								Route: kongRoute1,
+							},
+							{
+								Route: kongRoute2,
+							},
+						},
+						Consumers: []*Consumer{
+							{
+								Consumer: kongConsumer1,
+							},
+							{
+								Consumer: kongConsumer2,
+							},
+						},
+					},
+				},
+				RouteAttachedService: map[string]*Service{
+					"route1": {Service: kongService1},
+					"route2": {Service: kongService2},
+				},
+			},
+			foreignFieldCombinations: [][]entityForeignFieldValue{
+				{
+					{fieldName: "consumer", foreignEntityType: kong.EntityTypeConsumers, foreignEntityID: "consumer1"},
+					{fieldName: "route", foreignEntityType: kong.EntityTypeRoutes, foreignEntityID: "route1"},
+				},
+				{
+					{fieldName: "consumer", foreignEntityType: kong.EntityTypeConsumers, foreignEntityID: "consumer1"},
+					{fieldName: "route", foreignEntityType: kong.EntityTypeRoutes, foreignEntityID: "route2"},
+				},
+				{
+					{fieldName: "consumer", foreignEntityType: kong.EntityTypeConsumers, foreignEntityID: "consumer2"},
+					{fieldName: "route", foreignEntityType: kong.EntityTypeRoutes, foreignEntityID: "route1"},
+				},
+				{
+					{fieldName: "consumer", foreignEntityType: kong.EntityTypeConsumers, foreignEntityID: "consumer2"},
+					{fieldName: "route", foreignEntityType: kong.EntityTypeRoutes, foreignEntityID: "route2"},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			combinations := findCustomEntityForeignFields(
+				logr.Discard(),
+				tc.customEntity,
+				tc.schema,
+				tc.pluginRelEntities,
+				"",
+			)
+			for index, combination := range combinations {
+				sort.SliceStable(combination, func(i, j int) bool {
+					return combination[i].fieldName < combination[j].fieldName
+				})
+				combinations[index] = combination
+			}
+			for _, expectedCombination := range tc.foreignFieldCombinations {
+				require.Contains(t, combinations, expectedCombination)
+			}
+		})
+	}
+}
+
+func TestKongState_FillCustomEntities(t *testing.T) {
+	customEntityTypeMeta := metav1.TypeMeta{
+		APIVersion: kongv1alpha1.GroupVersion.Group + "/" + kongv1alpha1.GroupVersion.Version,
+		Kind:       "KongCustomEntity",
+	}
+	kongService1 := kong.Service{
+		Name: kong.String("service1"),
+		ID:   kong.String("service1"),
+	}
+	kongService2 := kong.Service{
+		Name: kong.String("service2"),
+		ID:   kong.String("service2"),
+	}
+	ksService1 := Service{
+		Service: kongService1,
+		K8sServices: map[string]*corev1.Service{
+			"default/service1": {
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "service1",
+					Annotations: map[string]string{
+						annotations.AnnotationPrefix + annotations.PluginsKey: "degraphql-1",
+					},
+				},
+			},
+		},
+	}
+	ksService2 := Service{
+		Service: kongService2,
+		K8sServices: map[string]*corev1.Service{
+			"default/service2": {
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "service2",
+					Annotations: map[string]string{
+						annotations.AnnotationPrefix + annotations.PluginsKey: "degraphql-1",
+					},
+				},
+			},
+		},
+	} // Service: service2
+
+	testCases := []struct {
+		name                        string
+		initialState                *KongState
+		customEntities              []*kongv1alpha1.KongCustomEntity
+		plugins                     []*kongv1.KongPlugin
+		schemas                     map[string]kong.Schema
+		expectedCustomEntities      map[string][]custom.Object
+		expectedTranslationFailures map[k8stypes.NamespacedName]string
+	}{
+		{
+			name:         "single custom entity",
+			initialState: &KongState{},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session1"}`),
+						},
+					},
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"sessions": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"name": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				"sessions": {
+					{
+						"name": "session1",
+					},
+				},
+			},
+		},
+		{
+			name:         "custom entity with unknown type",
+			initialState: &KongState{},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session1"}`),
+						},
+					},
+				},
+			},
+			expectedTranslationFailures: map[k8stypes.NamespacedName]string{
+				{
+					Namespace: "default",
+					Name:      "session-foo",
+				}: "failed to fetch entity schema for entity type sessions: schema not found",
+			},
+		},
+		{
+			name:         "multiple custom entities with same type",
+			initialState: &KongState{},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session-foo"}`),
+						},
+					},
+				},
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "session-bar",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session-bar"}`),
+						},
+					},
+				},
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default-1",
+						Name:      "session-foo",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "sessions",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"name":"session-foo-1"}`),
+						},
+					},
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"sessions": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"name": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				// Should be sorted by original KCE namespace/name.
+				"sessions": {
+					{
+						// from default/bar
+						"name": "session-bar",
+					},
+					{
+						// from default/foo
+						"name": "session-foo",
+					},
+					{
+						// from default-1/foo
+						"name": "session-foo-1",
+					},
+				},
+			},
+		},
+		{
+			name: "custom entities with reference to other entities (services)",
+			initialState: &KongState{
+				Services: []Service{ksService1}, // Services
+			},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "degraphql_routes",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"uri":"/api/me"}`),
+						},
+						ParentRef: &kongv1alpha1.ObjectReference{
+							Group: kong.String(kongv1.GroupVersion.Group),
+							Kind:  kong.String("KongPlugin"),
+							Name:  "degraphql-1",
+						},
+					},
+				},
+			},
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					PluginName: "degraphql",
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"degraphql_routes": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"uri": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+						map[string]interface{}{
+							"service": map[string]interface{}{
+								"type":      "foreign",
+								"reference": "services",
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				"degraphql_routes": {
+					{
+						"uri": "/api/me",
+						"service": map[string]interface{}{
+							// ID of Kong service "service1" in workspace "".
+							"id": "service1",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "custom entity attached to multiple services via plugin",
+			initialState: &KongState{
+				Services: []Service{
+					ksService1,
+					ksService2,
+				},
+			},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "degraphql_routes",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"uri":"/api/me"}`),
+						},
+						ParentRef: &kongv1alpha1.ObjectReference{
+							Group: kong.String(kongv1.GroupVersion.Group),
+							Kind:  kong.String("KongPlugin"),
+							Name:  "degraphql-1",
+						},
+					},
+				},
+			},
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					PluginName: "degraphql",
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"degraphql_routes": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"uri": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+						map[string]interface{}{
+							"service": map[string]interface{}{
+								"type":      "foreign",
+								"reference": "services",
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				"degraphql_routes": {
+					{
+						"uri": "/api/me",
+						"service": map[string]interface{}{
+							// ID of Kong service "service1" in workspace "".
+							"id": "service1",
+						},
+					},
+					{
+						"uri": "/api/me",
+						"service": map[string]interface{}{
+							// ID of Kong service "service2" in workspace "".
+							"id": "service2",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "custom entity attached to route",
+			initialState: &KongState{
+				Services: []Service{
+					{
+						Service: kongService1,
+						Routes: []Route{
+							{
+								Route: kong.Route{
+									Name: kong.String("route1"),
+									ID:   kong.String("route1"),
+								},
+								Ingress: util.K8sObjectInfo{
+									Name:      "ingerss1",
+									Namespace: "default",
+									Annotations: map[string]string{
+										annotations.AnnotationPrefix + annotations.PluginsKey: "degraphql-1",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "degraphql_routes",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"uri":"/api/me"}`),
+						},
+						ParentRef: &kongv1alpha1.ObjectReference{
+							Group: kong.String(kongv1.GroupVersion.Group),
+							Kind:  kong.String("KongPlugin"),
+							Name:  "degraphql-1",
+						},
+					},
+				},
+			},
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					PluginName: "degraphql",
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"degraphql_routes": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"uri": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+						map[string]interface{}{
+							"route": map[string]interface{}{
+								"type":      "foreign",
+								"reference": "routes",
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				"degraphql_routes": {
+					{
+						"uri": "/api/me",
+						"route": map[string]interface{}{
+							// ID of Kong route "route1".
+							"id": "route1",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "custom entity attached to two services and one consumer",
+			initialState: &KongState{
+				Services: []Service{
+					ksService1,
+					ksService2,
+				},
+				Consumers: []Consumer{
+					{
+						Consumer: kong.Consumer{
+							ID:       kong.String("consumer1"),
+							Username: kong.String("consumer1"),
+						},
+						K8sKongConsumer: kongv1.KongConsumer{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: "default",
+								Name:      "consumer1",
+								Annotations: map[string]string{
+									annotations.AnnotationPrefix + annotations.PluginsKey: "degraphql-1",
+								},
+							},
+						},
+					},
+				},
+			},
+			customEntities: []*kongv1alpha1.KongCustomEntity{
+				{
+					TypeMeta: customEntityTypeMeta,
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "fake-entity-1",
+					},
+					Spec: kongv1alpha1.KongCustomEntitySpec{
+						EntityType:     "fake_entities",
+						ControllerName: annotations.DefaultIngressClass,
+						Fields: apiextensionsv1.JSON{
+							Raw: []byte(`{"foo":"bar"}`),
+						},
+						ParentRef: &kongv1alpha1.ObjectReference{
+							Group: kong.String(kongv1.GroupVersion.Group),
+							Kind:  kong.String("KongPlugin"),
+							Name:  "degraphql-1",
+						},
+					},
+				},
+			},
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "degraphql-1",
+					},
+					PluginName: "degraphql",
+				},
+			},
+			schemas: map[string]kong.Schema{
+				"fake_entities": {
+					"fields": []interface{}{
+						map[string]interface{}{
+							"foo": map[string]interface{}{
+								"type":     "string",
+								"required": true,
+							},
+						},
+						map[string]interface{}{
+							"service": map[string]interface{}{
+								"type":      "foreign",
+								"reference": "services",
+							},
+						},
+						map[string]interface{}{
+							"consumer": map[string]interface{}{
+								"type":      "foreign",
+								"reference": "consumers",
+							},
+						},
+					},
+				},
+			},
+			expectedCustomEntities: map[string][]custom.Object{
+				"fake_entities": {
+					{
+						"foo": "bar",
+						"service": map[string]interface{}{
+							// ID of Kong service "service1".
+							"id": "service1",
+						},
+						"consumer": map[string]interface{}{
+							// ID of Kong consumer "consumer1".
+							"id": "consumer1",
+						},
+					},
+					{
+						"foo": "bar",
+						"service": map[string]interface{}{
+							// ID of Kong service "service2".
+							"id": "service2",
+						},
+						"consumer": map[string]interface{}{
+							// ID of Kong consumer "consumer1".
+							"id": "consumer1",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			s, err := store.NewFakeStore(store.FakeObjects{
+				KongCustomEntities: tc.customEntities,
+				KongPlugins:        tc.plugins,
+			})
+			require.NoError(t, err)
+			failuresCollector := failures.NewResourceFailuresCollector(logr.Discard())
+
+			ks := tc.initialState
+			ks.FillCustomEntities(
+				logr.Discard(), s,
+				failuresCollector,
+				&fakeSchemaGetter{schemas: tc.schemas}, "",
+			)
+			for entityType, expectedObjectList := range tc.expectedCustomEntities {
+				require.NotNil(t, ks.CustomEntities[entityType])
+				objectList := lo.Map(ks.CustomEntities[entityType].Entities, func(e CustomEntity, _ int) custom.Object {
+					return e.Object
+				})
+				require.Equal(t, expectedObjectList, objectList)
+			}
+
+			translationFailures := failuresCollector.PopResourceFailures()
+			for nsName, message := range tc.expectedTranslationFailures {
+				hasError := lo.ContainsBy(translationFailures, func(f failures.ResourceFailure) bool {
+					fmt.Println(f.Message())
+					return f.Message() == message && lo.ContainsBy(f.CausingObjects(), func(o client.Object) bool {
+						return o.GetNamespace() == nsName.Namespace && o.GetName() == nsName.Name
+					})
+				})
+				require.Truef(t, hasError, "translation error for KongCustomEntity %s not found", nsName)
+			}
+		})
+	}
+}
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index 6ab9c89c6..f6d5c190c 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -13,7 +13,6 @@ import (
 	"github.com/go-logr/logr/testr"
 	"github.com/go-logr/zapr"
 	"github.com/kong/go-kong/kong"
-	"github.com/kong/go-kong/kong/custom"
 	"github.com/samber/lo"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -1527,287 +1526,3 @@ func (s *fakeSchemaGetter) Get(_ context.Context, entityType string) (kong.Schem
 	}
 	return schema, nil
 }
-
-func TestKongState_FillCustomEntities(t *testing.T) {
-	customEntityTypeMeta := metav1.TypeMeta{
-		APIVersion: kongv1alpha1.GroupVersion.Group + "/" + kongv1alpha1.GroupVersion.Version,
-		Kind:       "KongCustomEntity",
-	}
-	kongService1 := kong.Service{
-		Name: kong.String("service1"),
-	}
-	getKongServiceID := func(s *kong.Service) string {
-		err := s.FillID("")
-		require.NoError(t, err)
-		return *s.ID
-	}
-
-	testCases := []struct {
-		name                        string
-		initialState                *KongState
-		customEntities              []*kongv1alpha1.KongCustomEntity
-		plugins                     []*kongv1.KongPlugin
-		schemas                     map[string]kong.Schema
-		expectedCustomEntities      map[string][]custom.Object
-		expectedTranslationFailures map[k8stypes.NamespacedName]string
-	}{
-		{
-			name:         "single custom entity",
-			initialState: &KongState{},
-			customEntities: []*kongv1alpha1.KongCustomEntity{
-				{
-					TypeMeta: customEntityTypeMeta,
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "session-foo",
-					},
-					Spec: kongv1alpha1.KongCustomEntitySpec{
-						EntityType:     "sessions",
-						ControllerName: annotations.DefaultIngressClass,
-						Fields: apiextensionsv1.JSON{
-							Raw: []byte(`{"name":"session1"}`),
-						},
-					},
-				},
-			},
-			schemas: map[string]kong.Schema{
-				"sessions": {
-					"fields": []interface{}{
-						map[string]interface{}{
-							"name": map[string]interface{}{
-								"type":     "string",
-								"required": true,
-							},
-						},
-					},
-				},
-			},
-			expectedCustomEntities: map[string][]custom.Object{
-				"sessions": {
-					{
-						"name": "session1",
-					},
-				},
-			},
-		},
-		{
-			name:         "custom entity with unknown type",
-			initialState: &KongState{},
-			customEntities: []*kongv1alpha1.KongCustomEntity{
-				{
-					TypeMeta: customEntityTypeMeta,
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "session-foo",
-					},
-					Spec: kongv1alpha1.KongCustomEntitySpec{
-						EntityType:     "sessions",
-						ControllerName: annotations.DefaultIngressClass,
-						Fields: apiextensionsv1.JSON{
-							Raw: []byte(`{"name":"session1"}`),
-						},
-					},
-				},
-			},
-			expectedTranslationFailures: map[k8stypes.NamespacedName]string{
-				{
-					Namespace: "default",
-					Name:      "session-foo",
-				}: "failed to fetch entity schema for entity type sessions: schema not found",
-			},
-		},
-		{
-			name:         "multiple custom entities with same type",
-			initialState: &KongState{},
-			customEntities: []*kongv1alpha1.KongCustomEntity{
-				{
-					TypeMeta: customEntityTypeMeta,
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "session-foo",
-					},
-					Spec: kongv1alpha1.KongCustomEntitySpec{
-						EntityType:     "sessions",
-						ControllerName: annotations.DefaultIngressClass,
-						Fields: apiextensionsv1.JSON{
-							Raw: []byte(`{"name":"session-foo"}`),
-						},
-					},
-				},
-				{
-					TypeMeta: customEntityTypeMeta,
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "session-bar",
-					},
-					Spec: kongv1alpha1.KongCustomEntitySpec{
-						EntityType:     "sessions",
-						ControllerName: annotations.DefaultIngressClass,
-						Fields: apiextensionsv1.JSON{
-							Raw: []byte(`{"name":"session-bar"}`),
-						},
-					},
-				},
-				{
-					TypeMeta: customEntityTypeMeta,
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default-1",
-						Name:      "session-foo",
-					},
-					Spec: kongv1alpha1.KongCustomEntitySpec{
-						EntityType:     "sessions",
-						ControllerName: annotations.DefaultIngressClass,
-						Fields: apiextensionsv1.JSON{
-							Raw: []byte(`{"name":"session-foo-1"}`),
-						},
-					},
-				},
-			},
-			schemas: map[string]kong.Schema{
-				"sessions": {
-					"fields": []interface{}{
-						map[string]interface{}{
-							"name": map[string]interface{}{
-								"type":     "string",
-								"required": true,
-							},
-						},
-					},
-				},
-			},
-			expectedCustomEntities: map[string][]custom.Object{
-				// Should be sorted by original KCE namespace/name.
-				"sessions": {
-					{
-						// from default/bar
-						"name": "session-bar",
-					},
-					{
-						// from default/foo
-						"name": "session-foo",
-					},
-					{
-						// from default-1/foo
-						"name": "session-foo-1",
-					},
-				},
-			},
-		},
-		{
-			name: "custom entities with reference to other entities (services)",
-			initialState: &KongState{
-				Services: []Service{
-					{
-						Service: kongService1,
-						K8sServices: map[string]*corev1.Service{
-							"default/service1": {
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "default",
-									Name:      "service1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "degraphql-1",
-									},
-								},
-							},
-						},
-					}, // Service: service1
-				}, // Services
-			},
-			customEntities: []*kongv1alpha1.KongCustomEntity{
-				{
-					TypeMeta: customEntityTypeMeta,
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "degraphql-1",
-					},
-					Spec: kongv1alpha1.KongCustomEntitySpec{
-						EntityType:     "degraphql_routes",
-						ControllerName: annotations.DefaultIngressClass,
-						Fields: apiextensionsv1.JSON{
-							Raw: []byte(`{"uri":"/api/me"}`),
-						},
-						ParentRef: &kongv1alpha1.ObjectReference{
-							Group: kong.String(kongv1.GroupVersion.Group),
-							Kind:  kong.String("KongPlugin"),
-							Name:  "degraphql-1",
-						},
-					},
-				},
-			},
-			plugins: []*kongv1.KongPlugin{
-				{
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "degraphql-1",
-					},
-					PluginName: "degraphql",
-				},
-			},
-			schemas: map[string]kong.Schema{
-				"degraphql_routes": {
-					"fields": []interface{}{
-						map[string]interface{}{
-							"uri": map[string]interface{}{
-								"type":     "string",
-								"required": true,
-							},
-						},
-						map[string]interface{}{
-							"service": map[string]interface{}{
-								"type":      "foreign",
-								"reference": "services",
-							},
-						},
-					},
-				},
-			},
-			expectedCustomEntities: map[string][]custom.Object{
-				"degraphql_routes": {
-					{
-						"uri": "/api/me",
-						"service": map[string]interface{}{
-							// ID generated from Kong service "service1" in workspace "".
-							"id": getKongServiceID(&kongService1),
-						},
-					},
-				},
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			s, err := store.NewFakeStore(store.FakeObjects{
-				KongCustomEntities: tc.customEntities,
-				KongPlugins:        tc.plugins,
-			})
-			require.NoError(t, err)
-			failuresCollector := failures.NewResourceFailuresCollector(logr.Discard())
-
-			ks := tc.initialState
-			ks.FillCustomEntities(
-				logr.Discard(), s,
-				failuresCollector,
-				&fakeSchemaGetter{schemas: tc.schemas}, "",
-			)
-			for entityType, expectedObjectList := range tc.expectedCustomEntities {
-				require.NotNil(t, ks.CustomEntities[entityType])
-				objectList := lo.Map(ks.CustomEntities[entityType].Entities, func(e CustomEntity, _ int) custom.Object {
-					return e.Object
-				})
-				require.Equal(t, expectedObjectList, objectList)
-			}
-
-			translationFailures := failuresCollector.PopResourceFailures()
-			for nsName, message := range tc.expectedTranslationFailures {
-				hasError := lo.ContainsBy(translationFailures, func(f failures.ResourceFailure) bool {
-					fmt.Println(f.Message())
-					return f.Message() == message && lo.ContainsBy(f.CausingObjects(), func(o client.Object) bool {
-						return o.GetNamespace() == nsName.Namespace && o.GetName() == nsName.Name
-					})
-				})
-				require.Truef(t, hasError, "translation error for KongCustomEntity %s not found", nsName)
-			}
-		})
-	}
-}
diff --git a/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/custom-entities-on_golden.yaml b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/custom-entities-on_golden.yaml
new file mode 100644
index 000000000..71a42e94b
--- /dev/null
+++ b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/custom-entities-on_golden.yaml
@@ -0,0 +1,66 @@
+_format_version: "3.0"
+degraphql_routes:
+- query: query{ contacts { name } }
+  uri: /contacts
+- query: query{ contacts { name } }
+  uri: /contacts
+plugins:
+- config:
+    graphql_server_path: /v1/graphql
+  name: degraphql
+  route: default.i1.s1..80
+  tags:
+  - k8s-name:p1
+  - k8s-namespace:default
+  - k8s-kind:KongPlugin
+  - k8s-uid:e97ca177-6bd2-425f-b3b1-10a521ac02a0
+  - k8s-group:configuration.konghq.com
+  - k8s-version:v1
+services:
+- connect_timeout: 60000
+  host: s1.default.80.svc
+  id: 9e262d22-26f2-5bcb-8d57-c5229fb8c5a2
+  name: default.s1.80
+  path: /
+  port: 80
+  protocol: http
+  read_timeout: 60000
+  retries: 5
+  routes:
+  - https_redirect_status_code: 426
+    id: 226e9894-47fd-5dd2-9af0-4b49a2b45c5e
+    name: default.i1.s1..80
+    path_handling: v0
+    paths:
+    - /
+    preserve_host: true
+    protocols:
+    - http
+    - https
+    regex_priority: 0
+    request_buffering: true
+    response_buffering: true
+    strip_path: true
+    tags:
+    - k8s-name:i1
+    - k8s-namespace:default
+    - k8s-kind:Ingress
+    - k8s-uid:39ec2b6a-f919-4308-ad2f-07511ab424dd
+    - k8s-group:networking.k8s.io
+    - k8s-version:v1
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
+  write_timeout: 60000
+upstreams:
+- algorithm: round-robin
+  name: s1.default.80.svc
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
diff --git a/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/custom-entities-on_settings.yaml b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/custom-entities-on_settings.yaml
new file mode 100644
index 000000000..85a9e80a4
--- /dev/null
+++ b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/custom-entities-on_settings.yaml
@@ -0,0 +1,2 @@
+feature_flags:
+  KongCustomEntity: true
diff --git a/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/default_golden.yaml b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/default_golden.yaml
new file mode 100644
index 000000000..7ff540d74
--- /dev/null
+++ b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/default_golden.yaml
@@ -0,0 +1,61 @@
+_format_version: "3.0"
+plugins:
+- config:
+    graphql_server_path: /v1/graphql
+  name: degraphql
+  route: default.i1.s1..80
+  tags:
+  - k8s-name:p1
+  - k8s-namespace:default
+  - k8s-kind:KongPlugin
+  - k8s-uid:e97ca177-6bd2-425f-b3b1-10a521ac02a0
+  - k8s-group:configuration.konghq.com
+  - k8s-version:v1
+services:
+- connect_timeout: 60000
+  host: s1.default.80.svc
+  id: 9e262d22-26f2-5bcb-8d57-c5229fb8c5a2
+  name: default.s1.80
+  path: /
+  port: 80
+  protocol: http
+  read_timeout: 60000
+  retries: 5
+  routes:
+  - https_redirect_status_code: 426
+    id: 226e9894-47fd-5dd2-9af0-4b49a2b45c5e
+    name: default.i1.s1..80
+    path_handling: v0
+    paths:
+    - /
+    preserve_host: true
+    protocols:
+    - http
+    - https
+    regex_priority: 0
+    request_buffering: true
+    response_buffering: true
+    strip_path: true
+    tags:
+    - k8s-name:i1
+    - k8s-namespace:default
+    - k8s-kind:Ingress
+    - k8s-uid:39ec2b6a-f919-4308-ad2f-07511ab424dd
+    - k8s-group:networking.k8s.io
+    - k8s-version:v1
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
+  write_timeout: 60000
+upstreams:
+- algorithm: round-robin
+  name: s1.default.80.svc
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
diff --git a/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/in.yaml b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/in.yaml
new file mode 100644
index 000000000..7f8082594
--- /dev/null
+++ b/internal/dataplane/testdata/golden/fallback-config-kong-custom-entities-ee/in.yaml
@@ -0,0 +1,281 @@
+# In this test case we have 4 independent sets of resources (each including Service, Ingress, Plugin, CustomEntity).
+# In each set, we make one of the objects broken by adding an annotation `test.konghq.com/broken: "true"`.
+
+# Set n. 1: Service s1, Ingress i1, Plugin p1, CustomEntity ce1. ce1 is broken.
+# We expect that the custom entity ce1 is not created, but the other resources are created.
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: s1
+  name: s1
+  namespace: default
+  uid: ddc15c38-7f1b-41cb-8b57-46465a7f4427
+spec:
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 8080
+  selector:
+    app: s1
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: i1
+  namespace: default
+  uid: 39ec2b6a-f919-4308-ad2f-07511ab424dd
+  annotations:
+    konghq.com/strip-path: "true"
+    konghq.com/plugins: "p1"
+spec:
+  ingressClassName: kong
+  rules:
+    -  http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: s1
+                port:
+                  number: 80
+---
+apiVersion: configuration.konghq.com/v1
+kind: KongPlugin
+metadata:
+  namespace: default
+  name: p1
+  uid: e97ca177-6bd2-425f-b3b1-10a521ac02a0
+plugin: degraphql
+config:
+  graphql_server_path: /v1/graphql
+---
+apiVersion: configuration.konghq.com/v1alpha1
+kind: KongCustomEntity
+metadata:
+  namespace: default
+  name: ce1
+  uid: af6056e0-e5c7-4bfa-8643-79ef16311686
+  annotations:
+    test.konghq.com/broken: "true"
+spec:
+  controllerName: kong
+  type: degraphql_routes
+  parentRef:
+    group: configuration.konghq.com
+    kind: KongPlugin
+    name: p1
+  fields:
+    uri: "/contacts"
+    query: "query{ contacts { name } }"
+
+# Set n. 2: Service s2, Ingress i2, Plugin p2, CustomEntity ce2. i2 is broken.
+# We expect that no service, route or plugin is created. Only the custom entity with no references is created.
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: s2
+  name: s2
+  namespace: default
+  uid: e1fb0dd7-6ebc-4fac-aa59-9a2d0010dfd6
+spec:
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 8080
+  selector:
+    app: s2
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: i2
+  namespace: default
+  uid: 7b26f36a-e484-424a-b8fd-7d31c63d06a3
+  annotations:
+    konghq.com/strip-path: "true"
+    konghq.com/plugins: "p2"
+    test.konghq.com/broken: "true"
+spec:
+  ingressClassName: kong
+  rules:
+    -  http:
+         paths:
+           - path: /
+             pathType: Prefix
+             backend:
+               service:
+                 name: s2
+                 port:
+                   number: 80
+---
+apiVersion: configuration.konghq.com/v1
+kind: KongPlugin
+metadata:
+  namespace: default
+  name: p2
+  uid: 993e367c-0cd4-4808-8a55-7da0289b0400
+plugin: degraphql
+config:
+  graphql_server_path: /v1/graphql
+---
+apiVersion: configuration.konghq.com/v1alpha1
+kind: KongCustomEntity
+metadata:
+  namespace: default
+  name: ce2
+  uid: cece05c0-543d-4080-b470-7cd7bd4151d3
+spec:
+  controllerName: kong
+  type: degraphql_routes
+  parentRef:
+    group: configuration.konghq.com
+    kind: KongPlugin
+    name: p2
+  fields:
+    uri: "/contacts"
+    query: "query{ contacts { name } }"
+
+# Set n. 3: Service s3, Ingress i3, Plugin p3, CustomEntity ce3. p3 is broken.
+# We expect that no entities are created at all as all of them depend on the broken plugin.
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: s3
+  name: s3
+  namespace: default
+  uid: f816403d-b8c5-4176-9b59-f85d9201c0e0
+spec:
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 8080
+  selector:
+    app: s3
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: i3
+  namespace: default
+  uid: 15bd6b19-3f62-44dc-863a-26d29f3c02be
+  annotations:
+    konghq.com/strip-path: "true"
+    konghq.com/plugins: "p3"
+spec:
+  ingressClassName: kong
+  rules:
+    -  http:
+         paths:
+           - path: /
+             pathType: Prefix
+             backend:
+               service:
+                 name: s3
+                 port:
+                   number: 80
+---
+apiVersion: configuration.konghq.com/v1
+kind: KongPlugin
+metadata:
+  namespace: default
+  name: p3
+  uid: 65dcd9c3-2cdb-4ce2-aff7-6e5764730029
+  annotations:
+    test.konghq.com/broken: "true"
+plugin: degraphql
+config:
+  graphql_server_path: /v1/graphql
+---
+apiVersion: configuration.konghq.com/v1alpha1
+kind: KongCustomEntity
+metadata:
+  namespace: default
+  name: ce3
+  uid: 24e0ef06-b08a-460f-8472-8823684644d5
+spec:
+  controllerName: kong
+  type: degraphql_routes
+  parentRef:
+    group: configuration.konghq.com
+    kind: KongPlugin
+    name: p3
+  fields:
+    uri: "/contacts"
+    query: "query{ contacts { name } }"
+
+## Set n. 4: Service s4, Ingress i4, Plugin p4, CustomEntity ce4. s4 is broken.
+## We expect that no service, route or plugin is created. Only the custom entity with no references is created.
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: s4
+  name: s4
+  namespace: default
+  uid: 72e2cba5-8e70-4414-b194-9a3293315525
+  annotations:
+    test.konghq.com/broken: "true"
+spec:
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 8080
+  selector:
+    app: s4
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: i4
+  namespace: default
+  uid: 0b7fa868-328f-440f-a3e9-b70986967885
+  annotations:
+    konghq.com/strip-path: "true"
+    konghq.com/plugins: "p4"
+spec:
+  ingressClassName: kong
+  rules:
+    -  http:
+         paths:
+           - path: /
+             pathType: Prefix
+             backend:
+               service:
+                 name: s4
+                 port:
+                   number: 80
+---
+apiVersion: configuration.konghq.com/v1
+kind: KongPlugin
+metadata:
+  namespace: default
+  name: p4
+  uid: 770be827-8328-4fc5-8bef-200157e532ab
+plugin: degraphql
+config:
+  graphql_server_path: /v1/graphql
+---
+apiVersion: configuration.konghq.com/v1alpha1
+kind: KongCustomEntity
+metadata:
+  namespace: default
+  name: degraphql-route
+  uid: 220db4f5-bba4-4a1d-924f-39280fb9049c
+spec:
+  controllerName: kong
+  type: degraphql_routes
+  parentRef:
+    group: configuration.konghq.com
+    kind: KongPlugin
+    name: p4
+  fields:
+    uri: "/contacts"
+    query: "query{ contacts { name } }"
diff --git a/internal/dataplane/testdata/golden/kong-custom-entity-ee/custom-entities-on_golden.yaml b/internal/dataplane/testdata/golden/kong-custom-entity-ee/custom-entities-on_golden.yaml
new file mode 100644
index 000000000..1715c6d04
--- /dev/null
+++ b/internal/dataplane/testdata/golden/kong-custom-entity-ee/custom-entities-on_golden.yaml
@@ -0,0 +1,54 @@
+_format_version: "3.0"
+degraphql_routes:
+- query: query{ contacts { name } }
+  service:
+    id: 9e262d22-26f2-5bcb-8d57-c5229fb8c5a2
+  uri: /contacts
+services:
+- connect_timeout: 60000
+  host: s1.default.80.svc
+  id: 9e262d22-26f2-5bcb-8d57-c5229fb8c5a2
+  name: default.s1.80
+  path: /
+  port: 80
+  protocol: http
+  read_timeout: 60000
+  retries: 5
+  routes:
+  - https_redirect_status_code: 426
+    id: 226e9894-47fd-5dd2-9af0-4b49a2b45c5e
+    name: default.i1.s1..80
+    path_handling: v0
+    paths:
+    - /
+    preserve_host: true
+    protocols:
+    - http
+    - https
+    regex_priority: 0
+    request_buffering: true
+    response_buffering: true
+    strip_path: true
+    tags:
+    - k8s-name:i1
+    - k8s-namespace:default
+    - k8s-kind:Ingress
+    - k8s-uid:39ec2b6a-f919-4308-ad2f-07511ab424dd
+    - k8s-group:networking.k8s.io
+    - k8s-version:v1
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
+  write_timeout: 60000
+upstreams:
+- algorithm: round-robin
+  name: s1.default.80.svc
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
diff --git a/internal/dataplane/testdata/golden/kong-custom-entity-ee/custom-entities-on_settings.yaml b/internal/dataplane/testdata/golden/kong-custom-entity-ee/custom-entities-on_settings.yaml
new file mode 100644
index 000000000..85a9e80a4
--- /dev/null
+++ b/internal/dataplane/testdata/golden/kong-custom-entity-ee/custom-entities-on_settings.yaml
@@ -0,0 +1,2 @@
+feature_flags:
+  KongCustomEntity: true
diff --git a/internal/dataplane/testdata/golden/kong-custom-entity-ee/default_golden.yaml b/internal/dataplane/testdata/golden/kong-custom-entity-ee/default_golden.yaml
new file mode 100644
index 000000000..004aa6a20
--- /dev/null
+++ b/internal/dataplane/testdata/golden/kong-custom-entity-ee/default_golden.yaml
@@ -0,0 +1,49 @@
+_format_version: "3.0"
+services:
+- connect_timeout: 60000
+  host: s1.default.80.svc
+  id: 9e262d22-26f2-5bcb-8d57-c5229fb8c5a2
+  name: default.s1.80
+  path: /
+  port: 80
+  protocol: http
+  read_timeout: 60000
+  retries: 5
+  routes:
+  - https_redirect_status_code: 426
+    id: 226e9894-47fd-5dd2-9af0-4b49a2b45c5e
+    name: default.i1.s1..80
+    path_handling: v0
+    paths:
+    - /
+    preserve_host: true
+    protocols:
+    - http
+    - https
+    regex_priority: 0
+    request_buffering: true
+    response_buffering: true
+    strip_path: true
+    tags:
+    - k8s-name:i1
+    - k8s-namespace:default
+    - k8s-kind:Ingress
+    - k8s-uid:39ec2b6a-f919-4308-ad2f-07511ab424dd
+    - k8s-group:networking.k8s.io
+    - k8s-version:v1
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
+  write_timeout: 60000
+upstreams:
+- algorithm: round-robin
+  name: s1.default.80.svc
+  tags:
+  - k8s-name:s1
+  - k8s-namespace:default
+  - k8s-kind:Service
+  - k8s-uid:ddc15c38-7f1b-41cb-8b57-46465a7f4427
+  - k8s-version:v1
diff --git a/internal/dataplane/testdata/golden/kong-custom-entity-ee/in.yaml b/internal/dataplane/testdata/golden/kong-custom-entity-ee/in.yaml
new file mode 100644
index 000000000..8603111bb
--- /dev/null
+++ b/internal/dataplane/testdata/golden/kong-custom-entity-ee/in.yaml
@@ -0,0 +1,64 @@
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: s1
+  name: s1
+  namespace: default
+  uid: ddc15c38-7f1b-41cb-8b57-46465a7f4427
+spec:
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 8080
+  selector:
+    app: s1
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: i1
+  namespace: default
+  uid: 39ec2b6a-f919-4308-ad2f-07511ab424dd
+  annotations:
+    konghq.com/strip-path: "true"
+    konghq.com/plugins: "p1"
+spec:
+  ingressClassName: kong
+  rules:
+    -  http:
+         paths:
+           - path: /
+             pathType: Prefix
+             backend:
+               service:
+                 name: s1
+                 port:
+                   number: 80
+---
+apiVersion: configuration.konghq.com/v1
+kind: KongPlugin
+metadata:
+  namespace: default
+  name: degraphql
+  uid: e97ca177-6bd2-425f-b3b1-10a521ac02a0
+plugin: degraphql
+config:
+  graphql_server_path: /v1/graphql
+---
+apiVersion: configuration.konghq.com/v1alpha1
+kind: KongCustomEntity
+metadata:
+  namespace: default
+  name: ce1
+  uid: af6056e0-e5c7-4bfa-8643-79ef16311686
+spec:
+  controllerName: kong
+  type: degraphql_routes
+  parentRef:
+    group: configuration.konghq.com
+    kind: KongPlugin
+    name: p1
+  fields:
+    uri: "/contacts"
+    query: "query{ contacts { name } }"
diff --git a/internal/dataplane/translator/subtranslator/ingress_atc_test.go b/internal/dataplane/translator/subtranslator/ingress_atc_test.go
index 5c213cd5d..8b8397729 100644
--- a/internal/dataplane/translator/subtranslator/ingress_atc_test.go
+++ b/internal/dataplane/translator/subtranslator/ingress_atc_test.go
@@ -31,6 +31,7 @@ func TestTranslateIngressATC(t *testing.T) {
 		{
 			name: "a basic ingress resource with a single rule and prefix path type",
 			ingress: &netv1.Ingress{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -74,8 +75,9 @@ func TestTranslateIngressATC(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:       kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -90,7 +92,7 @@ func TestTranslateIngressATC(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 						ExpressionRoutes: true,
 					}},
@@ -106,6 +108,7 @@ func TestTranslateIngressATC(t *testing.T) {
 		{
 			name: "a basic ingress resource with a single rule, and only one path results in a single kong service and route",
 			ingress: &netv1.Ingress{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -148,8 +151,9 @@ func TestTranslateIngressATC(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:       kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -164,7 +168,7 @@ func TestTranslateIngressATC(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 						ExpressionRoutes: true,
 					}},
@@ -180,6 +184,7 @@ func TestTranslateIngressATC(t *testing.T) {
 		{
 			name: "an ingress with method, protocol, and header annotations",
 			ingress: &netv1.Ingress{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress-annotations",
 					Namespace: corev1.NamespaceDefault,
@@ -234,6 +239,7 @@ func TestTranslateIngressATC(t *testing.T) {
 								"konghq.com/protocols":   "http",
 								"konghq.com/headers.foo": "bar",
 							},
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:       kong.String("default.test-ingress-annotations.test-service.konghq.com.80"),
@@ -249,7 +255,7 @@ func TestTranslateIngressATC(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress-annotations", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress-annotations", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 						ExpressionRoutes: true,
 					}},
@@ -276,6 +282,7 @@ func TestTranslateIngressATC(t *testing.T) {
 		{
 			name: "KongServiceFacade used as a backend",
 			ingress: &netv1.Ingress{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: "default",
@@ -334,8 +341,9 @@ func TestTranslateIngressATC(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:       kong.String("default.test-ingress.konghq.com.svc-facade.svc.facade"),
@@ -350,7 +358,7 @@ func TestTranslateIngressATC(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 						ExpressionRoutes: true,
 					}},
@@ -377,6 +385,7 @@ func TestTranslateIngressATC(t *testing.T) {
 		{
 			name: "not existing KongServiceFacade used as a backend",
 			ingress: &netv1.Ingress{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: "default",
diff --git a/internal/dataplane/translator/subtranslator/ingress_test.go b/internal/dataplane/translator/subtranslator/ingress_test.go
index 5a84f2488..ef7d8142f 100644
--- a/internal/dataplane/translator/subtranslator/ingress_test.go
+++ b/internal/dataplane/translator/subtranslator/ingress_test.go
@@ -14,6 +14,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	netv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
@@ -30,6 +31,15 @@ var (
 	pathTypeExact                  = netv1.PathTypeExact
 	pathTypeImplementationSpecific = netv1.PathTypeImplementationSpecific
 	pathTypePrefix                 = netv1.PathTypePrefix
+	ingressTypeMeta                = metav1.TypeMeta{
+		APIVersion: netv1.SchemeGroupVersion.Group + "/" + netv1.SchemeGroupVersion.Version,
+		Kind:       "Ingress",
+	}
+	ingressGVK = schema.GroupVersionKind{
+		Group:   netv1.SchemeGroupVersion.Group,
+		Version: netv1.SchemeGroupVersion.Version,
+		Kind:    "Ingress",
+	}
 )
 
 func expectedParentIngress() *netv1.Ingress {
@@ -53,6 +63,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "a basic ingress resource with a single rule and prefix path type",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -96,8 +107,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -109,7 +121,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -125,6 +137,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "a basic ingress resource with a single rule, and only one path results in a single kong service and route",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -167,8 +180,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -180,7 +194,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -196,6 +210,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "a basic ingress resource with a single rule and prefix path type",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -239,8 +254,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -252,7 +268,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -268,6 +284,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "an ingress with path type exact gets a kong route with an exact path match",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -311,8 +328,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -324,7 +342,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -340,6 +358,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "an Ingress resource with implementation specific path type doesn't modify the path",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -383,8 +402,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -396,7 +416,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -412,6 +432,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "an Ingress resource with paths with double /'s gets flattened",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -454,8 +475,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -467,7 +489,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -483,6 +505,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "empty paths get treated as '/'",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -525,8 +548,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -538,7 +562,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -554,6 +578,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "multiple and various paths get compiled together properly",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -661,8 +686,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:  kong.String("default.test-ingress.test-service.konghq.com.80"),
@@ -681,7 +707,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -697,6 +723,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "when no host is provided, all hosts are matched",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -803,8 +830,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name: kong.String("default.test-ingress.test-service..80"),
@@ -822,7 +850,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -838,6 +866,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "when there are multiple backends services, paths wont be combined and separate kong services will be provided",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -895,8 +924,9 @@ func TestTranslateIngress(t *testing.T) {
 					Routes: []kongstate.Route{
 						{
 							Ingress: util.K8sObjectInfo{
-								Name:      "test-ingress",
-								Namespace: corev1.NamespaceDefault,
+								Name:             "test-ingress",
+								Namespace:        corev1.NamespaceDefault,
+								GroupVersionKind: ingressGVK,
 							},
 							Route: kong.Route{
 								Name:              kong.String("default.test-ingress.test-service1.konghq.com.80"),
@@ -908,7 +938,7 @@ func TestTranslateIngress(t *testing.T) {
 								StripPath:         kong.Bool(false),
 								ResponseBuffering: kong.Bool(true),
 								RequestBuffering:  kong.Bool(true),
-								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 							},
 						},
 					},
@@ -936,8 +966,9 @@ func TestTranslateIngress(t *testing.T) {
 					Routes: []kongstate.Route{
 						{
 							Ingress: util.K8sObjectInfo{
-								Name:      "test-ingress",
-								Namespace: corev1.NamespaceDefault,
+								Name:             "test-ingress",
+								Namespace:        corev1.NamespaceDefault,
+								GroupVersionKind: ingressGVK,
 							},
 							Route: kong.Route{
 								Name:              kong.String("default.test-ingress.test-service2.konghq.com.80"),
@@ -949,7 +980,7 @@ func TestTranslateIngress(t *testing.T) {
 								StripPath:         kong.Bool(false),
 								ResponseBuffering: kong.Bool(true),
 								RequestBuffering:  kong.Bool(true),
-								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 							},
 						},
 					},
@@ -966,6 +997,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "when there are multiple ingress rules with overlapping host and service, separate kong services will be provided",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -1034,8 +1066,9 @@ func TestTranslateIngress(t *testing.T) {
 					Routes: []kongstate.Route{
 						{
 							Ingress: util.K8sObjectInfo{
-								Name:      "test-ingress",
-								Namespace: corev1.NamespaceDefault,
+								Name:             "test-ingress",
+								Namespace:        corev1.NamespaceDefault,
+								GroupVersionKind: ingressGVK,
 							},
 							Route: kong.Route{
 								Name:              kong.String("default.test-ingress.ad-service.konghq.com.80"),
@@ -1047,7 +1080,7 @@ func TestTranslateIngress(t *testing.T) {
 								StripPath:         kong.Bool(false),
 								ResponseBuffering: kong.Bool(true),
 								RequestBuffering:  kong.Bool(true),
-								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 							},
 						},
 					},
@@ -1075,8 +1108,9 @@ func TestTranslateIngress(t *testing.T) {
 					Routes: []kongstate.Route{
 						{
 							Ingress: util.K8sObjectInfo{
-								Name:      "test-ingress",
-								Namespace: corev1.NamespaceDefault,
+								Name:             "test-ingress",
+								Namespace:        corev1.NamespaceDefault,
+								GroupVersionKind: ingressGVK,
 							},
 							Route: kong.Route{
 								Name:              kong.String("default.test-ingress.mad-service.konghq.co.80"),
@@ -1088,7 +1122,7 @@ func TestTranslateIngress(t *testing.T) {
 								StripPath:         kong.Bool(false),
 								ResponseBuffering: kong.Bool(true),
 								RequestBuffering:  kong.Bool(true),
-								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 							},
 						},
 					},
@@ -1105,6 +1139,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "* in host is replaced to _",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
@@ -1147,8 +1182,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service._.konghq.com.80"),
@@ -1160,7 +1196,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -1176,14 +1212,11 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "use port name when service port number is not provided",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: corev1.NamespaceDefault,
 				},
-				TypeMeta: metav1.TypeMeta{
-					Kind:       "Ingress",
-					APIVersion: "networking.k8s.io/v1",
-				},
 				Spec: netv1.IngressSpec{
 					Rules: []netv1.IngressRule{{
 						Host: "konghq.com",
@@ -1221,8 +1254,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.test-service.konghq.com.http"),
@@ -1256,6 +1290,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "KongServiceFacade used as a backend",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: "default",
@@ -1310,8 +1345,9 @@ func TestTranslateIngress(t *testing.T) {
 					},
 					Routes: []kongstate.Route{{
 						Ingress: util.K8sObjectInfo{
-							Name:      "test-ingress",
-							Namespace: corev1.NamespaceDefault,
+							Name:             "test-ingress",
+							Namespace:        corev1.NamespaceDefault,
+							GroupVersionKind: ingressGVK,
 						},
 						Route: kong.Route{
 							Name:              kong.String("default.test-ingress.konghq.com.svc-facade.svc.facade"),
@@ -1323,7 +1359,7 @@ func TestTranslateIngress(t *testing.T) {
 							StripPath:         kong.Bool(false),
 							ResponseBuffering: kong.Bool(true),
 							RequestBuffering:  kong.Bool(true),
-							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+							Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 						},
 					}},
 					Backends: []kongstate.ServiceBackend{
@@ -1349,6 +1385,7 @@ func TestTranslateIngress(t *testing.T) {
 		{
 			name: "not existing KongServiceFacade used as a backend",
 			ingresses: []*netv1.Ingress{{
+				TypeMeta: ingressTypeMeta,
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-ingress",
 					Namespace: "default",
@@ -1379,6 +1416,7 @@ func TestTranslateIngress(t *testing.T) {
 			name: "KongServiceFacade used in multiple Ingresses",
 			ingresses: []*netv1.Ingress{
 				{
+					TypeMeta: ingressTypeMeta,
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "test-ingress",
 						Namespace: "default",
@@ -1404,6 +1442,7 @@ func TestTranslateIngress(t *testing.T) {
 					},
 				},
 				{
+					TypeMeta: ingressTypeMeta,
 					ObjectMeta: metav1.ObjectMeta{
 						Name:      "test-ingress-2",
 						Namespace: "default",
@@ -1460,8 +1499,9 @@ func TestTranslateIngress(t *testing.T) {
 					Routes: []kongstate.Route{
 						{
 							Ingress: util.K8sObjectInfo{
-								Name:      "test-ingress",
-								Namespace: corev1.NamespaceDefault,
+								Name:             "test-ingress",
+								Namespace:        corev1.NamespaceDefault,
+								GroupVersionKind: ingressGVK,
 							},
 							Route: kong.Route{
 								Name:              kong.String("default.test-ingress.konghq.com.svc-facade.svc.facade"),
@@ -1473,13 +1513,14 @@ func TestTranslateIngress(t *testing.T) {
 								StripPath:         kong.Bool(false),
 								ResponseBuffering: kong.Bool(true),
 								RequestBuffering:  kong.Bool(true),
-								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"),
+								Tags:              kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 							},
 						},
 						{
 							Ingress: util.K8sObjectInfo{
-								Name:      "test-ingress-2",
-								Namespace: corev1.NamespaceDefault,
+								Name:             "test-ingress-2",
+								Namespace:        corev1.NamespaceDefault,
+								GroupVersionKind: ingressGVK,
 							},
 							Route: kong.Route{
 								Name:              kong.String("default.test-ingress-2.konghq.com.svc-facade.svc.facade"),
@@ -1491,7 +1532,7 @@ func TestTranslateIngress(t *testing.T) {
 								StripPath:         kong.Bool(false),
 								ResponseBuffering: kong.Bool(true),
 								RequestBuffering:  kong.Bool(true),
-								Tags:              kong.StringSlice("k8s-name:test-ingress-2", "k8s-namespace:default"),
+								Tags:              kong.StringSlice("k8s-name:test-ingress-2", "k8s-namespace:default", "k8s-kind:Ingress", "k8s-group:networking.k8s.io", "k8s-version:v1"),
 							},
 						},
 					},
diff --git a/test/integration/isolated/custom_entity_test.go b/test/integration/isolated/custom_entity_test.go
index b89808189..b23bfd955 100644
--- a/test/integration/isolated/custom_entity_test.go
+++ b/test/integration/isolated/custom_entity_test.go
@@ -14,6 +14,9 @@ import (
 	"github.com/kong/kubernetes-testing-framework/pkg/clusters"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	netv1 "k8s.io/api/networking/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/e2e-framework/pkg/envconf"
 	"sigs.k8s.io/e2e-framework/pkg/features"
 
@@ -132,6 +135,65 @@ func TestCustomEntityExample(t *testing.T) {
 
 			return ctx
 		}).
+		Assess("another ingress using the same degraphql plugin should also work", func(ctx context.Context, t *testing.T, conf *envconf.Config) context.Context {
+			const (
+				ingressNamespace = "default"
+				serviceName      = "hasura"
+				ingressName      = "hasura-ingress-graphql"
+				alterServiceName = "hasura-alter"
+				alterIngressName = "hasura-ingress-graphql-alter"
+			)
+			r := conf.Client().Resources()
+
+			t.Log("creating alternative service")
+			svc := corev1.Service{}
+			require.NoError(t, r.Get(ctx, serviceName, ingressNamespace, &svc))
+			alterService := &corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        alterServiceName,
+					Namespace:   ingressNamespace,
+					Labels:      svc.Labels,
+					Annotations: svc.Annotations,
+				},
+			}
+			alterService.Spec = *svc.Spec.DeepCopy()
+			alterService.Spec.ClusterIP = ""
+			alterService.Spec.ClusterIPs = []string{}
+			require.NoError(t, r.Create(ctx, alterService))
+
+			t.Log("creating alternative ingress with the same degraphql plugin attached")
+			ingress := netv1.Ingress{}
+			require.NoError(t, r.Get(ctx, ingressName, ingressNamespace, &ingress))
+			alterIngress := &netv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        alterIngressName,
+					Namespace:   ingressNamespace,
+					Labels:      ingress.Labels,
+					Annotations: ingress.Annotations,
+				},
+			}
+			alterIngress.Spec = *ingress.Spec.DeepCopy()
+			for i := range alterIngress.Spec.Rules {
+				alterIngress.Spec.Rules[i].Host = "alter-graphql.service.example"
+				for j := range alterIngress.Spec.Rules[i].HTTP.Paths {
+					alterIngress.Spec.Rules[i].HTTP.Paths[j].Backend = netv1.IngressBackend{
+						Service: &netv1.IngressServiceBackend{
+							Name: alterServiceName,
+							Port: netv1.ServiceBackendPort{
+								Number: int32(80),
+							},
+						},
+					}
+				}
+			}
+			require.NoError(t, r.Create(ctx, alterIngress))
+
+			t.Log("verifying degraphQL plugin and degraphql_routes entity works")
+			proxyURL := GetHTTPURLFromCtx(ctx)
+			helpers.EventuallyGETPath(t, proxyURL, "alter-graphql.service.example", "/contacts", http.StatusOK, `"name":"Alice"`, map[string]string{"Host": "graphql.service.example"}, consts.IngressWait, consts.WaitTick)
+
+			return ctx
+		}).
 		Teardown(featureTeardown())
 
 	tenv.Test(t, f.Feature())
diff --git a/test/internal/testenv/testenv.go b/test/internal/testenv/testenv.go
index a66b6b151..e1bb4c42d 100644
--- a/test/internal/testenv/testenv.go
+++ b/test/internal/testenv/testenv.go
@@ -251,3 +251,8 @@ func IsCI() bool {
 	// set the CI environment variable.
 	return os.Getenv("CI") == "true"
 }
+
+// KongLicenseData returns the Kong license data to use in tests.
+func KongLicenseData() string {
+	return os.Getenv("KONG_LICENSE_DATA")
+}
diff --git a/test/kongintegration/containers/kong.go b/test/kongintegration/containers/kong.go
index d2d8b31bb..95818cf2c 100644
--- a/test/kongintegration/containers/kong.go
+++ b/test/kongintegration/containers/kong.go
@@ -140,11 +140,14 @@ func (c Kong) Terminate(ctx context.Context) error {
 
 // kongImageUnderTest returns the Kong image to be used for integration tests. If both TEST_KONG_IMAGE and
 // TEST_KONG_TAG are set, it will return the image and tag specified by them. Otherwise, it will return
-// the default image (kong:latest).
+// the default image (kong:latest or kong/kong-gateway if EE tests enabled).
 func kongImageUnderTest() string {
 	if testenv.KongImage() != "" && testenv.KongTag() != "" {
 		return fmt.Sprintf("%s:%s", testenv.KongImage(), testenv.KongTag())
 	}
 
+	if testenv.KongEnterpriseEnabled() {
+		return "kong/kong-gateway:latest"
+	}
 	return "kong:latest"
 }
diff --git a/test/kongintegration/kong_client_golden_tests_outputs_test.go b/test/kongintegration/kong_client_golden_tests_outputs_test.go
index 88b70288b..bba8d8e01 100644
--- a/test/kongintegration/kong_client_golden_tests_outputs_test.go
+++ b/test/kongintegration/kong_client_golden_tests_outputs_test.go
@@ -25,6 +25,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/sendconfig"
 	"github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers"
 	"github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers/konnect"
+	"github.com/kong/kubernetes-ingress-controller/v3/test/internal/testenv"
 	"github.com/kong/kubernetes-ingress-controller/v3/test/kongintegration/containers"
 )
 
@@ -38,11 +39,17 @@ func TestKongClientGoldenTestsOutputs(t *testing.T) {
 	t.Parallel()
 	ctx := context.Background()
 
-	// TODO: Test EE features as well (requires kong/kong-gateway + license).
-	// https://github.com/Kong/kubernetes-ingress-controller/issues/4815
+	// By default, run only non-EE tests.
 	goldenTestsOutputsPaths := lo.Filter(allGoldenTestsOutputsPaths(t), func(path string, _ int) bool {
 		return !strings.Contains(path, "-ee/") // Skip Enterprise tests.
 	})
+	// If the Kong Enterprise is enabled, run all tests.
+	if testenv.KongEnterpriseEnabled() {
+		if testenv.KongLicenseData() == "" {
+			t.Skip("Kong Enterprise enabled, but no license data provided")
+		}
+		goldenTestsOutputsPaths = allGoldenTestsOutputsPaths(t)
+	}
 
 	expressionRoutesOutputsPaths := lo.Filter(goldenTestsOutputsPaths, func(path string, _ int) bool {
 		return strings.Contains(path, "expression-routes-on_")