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: §ionName,
+ }},
+ },
+ 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)
+}