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 68cda311 fix: reject invalid L4RoutePolicy plugin config (#2876)
68cda311 is described below

commit 68cda31198143027c7ffed10a59af873465426f6
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Tue Sep 29 18:01:45 2026 +0800

    fix: reject invalid L4RoutePolicy plugin config (#2876)
---
 internal/adc/translator/l4route_test.go            | 40 +++++++++++
 internal/adc/translator/l4routepolicy_test.go      | 10 +--
 internal/adc/translator/plugin.go                  | 28 +-------
 internal/adc/translator/policies.go                | 16 +++--
 internal/adc/translator/tcproute.go                | 14 ++--
 internal/adc/translator/tlsroute.go                |  5 +-
 internal/adc/translator/udproute.go                |  6 +-
 internal/controller/l4routepolicy_test.go          | 82 ++++++++++++++++++++++
 internal/controller/policies.go                    | 24 ++++---
 .../plugin.go => pluginconfig/renderer.go}         |  8 +--
 internal/provider/apisix/provider_test.go          | 70 ++++++++++++++++++
 11 files changed, 246 insertions(+), 57 deletions(-)

diff --git a/internal/adc/translator/l4route_test.go 
b/internal/adc/translator/l4route_test.go
index 62d0ca03..5ac129da 100644
--- a/internal/adc/translator/l4route_test.go
+++ b/internal/adc/translator/l4route_test.go
@@ -43,6 +43,7 @@ func TestTranslateTCPRouteWithL4RoutePolicy(t *testing.T) {
                policy        *v1alpha1.L4RoutePolicy
                wantPlugins   []string
                wantNoPlugins bool
+               wantErr       bool
        }{
                {
                        name: "attaches plugins from matching L4RoutePolicy",
@@ -52,6 +53,13 @@ func TestTranslateTCPRouteWithL4RoutePolicy(t *testing.T) {
                        }),
                        wantPlugins: []string{"limit-conn", "ip-restriction"},
                },
+               {
+                       name: "rejects a policy with a non-object plugin 
config",
+                       policy: makeL4RoutePolicy("default", "tcp-policy", 
"TCPRoute", "my-tcp", []v1alpha1.Plugin{
+                               {Name: "ip-restriction", Config: 
mustJSON([]string{"10.0.0.0/8"})},
+                       }),
+                       wantErr: true,
+               },
                {
                        name: "does not attach plugins from policy targeting 
different route kind",
                        policy: makeL4RoutePolicy("default", "udp-policy", 
"UDPRoute", "my-tcp", []v1alpha1.Plugin{
@@ -96,6 +104,11 @@ func TestTranslateTCPRouteWithL4RoutePolicy(t *testing.T) {
                        }
 
                        result, err := translator.TranslateTCPRoute(tctx, route)
+                       if tt.wantErr {
+                               require.Error(t, err)
+                               assert.Nil(t, result)
+                               return
+                       }
                        require.NoError(t, err)
                        require.Len(t, result.Services, 1)
                        require.NotEmpty(t, result.Services[0].StreamRoutes)
@@ -118,6 +131,7 @@ func TestTranslateUDPRouteWithL4RoutePolicy(t *testing.T) {
                policy        *v1alpha1.L4RoutePolicy
                wantPlugins   []string
                wantNoPlugins bool
+               wantErr       bool
        }{
                {
                        name: "attaches plugins from matching L4RoutePolicy",
@@ -126,6 +140,13 @@ func TestTranslateUDPRouteWithL4RoutePolicy(t *testing.T) {
                        }),
                        wantPlugins: []string{"limit-conn"},
                },
+               {
+                       name: "rejects a policy with a non-object plugin 
config",
+                       policy: makeL4RoutePolicy("default", "udp-policy", 
"UDPRoute", "my-udp", []v1alpha1.Plugin{
+                               {Name: "ip-restriction", Config: 
mustJSON("10.0.0.0/8")},
+                       }),
+                       wantErr: true,
+               },
                {
                        name: "does not attach plugins from policy targeting 
TCPRoute",
                        policy: makeL4RoutePolicy("default", "tcp-policy", 
"TCPRoute", "my-udp", []v1alpha1.Plugin{
@@ -163,6 +184,11 @@ func TestTranslateUDPRouteWithL4RoutePolicy(t *testing.T) {
                        }
 
                        result, err := translator.TranslateUDPRoute(tctx, route)
+                       if tt.wantErr {
+                               require.Error(t, err)
+                               assert.Nil(t, result)
+                               return
+                       }
                        require.NoError(t, err)
                        require.Len(t, result.Services, 1)
                        require.NotEmpty(t, result.Services[0].StreamRoutes)
@@ -186,6 +212,7 @@ func TestTranslateTLSRouteWithL4RoutePolicy(t *testing.T) {
                hostnames     []string
                wantPlugins   []string
                wantNoPlugins bool
+               wantErr       bool
        }{
                {
                        name: "attaches plugins from matching L4RoutePolicy",
@@ -195,6 +222,14 @@ func TestTranslateTLSRouteWithL4RoutePolicy(t *testing.T) {
                        hostnames:   []string{"example.com"},
                        wantPlugins: []string{"ip-restriction"},
                },
+               {
+                       name: "rejects a policy with a non-object plugin 
config",
+                       policy: makeL4RoutePolicy("default", "tls-policy", 
"TLSRoute", "my-tls", []v1alpha1.Plugin{
+                               {Name: "ip-restriction", Config: 
mustJSON(true)},
+                       }),
+                       hostnames: []string{"example.com"},
+                       wantErr:   true,
+               },
                {
                        name: "plugins attached once per rule even with 
multiple SNI hostnames",
                        policy: makeL4RoutePolicy("default", "tls-policy", 
"TLSRoute", "my-tls", []v1alpha1.Plugin{
@@ -248,6 +283,11 @@ func TestTranslateTLSRouteWithL4RoutePolicy(t *testing.T) {
                        }
 
                        result, err := translator.TranslateTLSRoute(tctx, route)
+                       if tt.wantErr {
+                               require.Error(t, err)
+                               assert.Nil(t, result)
+                               return
+                       }
                        require.NoError(t, err)
                        require.Len(t, result.Services, 1)
 
diff --git a/internal/adc/translator/l4routepolicy_test.go 
b/internal/adc/translator/l4routepolicy_test.go
index 289dab98..833db371 100644
--- a/internal/adc/translator/l4routepolicy_test.go
+++ b/internal/adc/translator/l4routepolicy_test.go
@@ -74,7 +74,7 @@ func TestAttachL4RoutePolicyPlugins_AttachesMatchingPolicy(t 
*testing.T) {
        }
 
        plugins := adctypes.Plugins{}
-       tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route", 
"TCPRoute", plugins, nil)
+       assert.NoError(t, tr.AttachL4RoutePolicyPlugins(policies, "default", 
"my-tcp-route", "TCPRoute", plugins, nil))
 
        assert.Len(t, plugins, 2)
        assert.Contains(t, plugins, "limit-conn")
@@ -97,7 +97,7 @@ func TestAttachL4RoutePolicyPlugins_NoMatchOnKind(t 
*testing.T) {
 
        plugins := adctypes.Plugins{}
        // Looking for TCPRoute, but policy targets UDPRoute — should not match.
-       tr.AttachL4RoutePolicyPlugins(policies, "default", "my-udp-route", 
"TCPRoute", plugins, nil)
+       assert.NoError(t, tr.AttachL4RoutePolicyPlugins(policies, "default", 
"my-udp-route", "TCPRoute", plugins, nil))
 
        assert.Empty(t, plugins)
 }
@@ -115,7 +115,7 @@ func TestAttachL4RoutePolicyPlugins_NoMatchOnNamespace(t 
*testing.T) {
 
        plugins := adctypes.Plugins{}
        // Route is in "default" namespace, policy is in "other-ns" — should 
not match.
-       tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route", 
"TCPRoute", plugins, nil)
+       assert.NoError(t, tr.AttachL4RoutePolicyPlugins(policies, "default", 
"my-tcp-route", "TCPRoute", plugins, nil))
 
        assert.Empty(t, plugins)
 }
@@ -130,7 +130,7 @@ func TestAttachL4RoutePolicyPlugins_EmptyPlugins(t 
*testing.T) {
        }
 
        plugins := adctypes.Plugins{}
-       tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route", 
"TCPRoute", plugins, nil)
+       assert.NoError(t, tr.AttachL4RoutePolicyPlugins(policies, "default", 
"my-tcp-route", "TCPRoute", plugins, nil))
 
        assert.Empty(t, plugins)
 }
@@ -138,6 +138,6 @@ func TestAttachL4RoutePolicyPlugins_EmptyPlugins(t 
*testing.T) {
 func TestAttachL4RoutePolicyPlugins_EmptyPolicies(t *testing.T) {
        tr := NewTranslator(logr.Discard(), "")
        plugins := adctypes.Plugins{}
-       tr.AttachL4RoutePolicyPlugins(nil, "default", "my-tcp-route", 
"TCPRoute", plugins, nil)
+       assert.NoError(t, tr.AttachL4RoutePolicyPlugins(nil, "default", 
"my-tcp-route", "TCPRoute", plugins, nil))
        assert.Empty(t, plugins)
 }
diff --git a/internal/adc/translator/plugin.go 
b/internal/adc/translator/plugin.go
index d16180fc..b0817693 100644
--- a/internal/adc/translator/plugin.go
+++ b/internal/adc/translator/plugin.go
@@ -18,40 +18,16 @@
 package translator
 
 import (
-       "encoding/json"
-       "fmt"
-
        corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/types"
 
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
-       pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils"
+       "github.com/apache/apisix-ingress-controller/internal/pluginconfig"
 )
 
 // renderPluginConfig renders the configuration of an 
apisix.apache.org/v1alpha1 Plugin.
 // The data of the referenced Secret is merged over spec.config, with each 
Secret key
 // read as a dot separated path so that `session.secret` nests under `session`.
 func renderPluginConfig(plugin v1alpha1.Plugin, namespace string, secrets 
map[types.NamespacedName]*corev1.Secret) (map[string]any, error) {
-       config := make(map[string]any)
-       if len(plugin.Config.Raw) > 0 {
-               if err := json.Unmarshal(plugin.Config.Raw, &config); err != 
nil {
-                       return nil, fmt.Errorf("failed to unmarshal config of 
plugin %s: %w", plugin.Name, err)
-               }
-       }
-       // A literal `config: null` unmarshals to a nil map, which serializes 
back to
-       // null and is rejected by most APISIX plugins; normalize it to an 
empty object.
-       if config == nil {
-               config = make(map[string]any)
-       }
-       if plugin.SecretRef == nil || plugin.SecretRef.Name == "" {
-               return config, nil
-       }
-       secret, ok := secrets[types.NamespacedName{Namespace: namespace, Name: 
plugin.SecretRef.Name}]
-       if !ok || secret == nil {
-               return nil, fmt.Errorf("secret %s/%s referenced by plugin %s 
not found", namespace, plugin.SecretRef.Name, plugin.Name)
-       }
-       for key, value := range secret.Data {
-               pkgutils.InsertKeyInMap(key, string(value), config)
-       }
-       return config, nil
+       return pluginconfig.Render(plugin, namespace, secrets)
 }
diff --git a/internal/adc/translator/policies.go 
b/internal/adc/translator/policies.go
index aad0ff3f..f60bba21 100644
--- a/internal/adc/translator/policies.go
+++ b/internal/adc/translator/policies.go
@@ -18,6 +18,8 @@
 package translator
 
 import (
+       "fmt"
+
        corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/types"
        "k8s.io/utils/ptr"
@@ -229,9 +231,9 @@ func (t *Translator) AttachL4RoutePolicyPlugins(
        routeNamespace, routeName, routeKind string,
        plugins adctypes.Plugins,
        secrets map[types.NamespacedName]*corev1.Secret,
-) {
+) error {
        if len(policies) == 0 {
-               return
+               return nil
        }
        for _, policy := range policies {
                if policy.Namespace != routeNamespace {
@@ -252,19 +254,19 @@ func (t *Translator) AttachL4RoutePolicyPlugins(
                        if ref.SectionName != nil && *ref.SectionName != "" {
                                continue
                        }
-                       t.mergeL4PolicyPlugins(policy, plugins, secrets)
-                       return
+                       return t.mergeL4PolicyPlugins(policy, plugins, secrets)
                }
        }
+       return nil
 }
 
-func (t *Translator) mergeL4PolicyPlugins(policy *v1alpha1.L4RoutePolicy, 
plugins adctypes.Plugins, secrets map[types.NamespacedName]*corev1.Secret) {
+func (t *Translator) mergeL4PolicyPlugins(policy *v1alpha1.L4RoutePolicy, 
plugins adctypes.Plugins, secrets map[types.NamespacedName]*corev1.Secret) 
error {
        for _, plugin := range policy.Spec.Plugins {
                cfg, err := renderPluginConfig(plugin, policy.Namespace, 
secrets)
                if err != nil {
-                       t.Log.Error(err, "failed to render L4RoutePolicy plugin 
config", "plugin", plugin.Name, "policy", policy.Name)
-                       continue
+                       return fmt.Errorf("failed to render plugin %q from 
L4RoutePolicy %s/%s: %w", plugin.Name, policy.Namespace, policy.Name, err)
                }
                plugins[plugin.Name] = cfg
        }
+       return nil
 }
diff --git a/internal/adc/translator/tcproute.go 
b/internal/adc/translator/tcproute.go
index 7de4a67e..a57f316a 100644
--- a/internal/adc/translator/tcproute.go
+++ b/internal/adc/translator/tcproute.go
@@ -67,7 +67,7 @@ func listenerPortSet(tctx *provider.TranslateContext) 
map[int32]struct{} {
 // the match to work, so injection is opt-in (explicit sectionName/port 
targeting,
 // or more than one listener port). When it is not injected we keep the 
previous
 // single portless StreamRoute, preserving backward compatibility.
-func (t *Translator) buildL4StreamRoutes(tctx *provider.TranslateContext, 
namespace, name string, ruleIndex int, typ, routeKind string, labels 
map[string]string) []*adctypes.StreamRoute {
+func (t *Translator) buildL4StreamRoutes(tctx *provider.TranslateContext, 
namespace, name string, ruleIndex int, typ, routeKind string, labels 
map[string]string) ([]*adctypes.StreamRoute, error) {
        var ports []int32
        if portSet := listenerPortSet(tctx); 
t.shouldInjectServerPortVars(tctx.HasExplicitListenerMatch, portSet) {
                ports = make([]int32, 0, len(portSet))
@@ -98,10 +98,12 @@ func (t *Translator) buildL4StreamRoutes(tctx 
*provider.TranslateContext, namesp
                // Attach L4RoutePolicy plugins at the stream_route level: the 
APISIX stream proxy
                // applies plugins from the stream_route, not from the service.
                streamRoute.Plugins = make(adctypes.Plugins)
-               t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, namespace, 
name, routeKind, streamRoute.Plugins, tctx.Secrets)
+               if err := t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, 
namespace, name, routeKind, streamRoute.Plugins, tctx.Secrets); err != nil {
+                       return nil, err
+               }
                streamRoutes = append(streamRoutes, streamRoute)
        }
-       return streamRoutes
+       return streamRoutes, nil
 }
 
 func (t *Translator) TranslateTCPRoute(tctx *provider.TranslateContext, 
tcpRoute *gatewayv1.TCPRoute) (*TranslateResult, error) {
@@ -212,7 +214,11 @@ func (t *Translator) TranslateTCPRoute(tctx 
*provider.TranslateContext, tcpRoute
                        }
                }
                // TODO: support remote_addr, server_addr, sni
-               service.StreamRoutes = t.buildL4StreamRoutes(tctx, 
tcpRoute.Namespace, tcpRoute.Name, ruleIndex, "TCP", "TCPRoute", labels)
+               streamRoutes, err := t.buildL4StreamRoutes(tctx, 
tcpRoute.Namespace, tcpRoute.Name, ruleIndex, "TCP", "TCPRoute", labels)
+               if err != nil {
+                       return nil, err
+               }
+               service.StreamRoutes = streamRoutes
 
                result.Services = append(result.Services, service)
        }
diff --git a/internal/adc/translator/tlsroute.go 
b/internal/adc/translator/tlsroute.go
index 1dc95631..2faa16b6 100644
--- a/internal/adc/translator/tlsroute.go
+++ b/internal/adc/translator/tlsroute.go
@@ -144,7 +144,10 @@ func (t *Translator) TranslateTLSRoute(tctx 
*provider.TranslateContext, tlsRoute
                }
 
                for _, host := range hosts {
-                       streamRoutes := t.buildL4StreamRoutes(tctx, 
tlsRoute.Namespace, tlsRoute.Name, ruleIndex, "TLS", "TLSRoute", labels)
+                       streamRoutes, err := t.buildL4StreamRoutes(tctx, 
tlsRoute.Namespace, tlsRoute.Name, ruleIndex, "TLS", "TLSRoute", labels)
+                       if err != nil {
+                               return nil, err
+                       }
                        for _, streamRoute := range streamRoutes {
                                streamRoute.SNI = host
                        }
diff --git a/internal/adc/translator/udproute.go 
b/internal/adc/translator/udproute.go
index 6e6e23ac..7706aa60 100644
--- a/internal/adc/translator/udproute.go
+++ b/internal/adc/translator/udproute.go
@@ -139,7 +139,11 @@ func (t *Translator) TranslateUDPRoute(tctx 
*provider.TranslateContext, udpRoute
                        }
                }
                // TODO: support remote_addr, server_addr, sni
-               service.StreamRoutes = t.buildL4StreamRoutes(tctx, 
udpRoute.Namespace, udpRoute.Name, ruleIndex, "UDP", "UDPRoute", labels)
+               streamRoutes, err := t.buildL4StreamRoutes(tctx, 
udpRoute.Namespace, udpRoute.Name, ruleIndex, "UDP", "UDPRoute", labels)
+               if err != nil {
+                       return nil, err
+               }
+               service.StreamRoutes = streamRoutes
 
                result.Services = append(result.Services, service)
        }
diff --git a/internal/controller/l4routepolicy_test.go 
b/internal/controller/l4routepolicy_test.go
new file mode 100644
index 00000000..b9235a31
--- /dev/null
+++ b/internal/controller/l4routepolicy_test.go
@@ -0,0 +1,82 @@
+// 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/go-logr/logr"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       apiextensionsv1 
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+       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/fake"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+func TestProcessL4RoutePolicy_InvalidPluginConfigSetsRejectedStatus(t 
*testing.T) {
+       policy := &v1alpha1.L4RoutePolicy{
+               ObjectMeta: metav1.ObjectMeta{
+                       Namespace:  "default",
+                       Name:       "tcp-policy",
+                       Generation: 3,
+               },
+               Spec: v1alpha1.L4RoutePolicySpec{
+                       TargetRefs: 
[]gatewayv1.LocalPolicyTargetReferenceWithSectionName{{
+                               LocalPolicyTargetReference: 
gatewayv1.LocalPolicyTargetReference{
+                                       Group: gatewayv1.GroupName,
+                                       Kind:  "TCPRoute",
+                                       Name:  "tcp-route",
+                               },
+                       }},
+                       Plugins: []v1alpha1.Plugin{{
+                               Name:   "ip-restriction",
+                               Config: apiextensionsv1.JSON{Raw: 
[]byte(`["10.0.0.0/8"]`)},
+                       }},
+               },
+       }
+       scheme := runtime.NewScheme()
+       require.NoError(t, v1alpha1.AddToScheme(scheme))
+       cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(policy).
+               WithIndex(&v1alpha1.L4RoutePolicy{}, indexer.PolicyTargetRefs, 
indexer.L4RoutePolicyIndexFunc).
+               Build()
+       tctx := provider.NewDefaultTranslateContext(context.Background())
+       tctx.RouteParentRefs = []gatewayv1.ParentReference{{Name: "gateway"}}
+
+       ProcessL4RoutePolicy(cli, logr.Discard(), tctx, "default", "tcp-route", 
"TCPRoute")
+
+       key := k8stypes.NamespacedName{Namespace: "default", Name: "tcp-policy"}
+       require.NotNil(t, tctx.L4RoutePolicies[key], "the policy must reach 
translation so rendering stops the update")
+       require.Len(t, tctx.StatusUpdaters, 1)
+       mutated := 
tctx.StatusUpdaters[0].Mutator.Mutate(&v1alpha1.L4RoutePolicy{}).(*v1alpha1.L4RoutePolicy)
+       require.Len(t, mutated.Status.Ancestors, 1)
+       require.Len(t, mutated.Status.Ancestors[0].Conditions, 1)
+       condition := mutated.Status.Ancestors[0].Conditions[0]
+       assert.Equal(t, string(gatewayv1.PolicyConditionAccepted), 
condition.Type)
+       assert.Equal(t, metav1.ConditionFalse, condition.Status)
+       assert.Equal(t, string(gatewayv1.PolicyReasonInvalid), condition.Reason)
+       assert.Equal(t, int64(3), condition.ObservedGeneration)
+       assert.Equal(t, `plugin "ip-restriction" has invalid configuration`, 
condition.Message)
+}
diff --git a/internal/controller/policies.go b/internal/controller/policies.go
index 1cfb5c7f..9643200d 100644
--- a/internal/controller/policies.go
+++ b/internal/controller/policies.go
@@ -38,6 +38,7 @@ import (
        "github.com/apache/apisix-ingress-controller/internal/controller/config"
        
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
        "github.com/apache/apisix-ingress-controller/internal/controller/status"
+       "github.com/apache/apisix-ingress-controller/internal/pluginconfig"
        "github.com/apache/apisix-ingress-controller/internal/provider"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
        "github.com/apache/apisix-ingress-controller/internal/utils"
@@ -300,26 +301,33 @@ func ProcessL4RoutePolicy(
        })
 
        winner := list.Items[0].DeepCopy()
-       // A policy whose Secrets cannot be read is not attached at all, so a 
route is never
-       // programmed with a subset of the plugins the policy asks for.
-       secretErr := loadPluginSecrets(tctx, c, tctx, winner.Namespace, 
winner.Spec.Plugins)
-       if secretErr != nil {
-               log.Error(secretErr, "failed to load Secrets referenced by 
L4RoutePolicy plugins", "policy", types.NamespacedName{Namespace: 
winner.Namespace, Name: winner.Name})
+       renderErr := loadPluginSecrets(tctx, c, tctx, winner.Namespace, 
winner.Spec.Plugins)
+       if renderErr == nil {
+               for _, plugin := range winner.Spec.Plugins {
+                       if _, err := pluginconfig.Render(plugin, 
winner.Namespace, tctx.Secrets); err != nil {
+                               log.Error(err, "failed to render L4RoutePolicy 
plugin config", "plugin", plugin.Name, "policy", 
types.NamespacedName{Namespace: winner.Namespace, Name: winner.Name})
+                               renderErr = fmt.Errorf("plugin %q has invalid 
configuration", plugin.Name)
+                               break
+                       }
+               }
        } else {
-               tctx.L4RoutePolicies[types.NamespacedName{Namespace: 
winner.Namespace, Name: winner.Name}] = winner
+               log.Error(renderErr, "failed to load Secrets referenced by 
L4RoutePolicy plugins", "policy", types.NamespacedName{Namespace: 
winner.Namespace, Name: winner.Name})
        }
+       // Keep the winning policy in the translation context even when 
rendering failed.
+       // Translation must return the error instead of publishing the route 
without it.
+       tctx.L4RoutePolicies[types.NamespacedName{Namespace: winner.Namespace, 
Name: winner.Name}] = winner
 
        for i := range list.Items {
                policy := list.Items[i]
                var condition metav1.Condition
-               if i == 0 && secretErr != nil {
+               if i == 0 && renderErr != nil {
                        condition = metav1.Condition{
                                Type:               
string(gatewayv1.PolicyConditionAccepted),
                                Status:             metav1.ConditionFalse,
                                ObservedGeneration: policy.GetGeneration(),
                                LastTransitionTime: metav1.Now(),
                                Reason:             
string(gatewayv1.PolicyReasonInvalid),
-                               Message:            secretErr.Error(),
+                               Message:            renderErr.Error(),
                        }
                } else if i == 0 {
                        condition = metav1.Condition{
diff --git a/internal/adc/translator/plugin.go 
b/internal/pluginconfig/renderer.go
similarity index 82%
copy from internal/adc/translator/plugin.go
copy to internal/pluginconfig/renderer.go
index d16180fc..06857ed0 100644
--- a/internal/adc/translator/plugin.go
+++ b/internal/pluginconfig/renderer.go
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package translator
+package pluginconfig
 
 import (
        "encoding/json"
@@ -28,10 +28,8 @@ import (
        pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils"
 )
 
-// renderPluginConfig renders the configuration of an 
apisix.apache.org/v1alpha1 Plugin.
-// The data of the referenced Secret is merged over spec.config, with each 
Secret key
-// read as a dot separated path so that `session.secret` nests under `session`.
-func renderPluginConfig(plugin v1alpha1.Plugin, namespace string, secrets 
map[types.NamespacedName]*corev1.Secret) (map[string]any, error) {
+// Render renders a v1alpha1 plugin configuration and merges referenced Secret 
data.
+func Render(plugin v1alpha1.Plugin, namespace string, secrets 
map[types.NamespacedName]*corev1.Secret) (map[string]any, error) {
        config := make(map[string]any)
        if len(plugin.Config.Raw) > 0 {
                if err := json.Unmarshal(plugin.Config.Raw, &config); err != 
nil {
diff --git a/internal/provider/apisix/provider_test.go 
b/internal/provider/apisix/provider_test.go
index 01412d10..1bd8b6b9 100644
--- a/internal/provider/apisix/provider_test.go
+++ b/internal/provider/apisix/provider_test.go
@@ -31,14 +31,19 @@ import (
        "github.com/go-logr/logr/funcr"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
+       apiextensionsv1 
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       k8stypes "k8s.io/apimachinery/pkg/types"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/adc/cache"
        adcclient 
"github.com/apache/apisix-ingress-controller/internal/adc/client"
+       "github.com/apache/apisix-ingress-controller/internal/adc/translator"
        "github.com/apache/apisix-ingress-controller/internal/controller/label"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
        "github.com/apache/apisix-ingress-controller/internal/provider/common"
        "github.com/apache/apisix-ingress-controller/internal/types"
        "github.com/apache/apisix-ingress-controller/internal/utils"
@@ -104,6 +109,71 @@ func TestDeleteNotifiesSyncOnlyWhenConfigWasRemoved(t 
*testing.T) {
        require.Len(t, d.syncCh, 1, "removing configuration this controller 
pushed must trigger a sync")
 }
 
+func TestUpdateKeepsLastKnownGoodStateWhenL4PolicyCannotRender(t *testing.T) {
+       d := newTestProvider(t)
+       d.translator = translator.NewTranslator(logr.Discard(), "")
+
+       route := &gatewayv1.TCPRoute{
+               TypeMeta: metav1.TypeMeta{Kind: "TCPRoute", APIVersion: 
gatewayv1.GroupVersion.String()},
+               ObjectMeta: metav1.ObjectMeta{
+                       Namespace: "default",
+                       Name:      "tcp-route",
+               },
+               Spec: gatewayv1.TCPRouteSpec{Rules: 
[]gatewayv1.TCPRouteRule{{}}},
+       }
+       gatewayProxy := v1alpha1.GatewayProxy{
+               TypeMeta:   metav1.TypeMeta{Kind: "GatewayProxy", APIVersion: 
v1alpha1.GroupVersion.String()},
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: 
"proxy"},
+               Spec: v1alpha1.GatewayProxySpec{Provider: 
&v1alpha1.GatewayProxyProvider{
+                       Type: v1alpha1.ProviderTypeControlPlane,
+                       ControlPlane: &v1alpha1.ControlPlaneProvider{
+                               Endpoints: []string{"http://apisix:9180"},
+                               Auth: v1alpha1.ControlPlaneAuth{
+                                       Type:     v1alpha1.AuthTypeAdminKey,
+                                       AdminKey: &v1alpha1.AdminKeyAuth{Value: 
"key"},
+                               },
+                       },
+               }},
+       }
+       configName := utils.NamespacedNameKind(&gatewayProxy).String()
+       lastKnownGood := adctypes.NewDefaultService()
+       lastKnownGood.Name = "last-known-good"
+       lastKnownGood.ID = "last-known-good"
+       lastKnownGood.Labels = label.GenLabel(route)
+       require.NoError(t, d.store.Insert(configName, 
[]string{adctypes.TypeService}, &adctypes.Resources{
+               Services: []*adctypes.Service{lastKnownGood},
+       }, lastKnownGood.Labels))
+
+       policy := &v1alpha1.L4RoutePolicy{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: 
"tcp-policy"},
+               Spec: v1alpha1.L4RoutePolicySpec{
+                       TargetRefs: 
[]gatewayv1.LocalPolicyTargetReferenceWithSectionName{{
+                               LocalPolicyTargetReference: 
gatewayv1.LocalPolicyTargetReference{
+                                       Group: gatewayv1.GroupName,
+                                       Kind:  "TCPRoute",
+                                       Name:  "tcp-route",
+                               },
+                       }},
+                       Plugins: []v1alpha1.Plugin{{
+                               Name:   "ip-restriction",
+                               Config: apiextensionsv1.JSON{Raw: []byte(`[]`)},
+                       }},
+               },
+       }
+       tctx := provider.NewDefaultTranslateContext(context.Background())
+       tctx.GatewayProxies[utils.NamespacedNameKind(&gatewayProxy)] = 
gatewayProxy
+       tctx.L4RoutePolicies[k8stypes.NamespacedName{Namespace: 
policy.Namespace, Name: policy.Name}] = policy
+
+       err := d.Update(context.Background(), tctx, route)
+
+       require.Error(t, err)
+       assert.Empty(t, d.syncCh)
+       resources, getErr := d.store.GetResources(configName)
+       require.NoError(t, getErr)
+       require.Len(t, resources.Services, 1)
+       assert.Equal(t, "last-known-good", resources.Services[0].Name)
+}
+
 func TestDeleteLogsObjectIdentity(t *testing.T) {
        const credential = "consumer-key-value"
        var logged strings.Builder

Reply via email to