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())).