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 d029b967 fix: honor BackendTrafficPolicy targetRefs.sectionName for 
Service ports (#2796)
d029b967 is described below

commit d029b96779ec0cbd7f51a633cf02a624ed76999a
Author: AlinsRan <[email protected]>
AuthorDate: Tue Jun 23 09:01:05 2026 +0800

    fix: honor BackendTrafficPolicy targetRefs.sectionName for Service ports 
(#2796)
---
 internal/adc/translator/grpcroute.go           |   2 +-
 internal/adc/translator/httproute.go           |   2 +-
 internal/adc/translator/httproute_test.go      | 122 +++++++++++++++++++++++++
 internal/adc/translator/ingress.go             |   2 +-
 internal/adc/translator/policies.go            |  61 ++++++++++++-
 internal/adc/translator/tcproute.go            |   2 +-
 internal/adc/translator/tlsroute.go            |   2 +-
 internal/adc/translator/udproute.go            |   2 +-
 internal/controller/policies.go                |   9 +-
 test/e2e/crds/v1alpha1/backendtrafficpolicy.go | 104 +++++++++++++++++++++
 10 files changed, 296 insertions(+), 12 deletions(-)

diff --git a/internal/adc/translator/grpcroute.go 
b/internal/adc/translator/grpcroute.go
index 631b34d5..88e12cf9 100644
--- a/internal/adc/translator/grpcroute.go
+++ b/internal/adc/translator/grpcroute.go
@@ -192,7 +192,7 @@ func (t *Translator) TranslateGRPCRoute(tctx 
*provider.TranslateContext, grpcRou
                                continue
                        }
 
-                       
t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef, 
tctx.BackendTrafficPolicies, upstream)
+                       
t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef, 
tctx.BackendTrafficPolicies, upstream, tctx.Services)
                        upstream.Nodes = upNodes
 
                        var (
diff --git a/internal/adc/translator/httproute.go 
b/internal/adc/translator/httproute.go
index 83c728b0..80457957 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -555,7 +555,7 @@ func (t *Translator) translateBackendsToUpstreams(
                        enableWebsocket = ptr.To(true)
                }
 
-               t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef, 
tctx.BackendTrafficPolicies, upstream)
+               t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef, 
tctx.BackendTrafficPolicies, upstream, tctx.Services)
                upstream.Nodes = upNodes
                if upstream.Scheme == "" {
                        upstream.Scheme = appProtocolToUpstreamScheme(protocol)
diff --git a/internal/adc/translator/httproute_test.go 
b/internal/adc/translator/httproute_test.go
index f26831b0..94204109 100644
--- a/internal/adc/translator/httproute_test.go
+++ b/internal/adc/translator/httproute_test.go
@@ -506,3 +506,125 @@ func TestAttachBackendTrafficPolicyHealthCheck(t 
*testing.T) {
                })
        }
 }
