This is an automated email from the ASF dual-hosted git repository.

shreemaan-abhishek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 43637b8f fix: return an error response for unresolved ExtensionRef 
(#2875)
43637b8f is described below

commit 43637b8f4aaed9479840af97ff5bef17899aec02
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Fri Sep 18 10:29:19 2026 +0800

    fix: return an error response for unresolved ExtensionRef (#2875)
---
 .../adc/translator/extensionref_resolution_test.go | 225 +++++++++++++++++++++
 internal/adc/translator/grpcroute.go               |   7 +-
 internal/adc/translator/httproute.go               |  25 ++-
 internal/adc/translator/plugin_test.go             |  15 +-
 internal/controller/extensionref.go                |  69 +++++++
 .../controller/extensionref_resolution_test.go     | 178 ++++++++++++++++
 internal/controller/grpcroute_controller.go        |  21 +-
 internal/controller/httproute_controller.go        |  21 +-
 internal/controller/indexer/extensionref_test.go   |  69 +++++++
 internal/controller/indexer/grpcroute.go           |   2 +-
 internal/controller/indexer/indexer.go             |   2 +-
 internal/types/error.go                            |   8 +
 internal/types/k8s.go                              |  23 +++
 test/e2e/gatewayapi/httproute.go                   |  60 ++++++
 14 files changed, 676 insertions(+), 49 deletions(-)

diff --git a/internal/adc/translator/extensionref_resolution_test.go 
b/internal/adc/translator/extensionref_resolution_test.go
new file mode 100644
index 00000000..573b6707
--- /dev/null
+++ b/internal/adc/translator/extensionref_resolution_test.go
@@ -0,0 +1,225 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+       "context"
+       "strings"
+       "testing"
+
+       "github.com/go-logr/logr"
+       "github.com/go-logr/logr/funcr"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       discoveryv1 "k8s.io/api/discovery/v1"
+       apiextensionsv1 
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/types"
+       "k8s.io/utils/ptr"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+const (
+       extensionRefTestNamespace = "default"
+       extensionRefTestBackend   = "backend"
+       extensionRefTestPort      = int32(8080)
+)
+
+func newExtensionRefTranslateContext() *provider.TranslateContext {
+       tctx := provider.NewDefaultTranslateContext(context.Background())
+       key := types.NamespacedName{Namespace: extensionRefTestNamespace, Name: 
extensionRefTestBackend}
+       tctx.Services[key] = &corev1.Service{
+               ObjectMeta: metav1.ObjectMeta{Name: key.Name, Namespace: 
key.Namespace},
+               Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{
+                       Name: "http",
+                       Port: extensionRefTestPort,
+               }}},
+       }
+       tctx.EndpointSlices[key] = []discoveryv1.EndpointSlice{{
+               ObjectMeta: metav1.ObjectMeta{Name: "backend-1", Namespace: 
key.Namespace},
+               Ports: []discoveryv1.EndpointPort{{
+                       Name: ptr.To("http"),
+                       Port: ptr.To(extensionRefTestPort),
+               }},
+               Endpoints: []discoveryv1.Endpoint{{
+                       Addresses:  []string{"10.0.0.1"},
+                       Conditions: discoveryv1.EndpointConditions{Ready: 
ptr.To(true)},
+               }},
+       }}
+       return tctx
+}
+
+func extensionRefTestBackendRef() gatewayv1.BackendRef {
+       return gatewayv1.BackendRef{BackendObjectReference: 
gatewayv1.BackendObjectReference{
+               Name: gatewayv1.ObjectName(extensionRefTestBackend),
+               Port: ptr.To(extensionRefTestPort),
+       }}
+}
+
+func assertExtensionRefResponse(t *testing.T, plugins map[string]any) {
+       t.Helper()
+       fault, ok := plugins["fault-injection"].(map[string]any)
+       require.True(t, ok)
+       abort, ok := fault["abort"].(map[string]any)
+       require.True(t, ok)
+       assert.Equal(t, 500, abort["http_status"])
+}
+
+func newExtensionRefTestLogger() (logr.Logger, *strings.Builder) {
+       var logged strings.Builder
+       logger := funcr.New(func(prefix, args string) {
+               logged.WriteString(prefix)
+               logged.WriteString(args)
+       }, funcr.Options{Verbosity: 10})
+       return logger, &logged
+}
+
+func assertExtensionRefDiagnostic(t *testing.T, logged string, routeKind 
string) {
+       t.Helper()
+       assert.Contains(t, logged, "failed to fill plugins from "+routeKind+" 
filters")
+       assert.Contains(t, logged, `"namespace"="default"`)
+       assert.Contains(t, logged, `"name"="route"`)
+       assert.Contains(t, logged, `"ruleIndex"=0`)
+}
+
+func TestTranslateHTTPRouteUnresolvedExtensionRefIsScopedToRule(t *testing.T) {
+       tests := []struct {
+               name         string
+               ref          gatewayv1.LocalObjectReference
+               pluginConfig *v1alpha1.PluginConfig
+       }{
+               {
+                       name: "unsupported group",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: "example.com",
+                               Kind:  internaltypes.KindPluginConfig,
+                               Name:  "filter",
+                       },
+                       pluginConfig: &v1alpha1.PluginConfig{ObjectMeta: 
metav1.ObjectMeta{
+                               Namespace: extensionRefTestNamespace,
+                               Name:      "filter",
+                       }},
+               },
+               {
+                       name: "unsupported kind",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: 
gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                               Kind:  "OtherFilter",
+                               Name:  "filter",
+                       },
+               },
+               {
+                       name: "missing PluginConfig",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: 
gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                               Kind:  internaltypes.KindPluginConfig,
+                               Name:  "missing",
+                       },
+               },
+               {
+                       name: "PluginConfig cannot be rendered",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: 
gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                               Kind:  internaltypes.KindPluginConfig,
+                               Name:  "filter",
+                       },
+                       pluginConfig: &v1alpha1.PluginConfig{
+                               ObjectMeta: metav1.ObjectMeta{Namespace: 
extensionRefTestNamespace, Name: "filter"},
+                               Spec: v1alpha1.PluginConfigSpec{Plugins: 
[]v1alpha1.Plugin{{
+                                       Name:   "ip-restriction",
+                                       Config: apiextensionsv1.JSON{Raw: 
[]byte(`["10.0.0.0/8"]`)},
+                               }}},
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tctx := newExtensionRefTranslateContext()
+                       logger, logged := newExtensionRefTestLogger()
+                       if tt.pluginConfig != nil {
+                               tctx.PluginConfigs[types.NamespacedName{
+                                       Namespace: tt.pluginConfig.Namespace,
+                                       Name:      tt.pluginConfig.Name,
+                               }] = tt.pluginConfig
+                       }
+
+                       route := &gatewayv1.HTTPRoute{
+                               ObjectMeta: metav1.ObjectMeta{Name: "route", 
Namespace: extensionRefTestNamespace},
+                               Spec: gatewayv1.HTTPRouteSpec{Rules: 
[]gatewayv1.HTTPRouteRule{
+                                       {
+                                               Filters: 
[]gatewayv1.HTTPRouteFilter{{
+                                                       Type:         
gatewayv1.HTTPRouteFilterExtensionRef,
+                                                       ExtensionRef: &tt.ref,
+                                               }},
+                                               BackendRefs: 
[]gatewayv1.HTTPBackendRef{{BackendRef: extensionRefTestBackendRef()}},
+                                       },
+                                       {BackendRefs: 
[]gatewayv1.HTTPBackendRef{{BackendRef: extensionRefTestBackendRef()}}},
+                               }},
+                       }
+
+                       result, err := NewTranslator(logger, 
"").TranslateHTTPRoute(tctx, route)
+                       require.NoError(t, err)
+                       require.Len(t, result.Services, 2)
+                       assertExtensionRefResponse(t, 
result.Services[0].Plugins)
+                       _, unaffectedRuleHasFault := 
result.Services[1].Plugins["fault-injection"]
+                       assert.False(t, unaffectedRuleHasFault)
+                       assertExtensionRefDiagnostic(t, logged.String(), 
"HTTPRoute")
+                       assert.NotContains(t, logged.String(), "10.0.0.0/8")
+               })
+       }
+}
+
+func TestTranslateGRPCRouteUnresolvedExtensionRefIsScopedToRule(t *testing.T) {
+       tctx := newExtensionRefTranslateContext()
+       logger, logged := newExtensionRefTestLogger()
+       ref := gatewayv1.LocalObjectReference{
+               Group: "example.com",
+               Kind:  internaltypes.KindPluginConfig,
+               Name:  "filter",
+       }
+       tctx.PluginConfigs[types.NamespacedName{Namespace: 
extensionRefTestNamespace, Name: "filter"}] =
+               &v1alpha1.PluginConfig{ObjectMeta: metav1.ObjectMeta{Namespace: 
extensionRefTestNamespace, Name: "filter"}}
+
+       route := &gatewayv1.GRPCRoute{
+               ObjectMeta: metav1.ObjectMeta{Name: "route", Namespace: 
extensionRefTestNamespace},
+               Spec: gatewayv1.GRPCRouteSpec{Rules: []gatewayv1.GRPCRouteRule{
+                       {
+                               Filters: []gatewayv1.GRPCRouteFilter{{
+                                       Type:         
gatewayv1.GRPCRouteFilterExtensionRef,
+                                       ExtensionRef: &ref,
+                               }},
+                               BackendRefs: 
[]gatewayv1.GRPCBackendRef{{BackendRef: extensionRefTestBackendRef()}},
+                       },
+                       {BackendRefs: []gatewayv1.GRPCBackendRef{{BackendRef: 
extensionRefTestBackendRef()}}},
+               }},
+       }
+
+       result, err := NewTranslator(logger, "").TranslateGRPCRoute(tctx, route)
+       require.NoError(t, err)
+       require.Len(t, result.Services, 2)
+       assertExtensionRefResponse(t, result.Services[0].Plugins)
+       _, unaffectedRuleHasFault := 
result.Services[1].Plugins["fault-injection"]
+       assert.False(t, unaffectedRuleHasFault)
+       assertExtensionRefDiagnostic(t, logged.String(), "GRPCRoute")
+}
diff --git a/internal/adc/translator/grpcroute.go 
b/internal/adc/translator/grpcroute.go
index 89f31355..73b9cde4 100644
--- a/internal/adc/translator/grpcroute.go
+++ b/internal/adc/translator/grpcroute.go
@@ -291,7 +291,12 @@ func (t *Translator) TranslateGRPCRoute(tctx 
*provider.TranslateContext, grpcRou
                }
 
                if err := t.fillPluginsFromGRPCRouteFilters(service.Plugins, 
grpcRoute.GetNamespace(), rule.Filters, tctx); err != nil {
-                       return nil, err
+                       t.Log.Error(err, "failed to fill plugins from GRPCRoute 
filters",
+                               "namespace", grpcRoute.GetNamespace(),
+                               "name", grpcRoute.GetName(),
+                               "ruleIndex", ruleIndex,
+                       )
+                       setExtensionRefErrorResponse(service)
                }
 
                matches := rule.Matches
