diff --git a/config/samples/config.yaml b/config/samples/config.yaml index 1b727820..8935f49d 100644 --- a/config/samples/config.yaml +++ b/config/samples/config.yaml @@ -37,6 +37,12 @@ exec_adc_timeout: 15s # The timeout for the ADC to execute. 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: "api7ee" diff --git a/internal/adc/translator/annotations_test.go b/internal/adc/translator/annotations_test.go index 8c8b1b96..7b4d1ec4 100644 --- a/internal/adc/translator/annotations_test.go +++ b/internal/adc/translator/annotations_test.go @@ -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 { @@ -342,7 +345,7 @@ 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) @@ -350,3 +353,247 @@ func TestTranslateIngressAnnotations(t *testing.T) { }) } } + +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: §ionName}, + }, + 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: §ionName}, + }, + 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)) + }) + } +} diff --git a/internal/adc/translator/apisixconsumer_test.go b/internal/adc/translator/apisixconsumer_test.go index e6a1ee4a..2bb6ffff 100644 --- a/internal/adc/translator/apisixconsumer_test.go +++ b/internal/adc/translator/apisixconsumer_test.go @@ -31,7 +31,7 @@ import ( ) func TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerLabels(t *testing.T) { - translator := NewTranslator(logr.Discard()) + translator := NewTranslator(logr.Discard(), "") tctx := provider.NewDefaultTranslateContext(context.Background()) consumer := &apiv2.ApisixConsumer{ @@ -49,7 +49,7 @@ func TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerL }, }, Spec: apiv2.ApisixConsumerSpec{ - AuthParameter: apiv2.ApisixConsumerAuthParameter{ + AuthParameter: &apiv2.ApisixConsumerAuthParameter{ BasicAuth: &apiv2.ApisixConsumerBasicAuth{ Value: &apiv2.ApisixConsumerBasicAuthValue{ Username: "demo", diff --git a/internal/adc/translator/apisixroute_test.go b/internal/adc/translator/apisixroute_test.go index f295757b..6ac355eb 100644 --- a/internal/adc/translator/apisixroute_test.go +++ b/internal/adc/translator/apisixroute_test.go @@ -29,7 +29,7 @@ import ( ) func TestBuildRoute_HostsNotSet(t *testing.T) { - translator := NewTranslator(logr.Discard()) + translator := NewTranslator(logr.Discard(), "") ar := &apiv2.ApisixRoute{ ObjectMeta: metav1.ObjectMeta{ @@ -60,7 +60,7 @@ func TestBuildRoute_HostsNotSet(t *testing.T) { } func TestBuildService_HostsSet(t *testing.T) { - translator := NewTranslator(logr.Discard()) + translator := NewTranslator(logr.Discard(), "") ar := &apiv2.ApisixRoute{ ObjectMeta: metav1.ObjectMeta{ @@ -84,7 +84,7 @@ func TestBuildService_HostsSet(t *testing.T) { } func TestBuildRoute_MetadataLabelsDoNotOverwriteControllerLabels(t *testing.T) { - translator := NewTranslator(logr.Discard()) + translator := NewTranslator(logr.Discard(), "") ar := &apiv2.ApisixRoute{ TypeMeta: metav1.TypeMeta{ diff --git a/internal/adc/translator/consumer_test.go b/internal/adc/translator/consumer_test.go index c62527e3..6f5d08ce 100644 --- a/internal/adc/translator/consumer_test.go +++ b/internal/adc/translator/consumer_test.go @@ -31,7 +31,7 @@ import ( ) func TestTranslateConsumerV1alpha1_UsesMetadataLabelsWithoutOverwritingControllerLabels(t *testing.T) { - translator := NewTranslator(logr.Discard()) + translator := NewTranslator(logr.Discard(), "") tctx := provider.NewDefaultTranslateContext(context.Background()) consumer := &v1alpha1.Consumer{ diff --git a/internal/adc/translator/grpcroute.go b/internal/adc/translator/grpcroute.go index abe6dfab..631b34d5 100644 --- a/internal/adc/translator/grpcroute.go +++ b/internal/adc/translator/grpcroute.go @@ -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) diff --git a/internal/adc/translator/grpcroute_test.go b/internal/adc/translator/grpcroute_test.go new file mode 100644 index 00000000..df95a35a --- /dev/null +++ b/internal/adc/translator/grpcroute_test.go @@ -0,0 +1,215 @@ +// 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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + "github.com/apache/apisix-ingress-controller/internal/controller/config" + "github.com/apache/apisix-ingress-controller/internal/provider" +) + +func TestTranslateGRPCRouteServerPortVarsByMode(t *testing.T) { + sectionName := gatewayv1.SectionName("grpc-main") + parentPort := gatewayv1.PortNumber(9080) + + singlePortVars := adctypes.Vars{ + { + {StrVal: "server_port"}, + {StrVal: "=="}, + {StrVal: "9080"}, + }, + } + multiPortVars := adctypes.Vars{ + { + {StrVal: "server_port"}, + {StrVal: "in"}, + {SliceVal: []adctypes.StringOrSlice{ + {StrVal: "9080"}, + {StrVal: "9081"}, + }}, + }, + } + + tests := []struct { + name string + mode config.ListenerPortMatchMode + parentRefs []gatewayv1.ParentReference + 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}, + }, + listeners: []gatewayv1.Listener{ + {Name: "grpc-main", Protocol: gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)}, + }, + expected: singlePortVars, + }, + { + 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)}, + }, + 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}, + }, + listeners: []gatewayv1.Listener{ + {Name: "grpc-main", Protocol: gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)}, + }, + expected: singlePortVars, + }, + { + 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)}, + }, + expected: nil, + }, + { + name: "off mode: no injection even with sectionName target", + mode: config.ListenerPortMatchModeOff, + parentRefs: []gatewayv1.ParentReference{ + {Name: "gw", SectionName: §ionName}, + }, + listeners: []gatewayv1.Listener{ + {Name: "grpc-main", Protocol: gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)}, + }, + expected: nil, + }, + { + 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)}, + }, + expected: nil, + }, + { + name: "empty mode normalizes to auto", + mode: "", + parentRefs: []gatewayv1.ParentReference{ + {Name: "gw", Port: &parentPort}, + }, + listeners: []gatewayv1.Listener{ + {Name: "grpc-main", Protocol: gatewayv1.HTTPProtocolType, Port: gatewayv1.PortNumber(9080)}, + }, + expected: singlePortVars, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tctx := provider.NewDefaultTranslateContext(context.Background()) + tctx.RouteParentRefs = tt.parentRefs + tctx.Listeners = tt.listeners + + grpcRoute := &gatewayv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route", + Namespace: "default", + }, + Spec: gatewayv1.GRPCRouteSpec{ + Rules: []gatewayv1.GRPCRouteRule{ + {}, + }, + }, + } + + translator := NewTranslator(logr.Discard(), tt.mode) + got, err := translator.TranslateGRPCRoute(tctx, grpcRoute) + assert.NoError(t, err) + if assert.Len(t, got.Services, 1) && assert.Len(t, got.Services[0].Routes, 1) { + assert.Equal(t, tt.expected, got.Services[0].Routes[0].Vars) + } + }) + } +} diff --git a/internal/adc/translator/httproute.go b/internal/adc/translator/httproute.go index 4acabd31..cd2e51da 100644 --- a/internal/adc/translator/httproute.go +++ b/internal/adc/translator/httproute.go @@ -21,6 +21,7 @@ import ( "cmp" "encoding/json" "fmt" + "sort" "strings" "github.com/pkg/errors" @@ -525,6 +526,167 @@ func calculateHTTPRoutePriority(match *gatewayv1.HTTPRouteMatch, ruleIndex int, return priority } +// translateBackendsToUpstreams processes the BackendRefs of an HTTPRouteRule, +// builds upstreams, assigns them to the service (single upstream or traffic-split +// plugin for multiple), and injects fault-injection on backend errors. +func (t *Translator) translateBackendsToUpstreams( + tctx *provider.TranslateContext, + rule gatewayv1.HTTPRouteRule, + httpRoute *gatewayv1.HTTPRoute, + service *adctypes.Service, +) (enableWebsocket *bool, backendErr error) { + upstreams := make([]*adctypes.Upstream, 0) + weightedUpstreams := make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0) + + for _, backend := range rule.BackendRefs { + if backend.Namespace == nil { + namespace := gatewayv1.Namespace(httpRoute.Namespace) + backend.Namespace = &namespace + } + upstream := adctypes.NewDefaultUpstream() + upNodes, protocol, err := t.translateBackendRef(tctx, backend.BackendRef, DefaultEndpointFilter) + if err != nil { + backendErr = err + continue + } + if len(upNodes) == 0 { + continue + } + if protocol == internaltypes.AppProtocolWS || protocol == internaltypes.AppProtocolWSS { + enableWebsocket = ptr.To(true) + } + + t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef, tctx.BackendTrafficPolicies, upstream) + upstream.Nodes = upNodes + if upstream.Scheme == "" { + upstream.Scheme = appProtocolToUpstreamScheme(protocol) + } + var ( + kind string + port int32 + ) + if backend.Kind == nil { + kind = internaltypes.KindService + } else { + kind = string(*backend.Kind) + } + if backend.Port != nil { + port = int32(*backend.Port) + } + namespace := string(*backend.Namespace) + name := string(backend.Name) + upstreamName := adctypes.ComposeUpstreamNameForBackendRef(kind, namespace, name, port) + upstream.Name = upstreamName + upstream.Scheme = cmp.Or(upstream.Scheme, apiv2.SchemeHTTP) + upstream.ID = id.GenID(upstreamName) + upstreams = append(upstreams, upstream) + } + + // Handle multiple backends with traffic-split plugin + if len(upstreams) == 0 { + // Create a default upstream if no valid backends + service.Upstream = adctypes.NewDefaultUpstream() + } else if len(upstreams) == 1 { + // Single backend - use directly as service upstream + service.Upstream = upstreams[0] + // remove the id and name of the service.upstream, adc schema does not need id and name for it + service.Upstream.ID = "" + service.Upstream.Name = "" + } else { + // Multiple backends - use traffic-split plugin + service.Upstream = upstreams[0] + // remove the id and name of the service.upstream, adc schema does not need id and name for it + service.Upstream.ID = "" + service.Upstream.Name = "" + + upstreams = upstreams[1:] + + if len(upstreams) > 0 { + service.Upstreams = upstreams + } + + // Set weight in traffic-split for the default upstream + weight := apiv2.DefaultWeight + if rule.BackendRefs[0].Weight != nil { + weight = int(*rule.BackendRefs[0].Weight) + } + weightedUpstreams = append(weightedUpstreams, adctypes.TrafficSplitConfigRuleWeightedUpstream{ + Weight: weight, + }) + + // Set other upstreams in traffic-split using upstream_id + for i, upstream := range upstreams { + weight := apiv2.DefaultWeight + // get weight from the backend refs starting from the second backend + if i+1 < len(rule.BackendRefs) && rule.BackendRefs[i+1].Weight != nil { + weight = int(*rule.BackendRefs[i+1].Weight) + } + weightedUpstreams = append(weightedUpstreams, adctypes.TrafficSplitConfigRuleWeightedUpstream{ + UpstreamID: upstream.ID, + Weight: weight, + }) + } + + if len(weightedUpstreams) > 0 { + if service.Plugins == nil { + service.Plugins = make(map[string]any) + } + service.Plugins["traffic-split"] = &adctypes.TrafficSplitConfig{ + Rules: []adctypes.TrafficSplitConfigRule{ + { + WeightedUpstreams: weightedUpstreams, + }, + }, + } + } + } + + if backendErr != nil && (service.Upstream == nil || len(service.Upstream.Nodes) == 0) { + if service.Plugins == nil { + service.Plugins = make(map[string]any) + } + service.Plugins["fault-injection"] = map[string]any{ + "abort": map[string]any{ + "http_status": 500, + "body": "No existing backendRef provided", + }, + } + } + + return enableWebsocket, backendErr +} + +// addServerPortVars appends server_port matching vars to a route for the given ports. +func addServerPortVars(route *adctypes.Route, ports map[int32]struct{}) { + if len(ports) == 0 { + return + } + + portList := make([]int32, 0, len(ports)) + for p := range ports { + portList = append(portList, p) + } + sort.Slice(portList, func(i, j int) bool { return portList[i] < portList[j] }) + + if len(portList) == 1 { + route.Vars = append(route.Vars, []adctypes.StringOrSlice{ + {StrVal: "server_port"}, + {StrVal: "=="}, + {StrVal: fmt.Sprintf("%d", portList[0])}, + }) + } else { + portSlice := make([]adctypes.StringOrSlice, 0, len(portList)) + for _, p := range portList { + portSlice = append(portSlice, adctypes.StringOrSlice{StrVal: fmt.Sprintf("%d", p)}) + } + route.Vars = append(route.Vars, []adctypes.StringOrSlice{ + {StrVal: "server_port"}, + {StrVal: "in"}, + {SliceVal: portSlice}, + }) + } +} + func (t *Translator) TranslateHTTPRoute(tctx *provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) (*TranslateResult, error) { result := &TranslateResult{} @@ -545,128 +707,7 @@ func (t *Translator) TranslateHTTPRoute(tctx *provider.TranslateContext, httpRou service.ID = id.GenID(service.Name) service.Hosts = hosts - var ( - upstreams = make([]*adctypes.Upstream, 0) - weightedUpstreams = make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0) - backendErr error - enableWebsocket *bool - ) - - for _, backend := range rule.BackendRefs { - if backend.Namespace == nil { - namespace := gatewayv1.Namespace(httpRoute.Namespace) - backend.Namespace = &namespace - } - upstream := adctypes.NewDefaultUpstream() - upNodes, protocol, err := t.translateBackendRef(tctx, backend.BackendRef, DefaultEndpointFilter) - if err != nil { - backendErr = err - continue - } - if len(upNodes) == 0 { - continue - } - if protocol == internaltypes.AppProtocolWS || protocol == internaltypes.AppProtocolWSS { - enableWebsocket = ptr.To(true) - } - - t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef, tctx.BackendTrafficPolicies, upstream) - upstream.Nodes = upNodes - if upstream.Scheme == "" { - upstream.Scheme = appProtocolToUpstreamScheme(protocol) - } - var ( - kind string - port int32 - ) - if backend.Kind == nil { - kind = internaltypes.KindService - } else { - kind = string(*backend.Kind) - } - if backend.Port != nil { - port = int32(*backend.Port) - } - namespace := string(*backend.Namespace) - name := string(backend.Name) - upstreamName := adctypes.ComposeUpstreamNameForBackendRef(kind, namespace, name, port) - upstream.Name = upstreamName - upstream.Scheme = cmp.Or(upstream.Scheme, apiv2.SchemeHTTP) - upstream.ID = id.GenID(upstreamName) - upstreams = append(upstreams, upstream) - } - - // Handle multiple backends with traffic-split plugin - if len(upstreams) == 0 { - // Create a default upstream if no valid backends - upstream := adctypes.NewDefaultUpstream() - service.Upstream = upstream - } else if len(upstreams) == 1 { - // Single backend - use directly as service upstream - service.Upstream = upstreams[0] - // remove the id and name of the service.upstream, adc schema does not need id and name for it - service.Upstream.ID = "" - service.Upstream.Name = "" - } else { - // Multiple backends - use traffic-split plugin - service.Upstream = upstreams[0] - // remove the id and name of the service.upstream, adc schema does not need id and name for it - service.Upstream.ID = "" - service.Upstream.Name = "" - - upstreams = upstreams[1:] - - if len(upstreams) > 0 { - service.Upstreams = upstreams - } - - // Set weight in traffic-split for the default upstream - weight := apiv2.DefaultWeight - if rule.BackendRefs[0].Weight != nil { - weight = int(*rule.BackendRefs[0].Weight) - } - weightedUpstreams = append(weightedUpstreams, adctypes.TrafficSplitConfigRuleWeightedUpstream{ - Weight: weight, - }) - - // Set other upstreams in traffic-split using upstream_id - for i, upstream := range upstreams { - weight := apiv2.DefaultWeight - // get weight from the backend refs starting from the second backend - if i+1 < len(rule.BackendRefs) && rule.BackendRefs[i+1].Weight != nil { - weight = int(*rule.BackendRefs[i+1].Weight) - } - weightedUpstreams = append(weightedUpstreams, adctypes.TrafficSplitConfigRuleWeightedUpstream{ - UpstreamID: upstream.ID, - Weight: weight, - }) - } - - if len(weightedUpstreams) > 0 { - if service.Plugins == nil { - service.Plugins = make(map[string]any) - } - service.Plugins["traffic-split"] = &adctypes.TrafficSplitConfig{ - Rules: []adctypes.TrafficSplitConfigRule{ - { - WeightedUpstreams: weightedUpstreams, - }, - }, - } - } - } - - if backendErr != nil && (service.Upstream == nil || len(service.Upstream.Nodes) == 0) { - if service.Plugins == nil { - service.Plugins = make(map[string]any) - } - service.Plugins["fault-injection"] = map[string]any{ - "abort": map[string]any{ - "http_status": 500, - "body": "No existing backendRef provided", - }, - } - } + enableWebsocket, _ := t.translateBackendsToUpstreams(tctx, rule, httpRoute, service) t.fillPluginsFromHTTPRouteFilters(service.Plugins, httpRoute.GetNamespace(), rule.Filters, rule.Matches, tctx) @@ -703,6 +744,21 @@ func (t *Translator) TranslateHTTPRoute(tctx *provider.TranslateContext, httpRou 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{}{} + } + + // Add server_port matching only when a route explicitly targets a listener + // or when multiple listener ports need to be disambiguated. + if t.shouldInjectServerPortVars(tctx.RouteParentRefs, listenerPorts) { + for _, route := range routes { + addServerPortVars(route, listenerPorts) + } + } + t.fillHTTPRoutePoliciesForHTTPRoute(tctx, routes, rule) service.Routes = routes diff --git a/internal/adc/translator/httproute_test.go b/internal/adc/translator/httproute_test.go index 28fdea83..ab6b8ef2 100644 --- a/internal/adc/translator/httproute_test.go +++ b/internal/adc/translator/httproute_test.go @@ -60,7 +60,7 @@ func TestTranslateHTTPRouteUpstreamScheme(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - translator := NewTranslator(logr.Discard()) + translator := NewTranslator(logr.Discard(), "") tctx := provider.NewDefaultTranslateContext(context.Background()) const ( diff --git a/internal/adc/translator/translator.go b/internal/adc/translator/translator.go index aeaef250..e294c7da 100644 --- a/internal/adc/translator/translator.go +++ b/internal/adc/translator/translator.go @@ -19,17 +19,72 @@ package translator import ( "github.com/go-logr/logr" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" adctypes "github.com/apache/apisix-ingress-controller/api/adc" + "github.com/apache/apisix-ingress-controller/internal/controller/config" ) type Translator struct { - Log logr.Logger + Log logr.Logger + ListenerPortMatchMode config.ListenerPortMatchMode } -func NewTranslator(log logr.Logger) *Translator { +func normalizeMode(mode config.ListenerPortMatchMode) config.ListenerPortMatchMode { + switch mode { + case "", config.ListenerPortMatchModeAuto: + return config.ListenerPortMatchModeAuto + case config.ListenerPortMatchModeExplicit, config.ListenerPortMatchModeOff: + return mode + default: + return config.ListenerPortMatchModeAuto + } +} + +func NewTranslator(log logr.Logger, mode config.ListenerPortMatchMode) *Translator { return &Translator{ - Log: log.WithName("translator"), + Log: log.WithName("translator"), + ListenerPortMatchMode: normalizeMode(mode), + } +} + +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 +} + +func (t *Translator) shouldInjectServerPortVars(parentRefs []gatewayv1.ParentReference, 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)) + } + return false + case config.ListenerPortMatchModeExplicit: + return explicit + case config.ListenerPortMatchModeAuto: + return explicit || len(ports) > 1 + default: + return explicit || len(ports) > 1 } } diff --git a/internal/controller/config/config.go b/internal/controller/config/config.go index 2b114004..d7ae0f2e 100644 --- a/internal/controller/config/config.go +++ b/internal/controller/config/config.go @@ -56,7 +56,8 @@ func NewDefaultConfig() *Config { SyncPeriod: types.TimeDuration{Duration: 0}, InitSyncDelay: types.TimeDuration{Duration: 20 * time.Minute}, }, - Webhook: NewWebhookConfig(), + Webhook: NewWebhookConfig(), + ListenerPortMatchMode: ListenerPortMatchModeAuto, } } @@ -122,6 +123,15 @@ func (c *Config) Validate() error { if c.ControllerName == "" { return fmt.Errorf("controller_name is required") } + + if c.ListenerPortMatchMode != "" { + switch c.ListenerPortMatchMode { + case ListenerPortMatchModeAuto, ListenerPortMatchModeExplicit, ListenerPortMatchModeOff: + default: + return fmt.Errorf("invalid listener_port_match_mode: %q (must be auto, explicit, or off)", c.ListenerPortMatchMode) + } + } + if err := validateProvider(c.ProviderConfig); err != nil { return err } diff --git a/internal/controller/config/config_test.go b/internal/controller/config/config_test.go index dd8a3bc8..ac754674 100644 --- a/internal/controller/config/config_test.go +++ b/internal/controller/config/config_test.go @@ -13,6 +13,56 @@ func TestNewDefaultConfig(t *testing.T) { assert.Equal(t, DefaultLogLevel, cfg.LogLevel) assert.Equal(t, DefaultControllerName, cfg.ControllerName) assert.Equal(t, DefaultLeaderElectionID, cfg.LeaderElectionID) + assert.Equal(t, ListenerPortMatchModeAuto, cfg.ListenerPortMatchMode) +} + +func TestConfigValidateListenerPortMatchMode(t *testing.T) { + tests := []struct { + name string + mode ListenerPortMatchMode + expectErr bool + }{ + { + name: "default auto", + mode: ListenerPortMatchModeAuto, + expectErr: false, + }, + { + name: "explicit", + mode: ListenerPortMatchModeExplicit, + expectErr: false, + }, + { + name: "off", + mode: ListenerPortMatchModeOff, + expectErr: false, + }, + { + name: "empty mode is allowed", + mode: "", + expectErr: false, + }, + { + name: "invalid mode", + mode: "invalid", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := NewDefaultConfig() + cfg.ListenerPortMatchMode = tt.mode + + err := cfg.Validate() + if tt.expectErr { + assert.Error(t, err) + assert.ErrorContains(t, err, "invalid listener_port_match_mode") + } else { + assert.NoError(t, err) + } + }) + } } func TestNewConfigFromFile(t *testing.T) { diff --git a/internal/controller/config/types.go b/internal/controller/config/types.go index 9bb213a8..d39af96b 100644 --- a/internal/controller/config/types.go +++ b/internal/controller/config/types.go @@ -29,6 +29,14 @@ const ( ProviderTypeAPISIX ProviderType = "apisix" ) +type ListenerPortMatchMode string + +const ( + ListenerPortMatchModeAuto ListenerPortMatchMode = "auto" + ListenerPortMatchModeExplicit ListenerPortMatchMode = "explicit" + ListenerPortMatchModeOff ListenerPortMatchMode = "off" +) + const ( // IngressAPISIXLeader is the default election id for the controller // leader election. @@ -56,20 +64,21 @@ const ( // Config contains all config items which are necessary for // apisix-ingress-controller's running. type Config struct { - LogLevel string `json:"log_level" yaml:"log_level"` - ControllerName string `json:"controller_name" yaml:"controller_name"` - LeaderElectionID string `json:"leader_election_id" yaml:"leader_election_id"` - MetricsAddr string `json:"metrics_addr" yaml:"metrics_addr"` - ServerAddr string `json:"server_addr" yaml:"server_addr"` - EnableServer bool `json:"enable_server" yaml:"enable_server"` - EnableHTTP2 bool `json:"enable_http2" yaml:"enable_http2"` - ProbeAddr string `json:"probe_addr" yaml:"probe_addr"` - SecureMetrics bool `json:"secure_metrics" yaml:"secure_metrics"` - LeaderElection *LeaderElection `json:"leader_election" yaml:"leader_election"` - ExecADCTimeout types.TimeDuration `json:"exec_adc_timeout" yaml:"exec_adc_timeout"` - ProviderConfig ProviderConfig `json:"provider" yaml:"provider"` - Webhook *WebhookConfig `json:"webhook" yaml:"webhook"` - DisableGatewayAPI bool `json:"disable_gateway_api" yaml:"disable_gateway_api"` + LogLevel string `json:"log_level" yaml:"log_level"` + ControllerName string `json:"controller_name" yaml:"controller_name"` + LeaderElectionID string `json:"leader_election_id" yaml:"leader_election_id"` + MetricsAddr string `json:"metrics_addr" yaml:"metrics_addr"` + ServerAddr string `json:"server_addr" yaml:"server_addr"` + EnableServer bool `json:"enable_server" yaml:"enable_server"` + EnableHTTP2 bool `json:"enable_http2" yaml:"enable_http2"` + ProbeAddr string `json:"probe_addr" yaml:"probe_addr"` + SecureMetrics bool `json:"secure_metrics" yaml:"secure_metrics"` + LeaderElection *LeaderElection `json:"leader_election" yaml:"leader_election"` + ExecADCTimeout types.TimeDuration `json:"exec_adc_timeout" yaml:"exec_adc_timeout"` + ProviderConfig ProviderConfig `json:"provider" yaml:"provider"` + Webhook *WebhookConfig `json:"webhook" yaml:"webhook"` + DisableGatewayAPI bool `json:"disable_gateway_api" yaml:"disable_gateway_api"` + ListenerPortMatchMode ListenerPortMatchMode `json:"listener_port_match_mode" yaml:"listener_port_match_mode"` } type GatewayConfig struct { diff --git a/internal/controller/context.go b/internal/controller/context.go index 5398f044..686dd046 100644 --- a/internal/controller/context.go +++ b/internal/controller/context.go @@ -27,6 +27,7 @@ type RouteParentRefContext struct { ListenerName string Listener *gatewayv1.Listener + Listeners []gatewayv1.Listener Conditions []metav1.Condition } diff --git a/internal/controller/grpcroute_controller.go b/internal/controller/grpcroute_controller.go index 3b423417..f92fd34a 100644 --- a/internal/controller/grpcroute_controller.go +++ b/internal/controller/grpcroute_controller.go @@ -200,8 +200,13 @@ func (r *GRPCRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( acceptStatus.status = false acceptStatus.msg = err.Error() } - if gateway.Listener != nil { - tctx.Listeners = append(tctx.Listeners, *gateway.Listener) + // Populate listeners for port-based routing. + // Use Listeners slice if available (multiple listener support) + if len(gateway.Listeners) > 0 { + tctx.Listeners = appendListeners(tctx.Listeners, gateway.Listeners...) + } else if gateway.Listener != nil { + // Fallback for backward compatibility + tctx.Listeners = appendListeners(tctx.Listeners, *gateway.Listener) } } diff --git a/internal/controller/httproute_controller.go b/internal/controller/httproute_controller.go index 216b50e1..c7e42f05 100644 --- a/internal/controller/httproute_controller.go +++ b/internal/controller/httproute_controller.go @@ -203,6 +203,14 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( acceptStatus.status = false acceptStatus.msg = err.Error() } + // Populate listeners for port-based routing. + // Use Listeners slice if available (multiple listener support) + if len(gateway.Listeners) > 0 { + tctx.Listeners = appendListeners(tctx.Listeners, gateway.Listeners...) + } else if gateway.Listener != nil { + // Fallback for backward compatibility + tctx.Listeners = appendListeners(tctx.Listeners, *gateway.Listener) + } } var backendRefErr error diff --git a/internal/controller/utils.go b/internal/controller/utils.go index f34f01a4..7c92c4fe 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -372,6 +372,10 @@ func ParseRouteParentRefs( reason := gatewayv1.RouteReasonNoMatchingParent var listenerName string var matchedListener gatewayv1.Listener + var matchedListeners []gatewayv1.Listener + + // Track if sectionName was explicitly specified + sectionNameSpecified := parentRef.SectionName != nil && *parentRef.SectionName != "" for _, listener := range gateway.Spec.Listeners { if parentRef.SectionName != nil { @@ -395,7 +399,6 @@ func ParseRouteParentRefs( continue } - listenerName = string(listener.Name) ok, err := routeMatchesListenerAllowedRoutes(ctx, mgrc, route, listener.AllowedRoutes, gateway.Namespace, parentRef.Namespace) if err != nil { log.Error(err, "failed matching listener to a route for gateway", @@ -410,9 +413,23 @@ func ParseRouteParentRefs( // TODO: check if the listener status is programmed + if sectionNameSpecified { + listenerName = string(listener.Name) + } + + if !matched { + // First match - store for backward compatibility + matchedListener = listener + } + + // Always add to the list of matched listeners + matchedListeners = append(matchedListeners, listener) matched = true - matchedListener = listener - break + + // Only break if sectionName was explicitly specified + if sectionNameSpecified { + break + } } if matched { @@ -420,6 +437,7 @@ func ParseRouteParentRefs( Gateway: &gateway, ListenerName: listenerName, Listener: &matchedListener, + Listeners: matchedListeners, Conditions: []metav1.Condition{{ Type: string(gatewayv1.RouteConditionAccepted), Status: metav1.ConditionTrue, @@ -431,7 +449,8 @@ func ParseRouteParentRefs( gateways = append(gateways, RouteParentRefContext{ Gateway: &gateway, ListenerName: listenerName, - Listener: &matchedListener, + Listener: nil, + Listeners: matchedListeners, Conditions: []metav1.Condition{{ Type: string(gatewayv1.RouteConditionAccepted), Status: metav1.ConditionFalse, @@ -1113,35 +1132,51 @@ func getUnionOfGatewayHostnames(gateways []RouteParentRefContext) ([]gatewayv1.H hostnames := make([]gatewayv1.Hostname, 0) for _, gateway := range gateways { - if gateway.ListenerName != "" { - // If a listener name is specified, only check that listener - for _, listener := range gateway.Gateway.Spec.Listeners { - if string(listener.Name) == gateway.ListenerName { - // If a listener does not specify a hostname, it can match any hostname - if listener.Hostname == nil { - return nil, true - } - hostnames = append(hostnames, *listener.Hostname) - break - } + for _, listener := range listenersForGatewayContext(gateway) { + // Only consider listeners that can effectively configure hostnames (HTTP, HTTPS, or TLS). + if !isListenerHostnameEffective(listener) { + continue } - } else { - // Otherwise, check all listeners - for _, listener := range gateway.Gateway.Spec.Listeners { - // Only consider listeners that can effectively configure hostnames (HTTP, HTTPS, or TLS) - if isListenerHostnameEffective(listener) { - if listener.Hostname == nil { - return nil, true - } - hostnames = append(hostnames, *listener.Hostname) - } + if listener.Hostname == nil { + return nil, true } + hostnames = append(hostnames, *listener.Hostname) } } return hostnames, false } +// listenersForGatewayContext returns the listeners relevant for hostname resolution. +// When a specific listener was matched by sectionName, only that listener is returned. +// Otherwise, all gateway spec listeners are considered. +func listenersForGatewayContext(gateway RouteParentRefContext) []gatewayv1.Listener { + if gateway.ListenerName != "" { + for _, listener := range gateway.Gateway.Spec.Listeners { + if string(listener.Name) == gateway.ListenerName { + return []gatewayv1.Listener{listener} + } + } + return nil + } + return gateway.Gateway.Spec.Listeners +} + +// appendListeners appends listeners to the slice, avoiding duplicates by port+name. +func appendListeners(existing []gatewayv1.Listener, toAdd ...gatewayv1.Listener) []gatewayv1.Listener { + seen := make(map[string]struct{}, len(existing)) + for _, l := range existing { + seen[string(l.Name)] = struct{}{} + } + for _, l := range toAdd { + if _, ok := seen[string(l.Name)]; !ok { + existing = append(existing, l) + seen[string(l.Name)] = struct{}{} + } + } + return existing +} + // getMinimumHostnameIntersection returns the smallest intersection hostname // - If the listener hostname is empty, return the HTTPRoute hostname // - If the listener hostname is a wildcard of the HTTPRoute hostname, return the HTTPRoute hostname diff --git a/internal/manager/run.go b/internal/manager/run.go index 56c4976f..202c6aeb 100644 --- a/internal/manager/run.go +++ b/internal/manager/run.go @@ -184,9 +184,10 @@ func Run(ctx context.Context, logger logr.Logger) error { providerType := string(config.ControllerConfig.ProviderConfig.Type) providerOptions := &provider.Options{ - SyncTimeout: config.ControllerConfig.ExecADCTimeout.Duration, - SyncPeriod: config.ControllerConfig.ProviderConfig.SyncPeriod.Duration, - InitSyncDelay: config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration, + SyncTimeout: config.ControllerConfig.ExecADCTimeout.Duration, + SyncPeriod: config.ControllerConfig.ProviderConfig.SyncPeriod.Duration, + InitSyncDelay: config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration, + ListenerPortMatchMode: config.ControllerConfig.ListenerPortMatchMode, } provider, err := provider.New(providerType, logger, updater.Writer(), readier, providerOptions) if err != nil { diff --git a/internal/provider/api7ee/provider.go b/internal/provider/api7ee/provider.go index 2ead5029..50b86d3d 100644 --- a/internal/provider/api7ee/provider.go +++ b/internal/provider/api7ee/provider.go @@ -85,7 +85,7 @@ func New(log logr.Logger, updater status.Updater, readier readiness.ReadinessMan return &api7eeProvider{ client: cli, Options: o, - translator: &translator.Translator{}, + translator: translator.NewTranslator(log, o.ListenerPortMatchMode), updater: updater, readier: readier, syncCh: make(chan struct{}, 1), diff --git a/internal/provider/api7ee/status.go b/internal/provider/api7ee/status.go index 0fc5fb79..9231a8ec 100644 --- a/internal/provider/api7ee/status.go +++ b/internal/provider/api7ee/status.go @@ -122,6 +122,7 @@ func (d *api7eeProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1.HTTPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1.HTTPRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs @@ -157,6 +158,7 @@ func (d *api7eeProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1alpha2.UDPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1alpha2.UDPRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs @@ -192,6 +194,7 @@ func (d *api7eeProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1alpha2.TCPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1alpha2.TCPRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs @@ -227,6 +230,7 @@ func (d *api7eeProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1.GRPCRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1.GRPCRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs diff --git a/internal/provider/apisix/provider.go b/internal/provider/apisix/provider.go index 791e1780..fd7f1773 100644 --- a/internal/provider/apisix/provider.go +++ b/internal/provider/apisix/provider.go @@ -86,7 +86,7 @@ func New(log logr.Logger, updater status.Updater, readier readiness.ReadinessMan return &apisixProvider{ client: cli, Options: o, - translator: translator.NewTranslator(log), + translator: translator.NewTranslator(log, o.ListenerPortMatchMode), updater: updater, readier: readier, syncCh: make(chan struct{}, 1), diff --git a/internal/provider/apisix/status.go b/internal/provider/apisix/status.go index 327856ec..1fced4e8 100644 --- a/internal/provider/apisix/status.go +++ b/internal/provider/apisix/status.go @@ -123,6 +123,7 @@ func (d *apisixProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1.HTTPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1.HTTPRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs @@ -158,6 +159,7 @@ func (d *apisixProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1alpha2.UDPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1alpha2.UDPRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs @@ -193,6 +195,7 @@ func (d *apisixProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1alpha2.TCPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1alpha2.TCPRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs @@ -228,6 +231,7 @@ func (d *apisixProvider) updateStatus(nnk types.NamespacedNameKind, condition me Resource: &gatewayv1.GRPCRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*gatewayv1.GRPCRoute).DeepCopy() + condition.ObservedGeneration = cp.GetGeneration() gatewayNs := cp.GetNamespace() for i, ref := range cp.Status.Parents { ns := gatewayNs diff --git a/internal/provider/options.go b/internal/provider/options.go index dbb0760b..c47e7ce9 100644 --- a/internal/provider/options.go +++ b/internal/provider/options.go @@ -19,6 +19,8 @@ package provider import ( "time" + + "github.com/apache/apisix-ingress-controller/internal/controller/config" ) type Option interface { @@ -31,6 +33,7 @@ type Options struct { InitSyncDelay time.Duration DefaultBackendMode string DefaultResolveEndpoints bool + ListenerPortMatchMode config.ListenerPortMatchMode } func (o *Options) ApplyToList(lo *Options) { @@ -49,6 +52,9 @@ func (o *Options) ApplyToList(lo *Options) { if o.DefaultResolveEndpoints { lo.DefaultResolveEndpoints = o.DefaultResolveEndpoints } + if o.ListenerPortMatchMode != "" { + lo.ListenerPortMatchMode = o.ListenerPortMatchMode + } } func (o *Options) ApplyOptions(opts []Option) *Options { diff --git a/internal/webhook/v1/adc_validation.go b/internal/webhook/v1/adc_validation.go index f505fe9a..434115fb 100644 --- a/internal/webhook/v1/adc_validation.go +++ b/internal/webhook/v1/adc_validation.go @@ -56,7 +56,7 @@ func newADCAdmissionValidator(kubeClient client.Client, log logr.Logger) (*adcAd return &adcAdmissionValidator{ kubeClient: kubeClient, client: cli, - translator: adctranslator.NewTranslator(log), + translator: adctranslator.NewTranslator(log, config.ControllerConfig.ListenerPortMatchMode), log: log.WithName("adc-validation"), defaultResolveEndpoint: config.ControllerConfig.ProviderConfig.Type == config.ProviderTypeStandalone, }, nil diff --git a/test/e2e/gatewayapi/status.go b/test/e2e/gatewayapi/status.go index 3c50499f..999ec31c 100644 --- a/test/e2e/gatewayapi/status.go +++ b/test/e2e/gatewayapi/status.go @@ -173,6 +173,7 @@ spec: And( ContainSubstring(`status: "True"`), ContainSubstring(`reason: Accepted`), + ContainSubstring(`observedGeneration: 1`), ), )