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 dac9e1ab fix(gateway-api): honor TLSRoute listener ports (#2881)
dac9e1ab is described below

commit dac9e1ab1df9ce4c6cf5640cac149b2b281df37e
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Mon Sep 21 12:13:04 2026 +0800

    fix(gateway-api): honor TLSRoute listener ports (#2881)
---
 internal/adc/translator/l4route_serverport_test.go |  95 +++++++++++++++++
 internal/adc/translator/tlsroute.go                |  17 +--
 internal/controller/tlsroute_controller.go         |   8 ++
 internal/controller/tlsroute_listener_test.go      | 116 +++++++++++++++++++++
 4 files changed, 224 insertions(+), 12 deletions(-)

diff --git a/internal/adc/translator/l4route_serverport_test.go 
b/internal/adc/translator/l4route_serverport_test.go
index 71a226c8..4d8a0bbd 100644
--- a/internal/adc/translator/l4route_serverport_test.go
+++ b/internal/adc/translator/l4route_serverport_test.go
@@ -47,6 +47,14 @@ func udpListener(name string, port int32) gatewayv1.Listener 
{
        }
 }
 
+func tlsListener(name string, port int32) gatewayv1.Listener {
+       return gatewayv1.Listener{
+               Name:     gatewayv1.SectionName(name),
+               Protocol: gatewayv1.TLSProtocolType,
+               Port:     port,
+       }
+}
+
 func TestTranslateTCPRouteServerPort(t *testing.T) {
        tests := []struct {
                name string
@@ -208,3 +216,90 @@ func TestTranslateUDPRouteServerPort(t *testing.T) {
                })
        }
 }
