Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8c643ae
feat: support port-based routing for Gateway API routes
johannes-engler-mw Jan 16, 2026
7b31cf6
docs: update Gateway API port support documentation
johannes-engler-mw Jan 16, 2026
587e27f
fix(test): check return error of fmt.Sscanf in E2E test
johannes-engler-mw Jan 16, 2026
dd58606
fix(e2e): pull alpine/curl image in pull-infra-images target
johannes-engler-mw Jan 16, 2026
8aac247
test: add unit tests for addServerPortVars and fix test issues
johannes-engler-mw Jan 16, 2026
5ba15c8
fix: sort listener ports to prevent route config flapping
johannes-engler-mw Jan 17, 2026
d49a767
fix(gateway-api): scope listener-port routing and hostname matching
johannes-engler-mw Feb 12, 2026
1e39807
fix(gateway-api): deduplicate tctx.Listeners across overlapping paren…
johannes-engler-mw Feb 23, 2026
f4da8a6
fix: failing e2e tests fro http routes with section names
johannes-engler-mw Feb 24, 2026
3f7ba25
refactor(translator): reduce cyclomatic complexity of TranslateHTTPRoute
johannes-engler-mw Feb 25, 2026
1db1aa7
feat(gateway-api): add listener_port_match_mode config option
johannes-engler-mw Feb 25, 2026
3385ebd
fix: resolve merge conflict in httproute.go translator
johannes-engler-mw Feb 27, 2026
3dce4b2
test(e2e): increase benchmark ADC sync timeout
johannes-engler-mw Mar 4, 2026
04c02fb
test(e2e): deduplicate grpcroute multi-listener checks
johannes-engler-mw Mar 5, 2026
09a5ef9
test: gofmt annotations translator test imports
johannes-engler-mw Mar 6, 2026
c87094a
fix: address PR review feedback for port match changes
johannes-engler-mw Mar 6, 2026
9176520
Fix listener aggregation across gateways
johannes-engler-mw Mar 10, 2026
efbb505
fix: implement PR commentS
johannes-engler-mw Mar 11, 2026
6ac00ee
fix: CURL image tag
johannes-engler-mw Mar 12, 2026
c543818
fix(e2e): capture dig output from kubectl run
johannes-engler-mw Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/benchmark-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,6 @@ jobs:
PROVIDER_TYPE: ${{ matrix.provider_type }}
TEST_LABEL: ${{ matrix.cases_subset }}
TEST_ENV: CI
E2E_EXEC_ADC_TIMEOUT: "30s"
run: |
make benchmark-test
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,10 @@ kind-down:

.PHONY: kind-load-images
kind-load-images: pull-infra-images kind-load-ingress-image kind-load-adc-image
@kind load docker-image kennethreitz/httpbin:latest --name $(KIND_NAME)
@kind load docker-image kennethreitz/httpbin:latest --name $(KIND_NAME)
@kind load docker-image jmalloc/echo-server:latest --name $(KIND_NAME)
@kind load docker-image openresty/openresty:1.27.1.2-4-bullseye-fat --name $(KIND_NAME)
@kind load docker-image alpine/curl:8.17.0 --name $(KIND_NAME)

.PHONY: kind-load-ingress-image
kind-load-ingress-image:
Expand All @@ -204,6 +205,7 @@ pull-infra-images:
@docker pull kennethreitz/httpbin:latest
@docker pull jmalloc/echo-server:latest
@docker pull openresty/openresty:1.27.1.2-4-bullseye-fat
@docker pull alpine/curl:8.17.0

##@ Build

Expand Down
5 changes: 5 additions & 0 deletions config/samples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ exec_adc_timeout: 15s # The timeout for the ADC to execute.
# The default value is 15 seconds.
disable_gateway_api: false # Whether to disable the Gateway API support.
# The default value is false.
listener_port_match_mode: "auto" # Mode for injecting server_port route vars from Gateway listener ports.
# - "auto": inject when parentRefs explicitly target listeners (sectionName/port) or when multiple listener ports are matched.
# - "explicit": inject only when parentRefs explicitly target listeners.
# - "off": never inject server_port vars.
# The default value is "auto".

