Repository: Kong/kubernetes-ingress-controller
Pull request #6814 (2024-12-11 16:34:45)

feat(KongPlugin): generate K8s Events for misconfiguration
<!-- Thanks for sending a pull request! Here are some tips for you:
1. If this is your first time, read our contributor guidelines https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#the-pull-request-submit-process and developer guide https://github.com/kubernetes/community/blob/master/contributors/devel/development.md
2. If you want *faster* PR reviews, read how: https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
3. Follow the instructions for writing a release note: https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md and ensure your changes are being reflected in CHANGELOG.md for the next upcoming release
-->

**What this PR does / why we need it**:

One of the cases mentioned in the original issue https://github.com/Kong/kubernetes-ingress-controller/issues/6540:

`a Plugin on which an HTTPRoute depends`

can be extrapolated to any plugin attachment referring to a nonexisting plugin leads to a logs message, but no K8s Event is sent.

Status for `HTTPRoute` that references non-existing plugin is

```yml
...
  status:
    parents:
    - conditions:
      - lastTransitionTime: "2024-12-12T16:06:22Z"
        message: ""
        observedGeneration: 1
        reason: Accepted
        status: "True"
        type: Accepted
      - lastTransitionTime: "2024-12-12T16:06:22Z"
        message: ""
        observedGeneration: 1
        reason: ResolvedRefs
        status: "True"
        type: ResolvedRefs
      - lastTransitionTime: "2024-12-12T16:06:22Z"
        message: ""
        observedGeneration: 1
        reason: Unknown
        status: Unknown
        type: Programmed
      controllerName: konghq.com/kic-gateway-controller
      parentRef:
        group: gateway.networking.k8s.io
        kind: Gateway
        name: kong
        namespace: default
```

and respective K8s Events are presented

```log
default              2m51s       Warning   KongConfigurationTranslationFailed   httproute/httproute-testing                           referenced KongPlugin or KongClusterPlugin "my-amazing" does not exist
default              2m51s       Warning   KongConfigurationTranslationFailed   httproute/httproute-testing                           referenced KongPlugin or KongClusterPlugin "second" does not exist
```


