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