Repository: Kong/kubernetes-ingress-controller
Pull request #6841 (2024-12-16 10:49:26)

feat(KongPlugin): generate K8s Events for missing grant
<!-- 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**:

The previous PR

- https://github.com/Kong/kubernetes-ingress-controller/pull/6814

mentioned

> 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.

this PR adresses it, now there is no error log in such situation and respective K8s Event is generated

```log
default     4m8s        Warning   KongConfigurationTranslationFailed   httproute/httproute-testing-2        no grant found to referenced "third:fourth" plugin in the requested remote KongPlugin bind
```

for `httproute/httproute-testing-2` that has annotation `konghq.com/plugins` that contains non-existent `third:fourth`.

<!-- Please describe why this particular PR is necessary or why you see it as a nice addition -->

**Which issue this PR fixes**:

Together with a dedicated (leftover mentioned in PR #6814) issue https://github.com/Kong/kubernetes-ingress-controller/issues/6842 this PR closes 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**:

Besides adjustment of the returned error function, `isRemotePluginReferenceAllowed` is moved closer to it actual and single usage to make the code easier to follow.

<!-- 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/6841?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 `81.33333%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 77.1%. Comparing base [(`425d3ef`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/425d3ef39a411fea73bd9c3b61a726d7254578a5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) to head [(`5dedf7c`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/5dedf7c50051ec9206244faa6fddf56e35224b26?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong).

| [Files with missing lines](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6841?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/6841?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=) | 83.8% | [9 Missing and 2 partials :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6841?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) |
| [internal/gatewayapi/contraints.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6841?src=pr&el=tree&filepath=internal%2Fgatewayapi%2Fcontraints.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS9jb250cmFpbnRzLmdv) | 40.0% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6841?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   #6841   +/-   ##
=====================================
  Coverage   77.1%   77.1%           
=====================================
  Files        210     211    +1     
  Lines      25281   25295   +14     
=====================================
+ Hits       19508   19522   +14     
+ Misses      4762    4753    -9     
- Partials    1011    1020    +9     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6841?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 6b8d8de6b..223d52a1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -133,10 +133,11 @@ 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.
+- Do not log `ERROR` when referenced `KongPlugin` or `KongClusterPlugin` does not exist
+  or there is no grant that allows referencing it, instead generate a Kubernetes Event
+  `KongConfigurationTranslationFailed` for object that references it.
   [#6814](https://github.com/Kong/kubernetes-ingress-controller/pull/6814)
+  [#6841](https://github.com/Kong/kubernetes-ingress-controller/pull/6841)
 - 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/internal/dataplane/kongstate/customentity.go b/internal/dataplane/kongstate/customentity.go
index 179ec2fdf..5536ea673 100644
--- a/internal/dataplane/kongstate/customentity.go
+++ b/internal/dataplane/kongstate/customentity.go
@@ -178,7 +178,7 @@ func (ks *KongState) FillCustomEntities(
 	}
 	// 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)
+	pluginRels := ks.getPluginRelatedEntitiesRef(s, logger, failuresCollector)
 
 	for _, entity := range entities {
 		// reject the custom entity if its type is in "known" entity types that are already processed.
@@ -333,7 +333,7 @@ func findCustomEntityRelatedPlugin(logger logr.Logger, cacheStore store.Storer,
 			Namespace: parentRefNamespace,
 			Name:      parentRef.Name,
 		}
-		paretRefNamespace, err := extractReferredPluginNamespace(logger, cacheStore, k8sEntity, nn)
+		paretRefNamespace, err := extractReferredPluginNamespaceIfAllowed(logger, cacheStore, k8sEntity, nn)
 		if err != nil {
 			return "", false, err
 		}
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index 370c48179..addc3c64b 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -3,6 +3,7 @@ package kongstate
 import (
 	"bytes"
 	"crypto/sha256"
+	"errors"
 	"fmt"
 	"strconv"
 	"strings"
@@ -378,7 +379,9 @@ type NamespacedKongPlugin struct {
 	Name      string
 }
 
-func (ks *KongState) getPluginRelations(cacheStore store.Storer, log logr.Logger) map[string]util.ForeignRelations {
+func (ks *KongState) getPluginRelations(
+	cacheStore store.Storer, log logr.Logger, failuresCollector *failures.ResourceFailuresCollector,
+) map[string]util.ForeignRelations {
 	// KongPlugin key (KongPlugin's name:namespace) to corresponding associations
 	pluginRels := map[string]util.ForeignRelations{}
 
@@ -400,9 +403,13 @@ func (ks *KongState) getPluginRelations(cacheStore store.Storer, log logr.Logger
 		//
 		// Code in buildPlugins() will combine plugin associations into
 		// multi-entity plugins within the local namespace
-		namespace, err := extractReferredPluginNamespace(log, cacheStore, referrer, plugin)
+		namespace, err := extractReferredPluginNamespaceIfAllowed(log, cacheStore, referrer, plugin)
 		if err != nil {
-			log.Error(err, "could not bind requested plugin", "plugin", plugin.Name, "namespace", plugin.Namespace)
+			if errors.As(err, &referredPluginNotAllowedError{}) {
+				failuresCollector.PushResourceFailure(err.Error(), referrer)
+			} else {
+				log.Error(err, "could not bind requested plugin", "plugin", plugin.Name, "namespace", plugin.Namespace)
+			}
 			return
 		}
 
@@ -489,58 +496,6 @@ type pluginReference struct {
 	Name      string
 }
 
-func isRemotePluginReferenceAllowed(log logr.Logger, s store.Storer, r pluginReference) error {
-	// remote plugin. considered part of this namespace if a suitable ReferenceGrant exists
-	grants, err := s.ListReferenceGrants()
-	if err != nil {
-		return fmt.Errorf("could not retrieve ReferenceGrants from store when building plugin relations map: %w", err)
-	}
-	allowed := gatewayapi.GetPermittedForReferenceGrantFrom(
-		log,
-		gatewayapi.ReferenceGrantFrom{
-			Group:     gatewayapi.Group(r.Referrer.GetObjectKind().GroupVersionKind().Group),
-			Kind:      gatewayapi.Kind(r.Referrer.GetObjectKind().GroupVersionKind().Kind),
-			Namespace: gatewayapi.Namespace(r.Referrer.GetNamespace()),
-		},
-		grants,
-	)
-
-	// TODO https://github.com/Kong/kubernetes-ingress-controller/issues/6000
-	// Our reference checking code wasn't originally designed to handle multiple object types and relationships and
-	// wasn't designed with much guidance for future usage. It expects something akin to the BackendRef section of an
-	// HTTPRoute or similar, since that's what it was originally designed for. A future simplified model should probably
-	// work with source and target client.Object resources derived from the particular resources' reference specs, as
-	// those reference formats aren't necessarily consistent. We should possibly push for a standard upstream utility as
-	// part of https://github.com/kubernetes/enhancements/issues/3766 if it proceeds further, as this is can be difficult
-	// to wrap your head around when deep in the code that's checking an individual use case. A standard model for "these
-	// are the inputs and outputs for a reference grant check, derive them from your spec" should help avoid inconsistency
-	// in check implementation.
-
-	// we don't have a full plugin resource here for the grant checker, so we build a fake one with the correct
-	// name and namespace.
-	pluginReference := gatewayapi.PluginLabelReference{
-		Namespace: &r.Namespace,
-		Name:      r.Name,
-	}
-
-	// Because we should check whether it is allowed to refer FROM the referrer TO the plugin here,
-	// we should put the referrer on the "target" and the plugin on the "backendRef".
-
-	log.V(logging.TraceLevel).Info("requested grant to plugins",
-		"from-namespace", r.Referrer.GetNamespace(),
-		"from-group", r.Referrer.GetObjectKind().GroupVersionKind().Group,
-		"from-kind", r.Referrer.GetObjectKind().GroupVersionKind().Kind,
-		"to-namespace", r.Namespace,
-		"to-name", r.Name,
-	)
-
-	if !gatewayapi.NewRefCheckerForKongPlugin(log, r.Referrer, pluginReference).IsRefAllowedByGrant(allowed) {
-		return fmt.Errorf("no grant found for %s in %s to plugin %s in %s requested remote KongPlugin bind",
-			r.Referrer.GetObjectKind().GroupVersionKind().Kind, r.Referrer.GetNamespace(), r.Name, r.Namespace)
-	}
-	return nil
-}
-
 func buildPlugins(
 	logger logr.Logger,
 	s store.Storer,
@@ -691,7 +646,7 @@ func (ks *KongState) FillPlugins(
 	s store.Storer,
 	failuresCollector *failures.ResourceFailuresCollector,
 ) {
-	ks.Plugins = buildPlugins(log, s, failuresCollector, ks.getPluginRelations(s, log))
+	ks.Plugins = buildPlugins(log, s, failuresCollector, ks.getPluginRelations(s, log, failuresCollector))
 }
 
 // FillIDs iterates over the KongState and fills in the ID field for each entity
@@ -819,15 +774,21 @@ func getServiceIDFromPluginRels(
 //
 // TODO: refactor the building of plugin related entities and share the result between here and building plugins:
 // https://github.com/Kong/kubernetes-ingress-controller/issues/6115
-func (ks *KongState) getPluginRelatedEntitiesRef(cacheStore store.Storer, log logr.Logger) PluginRelatedEntitiesRefs {
+func (ks *KongState) getPluginRelatedEntitiesRef(
+	cacheStore store.Storer, log logr.Logger, failuresCollector *failures.ResourceFailuresCollector,
+) PluginRelatedEntitiesRefs {
 	pluginRels := PluginRelatedEntitiesRefs{
 		RelatedEntities:      map[string]RelatedEntitiesRef{},
 		RouteAttachedService: map[string]*Service{},
 	}
 	addRelation := func(referrer client.Object, plugin k8stypes.NamespacedName, entity any) {
-		namespace, err := extractReferredPluginNamespace(log, cacheStore, referrer, plugin)
+		namespace, err := extractReferredPluginNamespaceIfAllowed(log, cacheStore, referrer, plugin)
 		if err != nil {
-			log.Error(err, "could not bind requested plugin", "plugin", plugin.Name, "namespace", plugin.Namespace)
+			if errors.As(err, &referredPluginNotAllowedError{}) {
+				failuresCollector.PushResourceFailure(err.Error(), referrer)
+			} else {
+				log.Error(err, "could not bind requested plugin", "plugin", plugin.Name, "namespace", plugin.Namespace)
+			}
 			return
 		}
 		pluginKey := namespace + ":" + plugin.Name
@@ -882,7 +843,15 @@ func (ks *KongState) getPluginRelatedEntitiesRef(cacheStore store.Storer, log lo
 	return pluginRels
 }
 
-func extractReferredPluginNamespace(
+type referredPluginNotAllowedError struct {
+	pluginReference gatewayapi.PluginLabelReference
+}
+
+func (e referredPluginNotAllowedError) Error() string {
+	return fmt.Sprintf("no grant found to referenced %q plugin in the requested remote KongPlugin bind", e.pluginReference)
+}
+
+func extractReferredPluginNamespaceIfAllowed(
 	log logr.Logger, cacheStore store.Storer, referrer client.Object, plugin k8stypes.NamespacedName,
 ) (string, error) {
 	// There are 2 types of KongPlugin references: local and remote.
@@ -911,3 +880,56 @@ func extractReferredPluginNamespace(
 	}
 	return plugin.Namespace, nil
 }
+
+func isRemotePluginReferenceAllowed(log logr.Logger, s store.Storer, r pluginReference) error {
+	// remote plugin. considered part of this namespace if a suitable ReferenceGrant exists
+	grants, err := s.ListReferenceGrants()
+	if err != nil {
+		return fmt.Errorf("could not retrieve ReferenceGrants from store when building plugin relations map: %w", err)
+	}
+	allowed := gatewayapi.GetPermittedForReferenceGrantFrom(
+		log,
+		gatewayapi.ReferenceGrantFrom{
+			Group:     gatewayapi.Group(r.Referrer.GetObjectKind().GroupVersionKind().Group),
+			Kind:      gatewayapi.Kind(r.Referrer.GetObjectKind().GroupVersionKind().Kind),
+			Namespace: gatewayapi.Namespace(r.Referrer.GetNamespace()),
+		},
+		grants,
+	)
+
+	// TODO https://github.com/Kong/kubernetes-ingress-controller/issues/6000
+	// Our reference checking code wasn't originally designed to handle multiple object types and relationships and
+	// wasn't designed with much guidance for future usage. It expects something akin to the BackendRef section of an
+	// HTTPRoute or similar, since that's what it was originally designed for. A future simplified model should probably
+	// work with source and target client.Object resources derived from the particular resources' reference specs, as
+	// those reference formats aren't necessarily consistent. We should possibly push for a standard upstream utility as
+	// part of https://github.com/kubernetes/enhancements/issues/3766 if it proceeds further, as this is can be difficult
+	// to wrap your head around when deep in the code that's checking an individual use case. A standard model for "these
+	// are the inputs and outputs for a reference grant check, derive them from your spec" should help avoid inconsistency
+	// in check implementation.
+
+	// we don't have a full plugin resource here for the grant checker, so we build a fake one with the correct
+	// name and namespace.
+	pluginReference := gatewayapi.PluginLabelReference{
+		Namespace: &r.Namespace,
+		Name:      r.Name,
+	}
+
+	// Because we should check whether it is allowed to refer FROM the referrer TO the plugin here,
+	// we should put the referrer on the "target" and the plugin on the "backendRef".
+
+	log.V(logging.TraceLevel).Info("requested grant to plugins",
+		"from-namespace", r.Referrer.GetNamespace(),
+		"from-group", r.Referrer.GetObjectKind().GroupVersionKind().Group,
+		"from-kind", r.Referrer.GetObjectKind().GroupVersionKind().Kind,
+		"to-namespace", r.Namespace,
+		"to-name", r.Name,
+	)
+
+	if !gatewayapi.NewRefCheckerForKongPlugin(log, r.Referrer, pluginReference).IsRefAllowedByGrant(allowed) {
+		return referredPluginNotAllowedError{
+			pluginReference: pluginReference,
+		}
+	}
+	return nil
+}
diff --git a/internal/gatewayapi/contraints.go b/internal/gatewayapi/contraints.go
index c52e0f3ed..6008b5f8c 100644
--- a/internal/gatewayapi/contraints.go
+++ b/internal/gatewayapi/contraints.go
@@ -1,6 +1,10 @@
 package gatewayapi
 
-import "sigs.k8s.io/controller-runtime/pkg/client"
+import (
+	"fmt"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
 
 type HostnameT interface {
 	Hostname | string
@@ -33,3 +37,10 @@ type PluginLabelReference struct {
 	Namespace *string
 	Name      string
 }
+
+func (plr PluginLabelReference) String() string {
+	if plr.Namespace == nil {
+		return plr.Name
+	}
+	return fmt.Sprintf("%s:%s", *plr.Namespace, plr.Name)
+}


Test patch:
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index a4a620953..f7b099121 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -677,7 +677,7 @@ func TestGetPluginRelations(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			store, err := store.NewFakeStore(store.FakeObjects{})
 			require.NoError(t, err)
-			computedPluginRelations := tt.data.inputState.getPluginRelations(store, logr.Discard())
+			computedPluginRelations := tt.data.inputState.getPluginRelations(store, logr.Discard(), nil)
 			if diff := cmp.Diff(tt.data.expectedPluginRelations, computedPluginRelations); diff != "" {
 				t.Fatal("expected value differs from actual, see the human-readable diff:", diff)
 			}
@@ -816,10 +816,12 @@ func BenchmarkGetPluginRelations(b *testing.B) {
 
 	store, err := store.NewFakeStore(store.FakeObjects{})
 	require.NoError(b, err)
+	logger := logr.Discard()
+	failuresCollector := failures.NewResourceFailuresCollector(logger)
 	b.ResetTimer()
 
 	for i := 0; i < b.N; i++ {
-		fr := ks.getPluginRelations(store, logr.Discard())
+		fr := ks.getPluginRelations(store, logger, failuresCollector)
 		_ = fr
 	}
 }
diff --git a/test/envtest/configerrorevent_envtest_test.go b/test/envtest/configerrorevent_envtest_test.go
index 7342e30fa..fa513be7d 100644
--- a/test/envtest/configerrorevent_envtest_test.go
+++ b/test/envtest/configerrorevent_envtest_test.go
@@ -66,7 +66,7 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 		"konghq.com/protocol": "tcp",
 		"konghq.com/path":     "/aitmatov",
 		// Referencing non-existent KongPlugins.
-		"konghq.com/plugins": "foo,bar",
+		"konghq.com/plugins": "foo,bar,n1:p1",
 	}
 	service.Namespace = ns.Name
 	require.NoError(t, ctrlClient.Create(ctx, service))
@@ -102,7 +102,7 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 		}
 		t.Logf("got %d events", len(events.Items))
 
-		const numberOfExpectedEvents = 7
+		const numberOfExpectedEvents = 8
 		matches := make([]bool, numberOfExpectedEvents)
 		matches[0] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
 			return e.Type == corev1.EventTypeWarning &&
@@ -154,6 +154,13 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 				e.InvolvedObject.Name == ingress.Name &&
 				e.Message == `referenced KongPlugin or KongClusterPlugin "baz" does not exist`
 		})
+		matches[7] = 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 == `no grant found to referenced "n1:p1" plugin in the requested remote KongPlugin bind`
+		})
 		if lo.Count(matches, true) != numberOfExpectedEvents {
 			t.Logf("not all events matched: %+v", matches)
 			return false
@@ -311,7 +318,7 @@ func TestConfigErrorEventGenerationDBMode(t *testing.T) {
 			Annotations: map[string]string{
 				annotations.IngressClassKey: ingressClassName,
 				// Referencing non-existent KongPlugin.
-				"konghq.com/plugins": "foo",
+				"konghq.com/plugins": "foo, n1:p1",
 			},
 		},
 		Username: "donenbai",
@@ -342,7 +349,7 @@ func TestConfigErrorEventGenerationDBMode(t *testing.T) {
 		}
 		t.Logf("got %d events", len(events.Items))
 
-		const numberOfExpectedEvents = 2
+		const numberOfExpectedEvents = 3
 		matches := make([]bool, numberOfExpectedEvents)
 		matches[0] = lo.ContainsBy(events.Items, func(e corev1.Event) bool {
 			return e.Type == corev1.EventTypeWarning &&
@@ -358,6 +365,13 @@ func TestConfigErrorEventGenerationDBMode(t *testing.T) {
 				e.InvolvedObject.Name == consumer.Name &&
 				e.Message == `referenced KongPlugin or KongClusterPlugin "foo" does not exist`
 		})
+		matches[2] = 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 == `no grant found to referenced "n1:p1" plugin in the requested remote KongPlugin bind`
+		})
 		if lo.Count(matches, true) != numberOfExpectedEvents {
 			t.Logf("not all events matched: %+v", matches)
 			return false