+
+func TestTranslateTLSRouteServerPort(t *testing.T) {
+       tests := []struct {
+               name        string
+               mode        config.ListenerPortMatchMode
+               listeners   []gatewayv1.Listener
+               explicit    bool
+               wantPorts   []int32
+               wantNoMatch bool
+       }{
+               {
+                       name:      "explicit sectionName injects the matching 
listener port",
+                       mode:      config.ListenerPortMatchModeAuto,
+                       listeners: []gatewayv1.Listener{tlsListener("tls-main", 
9110)},
+                       explicit:  true,
+                       wantPorts: []int32{9110},
+               },
+               {
+                       name:      "multiple listener ports produce distinct 
StreamRoutes",
+                       mode:      config.ListenerPortMatchModeAuto,
+                       listeners: []gatewayv1.Listener{tlsListener("tls-main", 
9110), tlsListener("tls-alt", 9111)},
+                       wantPorts: []int32{9110, 9111},
+               },
+               {
+                       name:        "single listener without explicit 
targeting keeps a portless StreamRoute",
+                       mode:        config.ListenerPortMatchModeAuto,
+                       listeners:   
[]gatewayv1.Listener{tlsListener("tls-main", 9110)},
+                       wantNoMatch: true,
+               },
+               {
+                       name:        "off mode preserves a portless 
StreamRoute",
+                       mode:        config.ListenerPortMatchModeOff,
+                       listeners:   
[]gatewayv1.Listener{tlsListener("tls-main", 9110), tlsListener("tls-alt", 
9111)},
+                       explicit:    true,
+                       wantNoMatch: true,
+               },
+               {
+                       name:        "explicit mode ignores implicit 
multi-listener attachment",
+                       mode:        config.ListenerPortMatchModeExplicit,
+                       listeners:   
[]gatewayv1.Listener{tlsListener("tls-main", 9110), tlsListener("tls-alt", 
9111)},
+                       wantNoMatch: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       translator := NewTranslator(logr.Discard(), tt.mode)
+                       tctx := 
provider.NewDefaultTranslateContext(context.Background())
+                       tctx.Listeners = tt.listeners
+                       tctx.HasExplicitListenerMatch = tt.explicit
+
+                       route := &gatewayv1.TLSRoute{
+                               ObjectMeta: metav1.ObjectMeta{Name: "my-tls", 
Namespace: "default"},
+                               Spec: gatewayv1.TLSRouteSpec{
+                                       Hostnames: 
[]gatewayv1.Hostname{"api6.com"},
+                                       Rules: []gatewayv1.TLSRouteRule{
+                                               {BackendRefs: 
[]gatewayv1.BackendRef{}},
+                                       },
+                               },
+                       }
+
+                       result, err := translator.TranslateTLSRoute(tctx, route)
+                       require.NoError(t, err)
+                       require.Len(t, result.Services, 1)
+                       streamRoutes := result.Services[0].StreamRoutes
+
+                       if tt.wantNoMatch {
+                               require.Len(t, streamRoutes, 1)
+                               assert.Zero(t, streamRoutes[0].ServerPort)
+                               return
+                       }
+
+                       require.Len(t, streamRoutes, len(tt.wantPorts))
+                       gotPorts := make([]int32, 0, len(streamRoutes))
+                       ids := make(map[string]struct{})
+                       names := make(map[string]struct{})
+                       for _, sr := range streamRoutes {
+                               gotPorts = append(gotPorts, sr.ServerPort)
+                               ids[sr.ID] = struct{}{}
+                               names[sr.Name] = struct{}{}
+                       }
+                       assert.ElementsMatch(t, tt.wantPorts, gotPorts)
+                       assert.Len(t, ids, len(streamRoutes))
+                       assert.Len(t, names, len(streamRoutes))
+               })
+       }
+}
diff --git a/internal/adc/translator/tlsroute.go 
b/internal/adc/translator/tlsroute.go
index 8d1fd0a6..1dc95631 100644
--- a/internal/adc/translator/tlsroute.go
+++ b/internal/adc/translator/tlsroute.go
@@ -144,18 +144,11 @@ func (t *Translator) TranslateTLSRoute(tctx 
*provider.TranslateContext, tlsRoute
                }
 
                for _, host := range hosts {
-                       streamRoute := adctypes.NewDefaultStreamRoute()
-                       streamRouteName := 
adctypes.ComposeStreamRouteName(tlsRoute.Namespace, tlsRoute.Name, 
fmt.Sprintf("%d", ruleIndex), "TLS")
-                       streamRoute.Name = streamRouteName
-                       streamRoute.ID = id.GenID(streamRouteName)
-                       streamRoute.SNI = host
-                       streamRoute.Labels = labels
-                       // Attach L4RoutePolicy plugins at the stream_route 
level: the APISIX stream proxy
-                       // applies plugins from the stream_route, not from the 
service. With multiple SNIs
-                       // each stream_route carries its own copy of the 
plugins.
-                       streamRoute.Plugins = make(adctypes.Plugins)
-                       t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, 
tlsRoute.Namespace, tlsRoute.Name, "TLSRoute", streamRoute.Plugins, 
tctx.Secrets)
-                       service.StreamRoutes = append(service.StreamRoutes, 
streamRoute)
+                       streamRoutes := t.buildL4StreamRoutes(tctx, 
tlsRoute.Namespace, tlsRoute.Name, ruleIndex, "TLS", "TLSRoute", labels)
+                       for _, streamRoute := range streamRoutes {
+                               streamRoute.SNI = host
+                       }
+                       service.StreamRoutes = append(service.StreamRoutes, 
streamRoutes...)
                }
 
                result.Services = append(result.Services, service)
diff --git a/internal/controller/tlsroute_controller.go 
b/internal/controller/tlsroute_controller.go
index 5bbd5a59..779b7603 100644
--- a/internal/controller/tlsroute_controller.go
+++ b/internal/controller/tlsroute_controller.go
@@ -317,6 +317,14 @@ func (r *TLSRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (c
                        acceptStatus.status = false
                        acceptStatus.msg = err.Error()
                }
+               // Populate the matched listeners so the translator can derive 
the
+               // StreamRoute server_port from the listener the route attaches 
to.
+               if len(gateway.Listeners) > 0 {
+                       tctx.Listeners = appendListeners(tctx.Listeners, 
gateway.Listeners...)
+               } else if gateway.Listener != nil {
+                       tctx.Listeners = appendListeners(tctx.Listeners, 
*gateway.Listener)
+               }
+               tctx.HasExplicitListenerMatch = tctx.HasExplicitListenerMatch 
|| gateway.ExplicitListenerMatch
        }
 
        var backendRefErr error