+
+func TestAttachBackendTrafficPolicyToUpstreamSectionName(t *testing.T) {
+       const (
+               namespace   = "default"
+               serviceName = "backend"
+               webPort     = int32(80)
+               webName     = "web"
+               adminPort   = int32(9000)
+               adminName   = "admin"
+       )
+
+       serviceKey := types.NamespacedName{Namespace: namespace, Name: 
serviceName}
+       services := map[types.NamespacedName]*corev1.Service{
+               serviceKey: {
+                       ObjectMeta: metav1.ObjectMeta{Name: serviceName, 
Namespace: namespace},
+                       Spec: corev1.ServiceSpec{
+                               Ports: []corev1.ServicePort{
+                                       {Name: webName, Port: webPort},
+                                       {Name: adminName, Port: adminPort},
+                               },
+                       },
+               },
+       }
+
+       newRef := func(port int32) gatewayv1.BackendRef {
+               return gatewayv1.BackendRef{
+                       BackendObjectReference: 
gatewayv1.BackendObjectReference{
+                               Name:      gatewayv1.ObjectName(serviceName),
+                               Namespace: 
ptr.To(gatewayv1.Namespace(namespace)),
+                               Port:      ptr.To(gatewayv1.PortNumber(port)),
+                       },
+               }
+       }
+
+       newPolicy := func(name, sectionName, scheme string) 
*v1alpha1.BackendTrafficPolicy {
+               targetRef := 
v1alpha1.BackendPolicyTargetReferenceWithSectionName{
+                       LocalPolicyTargetReference: 
gatewayv1alpha2.LocalPolicyTargetReference{
+                               Name: gatewayv1alpha2.ObjectName(serviceName),
+                               Kind: 
gatewayv1alpha2.Kind(internaltypes.KindService),
+                       },
+               }
+               if sectionName != "" {
+                       targetRef.SectionName = 
ptr.To(gatewayv1alpha2.SectionName(sectionName))
+               }
+               return &v1alpha1.BackendTrafficPolicy{
+                       ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: 
namespace},
+                       Spec: v1alpha1.BackendTrafficPolicySpec{
+                               TargetRefs: 
[]v1alpha1.BackendPolicyTargetReferenceWithSectionName{targetRef},
+                               Scheme:     scheme,
+                       },
+               }
+       }
+
+       tests := []struct {
+               name       string
+               ref        gatewayv1.BackendRef
+               policies   
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy
+               wantScheme string
+       }{
+               {
+                       name: "sectionName matches the backend port name",
+                       ref:  newRef(webPort),
+                       policies: 
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy{
+                               {Namespace: namespace, Name: "p"}: 
newPolicy("p", webName, apiv2.SchemeHTTPS),
+                       },
+                       wantScheme: apiv2.SchemeHTTPS,
+               },
+               {
+                       name: "sectionName does not match the backend port 
name",
+                       ref:  newRef(adminPort),
+                       policies: 
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy{
+                               {Namespace: namespace, Name: "p"}: 
newPolicy("p", webName, apiv2.SchemeHTTPS),
+                       },
+                       wantScheme: "",
+               },
+               {
+                       name: "no sectionName applies to the whole service",
+                       ref:  newRef(adminPort),
+                       policies: 
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy{
+                               {Namespace: namespace, Name: "p"}: 
newPolicy("p", "", apiv2.SchemeHTTPS),
+                       },
+                       wantScheme: apiv2.SchemeHTTPS,
+               },
+               {
+                       name: "port-specific policy takes precedence over 
whole-service policy",
+                       ref:  newRef(adminPort),
+                       policies: 
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy{
+                               {Namespace: namespace, Name: "generic"}:  
newPolicy("generic", "", apiv2.SchemeHTTP),
+                               {Namespace: namespace, Name: "specific"}: 
newPolicy("specific", adminName, apiv2.SchemeHTTPS),
+                       },
+                       wantScheme: apiv2.SchemeHTTPS,
+               },
+               {
+                       name: "targetRef kind mismatch does not attach to a 
same-named service",
+                       ref:  newRef(webPort),
+                       policies: 
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy{
+                               {Namespace: namespace, Name: "p"}: {
+                                       ObjectMeta: metav1.ObjectMeta{Name: 
"p", Namespace: namespace},
+                                       Spec: v1alpha1.BackendTrafficPolicySpec{
+                                               TargetRefs: 
[]v1alpha1.BackendPolicyTargetReferenceWithSectionName{{
+                                                       
LocalPolicyTargetReference: gatewayv1alpha2.LocalPolicyTargetReference{
+                                                               Name: 
gatewayv1alpha2.ObjectName(serviceName),
+                                                               Kind: 
gatewayv1alpha2.Kind("ServiceImport"),
+                                                       },
+                                               }},
+                                               Scheme: apiv2.SchemeHTTPS,
+                                       },
+                               },
+                       },
+                       wantScheme: "",
+               },
+       }
+
+       translator := NewTranslator(logr.Discard(), "")
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       upstream := adctypes.NewDefaultUpstream()
+                       translator.AttachBackendTrafficPolicyToUpstream(tt.ref, 
tt.policies, upstream, services)
+                       assert.Equal(t, tt.wantScheme, upstream.Scheme)
+               })
+       }
+}
diff --git a/internal/adc/translator/ingress.go 
b/internal/adc/translator/ingress.go
index 4b6abf79..aca8aed4 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -187,7 +187,7 @@ func (t *Translator) resolveIngressUpstream(
                ns = config.ServiceNamespace
        }
        backendRef := convertBackendRef(ns, backendService.Name, 
internaltypes.KindService)
-       t.AttachBackendTrafficPolicyToUpstream(backendRef, 
tctx.BackendTrafficPolicies, upstream)
+       t.AttachBackendTrafficPolicyToUpstream(backendRef, 
tctx.BackendTrafficPolicies, upstream, tctx.Services)
        if config != nil {
                upConfig := config.Upstream
                if upConfig.Scheme != "" {
diff --git a/internal/adc/translator/policies.go 
b/internal/adc/translator/policies.go
index 9bf65999..f8a5b0a8 100644
--- a/internal/adc/translator/policies.go
+++ b/internal/adc/translator/policies.go
@@ -20,6 +20,7 @@ package translator
 import (
        "encoding/json"
 
+       corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/types"
        "k8s.io/utils/ptr"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -28,6 +29,7 @@ import (
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
 func convertBackendRef(namespace, name, kind string) gatewayv1.BackendRef {
@@ -38,28 +40,77 @@ func convertBackendRef(namespace, name, kind string) 
gatewayv1.BackendRef {
        return backendRef
 }
 
-func (t *Translator) AttachBackendTrafficPolicyToUpstream(ref 
gatewayv1.BackendRef, policies 
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy, upstream 
*adctypes.Upstream) {
+func (t *Translator) AttachBackendTrafficPolicyToUpstream(ref 
gatewayv1.BackendRef, policies 
map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy, upstream 
*adctypes.Upstream, services map[types.NamespacedName]*corev1.Service) {
        if len(policies) == 0 {
                return
        }
-       var policy *v1alpha1.BackendTrafficPolicy
+       // Resolve the backend ref group/kind, applying the Gateway API defaults
+       // (empty group = core, Service kind) so a targetRef is only matched 
against
+       // a backend of the same resource type.
+       refGroup := ""
+       if ref.Group != nil {
+               refGroup = string(*ref.Group)
+       }
+       refKind := internaltypes.KindService
+       if ref.Kind != nil {
+               refKind = string(*ref.Kind)
+       }
+       // A targetRef with sectionName scopes the policy to a specific Service 
port
+       // (matched by port name). It takes precedence over a whole-Service 
targetRef
+       // (no sectionName) that matches the same backend.
+       var genericPolicy, specificPolicy *v1alpha1.BackendTrafficPolicy
        for _, po := range policies {
                if ref.Namespace != nil && string(*ref.Namespace) != 
po.Namespace {
                        continue
                }
                for _, targetRef := range po.Spec.TargetRefs {
-                       if ref.Name == targetRef.Name {
-                               policy = po
-                               break
+                       if ref.Name != targetRef.Name {
+                               continue
+                       }
+                       if targetRef.Group != "" && string(targetRef.Group) != 
refGroup {
+                               continue
+                       }
+                       if targetRef.Kind != "" && string(targetRef.Kind) != 
refKind {
+                               continue
+                       }
+                       if targetRef.SectionName != nil && 
*targetRef.SectionName != "" {
+                               if backendRefMatchesSectionName(ref, 
po.Namespace, string(*targetRef.SectionName), services) {
+                                       specificPolicy = po
+                               }
+                               continue
                        }
+                       genericPolicy = po
                }
        }
+       policy := specificPolicy
+       if policy == nil {
+               policy = genericPolicy
+       }
        if policy == nil {
                return
        }
        t.attachBackendTrafficPolicyToUpstream(policy, upstream)
 }
 
+// backendRefMatchesSectionName reports whether the backend ref resolves to the
+// Service port named sectionName. Per the Gateway API policy semantics, when a
+// sectionName is specified but cannot be resolved, the policy must not attach.
+func backendRefMatchesSectionName(ref gatewayv1.BackendRef, namespace, 
sectionName string, services map[types.NamespacedName]*corev1.Service) bool {
+       if ref.Port == nil {
+               return false
+       }
+       svc, ok := services[types.NamespacedName{Namespace: namespace, Name: 
string(ref.Name)}]
+       if !ok || svc == nil {
+               return false
+       }
+       for _, port := range svc.Spec.Ports {
+               if port.Port == int32(*ref.Port) {
+                       return port.Name == sectionName
+               }
+       }
+       return false
+}
+
 func (t *Translator) attachBackendTrafficPolicyToUpstream(policy 
*v1alpha1.BackendTrafficPolicy, upstream *adctypes.Upstream) {
        if policy == nil {
                return
diff --git a/internal/adc/translator/tcproute.go 
b/internal/adc/translator/tcproute.go
index c7f00e0e..36c43880 100644
--- a/internal/adc/translator/tcproute.go
+++ b/internal/adc/translator/tcproute.go
@@ -69,7 +69,7 @@ func (t *Translator) TranslateTCPRoute(tctx 
*provider.TranslateContext, tcpRoute
                                continue
                        }
                        // TODO: Confirm BackendTrafficPolicy attachment with 
e2e test case.
-                       t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream)
+                       t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream, tctx.Services)
                        upstream.Nodes = upNodes
                        var (
                                kind string
diff --git a/internal/adc/translator/tlsroute.go 
b/internal/adc/translator/tlsroute.go
index 4b2ef33d..236612f4 100644
--- a/internal/adc/translator/tlsroute.go
+++ b/internal/adc/translator/tlsroute.go
@@ -62,7 +62,7 @@ func (t *Translator) TranslateTLSRoute(tctx 
*provider.TranslateContext, tlsRoute
                                continue
                        }
                        // TODO: Confirm BackendTrafficPolicy attachment with 
e2e test case.
-                       t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream)
+                       t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream, tctx.Services)
                        upstream.Nodes = upNodes
                        var (
                                kind string
diff --git a/internal/adc/translator/udproute.go 
b/internal/adc/translator/udproute.go
index 00c90f3e..650c4256 100644
--- a/internal/adc/translator/udproute.go
+++ b/internal/adc/translator/udproute.go
@@ -58,7 +58,7 @@ func (t *Translator) TranslateUDPRoute(tctx 
*provider.TranslateContext, udpRoute
                                continue
                        }
                        // TODO: Confirm BackendTrafficPolicy attachment with 
e2e test case.
-                       t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream)
+                       t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream, tctx.Services)
                        upstream.Nodes = upNodes
                        var (
                                kind string
diff --git a/internal/controller/policies.go b/internal/controller/policies.go
index ef563d2a..64c5d5ce 100644
--- a/internal/controller/policies.go
+++ b/internal/controller/policies.go
@@ -47,10 +47,14 @@ import (
 type PolicyTargetKey struct {
        NsName    types.NamespacedName
        GroupKind schema.GroupKind
+       // SectionName scopes the target to a specific section (for a Service, 
the
+       // port name). Policies that target different sections of the same 
resource
+       // do not conflict; an empty SectionName targets the whole resource.
+       SectionName string
 }
 
 func (p PolicyTargetKey) String() string {
-       return p.NsName.String() + "/" + p.GroupKind.String()
+       return p.NsName.String() + "/" + p.GroupKind.String() + "/" + 
p.SectionName
 }
 
 func BackendTrafficPolicyPredicateFunc(channel chan event.GenericEvent) 
predicate.Predicate {
@@ -143,6 +147,9 @@ func ProcessBackendTrafficPolicy(
                                NsName:    types.NamespacedName{Namespace: 
p.GetNamespace(), Name: string(targetRef.Name)},
                                GroupKind: schema.GroupKind{Group: "", Kind: 
internaltypes.KindService},
                        }
+                       if sectionName != nil {
+                               key.SectionName = string(*sectionName)
+                       }
                        condition := NewPolicyCondition(policy.Generation, 
true, "Policy has been accepted")
                        if sectionName != nil && 
!servicePortNameMap[fmt.Sprintf("%s/%s/%s", policy.Namespace, 
string(targetRef.Name), *sectionName)] {
                                condition = 
NewPolicyCondition(policy.Generation, false, fmt.Sprintf("No section name %s 
found in Service %s/%s", *sectionName, policy.Namespace, targetRef.Name))
diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go 
b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
index 3a551910..46ddeeb4 100644
--- a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
+++ b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
@@ -160,6 +160,110 @@ spec:
                })
        })
 
+       Context("Section Name", func() {
+               // httpbin-service-e2e-test exposes two named ports backed by 
the same pod:
+               // "http" (80) and "http-v2" (8080). A single HTTPRoute routes 
/get to port
+               // 80 and /headers to port 8080, so the two rules share the 
same Service but
+               // resolve to different ports.
+               var routeWithTwoPorts = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: httpbin
+  namespace: %s
+spec:
+  parentRefs:
+  - name: %s
+  hostnames:
+  - "httpbin.org"
+  rules:
+  - matches:
+    - path:
+        type: Exact
+        value: /get
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+  - matches:
+    - path:
+        type: Exact
+        value: /headers
+    backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 8080
+`
+
+               // sectionPolicy is scoped to the http-v2 (8080) port via 
sectionName.
+               var sectionPolicy = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: httpbin-section
+spec:
+  targetRefs:
+  - name: httpbin-service-e2e-test
+    kind: Service
+    group: ""
+    sectionName: http-v2
+  passHost: rewrite
+  upstreamHost: section.http-v2.example.com
+`
+
+               // wholePolicy has no sectionName, so it targets the whole 
Service.
+               var wholePolicy = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+  name: httpbin-whole
+spec:
+  targetRefs:
+  - name: httpbin-service-e2e-test
+    kind: Service
+    group: ""
+  passHost: rewrite
+  upstreamHost: whole.service.example.com
+`
+
+               BeforeEach(func() {
+                       gatewayBeforeEach()
+                       By("recreate the HTTPRoute with two rules to ports 80 
and 8080")
+                       s.ApplyHTTPRoute(types.NamespacedName{Namespace: 
s.Namespace(), Name: "httpbin"}, fmt.Sprintf(routeWithTwoPorts, s.Namespace(), 
s.Namespace()))
+               })
+
+               It("applies the sectionName-scoped policy only to the matching 
port", func() {
+                       s.ResourceApplied("BackendTrafficPolicy", 
"httpbin-section", sectionPolicy, 1)
+                       s.ResourceApplied("BackendTrafficPolicy", 
"httpbin-whole", wholePolicy, 1)
+
+                       // /headers -> port 8080: both policies match by name, 
but the
+                       // sectionName-scoped one wins, so the http-v2 host is 
used.
+                       By("the http-v2 (8080) port uses the sectionName-scoped 
policy")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/headers",
+                               Host:   "httpbin.org",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       scaffold.WithExpectedStatus(200),
+                                       
scaffold.WithExpectedBodyContains("section.http-v2.example.com"),
+                                       
scaffold.WithExpectedBodyNotContains("whole.service.example.com"),
+                               },
+                       })
+
+                       // /get -> port 80: the sectionName-scoped policy does 
not match this
+                       // port, so only the whole-Service policy applies.
+                       By("the http (80) port falls back to the whole-Service 
policy")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "httpbin.org",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       scaffold.WithExpectedStatus(200),
+                                       
scaffold.WithExpectedBodyContains("whole.service.example.com"),
+                                       
scaffold.WithExpectedBodyNotContains("section.http-v2.example.com"),
+                               },
+                       })
+               })
+       })
+
        Context("Health Check", func() {
                var policyWithActiveHealthCheck = `
 apiVersion: apisix.apache.org/v1alpha1

Reply via email to