diff --git a/internal/adc/translator/httproute.go 
b/internal/adc/translator/httproute.go
index d933a958..7edd451c 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -74,15 +74,15 @@ func (t *Translator) fillPluginFromExtensionRef(plugins 
adctypes.Plugins, namesp
        if extensionRef == nil {
                return nil
        }
-       if extensionRef.Kind != internaltypes.KindPluginConfig {
-               return nil
+       if err := internaltypes.ValidatePluginConfigExtensionRef(extensionRef); 
err != nil {
+               return err
        }
        pluginconfig := tctx.PluginConfigs[types.NamespacedName{
                Namespace: namespace,
                Name:      string(extensionRef.Name),
        }]
        if pluginconfig == nil {
-               return nil
+               return internaltypes.NewPluginConfigNotFoundError(namespace, 
string(extensionRef.Name))
        }
        names := make([]string, 0, len(pluginconfig.Spec.Plugins))
        for _, plugin := range pluginconfig.Spec.Plugins {
@@ -98,6 +98,18 @@ func (t *Translator) fillPluginFromExtensionRef(plugins 
adctypes.Plugins, namesp
        return nil
 }
 
+func setExtensionRefErrorResponse(service *adctypes.Service) {
+       if service.Plugins == nil {
+               service.Plugins = make(adctypes.Plugins)
+       }
+       service.Plugins["fault-injection"] = map[string]any{
+               "abort": map[string]any{
+                       "http_status": 500,
+                       "body":        "ExtensionRef filter could not be 
resolved",
+               },
+       }
+}
+
 func (t *Translator) fillPluginFromURLRewriteFilter(plugins adctypes.Plugins, 
urlRewrite *gatewayv1.HTTPURLRewriteFilter, matches []gatewayv1.HTTPRouteMatch) 
{
        pluginName := adctypes.PluginProxyRewrite
        obj := plugins[pluginName]
@@ -711,7 +723,12 @@ func (t *Translator) TranslateHTTPRoute(tctx 
*provider.TranslateContext, httpRou
                enableWebsocket, _ := t.translateBackendsToUpstreams(tctx, 
rule, httpRoute, service)
 
                if err := t.fillPluginsFromHTTPRouteFilters(service.Plugins, 
httpRoute.GetNamespace(), rule.Filters, rule.Matches, tctx); err != nil {
-                       return nil, err
+                       t.Log.Error(err, "failed to fill plugins from HTTPRoute 
filters",
+                               "namespace", httpRoute.GetNamespace(),
+                               "name", httpRoute.GetName(),
+                               "ruleIndex", ruleIndex,
+                       )
+                       setExtensionRefErrorResponse(service)
                }
 
                matches := rule.Matches
diff --git a/internal/adc/translator/plugin_test.go 
b/internal/adc/translator/plugin_test.go
index 6881ea42..35b49107 100644
--- a/internal/adc/translator/plugin_test.go
+++ b/internal/adc/translator/plugin_test.go
@@ -115,8 +115,9 @@ func TestFillPluginFromExtensionRef_ResolvesSecretRef(t 
*testing.T) {
 
        plugins := adctypes.Plugins{}
        require.NoError(t, translator.fillPluginFromExtensionRef(plugins, 
"default", &gatewayv1.LocalObjectReference{
-               Kind: internaltypes.KindPluginConfig,
-               Name: "oidc",
+               Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+               Kind:  internaltypes.KindPluginConfig,
+               Name:  "oidc",
        }, tctx))
 
        assert.Equal(t, map[string]any{
@@ -140,8 +141,9 @@ func 
TestFillPluginFromExtensionRef_MissingSecretFailsTranslation(t *testing.T)
 
        // The route must not be programmed without the plugin its filter asks 
for.
        err := translator.fillPluginFromExtensionRef(adctypes.Plugins{}, 
"default", &gatewayv1.LocalObjectReference{
-               Kind: internaltypes.KindPluginConfig,
-               Name: "oidc",
+               Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+               Kind:  internaltypes.KindPluginConfig,
+               Name:  "oidc",
        }, tctx)
        assert.ErrorContains(t, err, "default/oidc-credentials")
 }
@@ -168,8 +170,9 @@ func 
TestFillPluginFromExtensionRef_DoesNotLogSecretValues(t *testing.T) {
        }
 
        require.NoError(t, 
translator.fillPluginFromExtensionRef(adctypes.Plugins{}, "default", 
&gatewayv1.LocalObjectReference{
-               Kind: internaltypes.KindPluginConfig,
-               Name: "oidc",
+               Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+               Kind:  internaltypes.KindPluginConfig,
+               Name:  "oidc",
        }, tctx))
 
        assert.Contains(t, logged.String(), "openid-connect")
diff --git a/internal/controller/extensionref.go 
b/internal/controller/extensionref.go
new file mode 100644
index 00000000..353c604f
--- /dev/null
+++ b/internal/controller/extensionref.go
@@ -0,0 +1,69 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controller
+
+import (
+       "context"
+
+       apierrors "k8s.io/apimachinery/pkg/api/errors"
+       k8stypes "k8s.io/apimachinery/pkg/types"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+       "github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func loadPluginConfigExtensionRef(
+       ctx context.Context,
+       c client.Client,
+       tctx *provider.TranslateContext,
+       namespace string,
+       ref *gatewayv1.LocalObjectReference,
+) error {
+       if err := types.ValidatePluginConfigExtensionRef(ref); err != nil {
+               return err
+       }
+
+       pluginConfig := new(v1alpha1.PluginConfig)
+       if err := c.Get(ctx, client.ObjectKey{
+               Namespace: namespace,
+               Name:      string(ref.Name),
+       }, pluginConfig); err != nil {
+               if apierrors.IsNotFound(err) {
+                       return types.NewPluginConfigNotFoundError(namespace, 
string(ref.Name))
+               }
+               return err
+       }
+
+       tctx.PluginConfigs[k8stypes.NamespacedName{
+               Namespace: namespace,
+               Name:      string(ref.Name),
+       }] = pluginConfig
+       if err := loadPluginSecrets(ctx, c, tctx, namespace, 
pluginConfig.Spec.Plugins); err != nil {
+               if apierrors.IsNotFound(err) {
+                       return types.ReasonError{
+                               Reason:  
string(gatewayv1.RouteReasonBackendNotFound),
+                               Message: err.Error(),
+                       }
+               }
+               return err
+       }
+       return nil
+}
diff --git a/internal/controller/extensionref_resolution_test.go 
b/internal/controller/extensionref_resolution_test.go
new file mode 100644
index 00000000..c3a41970
--- /dev/null
+++ b/internal/controller/extensionref_resolution_test.go
@@ -0,0 +1,178 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controller
+
+import (
+       "context"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       apiMeta "k8s.io/apimachinery/pkg/api/meta"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       k8stypes "k8s.io/apimachinery/pkg/types"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func TestHTTPRouteExtensionRefResolutionCondition(t *testing.T) {
+       testRouteExtensionRefResolutionCondition(t, func(t *testing.T, cli 
client.Client, ref gatewayv1.LocalObjectReference) (*provider.TranslateContext, 
error) {
+               tctx := 
provider.NewDefaultTranslateContext(context.Background())
+               r := &HTTPRouteReconciler{Client: cli}
+               route := &gatewayv1.HTTPRoute{
+                       ObjectMeta: metav1.ObjectMeta{Name: "route", Namespace: 
"default"},
+                       Spec: gatewayv1.HTTPRouteSpec{Rules: 
[]gatewayv1.HTTPRouteRule{{
+                               Filters: []gatewayv1.HTTPRouteFilter{{
+                                       Type:         
gatewayv1.HTTPRouteFilterExtensionRef,
+                                       ExtensionRef: &ref,
+                               }},
+                       }}},
+               }
+               return tctx, r.processHTTPRoute(tctx, route)
+       })
+}
+
+func TestGRPCRouteExtensionRefResolutionCondition(t *testing.T) {
+       testRouteExtensionRefResolutionCondition(t, func(t *testing.T, cli 
client.Client, ref gatewayv1.LocalObjectReference) (*provider.TranslateContext, 
error) {
+               tctx := 
provider.NewDefaultTranslateContext(context.Background())
+               r := &GRPCRouteReconciler{Client: cli}
+               route := &gatewayv1.GRPCRoute{
+                       ObjectMeta: metav1.ObjectMeta{Name: "route", Namespace: 
"default"},
+                       Spec: gatewayv1.GRPCRouteSpec{Rules: 
[]gatewayv1.GRPCRouteRule{{
+                               Filters: []gatewayv1.GRPCRouteFilter{{
+                                       Type:         
gatewayv1.GRPCRouteFilterExtensionRef,
+                                       ExtensionRef: &ref,
+                               }},
+                       }}},
+               }
+               return tctx, r.processGRPCRoute(tctx, route)
+       })
+}
+
+func testRouteExtensionRefResolutionCondition(
+       t *testing.T,
+       process func(*testing.T, client.Client, gatewayv1.LocalObjectReference) 
(*provider.TranslateContext, error),
+) {
+       t.Helper()
+
+       tests := []struct {
+               name         string
+               ref          gatewayv1.LocalObjectReference
+               objects      []client.Object
+               wantReason   gatewayv1.RouteConditionReason
+               wantResolved bool
+               wantLoaded   bool
+       }{
+               {
+                       name: "supported reference",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: 
gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                               Kind:  internaltypes.KindPluginConfig,
+                               Name:  "filter",
+                       },
+                       objects: 
[]client.Object{&v1alpha1.PluginConfig{ObjectMeta: metav1.ObjectMeta{
+                               Namespace: "default",
+                               Name:      "filter",
+                       }}},
+                       wantReason:   gatewayv1.RouteReasonResolvedRefs,
+                       wantResolved: true,
+                       wantLoaded:   true,
+               },
+               {
+                       name: "unsupported group",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: "example.com",
+                               Kind:  internaltypes.KindPluginConfig,
+                               Name:  "filter",
+                       },
+                       objects: 
[]client.Object{&v1alpha1.PluginConfig{ObjectMeta: metav1.ObjectMeta{
+                               Namespace: "default",
+                               Name:      "filter",
+                       }}},
+                       wantReason: gatewayv1.RouteReasonInvalidKind,
+               },
+               {
+                       name: "unsupported kind",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: 
gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                               Kind:  "OtherFilter",
+                               Name:  "filter",
+                       },
+                       wantReason: gatewayv1.RouteReasonInvalidKind,
+               },
+               {
+                       name: "missing PluginConfig",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: 
gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                               Kind:  internaltypes.KindPluginConfig,
+                               Name:  "missing",
+                       },
+                       wantReason: gatewayv1.RouteReasonBackendNotFound,
+               },
+               {
+                       name: "missing plugin Secret",
+                       ref: gatewayv1.LocalObjectReference{
+                               Group: 
gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                               Kind:  internaltypes.KindPluginConfig,
+                               Name:  "filter",
+                       },
+                       objects: []client.Object{&v1alpha1.PluginConfig{
+                               ObjectMeta: metav1.ObjectMeta{Namespace: 
"default", Name: "filter"},
+                               Spec: v1alpha1.PluginConfigSpec{Plugins: 
[]v1alpha1.Plugin{{
+                                       Name:      "openid-connect",
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "missing"},
+                               }}},
+                       }},
+                       wantReason: gatewayv1.RouteReasonBackendNotFound,
+                       wantLoaded: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       scheme := runtime.NewScheme()
+                       require.NoError(t, corev1.AddToScheme(scheme))
+                       require.NoError(t, v1alpha1.AddToScheme(scheme))
+                       cli := 
fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.objects...).Build()
+
+                       tctx, err := process(t, cli, tt.ref)
+                       if tt.wantResolved {
+                               require.NoError(t, err)
+                       } else {
+                               require.Error(t, err)
+                       }
+
+                       status := gatewayv1.RouteParentStatus{}
+                       SetRouteConditionResolvedRefs(&status, 1, err)
+                       condition := 
apiMeta.FindStatusCondition(status.Conditions, 
string(gatewayv1.RouteConditionResolvedRefs))
+                       require.NotNil(t, condition)
+                       assert.Equal(t, tt.wantReason, 
gatewayv1.RouteConditionReason(condition.Reason))
+                       assert.Equal(t, tt.wantResolved, condition.Status == 
metav1.ConditionTrue)
+
+                       _, loaded := 
tctx.PluginConfigs[k8stypes.NamespacedName{Namespace: "default", Name: 
string(tt.ref.Name)}]
+                       assert.Equal(t, tt.wantLoaded, loaded)
+               })
+       }
+}
diff --git a/internal/controller/grpcroute_controller.go 
b/internal/controller/grpcroute_controller.go
index b2e0997c..5129af89 100644
--- a/internal/controller/grpcroute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -240,7 +240,7 @@ func (r *GRPCRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
        var backendRefErr error
        if err := r.processGRPCRoute(tctx, gr); err != nil {
                // When encountering a backend reference error, it should not 
affect the acceptance status
-               if types.IsSomeReasonError(err, 
gatewayv1.RouteReasonInvalidKind) {
+               if types.IsSomeReasonError(err, 
gatewayv1.RouteReasonInvalidKind, gatewayv1.RouteReasonBackendNotFound) {
                        backendRefErr = err
                } else {
                        acceptStatus.status = false
@@ -518,23 +518,8 @@ func (r *GRPCRouteReconciler) processGRPCRoute(tctx 
*provider.TranslateContext,
                        if filter.Type != gatewayv1.GRPCRouteFilterExtensionRef 
|| filter.ExtensionRef == nil {
                                continue
                        }
-                       if filter.ExtensionRef.Kind == "PluginConfig" {
-                               pluginconfig := new(v1alpha1.PluginConfig)
-                               if err := r.Get(context.Background(), 
client.ObjectKey{
-                                       Namespace: grpcroute.GetNamespace(),
-                                       Name:      
string(filter.ExtensionRef.Name),
-                               }, pluginconfig); err != nil {
-                                       terror = err
-                                       continue
-                               }
-                               tctx.PluginConfigs[k8stypes.NamespacedName{
-                                       Namespace: grpcroute.GetNamespace(),
-                                       Name:      
string(filter.ExtensionRef.Name),
-                               }] = pluginconfig
-                               if err := loadPluginSecrets(tctx, r.Client, 
tctx, grpcroute.GetNamespace(), pluginconfig.Spec.Plugins); err != nil {
-                                       terror = err
-                                       continue
-                               }
+                       if err := loadPluginConfigExtensionRef(tctx, r.Client, 
tctx, grpcroute.GetNamespace(), filter.ExtensionRef); err != nil {
+                               terror = err
                        }
                }
                for _, backend := range rule.BackendRefs {
diff --git a/internal/controller/httproute_controller.go 
b/internal/controller/httproute_controller.go
index 3c76ab92..2250cfb2 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/httproute_controller.go
@@ -227,7 +227,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
        var backendRefErr error
        if err := r.processHTTPRoute(tctx, hr); err != nil {
                // When encountering a backend reference error, it should not 
affect the acceptance status
-               if types.IsSomeReasonError(err, 
gatewayv1.RouteReasonInvalidKind) {
+               if types.IsSomeReasonError(err, 
gatewayv1.RouteReasonInvalidKind, gatewayv1.RouteReasonBackendNotFound) {
                        backendRefErr = err
                } else {
                        acceptStatus.status = false
@@ -603,23 +603,8 @@ func (r *HTTPRouteReconciler) processHTTPRoute(tctx 
*provider.TranslateContext,
                        if filter.Type != gatewayv1.HTTPRouteFilterExtensionRef 
|| filter.ExtensionRef == nil {
                                continue
                        }
-                       if filter.ExtensionRef.Kind == types.KindPluginConfig {
-                               pluginconfig := new(v1alpha1.PluginConfig)
-                               if err := r.Get(context.Background(), 
client.ObjectKey{
-                                       Namespace: httpRoute.GetNamespace(),
-                                       Name:      
string(filter.ExtensionRef.Name),
-                               }, pluginconfig); err != nil {
-                                       terror = err
-                                       continue
-                               }
-                               tctx.PluginConfigs[k8stypes.NamespacedName{
-                                       Namespace: httpRoute.GetNamespace(),
-                                       Name:      
string(filter.ExtensionRef.Name),
-                               }] = pluginconfig
-                               if err := loadPluginSecrets(tctx, r.Client, 
tctx, httpRoute.GetNamespace(), pluginconfig.Spec.Plugins); err != nil {
-                                       terror = err
-                                       continue
-                               }
+                       if err := loadPluginConfigExtensionRef(tctx, r.Client, 
tctx, httpRoute.GetNamespace(), filter.ExtensionRef); err != nil {
+                               terror = err
                        }
                }
                for _, backend := range rule.BackendRefs {
diff --git a/internal/controller/indexer/extensionref_test.go 
b/internal/controller/indexer/extensionref_test.go
new file mode 100644
index 00000000..9bbd23b3
--- /dev/null
+++ b/internal/controller/indexer/extensionref_test.go
@@ -0,0 +1,69 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package indexer
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func TestRouteExtensionRefIndexFuncUsesSupportedGroupAndKind(t *testing.T) {
+       supported := gatewayv1.LocalObjectReference{
+               Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+               Kind:  internaltypes.KindPluginConfig,
+               Name:  "supported",
+       }
+       wrongGroup := gatewayv1.LocalObjectReference{
+               Group: "example.com",
+               Kind:  internaltypes.KindPluginConfig,
+               Name:  "wrong-group",
+       }
+       wrongKind := gatewayv1.LocalObjectReference{
+               Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+               Kind:  "OtherFilter",
+               Name:  "wrong-kind",
+       }
+
+       httpRoute := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default"},
+               Spec: gatewayv1.HTTPRouteSpec{Rules: []gatewayv1.HTTPRouteRule{{
+                       Filters: []gatewayv1.HTTPRouteFilter{
+                               {Type: gatewayv1.HTTPRouteFilterExtensionRef, 
ExtensionRef: &supported},
+                               {Type: gatewayv1.HTTPRouteFilterExtensionRef, 
ExtensionRef: &wrongGroup},
+                               {Type: gatewayv1.HTTPRouteFilterExtensionRef, 
ExtensionRef: &wrongKind},
+                       },
+               }}},
+       }
+       assert.Equal(t, []string{GenIndexKey("default", "supported")}, 
HTTPRouteExtensionIndexFunc(httpRoute))
+
+       grpcRoute := &gatewayv1.GRPCRoute{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default"},
+               Spec: gatewayv1.GRPCRouteSpec{Rules: []gatewayv1.GRPCRouteRule{{
+                       Filters: []gatewayv1.GRPCRouteFilter{
+                               {Type: gatewayv1.GRPCRouteFilterExtensionRef, 
ExtensionRef: &supported},
+                               {Type: gatewayv1.GRPCRouteFilterExtensionRef, 
ExtensionRef: &wrongGroup},
+                               {Type: gatewayv1.GRPCRouteFilterExtensionRef, 
ExtensionRef: &wrongKind},
+                       },
+               }}},
+       }
+       assert.Equal(t, []string{GenIndexKey("default", "supported")}, 
GRPCRouteExtensionIndexFunc(grpcRoute))
+}
diff --git a/internal/controller/indexer/grpcroute.go 
b/internal/controller/indexer/grpcroute.go
index 656acf68..ebfe5ef6 100644
--- a/internal/controller/indexer/grpcroute.go
+++ b/internal/controller/indexer/grpcroute.go
@@ -97,7 +97,7 @@ func GRPCRouteExtensionIndexFunc(rawObj client.Object) 
[]string {
                        if filter.Type != gatewayv1.GRPCRouteFilterExtensionRef 
|| filter.ExtensionRef == nil {
                                continue
                        }
-                       if filter.ExtensionRef.Kind == 
internaltypes.KindPluginConfig {
+                       if 
internaltypes.IsPluginConfigExtensionRef(filter.ExtensionRef) {
                                keys = append(keys, 
GenIndexKey(gr.GetNamespace(), string(filter.ExtensionRef.Name)))
                        }
                }
diff --git a/internal/controller/indexer/indexer.go 
b/internal/controller/indexer/indexer.go
index 716e44e2..59e2dd69 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -933,7 +933,7 @@ func HTTPRouteExtensionIndexFunc(rawObj client.Object) 
[]string {
                        if filter.Type != gatewayv1.HTTPRouteFilterExtensionRef 
|| filter.ExtensionRef == nil {
                                continue
                        }
-                       if filter.ExtensionRef.Kind == 
internaltypes.KindPluginConfig {
+                       if 
internaltypes.IsPluginConfigExtensionRef(filter.ExtensionRef) {
                                keys = append(keys, 
GenIndexKey(hr.GetNamespace(), string(filter.ExtensionRef.Name)))
                        }
                }
diff --git a/internal/types/error.go b/internal/types/error.go
index 38f9ddba..960cbe03 100644
--- a/internal/types/error.go
+++ b/internal/types/error.go
@@ -81,6 +81,14 @@ func NewInvalidKindError[Kind ~string](kind Kind) 
ReasonError {
        }
 }
 
+// NewPluginConfigNotFoundError returns a route condition error for a missing 
PluginConfig.
+func NewPluginConfigNotFoundError(namespace, name string) ReasonError {
+       return ReasonError{
+               Reason:  string(gatewayv1.RouteReasonBackendNotFound),
+               Message: fmt.Sprintf("PluginConfig %s/%s not found", namespace, 
name),
+       }
+}
+
 type ADCExecutionErrors struct {
        Errors []ADCExecutionError
 }
diff --git a/internal/types/k8s.go b/internal/types/k8s.go
index 677b024c..5ea7e744 100644
--- a/internal/types/k8s.go
+++ b/internal/types/k8s.go
@@ -18,6 +18,8 @@
 package types
 
 import (
+       "fmt"
+
        corev1 "k8s.io/api/core/v1"
        netv1 "k8s.io/api/networking/v1"
        "k8s.io/apimachinery/pkg/runtime/schema"
@@ -28,6 +30,27 @@ import (
        v2 "github.com/apache/apisix-ingress-controller/api/v2"
 )
 
+// IsPluginConfigExtensionRef reports whether ref identifies the supported 
PluginConfig type.
+func IsPluginConfigExtensionRef(ref *gatewayv1.LocalObjectReference) bool {
+       return ref != nil &&
+               string(ref.Group) == v1alpha1.GroupVersion.Group &&
+               string(ref.Kind) == KindPluginConfig
+}
+
+// ValidatePluginConfigExtensionRef validates the group and kind of an 
ExtensionRef.
+func ValidatePluginConfigExtensionRef(ref *gatewayv1.LocalObjectReference) 
error {
+       if ref == nil || IsPluginConfigExtensionRef(ref) {
+               return nil
+       }
+       return ReasonError{
+               Reason: string(gatewayv1.RouteReasonInvalidKind),
+               Message: fmt.Sprintf(
+                       "Invalid ExtensionRef %s/%s, only %s/%s is supported",
+                       ref.Group, ref.Kind, v1alpha1.GroupVersion.Group, 
KindPluginConfig,
+               ),
+       }
+}
+
 const (
        DefaultIngressClassAnnotation = 
"ingressclass.kubernetes.io/is-default-class"
        IngressClassNameAnnotation    = "kubernetes.io/ingress.class"
diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go
index 9635d335..99d8e05a 100644
--- a/test/e2e/gatewayapi/httproute.go
+++ b/test/e2e/gatewayapi/httproute.go
@@ -2080,6 +2080,32 @@ spec:
     - name: httpbin-service-e2e-test
       port: 80
 `
+               var unsupportedExtensionRef = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: unsupported-extension-ref
+  namespace: %s
+spec:
+  parentRefs:
+  - name: %s
+  hostnames:
+  - httpbin.example
+  rules:
+  - matches:
+    - path:
+        type: Exact
+        value: /get
+    filters:
+    - type: ExtensionRef
+      extensionRef:
+        group: example.com
+        kind: PluginConfig
+        name: unavailable-filter
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
 
                var corsTestService = `
 apiVersion: v1
@@ -2420,6 +2446,40 @@ spec:
                        })
                })
 
+               It("HTTPRoute unsupported ExtensionRef", func() {
+                       By("create HTTPRoute")
+                       s.ResourceApplied(
+                               "HTTPRoute",
+                               "unsupported-extension-ref",
+                               fmt.Sprintf(unsupportedExtensionRef, 
s.Namespace(), s.Namespace()),
+                               1,
+                       )
+
+                       By("report the reference as unresolved")
+                       framework.HTTPRouteMustHaveCondition(
+                               s.GinkgoT,
+                               s.K8sClient,
+                               30*time.Second,
+                               types.NamespacedName{},
+                               types.NamespacedName{Namespace: s.Namespace(), 
Name: "unsupported-extension-ref"},
+                               metav1.Condition{
+                                       Type:   
string(gatewayv1.RouteConditionResolvedRefs),
+                                       Status: metav1.ConditionFalse,
+                                       Reason: 
string(gatewayv1.RouteReasonInvalidKind),
+                               },
+                       )
+
+                       By("return an error response for the affected rule")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:   "GET",
+                               Path:     "/get",
+                               Host:     "httpbin.example",
+                               Check:    
scaffold.WithExpectedStatus(http.StatusInternalServerError),
+                               Timeout:  time.Second * 30,
+                               Interval: time.Second * 2,
+                       })
+               })
+
                It("HTTPRoute ExtensionRef with plugin secretRef", func() {
                        By("create Secret and PluginConfig")
                        
Expect(s.CreateResourceFromStringWithNamespace(echoSecret, s.Namespace())).

Reply via email to