provider:
type: "apisix" # Provider type.
Expand Down
2 changes: 1 addition & 1 deletion docs/en/latest/concepts/gateway-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ The fields below are specified in the Gateway API specification but are either p

| Fields | Status | Notes |
|------------------------------------------------------|----------------------|------------------------------------------------------------------------------------------------|
| `spec.listeners[].port` | Not supported* | The configuration is required but ignored. This is due to limitations in the data plane: it cannot dynamically open new ports. Since the Ingress Controller does not manage the data plane deployment, it cannot automatically update the configuration or restart the data plane to apply port changes. |
| `spec.listeners[].port` | Partially supported | Controls `server_port` route-var injection; behaviour is configured via [`listener_port_match_mode`](../reference/configuration-file.md) (`auto` / `explicit` / `off`). The controller cannot dynamically open data plane ports, so APISIX must already listen on the specified port. |
| `spec.listeners[].tls.certificateRefs[].group` | Partially supported | Only `""` is supported; other group values cause validation failure. |
| `spec.listeners[].tls.certificateRefs[].kind` | Partially supported | Only `Secret` is supported. |
| `spec.listeners[].tls.mode` | Partially supported | `Terminate` is implemented; `Passthrough` is effectively unsupported for Gateway listeners. |
Expand Down
6 changes: 6 additions & 0 deletions docs/en/latest/reference/configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ secure_metrics: false # The secure metrics configuration.
exec_adc_timeout: 15s # The timeout for the ADC to execute.
# The default value is 15 seconds.

listener_port_match_mode: "auto" # Mode for injecting server_port route vars from Gateway listener ports.
# - "auto": inject when parentRefs explicitly target listeners (sectionName/port) or when multiple listener ports are matched.
# - "explicit": inject only when parentRefs explicitly target listeners.
# - "off": never inject server_port vars.
# The default value is "auto".

provider:
type: "apisix" # Provider type.
# Value can be "apisix" or "apisix-standalone".
Expand Down
2 changes: 1 addition & 1 deletion docs/en/latest/reference/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ spec:

❶ The controller name should be customized if you are running multiple distinct instances of the APISIX Ingress Controller in the same cluster (not a single instance with multiple replicas). Each ingress controller instance must use a unique controllerName in its [configuration file](configuration-file.md), and the corresponding GatewayClass should reference that value.

❷ The `port` in the Gateway listener is required but ignored. This is due to limitations in the data plane: it cannot dynamically open new ports. Since the Ingress Controller does not manage the data plane deployment, it cannot automatically update the configuration or restart the data plane to apply port changes.
❷ The `port` in the Gateway listener is used for routing matching based on `listener_port_match_mode` in the controller configuration (`auto`, `explicit`, or `off`). The controller cannot dynamically open new ports on the data plane, so ensure APISIX is configured to listen on the port.

❸ API group of the referenced resource.

Expand Down
249 changes: 248 additions & 1 deletion internal/adc/translator/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ 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"
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
"github.com/apache/apisix-ingress-controller/internal/controller/config"
)

type mockParser struct {
Expand Down Expand Up @@ -342,11 +345,255 @@ func TestTranslateIngressAnnotations(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
translator := &Translator{}
translator := &Translator{ListenerPortMatchMode: config.ListenerPortMatchModeAuto}
result := translator.TranslateIngressAnnotations(tt.anno)

assert.NotNil(t, result)
assert.Equal(t, tt.expected, result)
})
}
}

