This is an automated email from the ASF dual-hosted git repository.
AlinsRan 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 be4aaef5 fix: honor TCPRoute/UDPRoute sectionName and listener port
for StreamRoute matching (#2818)
be4aaef5 is described below
commit be4aaef56d3cf49d621b57cf1aba5afb1d283926
Author: AlinsRan <[email protected]>
AuthorDate: Mon Aug 3 16:30:36 2026 +0800
fix: honor TCPRoute/UDPRoute sectionName and listener port for StreamRoute
matching (#2818)
---
internal/adc/translator/annotations_test.go | 144 ++++----------
internal/adc/translator/grpcroute.go | 2 +-
internal/adc/translator/grpcroute_test.go | 113 +++--------
internal/adc/translator/httproute.go | 2 +-
internal/adc/translator/httproute_test.go | 120 +++---------
internal/adc/translator/l4route_serverport_test.go | 211 +++++++++++++++++++++
internal/adc/translator/tcproute.go | 76 ++++++--
internal/adc/translator/translator.go | 33 ++--
internal/adc/translator/udproute.go | 13 +-
internal/controller/context.go | 8 +
internal/controller/grpcroute_controller.go | 1 +
internal/controller/httproute_controller.go | 1 +
internal/controller/tcproute_controller.go | 8 +
internal/controller/udproute_controller.go | 8 +
internal/controller/utils.go | 14 ++
internal/controller/utils_parentref_test.go | 92 +++++++++
internal/provider/provider.go | 5 +
test/e2e/gatewayapi/tcproute.go | 10 +-
test/e2e/gatewayapi/udproute.go | 4 +-
19 files changed, 538 insertions(+), 327 deletions(-)
diff --git a/internal/adc/translator/annotations_test.go
b/internal/adc/translator/annotations_test.go
index 7b4d1ec4..b61249a6 100644
--- a/internal/adc/translator/annotations_test.go
+++ b/internal/adc/translator/annotations_test.go
@@ -21,8 +21,6 @@ import (
"github.com/incubator4/go-resty-expr/expr"
"github.com/stretchr/testify/assert"
- "k8s.io/utils/ptr"
- gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
@@ -457,135 +455,77 @@ func TestAddServerPortVars(t *testing.T) {
}
func TestShouldInjectServerPortVars(t *testing.T) {
- sectionName := gatewayv1.SectionName("http-main")
- port := gatewayv1.PortNumber(9080)
-
+ // explicit is the provenance-aware signal the controller derives from
the
+ // matched RouteParentRefContext
(TranslateContext.HasExplicitListenerMatch);
+ // here we exercise how each mode consumes it together with the port
count.
tests := []struct {
- name string
- mode config.ListenerPortMatchMode
- parentRefs []gatewayv1.ParentReference
- ports map[int32]struct{}
- expected bool
+ name string
+ mode config.ListenerPortMatchMode
+ explicit bool
+ ports map[int32]struct{}
+ expected bool
}{
{
name: "empty listener ports",
mode: config.ListenerPortMatchModeAuto,
+ explicit: true,
ports: map[int32]struct{}{},
expected: false,
},
{
- name: "single port without sectionName",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
- ports: map[int32]struct{}{
- 9080: {},
- },
+ name: "auto mode: single port without explicit
target",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: false,
+ ports: map[int32]struct{}{9080: {}},
expected: false,
},
{
- name: "single port with sectionName",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
- ports: map[int32]struct{}{
- 9080: {},
- },
+ name: "auto mode: single port with explicit target",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: true,
+ ports: map[int32]struct{}{9080: {}},
expected: true,
},
{
- name: "multiple ports without sectionName",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
- ports: map[int32]struct{}{
- 9080: {},
- 9081: {},
- },
+ name: "auto mode: multiple ports without explicit
target",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: false,
+ ports: map[int32]struct{}{9080: {}, 9081: {}},
expected: true,
},
{
- name: "explicit mode with multiple ports and no
explicit target",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
- ports: map[int32]struct{}{
- 9080: {},
- 9081: {},
- },
+ name: "explicit mode: multiple ports without
explicit target",
+ mode: config.ListenerPortMatchModeExplicit,
+ explicit: false,
+ ports: map[int32]struct{}{9080: {}, 9081: {}},
expected: false,
},
{
- name: "explicit mode with parentRef.port",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &port},
- },
- ports: map[int32]struct{}{
- 9080: {},
- },
+ name: "explicit mode: single port with explicit
target",
+ mode: config.ListenerPortMatchModeExplicit,
+ explicit: true,
+ ports: map[int32]struct{}{9080: {}},
expected: true,
},
{
- name: "explicit mode with single port and no explicit
target",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
- ports: map[int32]struct{}{
- 9080: {},
- },
+ name: "explicit mode: single port without explicit
target",
+ mode: config.ListenerPortMatchModeExplicit,
+ explicit: false,
+ ports: map[int32]struct{}{9080: {}},
expected: false,
},
{
- name: "off mode ignores explicit target",
- mode: config.ListenerPortMatchModeOff,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
- ports: map[int32]struct{}{
- 9080: {},
- 9081: {},
- },
+ name: "off mode: ignores explicit target with
multiple ports",
+ mode: config.ListenerPortMatchModeOff,
+ explicit: true,
+ ports: map[int32]struct{}{9080: {}, 9081: {}},
expected: false,
},
{
- name: "off mode ignores explicit parentRef.port target",
- mode: config.ListenerPortMatchModeOff,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &port},
- },
- ports: map[int32]struct{}{
- 9080: {},
- },
- expected: false,
- },
- {
- name: "explicit mode: non-Gateway parentRef with port
is not treated as explicit target",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- {Name: "svc", Kind:
ptr.To(gatewayv1.Kind("Service")), Port: &port},
- },
- ports: map[int32]struct{}{
- 9080: {},
- },
- expected: false,
- },
- {
- name: "auto mode: non-Gateway parentRef with port does
not trigger single-port injection",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- {Name: "svc", Kind:
ptr.To(gatewayv1.Kind("Service")), Port: &port},
- },
- ports: map[int32]struct{}{
- 9080: {},
- },
+ name: "off mode: ignores explicit target with
single port",
+ mode: config.ListenerPortMatchModeOff,
+ explicit: true,
+ ports: map[int32]struct{}{9080: {}},
expected: false,
},
}
@@ -593,7 +533,7 @@ func TestShouldInjectServerPortVars(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
translator := &Translator{ListenerPortMatchMode:
tt.mode}
- assert.Equal(t, tt.expected,
translator.shouldInjectServerPortVars(tt.parentRefs, tt.ports))
+ assert.Equal(t, tt.expected,
translator.shouldInjectServerPortVars(tt.explicit, tt.ports))
})
}
}
diff --git a/internal/adc/translator/grpcroute.go
b/internal/adc/translator/grpcroute.go
index 646cfb78..7a65c520 100644
--- a/internal/adc/translator/grpcroute.go
+++ b/internal/adc/translator/grpcroute.go
@@ -315,7 +315,7 @@ func (t *Translator) TranslateGRPCRoute(tctx
*provider.TranslateContext, grpcRou
// and a hostname listener is not dropped on the hostname port.
listenerPorts := collectServerPortMatchPorts(tctx.Listeners)
- if t.shouldInjectServerPortVars(tctx.RouteParentRefs,
listenerPorts) {
+ if t.shouldInjectServerPortVars(tctx.HasExplicitListenerMatch,
listenerPorts) {
matchPorts := allListenerPorts(tctx.Listeners)
for _, route := range routes {
addServerPortVars(route, matchPorts)
diff --git a/internal/adc/translator/grpcroute_test.go
b/internal/adc/translator/grpcroute_test.go
index 55df7b24..f02e75a8 100644
--- a/internal/adc/translator/grpcroute_test.go
+++ b/internal/adc/translator/grpcroute_test.go
@@ -31,9 +31,6 @@ import (
)
func TestTranslateGRPCRouteServerPortVarsByMode(t *testing.T) {
- sectionName := gatewayv1.SectionName("grpc-main")
- parentPort := gatewayv1.PortNumber(9080)
-
singlePortVars := adctypes.Vars{
{
{StrVal: "server_port"},
@@ -53,40 +50,27 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
}
tests := []struct {
- name string
- mode config.ListenerPortMatchMode
- parentRefs []gatewayv1.ParentReference
- listeners []gatewayv1.Listener
- expected adctypes.Vars
+ name string
+ mode config.ListenerPortMatchMode
+ // explicit is the provenance-aware signal the controller
stores on tctx
+ // (HasExplicitListenerMatch); the cross-Gateway
sectionName/port
+ // resolution that computes it is covered by the controller
tests.
+ explicit bool
+ listeners []gatewayv1.Listener
+ expected adctypes.Vars
}{
{
name: "auto mode: no injection for single listener
without explicit target",
mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
expected: nil,
},
{
- name: "auto mode: inject for sectionName target",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
- listeners: []gatewayv1.Listener{
- {Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
- },
- expected: singlePortVars,
- },
- {
- name: "auto mode: inject for port target",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &parentPort},
- },
+ name: "auto mode: inject for explicit sectionName
target",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -95,9 +79,6 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t *testing.T)
{
{
name: "auto mode: inject for multiple listener ports",
mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
{Name: "grpc-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -105,35 +86,9 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
expected: multiPortVars,
},
{
- name: "auto mode: inject for multiple listener ports
when listener names collide across gateways",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw-a"},
- {Name: "gw-b"},
- },
- listeners: []gatewayv1.Listener{
- {Name: "http", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
- {Name: "http", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
- },
- expected: multiPortVars,
- },
- {
- name: "explicit mode: inject for sectionName target",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
- listeners: []gatewayv1.Listener{
- {Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
- },
- expected: singlePortVars,
- },
- {
- name: "explicit mode: inject for port target",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &parentPort},
- },
+ name: "explicit mode: inject for explicit target",
+ mode: config.ListenerPortMatchModeExplicit,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -142,9 +97,6 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
{
name: "explicit mode: no injection for multiple
listener ports without explicit target",
mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
{Name: "grpc-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -152,11 +104,9 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
expected: nil,
},
{
- name: "off mode: no injection even with sectionName
target",
- mode: config.ListenerPortMatchModeOff,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
+ name: "off mode: no injection even with explicit
target",
+ mode: config.ListenerPortMatchModeOff,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -165,9 +115,6 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
{
name: "off mode: no injection for multiple listener
ports",
mode: config.ListenerPortMatchModeOff,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
{Name: "grpc-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -177,11 +124,9 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
{
// An unset mode must not start injecting predicates
behind the
// operator's back, so it resolves to off rather than
to auto.
- name: "empty mode normalizes to off",
- mode: "",
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &parentPort},
- },
+ name: "empty mode normalizes to off",
+ mode: "",
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -191,11 +136,9 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
// Same-port listeners differing by hostname are
isolated by host, not by
// port, so no server_port var must be emitted (it
would pin the route to
// the Gateway port and drop every request to 404).
- name: "auto mode: no injection for same-port listeners
differing by hostname",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
+ name: "auto mode: no injection for same-port
listeners differing by hostname",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname:
ptr.To(gatewayv1.Hostname("bar.com"))},
{Name: "grpc-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname:
ptr.To(gatewayv1.Hostname("foo.bar.com"))},
@@ -207,11 +150,9 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
// emitted it must cover every targeted port -
including the hostname
// listener's - or traffic arriving through the
hostname listener is
// silently dropped. So the predicate lists both ports,
not just 9080.
- name: "explicit mode: mixed hostname and hostname-less
listeners keep every targeted port",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
+ name: "explicit mode: mixed hostname and
hostname-less listeners keep every targeted port",
+ mode: config.ListenerPortMatchModeExplicit,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "grpc-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname:
ptr.To(gatewayv1.Hostname("bar.com"))},
{Name: "grpc-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -232,7 +173,7 @@ func TestTranslateGRPCRouteServerPortVarsByMode(t
*testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tctx :=
provider.NewDefaultTranslateContext(context.Background())
- tctx.RouteParentRefs = tt.parentRefs
+ tctx.HasExplicitListenerMatch = tt.explicit
tctx.Listeners = tt.listeners
grpcRoute := &gatewayv1.GRPCRoute{
diff --git a/internal/adc/translator/httproute.go
b/internal/adc/translator/httproute.go
index f8fc66ba..f129d799 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -748,7 +748,7 @@ func (t *Translator) TranslateHTTPRoute(tctx
*provider.TranslateContext, httpRou
// or when multiple listener ports need to be disambiguated.
When it is added,
// match on every targeted listener port so a route attached to
both a
// hostname-less and a hostname listener is not dropped on the
hostname port.
- if t.shouldInjectServerPortVars(tctx.RouteParentRefs,
listenerPorts) {
+ if t.shouldInjectServerPortVars(tctx.HasExplicitListenerMatch,
listenerPorts) {
matchPorts := allListenerPorts(tctx.Listeners)
for _, route := range routes {
addServerPortVars(route, matchPorts)
diff --git a/internal/adc/translator/httproute_test.go
b/internal/adc/translator/httproute_test.go
index 727334b4..5b985f96 100644
--- a/internal/adc/translator/httproute_test.go
+++ b/internal/adc/translator/httproute_test.go
@@ -41,8 +41,6 @@ import (
)
func TestTranslateHTTPRouteServerPortVarsByMode(t *testing.T) {
- sectionName := gatewayv1.SectionName("http-main")
- parentPort := gatewayv1.PortNumber(9080)
pathMatchType := gatewayv1.PathMatchPathPrefix
pathValue := "/"
@@ -65,40 +63,27 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
}
tests := []struct {
- name string
- mode config.ListenerPortMatchMode
- parentRefs []gatewayv1.ParentReference
- listeners []gatewayv1.Listener
- expected adctypes.Vars
+ name string
+ mode config.ListenerPortMatchMode
+ // explicit is the provenance-aware signal the controller
stores on tctx
+ // (HasExplicitListenerMatch); the cross-Gateway
sectionName/port
+ // resolution that computes it is covered by the controller
tests.
+ explicit bool
+ listeners []gatewayv1.Listener
+ expected adctypes.Vars
}{
{
name: "auto mode: no injection for single listener
without explicit target",
mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
expected: nil,
},
{
- name: "auto mode: inject for sectionName target",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
- listeners: []gatewayv1.Listener{
- {Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
- },
- expected: singlePortVars,
- },
- {
- name: "auto mode: inject for port target",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &parentPort},
- },
+ name: "auto mode: inject for explicit sectionName
target",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -107,9 +92,6 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
{
name: "auto mode: inject for multiple listener ports",
mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
{Name: "http-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -117,35 +99,9 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
expected: multiPortVars,
},
{
- name: "auto mode: inject for multiple listener ports
when listener names collide across gateways",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw-a"},
- {Name: "gw-b"},
- },
- listeners: []gatewayv1.Listener{
- {Name: "http", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
- {Name: "http", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
- },
- expected: multiPortVars,
- },
- {
- name: "explicit mode: inject for sectionName target",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
- listeners: []gatewayv1.Listener{
- {Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
- },
- expected: singlePortVars,
- },
- {
- name: "explicit mode: inject for port target",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &parentPort},
- },
+ name: "explicit mode: inject for explicit target",
+ mode: config.ListenerPortMatchModeExplicit,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -154,9 +110,6 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
{
name: "explicit mode: no injection for multiple
listener ports without explicit target",
mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
{Name: "http-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -164,11 +117,9 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
expected: nil,
},
{
- name: "off mode: no injection even with sectionName
target",
- mode: config.ListenerPortMatchModeOff,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
+ name: "off mode: no injection even with explicit
target",
+ mode: config.ListenerPortMatchModeOff,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -177,9 +128,6 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
{
name: "off mode: no injection for multiple listener
ports",
mode: config.ListenerPortMatchModeOff,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw"},
- },
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9081)},
{Name: "http-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -189,11 +137,9 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
{
// An unset mode must not start injecting predicates
behind the
// operator's back, so it resolves to off rather than
to auto.
- name: "empty mode normalizes to off",
- mode: "",
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", Port: &parentPort},
- },
+ name: "empty mode normalizes to off",
+ mode: "",
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
},
@@ -205,22 +151,18 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
// var here adds no isolation and, worse, pins the
route to the Gateway's
// declared port which need not equal APISIX's actual
node_listen port,
// dropping every request to 404. So no server_port var
must be emitted.
- name: "auto mode: no injection when sectionName target
listener has hostname",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
+ name: "auto mode: no injection when explicit target
listener has hostname",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname:
ptr.To(gatewayv1.Hostname("bar.com"))},
},
expected: nil,
},
{
- name: "auto mode: no injection for same-port listeners
differing by hostname",
- mode: config.ListenerPortMatchModeAuto,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
+ name: "auto mode: no injection for same-port
listeners differing by hostname",
+ mode: config.ListenerPortMatchModeAuto,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname:
ptr.To(gatewayv1.Hostname("bar.com"))},
{Name: "http-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname:
ptr.To(gatewayv1.Hostname("foo.bar.com"))},
@@ -232,11 +174,9 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
// emitted it must cover every targeted port -
including the hostname
// listener's - or traffic arriving through the
hostname listener is
// silently dropped. So the predicate lists both ports,
not just 9080.
- name: "explicit mode: mixed hostname and hostname-less
listeners keep every targeted port",
- mode: config.ListenerPortMatchModeExplicit,
- parentRefs: []gatewayv1.ParentReference{
- {Name: "gw", SectionName: §ionName},
- },
+ name: "explicit mode: mixed hostname and
hostname-less listeners keep every targeted port",
+ mode: config.ListenerPortMatchModeExplicit,
+ explicit: true,
listeners: []gatewayv1.Listener{
{Name: "http-main", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(80), Hostname:
ptr.To(gatewayv1.Hostname("bar.com"))},
{Name: "http-alt", Protocol:
gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)},
@@ -257,7 +197,7 @@ func TestTranslateHTTPRouteServerPortVarsByMode(t
*testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tctx :=
provider.NewDefaultTranslateContext(context.Background())
- tctx.RouteParentRefs = tt.parentRefs
+ tctx.HasExplicitListenerMatch = tt.explicit
tctx.Listeners = tt.listeners
httpRoute := &gatewayv1.HTTPRoute{
diff --git a/internal/adc/translator/l4route_serverport_test.go
b/internal/adc/translator/l4route_serverport_test.go
new file mode 100644
index 00000000..778d2f87
--- /dev/null
+++ b/internal/adc/translator/l4route_serverport_test.go
@@ -0,0 +1,211 @@
+// 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"
+ "testing"
+
+ "github.com/go-logr/logr"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/apache/apisix-ingress-controller/internal/controller/config"
+ "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+func tcpListener(name string, port int32) gatewayv1.Listener {
+ return gatewayv1.Listener{
+ Name: gatewayv1.SectionName(name),
+ Protocol: gatewayv1.TCPProtocolType,
+ Port: port,
+ }
+}
+
+func udpListener(name string, port int32) gatewayv1.Listener {
+ return gatewayv1.Listener{
+ Name: gatewayv1.SectionName(name),
+ Protocol: gatewayv1.UDPProtocolType,
+ Port: port,
+ }
+}
+
+func TestTranslateTCPRouteServerPort(t *testing.T) {
+ tests := []struct {
+ name string
+ // listeners the controller would have matched for this route's
parentRefs
+ listeners []gatewayv1.Listener
+ // explicit is the provenance-aware signal the controller
stores on tctx
+ // (HasExplicitListenerMatch) when the route targeted a
listener by an
+ // explicit sectionName or port.
+ explicit bool
+ wantPorts []int32
+ wantNoMatch bool
+ }{
+ {
+ name: "explicit sectionName injects the matching
listener port",
+ listeners: []gatewayv1.Listener{tcpListener("tcp-a",
9100)},
+ explicit: true,
+ wantPorts: []int32{9100},
+ },
+ {
+ name: "multiple listener ports fan out even
without explicit targeting",
+ listeners: []gatewayv1.Listener{tcpListener("tcp-a",
9100), tcpListener("tcp-b", 9101)},
+ wantPorts: []int32{9100, 9101},
+ },
+ {
+ name: "duplicate ports across gateways are
de-duplicated",
+ listeners: []gatewayv1.Listener{tcpListener("tcp-a",
9100), tcpListener("tcp-a2", 9100)},
+ explicit: true,
+ wantPorts: []int32{9100},
+ },
+ {
+ name: "single listener without explicit
targeting keeps a portless StreamRoute",
+ listeners: []gatewayv1.Listener{tcpListener("tcp-a",
9100)},
+ wantNoMatch: true,
+ },
+ {
+ name: "no matched listener falls back to a
single portless StreamRoute",
+ listeners: nil,
+ explicit: true,
+ wantNoMatch: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // listener_port_match_mode defaults to off; these
cases assert the
+ // injection behavior, so exercise the translator in
auto mode.
+ translator := NewTranslator(logr.Discard(),
config.ListenerPortMatchModeAuto)
+ tctx :=
provider.NewDefaultTranslateContext(context.Background())
+ tctx.Listeners = tt.listeners
+ tctx.HasExplicitListenerMatch = tt.explicit
+
+ route := &gatewayv1alpha2.TCPRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "my-tcp",
Namespace: "default"},
+ Spec: gatewayv1alpha2.TCPRouteSpec{
+ Rules: []gatewayv1alpha2.TCPRouteRule{
+ {BackendRefs:
[]gatewayv1alpha2.BackendRef{}},
+ },
+ },
+ }
+
+ result, err := translator.TranslateTCPRoute(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)
+ // Distinct name/ID per listener port so StreamRoutes
do not collide.
+ assert.Len(t, ids, len(streamRoutes))
+ assert.Len(t, names, len(streamRoutes))
+ })
+ }
+}
+
+func TestTranslateUDPRouteServerPort(t *testing.T) {
+ tests := []struct {
+ name string
+ listeners []gatewayv1.Listener
+ explicit bool
+ wantPorts []int32
+ wantNoMatch bool
+ }{
+ {
+ name: "explicit sectionName injects the matching
listener port",
+ listeners: []gatewayv1.Listener{udpListener("udp-a",
9200)},
+ explicit: true,
+ wantPorts: []int32{9200},
+ },
+ {
+ name: "two listeners on different ports produce
distinct StreamRoutes",
+ listeners: []gatewayv1.Listener{udpListener("udp-a",
9200), udpListener("udp-b", 9201)},
+ wantPorts: []int32{9200, 9201},
+ },
+ {
+ name: "single listener without explicit
targeting keeps a portless StreamRoute",
+ listeners: []gatewayv1.Listener{udpListener("udp-a",
9200)},
+ wantNoMatch: true,
+ },
+ {
+ name: "no matched listener falls back to a
single portless StreamRoute",
+ listeners: nil,
+ explicit: true,
+ wantNoMatch: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // listener_port_match_mode defaults to off; these
cases assert the
+ // injection behavior, so exercise the translator in
auto mode.
+ translator := NewTranslator(logr.Discard(),
config.ListenerPortMatchModeAuto)
+ tctx :=
provider.NewDefaultTranslateContext(context.Background())
+ tctx.Listeners = tt.listeners
+ tctx.HasExplicitListenerMatch = tt.explicit
+
+ route := &gatewayv1alpha2.UDPRoute{
+ ObjectMeta: metav1.ObjectMeta{Name: "my-udp",
Namespace: "default"},
+ Spec: gatewayv1alpha2.UDPRouteSpec{
+ Rules: []gatewayv1alpha2.UDPRouteRule{
+ {BackendRefs:
[]gatewayv1alpha2.BackendRef{}},
+ },
+ },
+ }
+
+ result, err := translator.TranslateUDPRoute(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{})
+ for _, sr := range streamRoutes {
+ gotPorts = append(gotPorts, sr.ServerPort)
+ ids[sr.ID] = struct{}{}
+ }
+ assert.ElementsMatch(t, tt.wantPorts, gotPorts)
+ assert.Len(t, ids, len(streamRoutes))
+ })
+ }
+}
diff --git a/internal/adc/translator/tcproute.go
b/internal/adc/translator/tcproute.go
index 7327e911..11eaa197 100644
--- a/internal/adc/translator/tcproute.go
+++ b/internal/adc/translator/tcproute.go
@@ -19,6 +19,7 @@ package translator
import (
"fmt"
+ "sort"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
@@ -42,6 +43,68 @@ func newDefaultUpstreamWithoutScheme() *adctypes.Upstream {
}
}
+// listenerPortSet returns the de-duplicated set of ports of the listeners the
+// route attaches to. tctx.Listeners is populated by the controller from the
+// listeners that ParseRouteParentRefs already matched against the route's
+// parentRefs (honoring sectionName, port, protocol and allowedRoutes).
+func listenerPortSet(tctx *provider.TranslateContext) map[int32]struct{} {
+ portSet := make(map[int32]struct{}, len(tctx.Listeners))
+ for _, listener := range tctx.Listeners {
+ portSet[listener.Port] = struct{}{}
+ }
+ return portSet
+}
+
+// buildL4StreamRoutes builds the StreamRoutes for one L4 route rule.
+//
+// A StreamRoute without a server_port match matches every connection on any
+// stream listener, so multiple L4 routes collide onto one backend (#2802). To
+// isolate them we set server_port from the matched listener port(s), emitting
one
+// StreamRoute per port.
+//
+// Whether to inject server_port is gated by shouldInjectServerPortVars (the
same
+// listener_port_match_mode used by HTTPRoute/GRPCRoute): the listener port is
a
+// logical Gateway value that must equal APISIX's physical stream listen port
for
+// 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 {
+ var ports []int32
+ if portSet := listenerPortSet(tctx);
t.shouldInjectServerPortVars(tctx.HasExplicitListenerMatch, portSet) {
+ ports = make([]int32, 0, len(portSet))
+ for port := range portSet {
+ ports = append(ports, port)
+ }
+ sort.Slice(ports, func(i, j int) bool { return ports[i] <
ports[j] })
+ }
+ if len(ports) == 0 {
+ // No server_port isolation: a single StreamRoute that matches
all
+ // connections on the stream listener, as before.
+ ports = []int32{0}
+ }
+ streamRoutes := make([]*adctypes.StreamRoute, 0, len(ports))
+ for _, port := range ports {
+ streamRoute := adctypes.NewDefaultStreamRoute()
+ ruleKey := fmt.Sprintf("%d", ruleIndex)
+ if port != 0 {
+ // Include the port in the name key so multiple
listeners produce
+ // distinct StreamRoute names/IDs instead of colliding.
+ ruleKey = fmt.Sprintf("%d-%d", ruleIndex, port)
+ streamRoute.ServerPort = port
+ }
+ streamRouteName := adctypes.ComposeStreamRouteName(namespace,
name, ruleKey, typ)
+ streamRoute.Name = streamRouteName
+ streamRoute.ID = id.GenID(streamRouteName)
+ 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.
+ streamRoute.Plugins = make(adctypes.Plugins)
+ t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, namespace,
name, routeKind, streamRoute.Plugins)
+ streamRoutes = append(streamRoutes, streamRoute)
+ }
+ return streamRoutes
+}
+
func (t *Translator) TranslateTCPRoute(tctx *provider.TranslateContext,
tcpRoute *gatewayv1alpha2.TCPRoute) (*TranslateResult, error) {
result := &TranslateResult{}
rules := tcpRoute.Spec.Rules
@@ -149,17 +212,8 @@ func (t *Translator) TranslateTCPRoute(tctx
*provider.TranslateContext, tcpRoute
}
}
}
- streamRoute := adctypes.NewDefaultStreamRoute()
- streamRouteName :=
adctypes.ComposeStreamRouteName(tcpRoute.Namespace, tcpRoute.Name,
fmt.Sprintf("%d", ruleIndex), "TCP")
- streamRoute.Name = streamRouteName
- streamRoute.ID = id.GenID(streamRouteName)
- streamRoute.Labels = labels
- // TODO: support remote_addr, server_addr, sni, server_port
- // 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,
tcpRoute.Namespace, tcpRoute.Name, "TCPRoute", streamRoute.Plugins)
- service.StreamRoutes = append(service.StreamRoutes, streamRoute)
+ // TODO: support remote_addr, server_addr, sni
+ service.StreamRoutes = t.buildL4StreamRoutes(tctx,
tcpRoute.Namespace, tcpRoute.Name, ruleIndex, "TCP", "TCPRoute", labels)
result.Services = append(result.Services, service)
}
diff --git a/internal/adc/translator/translator.go
b/internal/adc/translator/translator.go
index e0d000f1..ac14ee9e 100644
--- a/internal/adc/translator/translator.go
+++ b/internal/adc/translator/translator.go
@@ -50,24 +50,6 @@ func NewTranslator(log logr.Logger, mode
config.ListenerPortMatchMode) *Translat
}
}
-func hasExplicitListenerTarget(parentRefs []gatewayv1.ParentReference) bool {
- for _, parentRef := range parentRefs {
- // Skip non-Gateway parentRefs (e.g. GAMMA Service mesh refs) —
they
- // are not relevant to listener port injection.
- if parentRef.Kind != nil && *parentRef.Kind != "Gateway" {
- continue
- }
- if parentRef.SectionName != nil && *parentRef.SectionName != ""
{
- return true
- }
- if parentRef.Port != nil {
- return true
- }
- }
-
- return false
-}
-
// collectServerPortMatchPorts returns the hostname-less listener ports, which
is
// the set used to decide whether a server_port var is needed at all.
//
@@ -103,17 +85,24 @@ func allListenerPorts(listeners []gatewayv1.Listener)
map[int32]struct{} {
return ports
}
-func (t *Translator) shouldInjectServerPortVars(parentRefs
[]gatewayv1.ParentReference, ports map[int32]struct{}) bool {
+// shouldInjectServerPortVars decides whether to pin the route to the matched
+// listener port(s) via a server_port predicate.
+//
+// explicit reports whether the route attached to its Gateway through an
explicit
+// sectionName or port. It is computed by the controller from the matched
+// RouteParentRefContext (provider.TranslateContext.HasExplicitListenerMatch),
+// where each parentRef's Gateway and matched listeners are known, so an
invalid
+// explicit ref on one Gateway can never be satisfied by a same-named/ported
+// listener matched through a different parentRef's Gateway.
+func (t *Translator) shouldInjectServerPortVars(explicit bool, ports
map[int32]struct{}) bool {
if len(ports) == 0 {
return false
}
- explicit := hasExplicitListenerTarget(parentRefs)
-
switch t.ListenerPortMatchMode {
case config.ListenerPortMatchModeOff:
if explicit {
- t.Log.V(1).Info("listener_port_match_mode is 'off';
ignoring explicit listener targeting", "parent_refs", len(parentRefs))
+ t.Log.V(1).Info("listener_port_match_mode is 'off';
ignoring explicit listener targeting")
}
return false
case config.ListenerPortMatchModeExplicit:
diff --git a/internal/adc/translator/udproute.go
b/internal/adc/translator/udproute.go
index cc7b6361..3fdbd817 100644
--- a/internal/adc/translator/udproute.go
+++ b/internal/adc/translator/udproute.go
@@ -139,17 +139,8 @@ func (t *Translator) TranslateUDPRoute(tctx
*provider.TranslateContext, udpRoute
}
}
}
- streamRoute := adctypes.NewDefaultStreamRoute()
- streamRouteName :=
adctypes.ComposeStreamRouteName(udpRoute.Namespace, udpRoute.Name,
fmt.Sprintf("%d", ruleIndex), "UDP")
- streamRoute.Name = streamRouteName
- streamRoute.ID = id.GenID(streamRouteName)
- streamRoute.Labels = labels
- // TODO: support remote_addr, server_addr, sni, server_port
- // 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,
udpRoute.Namespace, udpRoute.Name, "UDPRoute", streamRoute.Plugins)
- service.StreamRoutes = append(service.StreamRoutes, streamRoute)
+ // TODO: support remote_addr, server_addr, sni
+ service.StreamRoutes = t.buildL4StreamRoutes(tctx,
udpRoute.Namespace, udpRoute.Name, ruleIndex, "UDP", "UDPRoute", labels)
result.Services = append(result.Services, service)
}
diff --git a/internal/controller/context.go b/internal/controller/context.go
index 686dd046..25c318bf 100644
--- a/internal/controller/context.go
+++ b/internal/controller/context.go
@@ -29,5 +29,13 @@ type RouteParentRefContext struct {
Listener *gatewayv1.Listener
Listeners []gatewayv1.Listener
+ // ExplicitListenerMatch reports whether this parentRef attached to its
+ // Gateway through an explicit sectionName or port that resolved to one
of
+ // the listeners above. It is computed here, where each parentRef's
Gateway
+ // and matched listeners are known, so the server_port injection
decision
+ // never matches a sectionName/port against a listener from a different
+ // parentRef's Gateway.
+ ExplicitListenerMatch bool
+
Conditions []metav1.Condition
}
diff --git a/internal/controller/grpcroute_controller.go
b/internal/controller/grpcroute_controller.go
index cab5e3ce..31a0b535 100644
--- a/internal/controller/grpcroute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -208,6 +208,7 @@ func (r *GRPCRouteReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
// Fallback for backward compatibility
tctx.Listeners = appendListeners(tctx.Listeners,
*gateway.Listener)
}
+ tctx.HasExplicitListenerMatch = tctx.HasExplicitListenerMatch
|| gateway.ExplicitListenerMatch
}
var backendRefErr error
diff --git a/internal/controller/httproute_controller.go
b/internal/controller/httproute_controller.go
index 22cccf04..41a8a116 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/httproute_controller.go
@@ -190,6 +190,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
// Fallback for backward compatibility
tctx.Listeners = appendListeners(tctx.Listeners,
*gateway.Listener)
}
+ tctx.HasExplicitListenerMatch = tctx.HasExplicitListenerMatch
|| gateway.ExplicitListenerMatch
}
var backendRefErr error
diff --git a/internal/controller/tcproute_controller.go
b/internal/controller/tcproute_controller.go
index a2360157..b623d715 100644
--- a/internal/controller/tcproute_controller.go
+++ b/internal/controller/tcproute_controller.go
@@ -293,6 +293,14 @@ func (r *TCPRouteReconciler) 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/udproute_controller.go
b/internal/controller/udproute_controller.go
index 17fb5f6a..c2ba1ad1 100644
--- a/internal/controller/udproute_controller.go
+++ b/internal/controller/udproute_controller.go
@@ -293,6 +293,14 @@ func (r *UDPRouteReconciler) 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/utils.go b/internal/controller/utils.go
index 8463113d..6c89ef4a 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -316,6 +316,16 @@ func SetRouteParentRef(routeParentStatus
*gatewayv1.RouteParentStatus, gatewayNa
routeParentStatus.ControllerName =
gatewayv1.GatewayController(config.ControllerConfig.ControllerName)
}
+// parentRefTargetsListenerExplicitly reports whether a parentRef names a
+// specific listener, via a non-empty sectionName or an explicit port. It is
only
+// meaningful for a parentRef that already matched a listener on its Gateway.
+func parentRefTargetsListenerExplicitly(parentRef gatewayv1.ParentReference)
bool {
+ if parentRef.SectionName != nil && *parentRef.SectionName != "" {
+ return true
+ }
+ return parentRef.Port != nil
+}
+
func ParseRouteParentRefs(
ctx context.Context,
mgrc client.Client,
@@ -464,6 +474,10 @@ func ParseRouteParentRefs(
ListenerName: listenerName,
Listener: &matchedListener,
Listeners: matchedListeners,
+ // The parentRef explicitly targeted a listener
only when an
+ // explicit sectionName or port was given and
it resolved to a
+ // matched listener on this Gateway (we are in
the matched branch).
+ ExplicitListenerMatch:
parentRefTargetsListenerExplicitly(parentRef),
Conditions: []metav1.Condition{{
Type:
string(gatewayv1.RouteConditionAccepted),
Status:
metav1.ConditionTrue,
diff --git a/internal/controller/utils_parentref_test.go
b/internal/controller/utils_parentref_test.go
index 3c915c58..9a66b49f 100644
--- a/internal/controller/utils_parentref_test.go
+++ b/internal/controller/utils_parentref_test.go
@@ -107,6 +107,98 @@ func TestParseRouteParentRefs_ReasonOrderIndependent(t
*testing.T) {
}
}
+// TestParseRouteParentRefs_ExplicitListenerMatch verifies
ExplicitListenerMatch
+// is derived per parentRef against its own Gateway. In particular a
sectionName
+// or port that does not resolve on the referenced Gateway must not be treated
as
+// an explicit match just because another Gateway, matched through a different
+// parentRef, happens to have an identically named or ported listener.
+func TestParseRouteParentRefs_ExplicitListenerMatch(t *testing.T) {
+ scheme := parentRefTestScheme(t)
+
+ // gwA only has a listener named "other"; a sectionName "web" reference
to it
+ // resolves to nothing.
+ gwA := &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name:
"gw-a"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName: "apisix",
+ Listeners: []gatewayv1.Listener{
+ {Name: "other", Port: 80, Protocol:
gatewayv1.HTTPProtocolType},
+ },
+ },
+ }
+ // gwB has a listener named "web" on port 8080 that an implicit
reference matches.
+ gwB := &gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name:
"gw-b"},
+ Spec: gatewayv1.GatewaySpec{
+ GatewayClassName: "apisix",
+ Listeners: []gatewayv1.Listener{
+ {Name: "web", Port: 8080, Protocol:
gatewayv1.HTTPProtocolType},
+ },
+ },
+ }
+ route := &gatewayv1.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "r"},
+ }
+ cli := fake.NewClientBuilder().WithScheme(scheme).
+ WithObjects(newParentRefGatewayClass(), gwA, gwB).Build()
+
+ webSection := gatewayv1.SectionName("web")
+ port8080 := gatewayv1.PortNumber(8080)
+
+ t.Run("invalid explicit ref is not satisfied by another gateway's
listener", func(t *testing.T) {
+ got, err := ParseRouteParentRefs(context.Background(), cli,
logr.Discard(), route,
+ []gatewayv1.ParentReference{
+ // Explicit sectionName "web" on gw-a, which
has no such listener.
+ {Name: "gw-a", SectionName: &webSection},
+ // Implicit ref to gw-b, which does have a
"web" listener.
+ {Name: "gw-b"},
+ })
+ require.NoError(t, err)
+ require.Len(t, got, 2)
+
+ byGateway := map[string]RouteParentRefContext{}
+ for _, ctx := range got {
+ byGateway[ctx.Gateway.Name] = ctx
+ }
+ // gw-a's explicit ref matched no listener, so it is neither
Accepted nor
+ // an explicit match.
+ assert.Nil(t, byGateway["gw-a"].Listener)
+ assert.False(t, byGateway["gw-a"].ExplicitListenerMatch)
+ // gw-b matched implicitly; the stray "web" sectionName on gw-a
must not
+ // promote it to an explicit match.
+ assert.NotNil(t, byGateway["gw-b"].Listener)
+ assert.False(t, byGateway["gw-b"].ExplicitListenerMatch,
+ "an implicit match must not be marked explicit by
another gateway's ref")
+ })
+
+ t.Run("explicit sectionName on the owning gateway is an explicit
match", func(t *testing.T) {
+ got, err := ParseRouteParentRefs(context.Background(), cli,
logr.Discard(), route,
+ []gatewayv1.ParentReference{{Name: "gw-b", SectionName:
&webSection}})
+ require.NoError(t, err)
+ require.Len(t, got, 1)
+ assert.NotNil(t, got[0].Listener)
+ assert.True(t, got[0].ExplicitListenerMatch)
+ })
+
+ t.Run("explicit port on the owning gateway is an explicit match",
func(t *testing.T) {
+ got, err := ParseRouteParentRefs(context.Background(), cli,
logr.Discard(), route,
+ []gatewayv1.ParentReference{{Name: "gw-b", Port:
&port8080}})
+ require.NoError(t, err)
+ require.Len(t, got, 1)
+ assert.NotNil(t, got[0].Listener)
+ assert.True(t, got[0].ExplicitListenerMatch)
+ })
+
+ t.Run("implicit ref is not an explicit match", func(t *testing.T) {
+ got, err := ParseRouteParentRefs(context.Background(), cli,
logr.Discard(), route,
+ []gatewayv1.ParentReference{{Name: "gw-b"}})
+ require.NoError(t, err)
+ require.Len(t, got, 1)
+ assert.NotNil(t, got[0].Listener)
+ assert.False(t, got[0].ExplicitListenerMatch)
+ })
+}
+
// TestParseRouteParentRefs_ConflictingTLSModePort verifies a route does not
// attach to a listener whose port carries a conflicting tls.mode: such a
listener
// is not programmable, so the route must not be Accepted.
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index f1807654..858c95bc 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -47,6 +47,11 @@ type TranslateContext struct {
GatewayTLSConfig []gatewayv1.ListenerTLSConfig
Credentials []v1alpha1.Credential
Listeners []gatewayv1.Listener
+ // HasExplicitListenerMatch is true when any accepted parentRef
attached to
+ // its Gateway through an explicit sectionName or port. It gates
server_port
+ // injection (see Translator.shouldInjectServerPortVars) and is
computed from
+ // the matched RouteParentRefContext, preserving each parentRef's
Gateway.
+ HasExplicitListenerMatch bool
EndpointSlices
map[k8stypes.NamespacedName][]discoveryv1.EndpointSlice
Secrets map[k8stypes.NamespacedName]*corev1.Secret
diff --git a/test/e2e/gatewayapi/tcproute.go b/test/e2e/gatewayapi/tcproute.go
index a85e22f7..6b72b6e0 100644
--- a/test/e2e/gatewayapi/tcproute.go
+++ b/test/e2e/gatewayapi/tcproute.go
@@ -45,7 +45,10 @@ spec:
listeners:
- name: tcp
protocol: TCP
- port: 80
+ # Must equal APISIX's physical stream_proxy TCP port: the e2e controller
runs
+ # with listener_port_match_mode=auto, so sectionName targeting injects
+ # server_port from this listener; it must match the port connections
arrive on.
+ port: 9100
allowedRoutes:
kinds:
- kind: TCPRoute
@@ -210,7 +213,10 @@ spec:
listeners:
- name: tcp
protocol: TCP
- port: 80
+ # Must equal APISIX's physical stream_proxy TCP port: the e2e controller
runs
+ # with listener_port_match_mode=auto, so sectionName targeting injects
+ # server_port from this listener; it must match the port connections
arrive on.
+ port: 9100
allowedRoutes:
kinds:
- kind: TCPRoute
diff --git a/test/e2e/gatewayapi/udproute.go b/test/e2e/gatewayapi/udproute.go
index ccd22eb2..151ebb37 100644
--- a/test/e2e/gatewayapi/udproute.go
+++ b/test/e2e/gatewayapi/udproute.go
@@ -40,7 +40,9 @@ spec:
listeners:
- name: udp
protocol: UDP
- port: 80
+ # Must equal APISIX's physical stream_proxy UDP port (see tcproute.go):
under
+ # listener_port_match_mode=auto the injected server_port must match it.
+ port: 9200
allowedRoutes:
kinds:
- kind: UDPRoute