Repository: Kong/kubernetes-ingress-controller
Pull request #6791 (2024-12-06 16:37:12)

fix: verify if the referenced KongPlugin/KongClusterPlugin exists and set Programmed condition accordingly
**What this PR does / why we need it**:

Verify if the referenced `KongPlugin` and `KongClusterPlugin` exists and set `KongCustomEntity`'s status condition accordingly

**Which issue this PR fixes**:

<!--
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**:

<!-- 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/6791?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 `86.66667%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 77.6%. Comparing base [(`76062fc`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/76062fc05245dfcfa5c3bf7275de4cab21ab4e65?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kong) to head [(`b52ff8b`)](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/commit/b52ff8be5df72ef1825db18c984bc058758d18ba?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/6791?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/customentity.go](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6791?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=) | 86.6% | [2 Missing :warning: ](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6791?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   #6791   +/-   ##
=====================================
  Coverage   77.6%   77.6%           
=====================================
  Files        207     207           
  Lines      24644   24662   +18     
=====================================
+ Hits       19131   19148   +17     
- Misses      4528    4542   +14     
+ Partials     985     972   -13     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Kong/kubernetes-ingress-controller/pull/6791?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 7c4dfb5f4..57da828a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -173,6 +173,9 @@ Adding a new version? You'll need three changes:
   - route R1 and consumer group CG
   - route R2 and consumer group CG
   [#6786](https://github.com/Kong/kubernetes-ingress-controller/pull/6786)
+- Check referenced `KongCustomEntity`'s `parentRef` and verify if referenced
+  `KongPlugin` or `KongClusterPlugin` exists.
+  [#6791](https://github.com/Kong/kubernetes-ingress-controller/pull/6791)
 
 ### Added
 
diff --git a/internal/dataplane/kongstate/customentity.go b/internal/dataplane/kongstate/customentity.go
index 288663157..04aa6e857 100644
--- a/internal/dataplane/kongstate/customentity.go
+++ b/internal/dataplane/kongstate/customentity.go
@@ -302,6 +302,11 @@ func findCustomEntityRelatedPlugin(logger logr.Logger, cacheStore store.Storer,
 		return "", false, nil
 	}
 
+	var (
+		namespace = k8sEntity.Namespace
+		name      = parentRef.Name
+	)
+
 	// Extract the plugin key to get the plugin relations.
 	parentRefNamespace := lo.FromPtrOr(parentRef.Namespace, "")
 	// if the namespace in parentRef is not same as the namespace of KCE itself, check if the reference is allowed by ReferenceGrant.
@@ -314,10 +319,21 @@ func findCustomEntityRelatedPlugin(logger logr.Logger, cacheStore store.Storer,
 		if err != nil {
 			return "", false, err
 		}
-		return paretRefNamespace + ":" + parentRef.Name, true, nil
+		namespace = paretRefNamespace
+	}
+
+	var err error
+	switch *parentRef.Kind {
+	case "KongPlugin":
+		_, err = cacheStore.GetKongPlugin(namespace, name)
+	case "KongClusterPlugin":
+		_, err = cacheStore.GetKongClusterPlugin(name)
+	}
+	if err != nil {
+		return "", false, err
 	}
 
-	return k8sEntity.Namespace + ":" + parentRef.Name, true, nil
+	return namespace + ":" + name, true, nil
 }
 
 func findCustomEntityForeignFields(


Test patch:
diff --git a/internal/dataplane/kongstate/customentity_test.go b/internal/dataplane/kongstate/customentity_test.go
index c5b1b5bbb..8224d9668 100644
--- a/internal/dataplane/kongstate/customentity_test.go
+++ b/internal/dataplane/kongstate/customentity_test.go
@@ -275,6 +275,7 @@ func TestFindCustomEntityForeignFields(t *testing.T) {
 	}
 	testCases := []struct {
 		name                     string
+		plugins                  []*kongv1.KongPlugin
 		referenceGrants          []*gatewayapi.ReferenceGrant
 		customEntity             *kongv1alpha1.KongCustomEntity
 		schema                   EntitySchema
@@ -283,7 +284,15 @@ func TestFindCustomEntityForeignFields(t *testing.T) {
 		foreignFieldCombinations [][]entityForeignFieldValue
 	}{
 		{
-			name:         "attached to single entity: service",
+			name: "attached to single entity: service",
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "fake-plugin",
+					},
+				},
+			},
 			customEntity: testCustomEntity,
 			schema: EntitySchema{
 				Fields: map[string]EntityField{
@@ -315,7 +324,15 @@ func TestFindCustomEntityForeignFields(t *testing.T) {
 			},
 		},
 		{
-			name:         "attached to routes and consumers",
+			name: "attached to routes and consumers",
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "fake-plugin",
+					},
+				},
+			},
 			customEntity: testCustomEntity,
 			schema: EntitySchema{
 				Fields: map[string]EntityField{
@@ -382,6 +399,14 @@ func TestFindCustomEntityForeignFields(t *testing.T) {
 					"service": {Name: "service", Type: EntityFieldTypeForeign, Reference: "services"},
 				},
 			},
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "another-namespace",
+						Name:      "fake-plugin",
+					},
+				},
+			},
 			referenceGrants: []*gatewayapi.ReferenceGrant{
 				{
 					ObjectMeta: metav1.ObjectMeta{
@@ -514,6 +539,7 @@ func TestFindCustomEntityForeignFields(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 			cacheStore, err := store.NewFakeStore(store.FakeObjects{
+				KongPlugins:     tc.plugins,
 				ReferenceGrants: tc.referenceGrants,
 			})
 			require.NoError(t, err)
@@ -1100,3 +1126,183 @@ func TestKongState_FillCustomEntities(t *testing.T) {
 		})
 	}
 }
+
+func TestFindCustomEntityRelatedPlugin(t *testing.T) {
+	testCases := []struct {
+		name            string
+		referenceGrants []*gatewayapi.ReferenceGrant
+		plugins         []*kongv1.KongPlugin
+		customEntity    *kongv1alpha1.KongCustomEntity
+		expectedRef     string
+		expectedResult  bool
+		expectedErr     bool
+	}{
+		{
+			name:    "referenced plugin does not exist in the store - error",
+			plugins: []*kongv1.KongPlugin{},
+			customEntity: &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"}`),
+					},
+					ParentRef: &kongv1alpha1.ObjectReference{
+						Group:     lo.ToPtr(kongv1.GroupVersion.Group),
+						Kind:      lo.ToPtr("KongPlugin"),
+						Name:      "ratelimiting-1",
+						Namespace: lo.ToPtr("default"),
+					},
+				},
+			},
+			expectedErr: true,
+		},
+		{
+			name: "referenced plugin exists in the store",
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "ratelimiting-1",
+					},
+					PluginName: "ratelimiting",
+				},
+			},
+			customEntity: &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"}`),
+					},
+					ParentRef: &kongv1alpha1.ObjectReference{
+						Group:     lo.ToPtr(kongv1.GroupVersion.Group),
+						Kind:      lo.ToPtr("KongPlugin"),
+						Name:      "ratelimiting-1",
+						Namespace: lo.ToPtr("default"),
+					},
+				},
+			},
+			expectedRef:    "default:ratelimiting-1",
+			expectedResult: true,
+		},
+		{
+			name: "referenced plugin exists but in different namespace without cross-namespace reference grant",
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "different-ns",
+						Name:      "ratelimiting-1",
+					},
+					PluginName: "ratelimiting",
+				},
+			},
+			customEntity: &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"}`),
+					},
+					ParentRef: &kongv1alpha1.ObjectReference{
+						Group:     lo.ToPtr(kongv1.GroupVersion.Group),
+						Kind:      lo.ToPtr("KongPlugin"),
+						Name:      "ratelimiting-1",
+						Namespace: lo.ToPtr("different-ns"),
+					},
+				},
+			},
+			expectedErr: true,
+		},
+		{
+			name: "referenced plugin exists but in different namespace with cross-namespace reference grant",
+			referenceGrants: []*gatewayapi.ReferenceGrant{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "different-ns",
+						Name:      "grant-kce-to-plugin",
+					},
+					Spec: gatewayapi.ReferenceGrantSpec{
+						From: []gatewayapi.ReferenceGrantFrom{
+							{
+								Namespace: gatewayapi.Namespace("default"),
+								Group:     gatewayapi.Group(kongv1alpha1.GroupVersion.Group),
+								Kind:      "KongCustomEntity",
+							},
+						},
+						To: []gatewayapi.ReferenceGrantTo{
+							{
+								Group: gatewayapi.Group(kongv1.GroupVersion.Group),
+								Kind:  gatewayapi.Kind("KongPlugin"),
+							},
+						},
+					},
+				},
+			},
+			plugins: []*kongv1.KongPlugin{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "different-ns",
+						Name:      "ratelimiting-1",
+					},
+					PluginName: "ratelimiting",
+				},
+			},
+			customEntity: &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"}`),
+					},
+					ParentRef: &kongv1alpha1.ObjectReference{
+						Group:     lo.ToPtr(kongv1.GroupVersion.Group),
+						Kind:      lo.ToPtr("KongPlugin"),
+						Name:      "ratelimiting-1",
+						Namespace: lo.ToPtr("different-ns"),
+					},
+				},
+			},
+			expectedRef:    "different-ns:ratelimiting-1",
+			expectedResult: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			s, err := store.NewFakeStore(store.FakeObjects{
+				ReferenceGrants: tc.referenceGrants,
+				KongPlugins:     tc.plugins,
+			})
+			require.NoError(t, err)
+			ref, ret, err := findCustomEntityRelatedPlugin(logr.Discard(), s, tc.customEntity)
+			if tc.expectedErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, tc.expectedRef, ref)
+			require.Equal(t, tc.expectedResult, ret)
+		})
+	}
+}
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
index 1715c6d04..558f60f01 100644
--- a/internal/dataplane/testdata/golden/kong-custom-entity-ee/default_golden.yaml
+++ b/internal/dataplane/testdata/golden/kong-custom-entity-ee/default_golden.yaml
@@ -4,6 +4,18 @@ degraphql_routes:
   service:
     id: 9e262d22-26f2-5bcb-8d57-c5229fb8c5a2
   uri: /contacts
+plugins:
+- config:
+    graphql_server_path: /v1/graphql
+  name: degraphql
+  route: default.i1.s1..80
+  tags:
+  - k8s-name:degraphql
+  - 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
diff --git a/internal/dataplane/testdata/golden/kong-custom-entity-ee/in.yaml b/internal/dataplane/testdata/golden/kong-custom-entity-ee/in.yaml
index 8603111bb..4f9accc95 100644
--- a/internal/dataplane/testdata/golden/kong-custom-entity-ee/in.yaml
+++ b/internal/dataplane/testdata/golden/kong-custom-entity-ee/in.yaml
@@ -22,7 +22,7 @@ metadata:
   uid: 39ec2b6a-f919-4308-ad2f-07511ab424dd
   annotations:
     konghq.com/strip-path: "true"
-    konghq.com/plugins: "p1"
+    konghq.com/plugins: "degraphql"
 spec:
   ingressClassName: kong
   rules:
@@ -58,7 +58,7 @@ spec:
   parentRef:
     group: configuration.konghq.com
     kind: KongPlugin
-    name: p1
+    name: degraphql
   fields:
     uri: "/contacts"
     query: "query{ contacts { name } }"