func TestAddServerPortVars(t *testing.T) {
tests := []struct {
name string
route *adctypes.Route
ports map[int32]struct{}
expected adctypes.Vars
}{
{
name: "empty ports map - no vars added",
route: &adctypes.Route{},
ports: map[int32]struct{}{},
expected: adctypes.Vars(nil),
},
{
name: "single port - uses == operator",
route: &adctypes.Route{},
ports: map[int32]struct{}{
9080: {},
},
expected: adctypes.Vars{
{
{StrVal: "server_port"},
{StrVal: "=="},
{StrVal: "9080"},
},
},
},
{
name: "two ports - uses 'in' operator",
route: &adctypes.Route{},
ports: map[int32]struct{}{
9080: {},
9081: {},
},
expected: adctypes.Vars{
{
{StrVal: "server_port"},
{StrVal: "in"},
{SliceVal: []adctypes.StringOrSlice{
{StrVal: "9080"},
{StrVal: "9081"},
}},
},
},
},
{
name: "three ports - uses 'in' operator",
route: &adctypes.Route{},
ports: map[int32]struct{}{
80: {},
443: {},
9080: {},
},
expected: adctypes.Vars{
{
{StrVal: "server_port"},
{StrVal: "in"},
{SliceVal: []adctypes.StringOrSlice{
{StrVal: "80"},
{StrVal: "443"},
{StrVal: "9080"},
}},
},
},
},
{
name: "vars are appended - preserves existing vars",
route: &adctypes.Route{
Vars: adctypes.Vars{
{
{StrVal: "uri"},
{StrVal: "~~"},
{StrVal: "^/api"},
},
},
},
ports: map[int32]struct{}{
9080: {},
},
expected: adctypes.Vars{
{
{StrVal: "uri"},
{StrVal: "~~"},
{StrVal: "^/api"},
},
{
{StrVal: "server_port"},
{StrVal: "=="},
{StrVal: "9080"},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addServerPortVars(tt.route, tt.ports)
assert.Equal(t, tt.expected, tt.route.Vars)
})
}
}

func TestShouldInjectServerPortVars(t *testing.T) {
sectionName := gatewayv1.SectionName("http-main")
port := gatewayv1.PortNumber(9080)

tests := []struct {
name string
mode config.ListenerPortMatchMode
parentRefs []gatewayv1.ParentReference
ports map[int32]struct{}
expected bool
}{
{
name: "empty listener ports",
mode: config.ListenerPortMatchModeAuto,
ports: map[int32]struct{}{},
expected: false,
},
{
name: "single port without sectionName",
mode: config.ListenerPortMatchModeAuto,
parentRefs: []gatewayv1.ParentReference{
{Name: "gw"},
},
ports: map[int32]struct{}{
9080: {},
},
expected: false,
},
{
name: "single port with sectionName",
mode: config.ListenerPortMatchModeAuto,
parentRefs: []gatewayv1.ParentReference{
{Name: "gw", SectionName: &sectionName},
},
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: {},
},
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: {},
},
expected: false,
},
{
name: "explicit mode with parentRef.port",
mode: config.ListenerPortMatchModeExplicit,
parentRefs: []gatewayv1.ParentReference{
{Name: "gw", Port: &port},
},
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: {},
},
expected: false,
},
{
name: "off mode ignores explicit target",
mode: config.ListenerPortMatchModeOff,
parentRefs: []gatewayv1.ParentReference{
{Name: "gw", SectionName: &sectionName},
},
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: {},
},
expected: false,
},
}

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))
})
}
}
13 changes: 13 additions & 0 deletions internal/adc/translator/grpcroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,19 @@ func (t *Translator) TranslateGRPCRoute(tctx *provider.TranslateContext, grpcRou

routes = append(routes, route)
}

// Collect unique listener ports for port-based routing.
listenerPorts := make(map[int32]struct{})
for _, listener := range tctx.Listeners {
listenerPorts[int32(listener.Port)] = struct{}{}
}

if t.shouldInjectServerPortVars(tctx.RouteParentRefs, listenerPorts) {
for _, route := range routes {
addServerPortVars(route, listenerPorts)
}
}

service.Routes = routes

result.Services = append(result.Services, service)
Expand Down
Loading
Loading