The mechanism for setting appropriate status is implemented only for generated controllers with the helper [EnsureProgrammedCondition](https://github.com/Kong/kubernetes-ingress-controller/blob/67c8a9596a3ec1c8d1d87a269c12eb491dbf25da/internal/controllers/utils/conditions.go#L36-L107), for make them robust, I'm suggesting creating a dedicated issue.

There are still logs that should be events and statuses related to plugins like in [this helper](https://github.com/Kong/kubernetes-ingress-controller/blob/67c8a9596a3ec1c8d1d87a269c12eb491dbf25da/internal/dataplane/kongstate/kongstate.go#L392-L429) for cross-namespace access, but it can be handled in a separate PR or issue .




<!-- Please describe why this particular PR is necessary or why you see it as a nice addition -->

**Which issue this PR fixes**:

https://github.com/Kong/kubernetes-ingress-controller/issues/6540

<!--
Here you can add any links to issues that this PR is relevant for.
You can use Github keywords (like: closes, fixes or resolves) to auto-resolve
the linked issue(s) when this PR gets merged.

For example: fixes #<issue number>
-->

**Special notes for your reviewer**:

I wonder whether performance and/or memory consumption will be significantly damaged...

I'm going to create an issue for the enhancement of statuses and removing those other logs for missing/misconfigured Plugins

<!-- Here you can add any open questions or notes that you might have for reviewers -->

**PR Readiness Checklist**:

Complete these before marking the PR as `ready to review`:

- [x] the `CHANGELOG.md` release notes have been updated to reflect any significant (and particularly user-facing) changes introduced by this PR


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) Report
Attention: Patch coverage is `83.33333%` with `13 lines` in your changes missing coverage. Please review.
> Project coverage is 77.2%. Comparing base [(`93856b1`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/93856b1bb353de1664f1aee0f21a36b41523179a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) to head [(`6226dac`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/6226dacf98c9ea0d7518cdda2091aacba1268ac9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) | Patch % | Lines |
|---|---|---|
| [internal/dataplane/kongstate/kongstate.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&filepath=internal%2Fdataplane%2Fkongstate%2Fkongstate.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2tvbmdzdGF0ZS9rb25nc3RhdGUuZ28=) | 86.4% | [6 Missing and 2 partials :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |
| [internal/dataplane/kongstate/customentity.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&filepath=internal%2Fdataplane%2Fkongstate%2Fcustomentity.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2tvbmdzdGF0ZS9jdXN0b21lbnRpdHkuZ28=) | 77.7% | [2 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |
| [internal/dataplane/kongstate/plugin.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&filepath=internal%2Fdataplane%2Fkongstate%2Fplugin.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2tvbmdzdGF0ZS9wbHVnaW4uZ28=) | 60.0% | [2 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |
| [internal/dataplane/deckgen/generate.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&filepath=internal%2Fdataplane%2Fdeckgen%2Fgenerate.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZGF0YXBsYW5lL2RlY2tnZW4vZ2VuZXJhdGUuZ28=) | 0.0% | [1 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6814?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |

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


```diff
@@           Coverage Diff           @@
##            main   #6814     +/-   ##
=======================================
- Coverage   79.1%   77.2%   -2.0%     
=======================================
  Files        209     209             
  Lines      25149   25180     +31     
=======================================
- Hits       19915   19451    -464     
- Misses      4219    4731    +512     
+ Partials    1015     998     -17     
```

</details>

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


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c4aa74b5..3120fcdb9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -133,6 +133,10 @@ Adding a new version? You'll need three changes:
   any needed information is reported in the status of the affected object or
   with Kubernetes events.
   [#6790](https://github.com/Kong/kubernetes-ingress-controller/pull/6790)
+- Do not log `ERROR` when referenced `KongPlugin` or `KongClusterPlugin` does not exist,
+  instead generate a Kubernetes Event - `KongConfigurationTranslationFailed` for object
+  that references it.
+  [#6814](https://github.com/Kong/kubernetes-ingress-controller/pull/6814)
 - From now on, upstreams produced by KIC from `Service`s that are configured as
   upstream services (either by `ingress.kubernetes.io/service-upstream` annotation
   or through `IngressClassNamespacedParameters`'s `serviceUpstream` field), will use
diff --git a/examples/gateway-backendtlspolicy.yaml b/examples/gateway-backendtlspolicy.yaml
index 6990b0bdd..ce0a27aa4 100644
--- a/examples/gateway-backendtlspolicy.yaml
+++ b/examples/gateway-backendtlspolicy.yaml
@@ -42,7 +42,6 @@ metadata:
     # NOTE: using the default config map selector.
     konghq.com/configmap: "true"
   name: ca
-  namespace: kong
 ---
 apiVersion: gateway.networking.k8s.io/v1alpha3
 kind: BackendTLSPolicy
diff --git a/internal/controllers/configuration/configmap_controller.go b/internal/controllers/configuration/configmap_controller.go
index ed642df10..d952ac363 100644
--- a/internal/controllers/configuration/configmap_controller.go
+++ b/internal/controllers/configuration/configmap_controller.go
@@ -75,7 +75,7 @@ func (r *CoreV1ConfigMapReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		}).
 		For(&corev1.ConfigMap{},
 			builder.WithPredicates(
-				predicate.And(
+				predicate.Or(
 					predicateFuncs,
 					labelPredicate,
 				)),
diff --git a/internal/controllers/configuration/secret_controller.go b/internal/controllers/configuration/secret_controller.go
index 3f57a09c9..da64d217d 100644
--- a/internal/controllers/configuration/secret_controller.go
+++ b/internal/controllers/configuration/secret_controller.go
@@ -79,7 +79,7 @@ func (r *CoreV1SecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		}).
 		For(&corev1.Secret{},
 			builder.WithPredicates(
-				predicate.And(
+				predicate.Or(
 					predicateFuncs,
 					labelPredicate,
 				)),
diff --git a/internal/dataplane/deckgen/generate.go b/internal/dataplane/deckgen/generate.go
index b200e3659..446e50f2e 100644
--- a/internal/dataplane/deckgen/generate.go
+++ b/internal/dataplane/deckgen/generate.go
@@ -246,7 +246,7 @@ func fillPlugin(ctx context.Context, plugin *file.FPlugin, schemas PluginSchemaS
 	}
 	schema, err := schemas.Schema(ctx, *plugin.Name)
 	if err != nil {
-		return fmt.Errorf("error retrieveing schema for plugin %s: %w", *plugin.Name, err)
+		return fmt.Errorf("error retrieving schema for plugin %s: %w", *plugin.Name, err)
 	}
 	if plugin.Config == nil {
 		plugin.Config = make(kong.Configuration)
diff --git a/internal/dataplane/kongstate/customentity.go b/internal/dataplane/kongstate/customentity.go
index 271d75b14..179ec2fdf 100644
--- a/internal/dataplane/kongstate/customentity.go
+++ b/internal/dataplane/kongstate/customentity.go
@@ -393,19 +393,19 @@ func findCustomEntityForeignFields(
 			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) {
+			foreignRelations.Route = lo.FilterMap(rels.Routes, func(r *Route, _ int) (util.FR, bool) {
 				if err := r.FillID(workspace); err != nil {
-					return "", false
+					return util.FR{}, false
 				}
-				return *r.ID, true
+				return util.FR{Identifier: *r.ID, Referer: k8sEntity}, true
 			})
 		case string(kong.EntityTypeConsumers):
 			foreignConsumerFields = append(foreignConsumerFields, fieldName)
-			foreignRelations.Consumer = lo.FilterMap(rels.Consumers, func(c *Consumer, _ int) (string, bool) {
+			foreignRelations.Consumer = lo.FilterMap(rels.Consumers, func(c *Consumer, _ int) (util.FR, bool) {
 				if err := c.FillID(workspace); err != nil {
-					return "", false
+					return util.FR{}, false
 				}
-				return *c.ID, true
+				return util.FR{Identifier: *c.ID, Referer: k8sEntity}, true
 			})
 		} // end of switch
 	}
@@ -419,21 +419,21 @@ func findCustomEntityForeignFields(
 			foreignFieldValues = append(foreignFieldValues, entityForeignFieldValue{
 				fieldName:         fieldName,
 				foreignEntityType: kong.EntityTypeServices,
-				foreignEntityID:   combination.Service,
+				foreignEntityID:   combination.Service.Identifier,
 			})
 		}
 		for _, fieldName := range foreignRouteFields {
 			foreignFieldValues = append(foreignFieldValues, entityForeignFieldValue{
 				fieldName:         fieldName,
 				foreignEntityType: kong.EntityTypeRoutes,
-				foreignEntityID:   combination.Route,
+				foreignEntityID:   combination.Route.Identifier,
 			})
 		}
 		for _, fieldName := range foreignConsumerFields {
 			foreignFieldValues = append(foreignFieldValues, entityForeignFieldValue{
 				fieldName:         fieldName,
 				foreignEntityType: kong.EntityTypeConsumers,
-				foreignEntityID:   combination.Consumer,
+				foreignEntityID:   combination.Consumer.Identifier,
 			})
 		}
 		ret = append(ret, foreignFieldValues)
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index dfc3ab61c..370c48179 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -411,15 +411,19 @@ func (ks *KongState) getPluginRelations(cacheStore store.Storer, log logr.Logger
 		if !ok {
 			relations = util.ForeignRelations{}
 		}
+		fr := util.FR{
+			Identifier: identifier,
+			Referer:    referrer,
+		}
 		switch t {
 		case ConsumerRelation:
-			relations.Consumer = append(relations.Consumer, identifier)
+			relations.Consumer = append(relations.Consumer, fr)
 		case ConsumerGroupRelation:
-			relations.ConsumerGroup = append(relations.ConsumerGroup, identifier)
+			relations.ConsumerGroup = append(relations.ConsumerGroup, fr)
 		case RouteRelation:
-			relations.Route = append(relations.Route, identifier)
+			relations.Route = append(relations.Route, fr)
 		case ServiceRelation:
-			relations.Service = append(relations.Service, identifier)
+			relations.Service = append(relations.Service, fr)
 		}
 		pluginRels[pluginKey] = relations
 	}
@@ -548,14 +552,27 @@ func buildPlugins(
 	for pluginIdentifier, relations := range pluginRels {
 		identifier := strings.Split(pluginIdentifier, ":")
 		namespace, kongPluginName := identifier[0], identifier[1]
-		k8sPlugin, k8sClusterPlugin, err := getKongPluginOrKongClusterPlugin(s, namespace, kongPluginName)
+		k8sPlugin, k8sClusterPlugin, pluginFound, err := getKongPluginOrKongClusterPlugin(s, namespace, kongPluginName)
 		if err != nil {
 			logger.Error(err, "Failed to fetch KongPlugin resource",
 				"kongplugin_name", kongPluginName,
 				"kongplugin_namespace", namespace)
 			continue
 		}
-
+		// For non-existing plugin push failure K8s Events for all K8s object that reference it.
+		if !pluginFound {
+			for _, rel := range relations.GetCombinations() {
+				for _, ref := range rel.ToList() {
+					if !ref.IsEmpty() {
+						failuresCollector.PushResourceFailure(
+							fmt.Sprintf("referenced KongPlugin or KongClusterPlugin %q does not exist", kongPluginName),
+							ref.Referer,
+						)
+					}
+				}
+			}
+			continue
+		}
 		var plugin Plugin
 		if k8sPlugin != nil {
 			plugin, err = kongPluginFromK8SPlugin(s, *k8sPlugin)
@@ -579,27 +596,30 @@ func buildPlugins(
 			var toHash bytes.Buffer
 
 			// ID is populated because that is read by decK and in_memory translator too
-			if rel.Service != "" {
-				plugin.Service = &kong.Service{ID: kong.String(rel.Service)}
-				toHash.WriteString("_service-" + rel.Service)
+			if !rel.Service.IsEmpty() {
+				relSvc := rel.Service.Identifier
+				plugin.Service = &kong.Service{ID: kong.String(relSvc)}
+				toHash.WriteString("_service-" + relSvc)
 			}
-			if rel.Route != "" {
-				plugin.Route = &kong.Route{ID: kong.String(rel.Route)}
-				toHash.WriteString("_route-" + rel.Route)
+			if !rel.Route.IsEmpty() {
+				relRoute := rel.Route.Identifier
+				plugin.Route = &kong.Route{ID: kong.String(relRoute)}
+				toHash.WriteString("_route-" + relRoute)
 			}
-			if rel.Consumer != "" {
-				plugin.Consumer = &kong.Consumer{ID: kong.String(rel.Consumer)}
-				toHash.WriteString("_consumer-" + rel.Consumer)
+			if !rel.Consumer.IsEmpty() {
+				relConsumer := rel.Consumer.Identifier
+				plugin.Consumer = &kong.Consumer{ID: kong.String(relConsumer)}
+				toHash.WriteString("_consumer-" + relConsumer)
 			}
-			if rel.ConsumerGroup != "" {
-				plugin.ConsumerGroup = &kong.ConsumerGroup{ID: kong.String(rel.ConsumerGroup)}
-				toHash.WriteString("_group-" + rel.ConsumerGroup)
+			if !rel.ConsumerGroup.IsEmpty() {
+				relConsumerGroup := rel.ConsumerGroup.Identifier
+				plugin.ConsumerGroup = &kong.ConsumerGroup{ID: kong.String(relConsumerGroup)}
+				toHash.WriteString("_group-" + relConsumerGroup)
 			}
 
 			// instance_name must be unique. Using the same KongPlugin on multiple resources will result in duplicates
 			// unless we add some sort of suffix.
 			if plugin.InstanceName != nil {
-
 				sha := sha256.Sum256(toHash.Bytes())
 				suffix := fmt.Sprintf("%x", sha)
 				short := suffix[:9]
@@ -755,36 +775,40 @@ func maybeLogKongIngressDeprecationError(logger logr.Logger, services []*corev1.
 
 // 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 {
+func getServiceIDFromPluginRels(
+	log logr.Logger, rels RelatedEntitiesRef, routeAttachedService map[string]*Service, workspace string,
+) []util.FR {
 	// Return IDs of directly referred services.
 	if len(rels.Services) > 0 {
-		return lo.FilterMap(rels.Services, func(s *Service, _ int) (string, bool) {
+		return lo.FilterMap(rels.Services, func(s *Service, _ int) (util.FR, bool) {
 			if err := s.FillID(workspace); err != nil {
 				log.Error(err, "failed to fill ID for service")
-				return "", false
+				return util.FR{}, false
 			}
-			return *s.ID, true
-		},
-		)
+			return util.FR{Identifier: *s.ID, Referer: s.Parent}, true
+		})
 	}
 	// Returns IDs of services where the referred routes attaches.
-	if len(rels.Routes) > 0 {
-		serviceIDs := lo.FilterMap(
-			rels.Routes, func(r *Route, _ int) (string, bool) {
-				svc, ok := routeAttachedService[*r.Name]
-				if !ok {
-					return "", false
-				}
-				if err := svc.FillID(workspace); err != nil {
-					log.Error(err, "failed to fill ID for service")
-					return "", false
-				}
-				return *svc.ID, true
-			},
-		)
-		return lo.Uniq(serviceIDs)
-	}
-	return nil
+	serviceIDs := lo.FilterMap(
+		rels.Routes,
+		func(r *Route, _ int) (util.FR, bool) {
+			svc, ok := routeAttachedService[*r.Name]
+			if !ok {
+				return util.FR{}, false
+			}
+			if err := svc.FillID(workspace); err != nil {
+				log.Error(err, "failed to fill ID for service")
+				return util.FR{}, false
+			}
+			return util.FR{
+				Identifier: *svc.ID,
+				Referer:    svc.Parent,
+			}, true
+		},
+	)
+	return lo.UniqBy(serviceIDs, func(fr util.FR) string {
+		return fr.Identifier
+	})
 }
 
 // getPluginRelatedEntitiesRef gets services/routes/consumers referred by each plugin and returns the pointer to them.
diff --git a/internal/dataplane/kongstate/plugin.go b/internal/dataplane/kongstate/plugin.go
index 2b866aa0e..106eccd8a 100644
--- a/internal/dataplane/kongstate/plugin.go
+++ b/internal/dataplane/kongstate/plugin.go
@@ -18,33 +18,34 @@ import (
 )
 
 // getKongPluginOrKongClusterPlugin fetches a KongPlugin or KongClusterPlugin (as fallback) from the store.
-// If both are not found, an error is returned.
+// If both don't exist pluginFound is set to false.
 func getKongPluginOrKongClusterPlugin(s store.Storer, namespace, name string) (
-	*kongv1.KongPlugin,
-	*kongv1.KongClusterPlugin,
-	error,
+	kp *kongv1.KongPlugin,
+	kcp *kongv1.KongClusterPlugin,
+	pluginFound bool,
+	err error,
 ) {
 	plugin, pluginErr := s.GetKongPlugin(namespace, name)
 	if pluginErr != nil {
 		if !errors.As(pluginErr, &store.NotFoundError{}) {
-			return nil, nil, fmt.Errorf("failed fetching KongPlugin: %w", pluginErr)
+			return nil, nil, false, fmt.Errorf("failed fetching KongPlugin: %w", pluginErr)
 		}
 
 		// If KongPlugin is not found, try to fetch KongClusterPlugin.
 		clusterPlugin, err := s.GetKongClusterPlugin(name)
 		if err != nil {
 			if !errors.As(err, &store.NotFoundError{}) {
-				return nil, nil, fmt.Errorf("failed fetching KongClusterPlugin: %w", err)
+				return nil, nil, false, fmt.Errorf("failed fetching KongClusterPlugin: %w", err)
 			}
 
 			// Both KongPlugin and KongClusterPlugin are not found.
-			return nil, nil, fmt.Errorf("no KongPlugin or KongClusterPlugin was found for %s/%s", namespace, name)
+			return nil, nil, false, nil
 		}
 
-		return nil, clusterPlugin, nil
+		return nil, clusterPlugin, true, nil
 	}
 
-	return plugin, nil, nil
+	return plugin, nil, true, nil
 }
 
 func kongPluginFromK8SClusterPlugin(
diff --git a/internal/util/relations.go b/internal/util/relations.go
index bc923d8c1..bcf9c155e 100644
--- a/internal/util/relations.go
+++ b/internal/util/relations.go
@@ -1,11 +1,29 @@
 package util
 
+import "sigs.k8s.io/controller-runtime/pkg/client"
+
+// FR represents a foreign relation.
+type FR struct {
+	Identifier string
+	// Referer represents K8s object that references a Plugin.
+	Referer client.Object
+}
+
+func (fr FR) IsEmpty() bool {
+	return fr == FR{}
+}
+
 type ForeignRelations struct {
-	Consumer, ConsumerGroup, Route, Service []string
+	Consumer, ConsumerGroup, Route, Service []FR
 }
 
 type Rel struct {
-	Consumer, ConsumerGroup, Route, Service string
+	Consumer, ConsumerGroup, Route, Service FR
+}
+
+// ToList returns a list of FRs of Consumer, ConsumerGroup, Route, Service as one list.
+func (r Rel) ToList() [4]FR {
+	return [4]FR{r.Consumer, r.ConsumerGroup, r.Route, r.Service}
 }
 
 func (relations *ForeignRelations) GetCombinations() []Rel {


Test patch:
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index 338855073..a4a620953 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -12,12 +12,14 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/go-logr/logr/testr"
 	"github.com/go-logr/zapr"
+	"github.com/google/go-cmp/cmp"
 	"github.com/kong/go-kong/kong"
 	"github.com/samber/lo"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
 	corev1 "k8s.io/api/core/v1"
+	netv1 "k8s.io/api/networking/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	k8stypes "k8s.io/apimachinery/pkg/types"
@@ -231,377 +233,453 @@ func ensureAllKongStateFieldsAreCoveredInTest(t *testing.T, testedFields []strin
 }
 
 func TestGetPluginRelations(t *testing.T) {
-	type args struct {
-		state KongState
+	type data struct {
+		inputState              KongState
+		expectedPluginRelations map[string]util.ForeignRelations
 	}
 	tests := []struct {
 		name string
-		args args
-		want map[string]util.ForeignRelations
+		data data
 	}{
 		{
 			name: "empty state",
-			want: map[string]util.ForeignRelations{},
+			data: data{
+				inputState:              KongState{},
+				expectedPluginRelations: map[string]util.ForeignRelations{},
+			},
 		},
 		{
 			name: "single consumer annotation",
-			args: args{
-				state: KongState{
-					Consumers: []Consumer{
-						{
-							Consumer: kong.Consumer{
-								Username: kong.String("foo-consumer"),
-							},
-							K8sKongConsumer: kongv1.KongConsumer{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-									},
+			data: func() data {
+				k8sKongConsumer := kongv1.KongConsumer{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns1",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+						},
+					},
+				}
+
+				return data{
+					inputState: KongState{
+						Consumers: []Consumer{
+							{
+								Consumer: kong.Consumer{
+									Username: kong.String("foo-consumer"),
 								},
+								K8sKongConsumer: k8sKongConsumer,
 							},
 						},
 					},
-				},
-			},
-			want: map[string]util.ForeignRelations{
-				"ns1:foo": {Consumer: []string{"foo-consumer"}},
-				"ns1:bar": {Consumer: []string{"foo-consumer"}},
-			},
+					expectedPluginRelations: map[string]util.ForeignRelations{
+						"ns1:foo": {Consumer: []util.FR{{Identifier: "foo-consumer", Referer: &k8sKongConsumer}}},
+						"ns1:bar": {Consumer: []util.FR{{Identifier: "foo-consumer", Referer: &k8sKongConsumer}}},
+					},
+				}
+			}(),
 		},
 		{
 			name: "single consumer group annotation",
-			args: args{
-				state: KongState{
-					ConsumerGroups: []ConsumerGroup{
-						{
-							ConsumerGroup: kong.ConsumerGroup{
-								Name: kong.String("foo-consumer-group"),
-							},
-							K8sKongConsumerGroup: kongv1beta1.KongConsumerGroup{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-									},
+			data: func() data {
+				k8sKongConsumerGroup := kongv1beta1.KongConsumerGroup{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns1",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+						},
+					},
+				}
+
+				return data{
+					inputState: KongState{
+						ConsumerGroups: []ConsumerGroup{
+							{
+								ConsumerGroup: kong.ConsumerGroup{
+									Name: kong.String("foo-consumer-group"),
 								},
+								K8sKongConsumerGroup: k8sKongConsumerGroup,
 							},
 						},
 					},
-				},
-			},
-			want: map[string]util.ForeignRelations{
-				"ns1:foo": {ConsumerGroup: []string{"foo-consumer-group"}},
-				"ns1:bar": {ConsumerGroup: []string{"foo-consumer-group"}},
-			},
+					expectedPluginRelations: map[string]util.ForeignRelations{
+						"ns1:foo": {ConsumerGroup: []util.FR{{Identifier: "foo-consumer-group", Referer: &k8sKongConsumerGroup}}},
+						"ns1:bar": {ConsumerGroup: []util.FR{{Identifier: "foo-consumer-group", Referer: &k8sKongConsumerGroup}}},
+					},
+				}
+			}(),
 		},
 		{
 			name: "single service annotation",
-			args: args{
-				state: KongState{
-					Services: []Service{
-						{
-							Service: kong.Service{
-								Name: kong.String("foo-service"),
-							},
-							K8sServices: map[string]*corev1.Service{
-								"foo-service": {
-									ObjectMeta: metav1.ObjectMeta{
-										Namespace: "ns1",
-										Annotations: map[string]string{
-											annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-										},
-									},
+			data: func() data {
+				k8sService := &corev1.Service{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns1",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+						},
+					},
+				}
+
+				return data{
+					inputState: KongState{
+						Services: []Service{
+							{
+								Service: kong.Service{
+									Name: kong.String("foo-service"),
+								},
+								K8sServices: map[string]*corev1.Service{
+									"foo-service": k8sService,
 								},
 							},
 						},
 					},
-				},
-			},
-			want: map[string]util.ForeignRelations{
-				"ns1:foo": {Service: []string{"foo-service"}},
-				"ns1:bar": {Service: []string{"foo-service"}},
-			},
+					expectedPluginRelations: map[string]util.ForeignRelations{
+						"ns1:foo": {Service: []util.FR{{Identifier: "foo-service", Referer: k8sService}}},
+						"ns1:bar": {Service: []util.FR{{Identifier: "foo-service", Referer: k8sService}}},
+					},
+				}
+			}(),
 		},
 		{
 			name: "single Ingress annotation",
-			args: args{
-				state: KongState{
-					Services: []Service{
-						{
-							Service: kong.Service{
-								Name: kong.String("foo-service"),
-							},
-							Routes: []Route{
-								{
-									Route: kong.Route{
-										Name: kong.String("foo-route"),
-									},
-									Ingress: util.K8sObjectInfo{
-										Name:      "some-ingress",
-										Namespace: "ns2",
-										Annotations: map[string]string{
-											annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+			data: func() data {
+				k8sIngress := netv1.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "some-ingress",
+						Namespace: "ns2",
+					},
+				}
+
+				return data{
+					inputState: KongState{
+						Services: []Service{
+							{
+								Service: kong.Service{
+									Name: kong.String("foo-service"),
+								},
+								Routes: []Route{
+									{
+										Route: kong.Route{
+											Name: kong.String("foo-route"),
+										},
+										Ingress: util.K8sObjectInfo{
+											Name:      k8sIngress.Name,
+											Namespace: k8sIngress.Namespace,
+											Annotations: map[string]string{
+												annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+											},
 										},
 									},
 								},
 							},
 						},
 					},
-				},
-			},
-			want: map[string]util.ForeignRelations{
-				"ns2:foo": {Route: []string{"foo-route"}},
-				"ns2:bar": {Route: []string{"foo-route"}},
-			},
+					expectedPluginRelations: map[string]util.ForeignRelations{
+						"ns2:foo": {Route: []util.FR{{Identifier: "foo-route", Referer: &k8sIngress}}},
+						"ns2:bar": {Route: []util.FR{{Identifier: "foo-route", Referer: &k8sIngress}}},
+					},
+				}
+			}(),
 		},
 		{
 			name: "multiple routes with annotation",
-			args: args{
-				state: KongState{
-					Services: []Service{
-						{
-							Service: kong.Service{
-								Name: kong.String("foo-service"),
-							},
-							Routes: []Route{
-								{
-									Route: kong.Route{
-										Name: kong.String("foo-route"),
-									},
-									Ingress: util.K8sObjectInfo{
-										Name:      "some-ingress",
-										Namespace: "ns2",
-										Annotations: map[string]string{
-											annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-										},
-									},
+			data: func() data {
+				k8sIngress := &netv1.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "some-ingress",
+						Namespace: "ns2",
+					},
+				}
+
+				return data{
+					inputState: KongState{
+						Services: []Service{
+							{
+								Service: kong.Service{
+									Name: kong.String("foo-service"),
 								},
-								{
-									Route: kong.Route{
-										Name: kong.String("bar-route"),
+								Routes: []Route{
+									{
+										Route: kong.Route{
+											Name: kong.String("foo-route"),
+										},
+										Ingress: util.K8sObjectInfo{
+											Name:      k8sIngress.Name,
+											Namespace: k8sIngress.Namespace,
+											Annotations: map[string]string{
+												annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+											},
+										},
 									},
-									Ingress: util.K8sObjectInfo{
-										Name:      "some-ingress",
-										Namespace: "ns2",
-										Annotations: map[string]string{
-											annotations.AnnotationPrefix + annotations.PluginsKey: "bar,baz",
+									{
+										Route: kong.Route{
+											Name: kong.String("bar-route"),
+										},
+										Ingress: util.K8sObjectInfo{
+											Name:      k8sIngress.Name,
+											Namespace: k8sIngress.Namespace,
+											Annotations: map[string]string{
+												annotations.AnnotationPrefix + annotations.PluginsKey: "bar,baz",
+											},
 										},
 									},
 								},
 							},
 						},
 					},
-				},
-			},
-			want: map[string]util.ForeignRelations{
-				"ns2:foo": {Route: []string{"foo-route"}},
-				"ns2:bar": {Route: []string{"foo-route", "bar-route"}},
-				"ns2:baz": {Route: []string{"bar-route"}},
-			},
+					expectedPluginRelations: map[string]util.ForeignRelations{
+						"ns2:foo": {Route: []util.FR{{Identifier: "foo-route", Referer: k8sIngress}}},
+						"ns2:bar": {Route: []util.FR{{Identifier: "foo-route", Referer: k8sIngress}, {Identifier: "bar-route", Referer: k8sIngress}}},
+						"ns2:baz": {Route: []util.FR{{Identifier: "bar-route", Referer: k8sIngress}}},
+					},
+				}
+			}(),
 		},
 		{
 			name: "multiple consumers, consumer groups, routes and services",
-			args: args{
-				state: KongState{
-					Consumers: []Consumer{
-						{
-							Consumer: kong.Consumer{
-								Username: kong.String("foo-consumer"),
-							},
-							K8sKongConsumer: kongv1.KongConsumer{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-									},
-								},
-							},
+			data: func() data {
+				// Kong consumers.
+				k8sKongConsumer1Foobar := kongv1.KongConsumer{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns1",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foobar",
 						},
-						{
-							Consumer: kong.Consumer{
-								Username: kong.String("foo-consumer"),
-							},
-							K8sKongConsumer: kongv1.KongConsumer{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns2",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-									},
-								},
-							},
+					},
+				}
+				k8sKongConsumer1FooBar := kongv1.KongConsumer{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns1",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
 						},
-						{
-							Consumer: kong.Consumer{
-								Username: kong.String("bar-consumer"),
-							},
-							K8sKongConsumer: kongv1.KongConsumer{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foobar",
-									},
-								},
-							},
+					},
+				}
+				k8sKongConsumer2FooBar := kongv1.KongConsumer{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns2",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
 						},
 					},
-					ConsumerGroups: []ConsumerGroup{
-						{
-							ConsumerGroup: kong.ConsumerGroup{
-								Name: kong.String("foo-consumer-group"),
-							},
-							K8sKongConsumerGroup: kongv1beta1.KongConsumerGroup{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-									},
+				}
+				// Kong consumer groups.
+				k8sKongConsumerGroup1FooBar := kongv1beta1.KongConsumerGroup{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns1",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+						},
+					},
+				}
+				k8sKongConsumerGroup2FooBar := kongv1beta1.KongConsumerGroup{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns2",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+						},
+					},
+				}
+
+				k8sKongConsumerGroup2BarBaz := kongv1beta1.KongConsumerGroup{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns2",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "bar,baz",
+						},
+					},
+				}
+				// Services
+				k8sService := &corev1.Service{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns1",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+						},
+					},
+				}
+				// Ingress
+				k8sIngress := &netv1.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "some-ingress",
+						Namespace: "ns2",
+					},
+				}
+
+				return data{
+					inputState: KongState{
+						Consumers: []Consumer{
+							{
+								Consumer: kong.Consumer{
+									Username: kong.String("foo-consumer"),
 								},
+								K8sKongConsumer: k8sKongConsumer1FooBar,
 							},
-						},
-						{
-							ConsumerGroup: kong.ConsumerGroup{
-								Name: kong.String("foo-consumer-group"),
+							{
+								Consumer: kong.Consumer{
+									Username: kong.String("foo-consumer"),
+								},
+								K8sKongConsumer: k8sKongConsumer2FooBar,
 							},
-							K8sKongConsumerGroup: kongv1beta1.KongConsumerGroup{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns2",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-									},
+							{
+								Consumer: kong.Consumer{
+									Username: kong.String("bar-consumer"),
 								},
+								K8sKongConsumer: k8sKongConsumer1Foobar,
 							},
 						},
-						{
-							ConsumerGroup: kong.ConsumerGroup{
-								Name: kong.String("bar-consumer-group"),
-							},
-							K8sKongConsumerGroup: kongv1beta1.KongConsumerGroup{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns2",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "bar,baz",
-									},
+						ConsumerGroups: []ConsumerGroup{
+							{
+								ConsumerGroup: kong.ConsumerGroup{
+									Name: kong.String("foo-consumer-group"),
 								},
+								K8sKongConsumerGroup: k8sKongConsumerGroup1FooBar,
 							},
-						},
-					},
-					Services: []Service{
-						{
-							Service: kong.Service{
-								Name: kong.String("foo-service"),
+							{
+								ConsumerGroup: kong.ConsumerGroup{
+									Name: kong.String("foo-consumer-group"),
+								},
+								K8sKongConsumerGroup: k8sKongConsumerGroup2FooBar,
 							},
-							K8sServices: map[string]*corev1.Service{
-								"foo-service": {
-									ObjectMeta: metav1.ObjectMeta{
-										Namespace: "ns1",
-										Annotations: map[string]string{
-											annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-										},
-									},
+							{
+								ConsumerGroup: kong.ConsumerGroup{
+									Name: kong.String("bar-consumer-group"),
 								},
+								K8sKongConsumerGroup: k8sKongConsumerGroup2BarBaz,
 							},
-							Routes: []Route{
-								{
-									Route: kong.Route{
-										Name: kong.String("foo-route"),
-									},
-									Ingress: util.K8sObjectInfo{
-										Name:      "some-ingress",
-										Namespace: "ns2",
-										Annotations: map[string]string{
-											annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
-										},
-									},
+						},
+						Services: []Service{
+							{
+								Service: kong.Service{
+									Name: kong.String("foo-service"),
+								},
+								K8sServices: map[string]*corev1.Service{
+									"foo-service": k8sService,
 								},
-								{
-									Route: kong.Route{
-										Name: kong.String("bar-route"),
+								Routes: []Route{
+									{
+										Route: kong.Route{
+											Name: kong.String("foo-route"),
+										},
+										Ingress: util.K8sObjectInfo{
+											Name:      "some-ingress",
+											Namespace: "ns2",
+											Annotations: map[string]string{
+												annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+											},
+										},
 									},
-									Ingress: util.K8sObjectInfo{
-										Name:      "some-ingress",
-										Namespace: "ns2",
-										Annotations: map[string]string{
-											annotations.AnnotationPrefix + annotations.PluginsKey: "bar,baz",
+									{
+										Route: kong.Route{
+											Name: kong.String("bar-route"),
+										},
+										Ingress: util.K8sObjectInfo{
+											Name:      "some-ingress",
+											Namespace: "ns2",
+											Annotations: map[string]string{
+												annotations.AnnotationPrefix + annotations.PluginsKey: "bar,baz",
+											},
 										},
 									},
 								},
 							},
 						},
 					},
-				},
-			},
-			want: map[string]util.ForeignRelations{
-				"ns1:foo":    {Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group"}, Service: []string{"foo-service"}},
-				"ns1:bar":    {Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group"}, Service: []string{"foo-service"}},
-				"ns1:foobar": {Consumer: []string{"bar-consumer"}},
-				"ns2:foo": {
-					Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group"}, Route: []string{"foo-route"},
-				},
-				"ns2:bar": {
-					Consumer: []string{"foo-consumer"}, ConsumerGroup: []string{"foo-consumer-group", "bar-consumer-group"}, Route: []string{"foo-route", "bar-route"},
-				},
-				"ns2:baz": {Route: []string{"bar-route"}, ConsumerGroup: []string{"bar-consumer-group"}},
-			},
+					expectedPluginRelations: map[string]util.ForeignRelations{
+						"ns1:foo": {
+							Consumer:      []util.FR{{Identifier: "foo-consumer", Referer: &k8sKongConsumer1FooBar}},
+							ConsumerGroup: []util.FR{{Identifier: "foo-consumer-group", Referer: &k8sKongConsumerGroup1FooBar}},
+							Service:       []util.FR{{Identifier: "foo-service", Referer: k8sService}},
+						},
+						"ns1:bar": {
+							Consumer:      []util.FR{{Identifier: "foo-consumer", Referer: &k8sKongConsumer1FooBar}},
+							ConsumerGroup: []util.FR{{Identifier: "foo-consumer-group", Referer: &k8sKongConsumerGroup1FooBar}},
+							Service:       []util.FR{{Identifier: "foo-service", Referer: k8sService}},
+						},
+						"ns1:foobar": {
+							Consumer: []util.FR{{Identifier: "bar-consumer", Referer: &k8sKongConsumer1Foobar}},
+						},
+						"ns2:foo": {
+							Consumer:      []util.FR{{Identifier: "foo-consumer", Referer: &k8sKongConsumer2FooBar}},
+							ConsumerGroup: []util.FR{{Identifier: "foo-consumer-group", Referer: &k8sKongConsumerGroup2FooBar}},
+							Route:         []util.FR{{Identifier: "foo-route", Referer: k8sIngress}},
+						},
+						"ns2:bar": {
+							Consumer:      []util.FR{{Identifier: "foo-consumer", Referer: &k8sKongConsumer2FooBar}},
+							ConsumerGroup: []util.FR{{Identifier: "foo-consumer-group", Referer: &k8sKongConsumerGroup2FooBar}, {Identifier: "bar-consumer-group", Referer: &k8sKongConsumerGroup2BarBaz}},
+							Route:         []util.FR{{Identifier: "foo-route", Referer: k8sIngress}, {Identifier: "bar-route", Referer: k8sIngress}},
+						},
+						"ns2:baz": {
+							Route:         []util.FR{{Identifier: "bar-route", Referer: k8sIngress}},
+							ConsumerGroup: []util.FR{{Identifier: "bar-consumer-group", Referer: &k8sKongConsumerGroup2BarBaz}},
+						},
+					},
+				}
+			}(),
 		},
 		{
 			name: "consumer with custom_id and a plugin attached",
-			args: args{
-				state: KongState{
-					Consumers: []Consumer{
-						{
-							Consumer: kong.Consumer{
-								CustomID: kong.String("1234-1234"),
-							},
-							K8sKongConsumer: kongv1.KongConsumer{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "default",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "rate-limiting-1",
-									},
-								},
-							},
+			data: func() data {
+				k8sKongConsumer := kongv1.KongConsumer{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.PluginsKey: "rate-limiting-1",
 						},
 					},
-					Plugins: []Plugin{
-						{
-							Plugin: kong.Plugin{
-								Name: kong.String("rate-limiting"),
-							},
-							K8sParent: &kongv1.KongPlugin{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "default",
-									Name:      "rate-limiting-1",
+				}
+
+				return data{
+					inputState: KongState{
+						Consumers: []Consumer{
+							{
+								Consumer: kong.Consumer{
+									CustomID: kong.String("1234-1234"),
 								},
-								PluginName: "rate-limiting",
+								K8sKongConsumer: k8sKongConsumer,
 							},
 						},
-						{
-							Plugin: kong.Plugin{
-								Name: kong.String("basic-auth"),
+						Plugins: []Plugin{
+							{
+								Plugin: kong.Plugin{
+									Name: kong.String("rate-limiting"),
+								},
+								K8sParent: &kongv1.KongPlugin{
+									ObjectMeta: metav1.ObjectMeta{
+										Namespace: "default",
+										Name:      "rate-limiting-1",
+									},
+									PluginName: "rate-limiting",
+								},
 							},
-							K8sParent: &kongv1.KongPlugin{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "default",
-									Name:      "basic-auth-1",
+							{
+								Plugin: kong.Plugin{
+									Name: kong.String("basic-auth"),
+								},
+								K8sParent: &kongv1.KongPlugin{
+									ObjectMeta: metav1.ObjectMeta{
+										Namespace: "default",
+										Name:      "basic-auth-1",
+									},
+									PluginName: "basic-auth",
 								},
-								PluginName: "basic-auth",
 							},
 						},
 					},
-				},
-			},
-			want: map[string]util.ForeignRelations{
-				"default:rate-limiting-1": {Consumer: []string{"1234-1234"}},
-			},
+					expectedPluginRelations: map[string]util.ForeignRelations{
+						"default:rate-limiting-1": {Consumer: []util.FR{{Identifier: "1234-1234", Referer: &k8sKongConsumer}}},
+					},
+				}
+			}(),
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			store, err := store.NewFakeStore(store.FakeObjects{})
 			require.NoError(t, err)
-			if got := tt.args.state.getPluginRelations(store, logr.Discard()); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("getPluginRelations() = %v, want %v", got, tt.want)
+			computedPluginRelations := tt.data.inputState.getPluginRelations(store, logr.Discard())
+			if diff := cmp.Diff(tt.data.expectedPluginRelations, computedPluginRelations); diff != "" {
+				t.Fatal("expected value differs from actual, see the human-readable diff:", diff)
 			}
 		})
 	}
@@ -1345,7 +1423,7 @@ func TestKongState_BuildPluginsCollisions(t *testing.T) {
 				"default:foo-plugin": {
 					// this shouldn't happen in practice, as all generated route names are unique
 					// however, it's hard to find a SHA256 collision with two different inputs
-					Route: []string{"collision", "collision"},
+					Route: []util.FR{{Identifier: "collision"}, {Identifier: "collision"}},
 				},
 			},
 			want: []string{
@@ -1366,8 +1444,8 @@ func TestKongState_BuildPluginsCollisions(t *testing.T) {
 			},
 			pluginRels: map[string]util.ForeignRelations{
 				"default:foo-plugin": {
-					Route:         []string{"route1", "route2", "route3"},
-					ConsumerGroup: []string{"group1"},
+					Route:         []util.FR{{Identifier: "route1"}, {Identifier: "route2"}, {Identifier: "route3"}},
+					ConsumerGroup: []util.FR{{Identifier: "group1"}},
 				},
 			},
 			want: []string{
@@ -1389,8 +1467,8 @@ func TestKongState_BuildPluginsCollisions(t *testing.T) {
 			},
 			pluginRels: map[string]util.ForeignRelations{
 				"default:foo-plugin": {
-					Route:    []string{"route1", "route2", "route3"},
-					Consumer: []string{"consumer1"},
+					Route:    []util.FR{{Identifier: "route1"}, {Identifier: "route2"}, {Identifier: "route3"}},
+					Consumer: []util.FR{{Identifier: "consumer1"}},
 				},
 			},
 			want: []string{
@@ -1412,8 +1490,8 @@ func TestKongState_BuildPluginsCollisions(t *testing.T) {
 			},
 			pluginRels: map[string]util.ForeignRelations{
 				"default:foo-plugin": {
-					Service:  []string{"service1", "service2", "service3"},
-					Consumer: []string{"consumer1"},
+					Service:  []util.FR{{Identifier: "service1"}, {Identifier: "service2"}, {Identifier: "service3"}},
+					Consumer: []util.FR{{Identifier: "consumer1"}},
 				},
 			},
 			want: []string{
diff --git a/internal/util/relations_test.go b/internal/util/relations_test.go
index 1e76640d1..aa223518e 100644
--- a/internal/util/relations_test.go
+++ b/internal/util/relations_test.go
@@ -26,15 +26,15 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on consumer only",
 			args: args{
 				relations: ForeignRelations{
-					Consumer: []string{"foo", "bar"},
+					Consumer: []FR{{Identifier: "foo"}, {Identifier: "bar"}},
 				},
 			},
 			want: []Rel{
 				{
-					Consumer: "foo",
+					Consumer: FR{Identifier: "foo"},
 				},
 				{
-					Consumer: "bar",
+					Consumer: FR{Identifier: "bar"},
 				},
 			},
 		},
@@ -42,15 +42,15 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on consumer group only",
 			args: args{
 				relations: ForeignRelations{
-					ConsumerGroup: []string{"foo", "bar"},
+					ConsumerGroup: []FR{{Identifier: "foo"}, {Identifier: "bar"}},
 				},
 			},
 			want: []Rel{
 				{
-					ConsumerGroup: "foo",
+					ConsumerGroup: FR{Identifier: "foo"},
 				},
 				{
-					ConsumerGroup: "bar",
+					ConsumerGroup: FR{Identifier: "bar"},
 				},
 			},
 		},
@@ -58,15 +58,15 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on service only",
 			args: args{
 				relations: ForeignRelations{
-					Service: []string{"foo", "bar"},
+					Service: []FR{{Identifier: "foo"}, {Identifier: "bar"}},
 				},
 			},
 			want: []Rel{
 				{
-					Service: "foo",
+					Service: FR{Identifier: "foo"},
 				},
 				{
-					Service: "bar",
+					Service: FR{Identifier: "bar"},
 				},
 			},
 		},
@@ -74,15 +74,15 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on routes only",
 			args: args{
 				relations: ForeignRelations{
-					Route: []string{"foo", "bar"},
+					Route: []FR{{Identifier: "foo"}, {Identifier: "bar"}},
 				},
 			},
 			want: []Rel{
 				{
-					Route: "foo",
+					Route: FR{Identifier: "foo"},
 				},
 				{
-					Route: "bar",
+					Route: FR{Identifier: "bar"},
 				},
 			},
 		},
@@ -90,22 +90,22 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on service and routes only",
 			args: args{
 				relations: ForeignRelations{
-					Route:   []string{"foo", "bar"},
-					Service: []string{"foo", "bar"},
+					Route:   []FR{{Identifier: "foo"}, {Identifier: "bar"}},
+					Service: []FR{{Identifier: "foo"}, {Identifier: "bar"}},
 				},
 			},
 			want: []Rel{
 				{
-					Service: "foo",
+					Service: FR{Identifier: "foo"},
 				},
 				{
-					Service: "bar",
+					Service: FR{Identifier: "bar"},
 				},
 				{
-					Route: "foo",
+					Route: FR{Identifier: "foo"},
 				},
 				{
-					Route: "bar",
+					Route: FR{Identifier: "bar"},
 				},
 			},
 		},
@@ -113,26 +113,26 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on combination of route and consumer",
 			args: args{
 				relations: ForeignRelations{
-					Route:    []string{"foo", "bar"},
-					Consumer: []string{"foo", "bar"},
+					Route:    []FR{{Identifier: "foo"}, {Identifier: "bar"}},
+					Consumer: []FR{{Identifier: "foo"}, {Identifier: "bar"}},
 				},
 			},
 			want: []Rel{
 				{
-					Consumer: "foo",
-					Route:    "foo",
+					Consumer: FR{Identifier: "foo"},
+					Route:    FR{Identifier: "foo"},
 				},
 				{
-					Consumer: "foo",
-					Route:    "bar",
+					Consumer: FR{Identifier: "foo"},
+					Route:    FR{Identifier: "bar"},
 				},
 				{
-					Consumer: "bar",
-					Route:    "foo",
+					Consumer: FR{Identifier: "bar"},
+					Route:    FR{Identifier: "foo"},
 				},
 				{
-					Consumer: "bar",
-					Route:    "bar",
+					Consumer: FR{Identifier: "bar"},
+					Route:    FR{Identifier: "bar"},
 				},
 			},
 		},
@@ -140,26 +140,26 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on combination of service and consumer",
 			args: args{
 				relations: ForeignRelations{
-					Service:  []string{"foo", "bar"},
-					Consumer: []string{"foo", "bar"},
+					Service:  []FR{{Identifier: "foo"}, {Identifier: "bar"}},
+					Consumer: []FR{{Identifier: "foo"}, {Identifier: "bar"}},
 				},
 			},
 			want: []Rel{
 				{
-					Consumer: "foo",
-					Service:  "foo",
+					Consumer: FR{Identifier: "foo"},
+					Service:  FR{Identifier: "foo"},
 				},
 				{
-					Consumer: "foo",
-					Service:  "bar",
+					Consumer: FR{Identifier: "foo"},
+					Service:  FR{Identifier: "bar"},
 				},
 				{
-					Consumer: "bar",
-					Service:  "foo",
+					Consumer: FR{Identifier: "bar"},
+					Service:  FR{Identifier: "foo"},
 				},
 				{
-					Consumer: "bar",
-					Service:  "bar",
+					Consumer: FR{Identifier: "bar"},
+					Service:  FR{Identifier: "bar"},
 				},
 			},
 		},
@@ -167,43 +167,43 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on combination of service,route and consumer",
 			args: args{
 				relations: ForeignRelations{
-					Consumer: []string{"c1", "c2"},
-					Route:    []string{"r1", "r2"},
-					Service:  []string{"s1", "s2"},
+					Consumer: []FR{{Identifier: "c1"}, {Identifier: "c2"}},
+					Route:    []FR{{Identifier: "r1"}, {Identifier: "r2"}},
+					Service:  []FR{{Identifier: "s1"}, {Identifier: "s2"}},
 				},
 			},
 			want: []Rel{
 				{
-					Consumer: "c1",
-					Service:  "s1",
+					Consumer: FR{Identifier: "c1"},
+					Service:  FR{Identifier: "s1"},
 				},
 				{
-					Consumer: "c1",
-					Service:  "s2",
+					Consumer: FR{Identifier: "c1"},
+					Service:  FR{Identifier: "s2"},
 				},
 				{
-					Consumer: "c1",
-					Route:    "r1",
+					Consumer: FR{Identifier: "c1"},
+					Route:    FR{Identifier: "r1"},
 				},
 				{
-					Consumer: "c1",
-					Route:    "r2",
+					Consumer: FR{Identifier: "c1"},
+					Route:    FR{Identifier: "r2"},
 				},
 				{
-					Consumer: "c2",
-					Service:  "s1",
+					Consumer: FR{Identifier: "c2"},
+					Service:  FR{Identifier: "s1"},
 				},
 				{
-					Consumer: "c2",
-					Service:  "s2",
+					Consumer: FR{Identifier: "c2"},
+					Service:  FR{Identifier: "s2"},
 				},
 				{
-					Consumer: "c2",
-					Route:    "r1",
+					Consumer: FR{Identifier: "c2"},
+					Route:    FR{Identifier: "r1"},
 				},
 				{
-					Consumer: "c2",
-					Route:    "r2",
+					Consumer: FR{Identifier: "c2"},
+					Route:    FR{Identifier: "r2"},
 				},
 			},
 		},
@@ -211,43 +211,43 @@ func TestGetCombinations(t *testing.T) {
 			name: "plugins on combination of service,route and consumer group",
 			args: args{
 				relations: ForeignRelations{
-					Route:         []string{"r1", "r2"},
-					Service:       []string{"s1", "s2"},
-					ConsumerGroup: []string{"cg1", "cg2"},
+					Route:         []FR{{Identifier: "r1"}, {Identifier: "r2"}},
+					Service:       []FR{{Identifier: "s1"}, {Identifier: "s2"}},
+					ConsumerGroup: []FR{{Identifier: "cg1"}, {Identifier: "cg2"}},
 				},
 			},
 			want: []Rel{
 				{
-					ConsumerGroup: "cg1",
-					Service:       "s1",
+					ConsumerGroup: FR{Identifier: "cg1"},
+					Service:       FR{Identifier: "s1"},
 				},
 				{
-					ConsumerGroup: "cg1",
-					Service:       "s2",
+					ConsumerGroup: FR{Identifier: "cg1"},
+					Service:       FR{Identifier: "s2"},
 				},
 				{
-					ConsumerGroup: "cg1",
-					Route:         "r1",
+					ConsumerGroup: FR{Identifier: "cg1"},
+					Route:         FR{Identifier: "r1"},
 				},
 				{
-					ConsumerGroup: "cg1",
-					Route:         "r2",
+					ConsumerGroup: FR{Identifier: "cg1"},
+					Route:         FR{Identifier: "r2"},
 				},
 				{
-					ConsumerGroup: "cg2",
-					Service:       "s1",
+					ConsumerGroup: FR{Identifier: "cg2"},
+					Service:       FR{Identifier: "s1"},
 				},
 				{
-					ConsumerGroup: "cg2",
-					Service:       "s2",
+					ConsumerGroup: FR{Identifier: "cg2"},
+					Service:       FR{Identifier: "s2"},
 				},
 				{
-					ConsumerGroup: "cg2",
-					Route:         "r1",
+					ConsumerGroup: FR{Identifier: "cg2"},
+					Route:         FR{Identifier: "r1"},
 				},
 				{
-					ConsumerGroup: "cg2",
-					Route:         "r2",
+					ConsumerGroup: FR{Identifier: "cg2"},
+					Route:         FR{Identifier: "r2"},
 				},
 			},
 		},
@@ -263,9 +263,9 @@ func BenchmarkGetCombinations(b *testing.B) {
 	b.Run("consumer groups", func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
 			relations := ForeignRelations{
-				Route:         []string{"r1", "r2"},
-				Service:       []string{"s1", "s2"},
-				ConsumerGroup: []string{"cg1", "cg2"},
+				Route:         []FR{{Identifier: "r1"}, {Identifier: "r2"}},
+				Service:       []FR{{Identifier: "s1"}, {Identifier: "s2"}},
+				ConsumerGroup: []FR{{Identifier: "cg1"}, {Identifier: "cg2"}},
 			}
 
 			rels := relations.GetCombinations()
@@ -275,9 +275,9 @@ func BenchmarkGetCombinations(b *testing.B) {
 	b.Run("consumers", func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
 			relations := ForeignRelations{
-				Route:    []string{"r1", "r2"},
-				Service:  []string{"s1", "s2"},
-				Consumer: []string{"c1", "c2", "c3"},
+				Route:    []FR{{Identifier: "r1"}, {Identifier: "r2"}},
+				Service:  []FR{{Identifier: "s1"}, {Identifier: "s2"}},
+				Consumer: []FR{{Identifier: "c1"}, {Identifier: "c2"}, {Identifier: "c3"}},
 			}
 
 			rels := relations.GetCombinations()
diff --git a/test/envtest/configerrorevent_envtest_test.go b/test/envtest/configerrorevent_envtest_test.go
index ae0f81065..7342e30fa 100644
--- a/test/envtest/configerrorevent_envtest_test.go
+++ b/test/envtest/configerrorevent_envtest_test.go
@@ -65,6 +65,8 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 		// TCP services cannot have paths, and we don't catch this as a translation error
 		"konghq.com/protocol": "tcp",
 		"konghq.com/path":     "/aitmatov",
+		// Referencing non-existent KongPlugins.
+		"konghq.com/plugins": "foo,bar",
 	}
 	service.Namespace = ns.Name
 	require.NoError(t, ctrlClient.Create(ctx, service))
@@ -75,6 +77,7 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 		"konghq.com/strip-path": "true",
 		"konghq.com/protocols":  "grpcs",
 		"konghq.com/methods":    "GET",
+		"konghq.com/plugins":    "baz",
 	}, service)
 	ingress.Spec.IngressClassName = lo.ToPtr(ingressClassName)
 	ingress.Namespace = ns.Name
@@ -99,33 +102,59 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 		}
 		t.Logf("got %d events", len(events.Items))
 
-		matches := make([]bool, 4)
+		const numberOfExpectedEvents = 7
+		matches := make([]bool, numberOfExpectedEvents)
 		matches[0] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
-			return e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
 				e.InvolvedObject.Kind == "Ingress" &&
 				e.InvolvedObject.Name == ingress.Name &&
 				e.Message == "invalid methods: cannot set 'methods' when 'protocols' is 'grpc' or 'grpcs'"
 		})
 		matches[1] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
-			return e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
 				e.InvolvedObject.Kind == "Service" &&
 				e.InvolvedObject.Name == service.Name &&
 				e.Message == "invalid path: value must be null"
 		})
 		matches[2] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
-			return e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
 				e.InvolvedObject.Kind == "Service" &&
 				e.InvolvedObject.Name == service.Name &&
 				e.Message == "invalid service:httpbin.httpbin.80: failed conditional validation given value of field 'protocol'"
 		})
 		matches[3] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
 			ok, err := regexp.MatchString(`failed to apply Kong configuration to http://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+: HTTP status 400 \(message: "failed posting new config to /config"\)`, e.Message)
-			return e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
 				e.InvolvedObject.Kind == "Pod" &&
 				e.InvolvedObject.Name == podName &&
 				ok && err == nil
 		})
-		if lo.Count(matches, true) != 4 {
+		matches[4] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationTranslationFailedEventReason &&
+				e.InvolvedObject.Kind == "Service" &&
+				e.InvolvedObject.Name == service.Name &&
+				e.Message == `referenced KongPlugin or KongClusterPlugin "foo" does not exist`
+		})
+		matches[5] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationTranslationFailedEventReason &&
+				e.InvolvedObject.Kind == "Service" &&
+				e.InvolvedObject.Name == service.Name &&
+				e.Message == `referenced KongPlugin or KongClusterPlugin "bar" does not exist`
+		})
+		matches[6] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationTranslationFailedEventReason &&
+				e.InvolvedObject.Kind == "Ingress" &&
+				e.InvolvedObject.Name == ingress.Name &&
+				e.Message == `referenced KongPlugin or KongClusterPlugin "baz" does not exist`
+		})
+		if lo.Count(matches, true) != numberOfExpectedEvents {
 			t.Logf("not all events matched: %+v", matches)
 			return false
 		}
@@ -281,6 +310,8 @@ func TestConfigErrorEventGenerationDBMode(t *testing.T) {
 			Name: "donenbai",
 			Annotations: map[string]string{
 				annotations.IngressClassKey: ingressClassName,
+				// Referencing non-existent KongPlugin.
+				"konghq.com/plugins": "foo",
 			},
 		},
 		Username: "donenbai",
@@ -311,14 +342,23 @@ func TestConfigErrorEventGenerationDBMode(t *testing.T) {
 		}
 		t.Logf("got %d events", len(events.Items))
 
-		matches := make([]bool, 1)
+		const numberOfExpectedEvents = 2
+		matches := make([]bool, numberOfExpectedEvents)
 		matches[0] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
-			return e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationApplyFailedEventReason &&
 				e.InvolvedObject.Kind == "KongConsumer" &&
 				e.InvolvedObject.Name == consumer.Name &&
 				e.Message == fmt.Sprintf("invalid consumer:%s: HTTP status 400 (message: \"2 schema violations (at least one of these fields must be non-empty: 'custom_id', 'username'; fake: unknown field)\")", consumer.Name)
 		})
-		if lo.Count(matches, true) != 1 {
+		matches[1] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
+			return e.Type == corev1.EventTypeWarning &&
+				e.Reason == dataplane.KongConfigurationTranslationFailedEventReason &&
+				e.InvolvedObject.Kind == "KongConsumer" &&
+				e.InvolvedObject.Name == consumer.Name &&
+				e.Message == `referenced KongPlugin or KongClusterPlugin "foo" does not exist`
+		})
+		if lo.Count(matches, true) != numberOfExpectedEvents {
 			t.Logf("not all events matched: %+v", matches)
 			return false
 		}