diff --git a/internal/controller/tlsroute_listener_test.go 
b/internal/controller/tlsroute_listener_test.go
new file mode 100644
index 00000000..c6865de0
--- /dev/null
+++ b/internal/controller/tlsroute_listener_test.go
@@ -0,0 +1,116 @@
+// 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"
+       "net/http"
+       "testing"
+
+       "github.com/go-logr/logr"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       k8stypes "k8s.io/apimachinery/pkg/types"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "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/internal/controller/status"
+       "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+type tlsRouteRecordingProvider struct {
+       tctx *provider.TranslateContext
+}
+
+func (p *tlsRouteRecordingProvider) Register(string, *http.ServeMux) {}
+
+func (p *tlsRouteRecordingProvider) Update(_ context.Context, tctx 
*provider.TranslateContext, _ client.Object) error {
+       p.tctx = tctx
+       return nil
+}
+
+func (p *tlsRouteRecordingProvider) Delete(context.Context, client.Object) 
error { return nil }
+
+func (p *tlsRouteRecordingProvider) Start(context.Context) error { return nil }
+
+func (p *tlsRouteRecordingProvider) NeedLeaderElection() bool { return true }
+
+type discardStatusUpdater struct{}
+
+func (discardStatusUpdater) Update(status.Update) {}
+
+func TestTLSRouteReconcilePropagatesExplicitListener(t *testing.T) {
+       scheme := runtime.NewScheme()
+       require.NoError(t, gatewayv1.Install(scheme))
+
+       hostname := gatewayv1.Hostname("api6.com")
+       sectionName := gatewayv1.SectionName("tls-main")
+       port := gatewayv1.PortNumber(9110)
+       gatewayClass := newParentRefGatewayClass()
+       gateway := &gatewayv1.Gateway{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: 
"gateway"},
+               Spec: gatewayv1.GatewaySpec{
+                       GatewayClassName: 
gatewayv1.ObjectName(gatewayClass.Name),
+                       Listeners: []gatewayv1.Listener{{
+                               Name:     sectionName,
+                               Protocol: gatewayv1.TLSProtocolType,
+                               Port:     port,
+                               Hostname: &hostname,
+                       }},
+               },
+       }
+       route := &gatewayv1.TLSRoute{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: 
"route"},
+               Spec: gatewayv1.TLSRouteSpec{
+                       CommonRouteSpec: gatewayv1.CommonRouteSpec{
+                               ParentRefs: []gatewayv1.ParentReference{{
+                                       Name:        
gatewayv1.ObjectName(gateway.Name),
+                                       SectionName: &sectionName,
+                               }},
+                       },
+                       Hostnames: []gatewayv1.Hostname{hostname},
+               },
+       }
+       cli := 
fake.NewClientBuilder().WithScheme(scheme).WithObjects(gatewayClass, gateway, 
route).Build()
+       provider := &tlsRouteRecordingProvider{}
+       readier := readiness.NewReadinessManager(cli, logr.Discard())
+       require.NoError(t, readier.Start(context.Background()))
+       reconciler := &TLSRouteReconciler{
+               Client:   cli,
+               Scheme:   scheme,
+               Log:      logr.Discard(),
+               Provider: provider,
+               Updater:  discardStatusUpdater{},
+               Readier:  readier,
+       }
+
+       _, err := reconciler.Reconcile(context.Background(), ctrl.Request{
+               NamespacedName: k8stypes.NamespacedName{Namespace: 
route.Namespace, Name: route.Name},
+       })
+       require.NoError(t, err)
+       require.NotNil(t, provider.tctx)
+       require.Len(t, provider.tctx.Listeners, 1)
+       assert.Equal(t, sectionName, provider.tctx.Listeners[0].Name)
+       assert.Equal(t, port, provider.tctx.Listeners[0].Port)
+       assert.True(t, provider.tctx.HasExplicitListenerMatch)
+}

Reply via email to