From 5f799e2f21fbc16de45522236890f19f1ace5ad7 Mon Sep 17 00:00:00 2001 From: Vishal Date: Sat, 4 Jul 2026 14:26:35 -0400 Subject: [PATCH 1/2] fix: report BackendTrafficPolicy TargetNotFound when it attaches to no target A BackendTrafficPolicy that resolves to zero targets left its status stale: - targetRef/targetRefs pointing at a nonexistent object kept Accepted=True with an out-of-date observedGeneration, and emitted no negative condition (#8926). The policy was added to the result set but processing returned early once the target object could not be resolved, so no ancestor status was ever written. - targetSelectors that match no objects never added the policy to the result set at all, so its status was never reconciled and stayed stale (#8927). After all target-processing loops, report any policy that resolved to zero ancestors with Accepted=False, reason TargetNotFound, and the current generation. Policies using explicit targetRefs use the unresolved target as the ancestor ref; selector-only policies reference the policy itself. Fixes #8926 Fixes #8927 Signed-off-by: Vishal Co-authored-by: Cursor --- internal/gatewayapi/backendtrafficpolicy.go | 75 +++++++ ...endtrafficpolicy-selector-no-match.in.yaml | 50 +++++ ...ndtrafficpolicy-selector-no-match.out.yaml | 203 ++++++++++++++++++ ...ndtrafficpolicy-status-conditions.out.yaml | 60 +++++- ...dtrafficpolicy-targetref-not-found.in.yaml | 47 ++++ ...trafficpolicy-targetref-not-found.out.yaml | 201 +++++++++++++++++ ...or-missing-referencegrant-gateway.out.yaml | 29 +++ ...ctor-missing-referencegrant-route.out.yaml | 29 +++ .../8926-btp-status-target-not-found.md | 1 + 9 files changed, 691 insertions(+), 4 deletions(-) create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.out.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.out.yaml create mode 100644 release-notes/current/bug_fixes/8926-btp-status-target-not-found.md diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 049cd15b7b..eaf08750b1 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -362,6 +362,40 @@ func (t *Translator) ProcessBackendTrafficPolicies( } } + // Report policies that did not attach to any target so their status does not + // go stale. This covers two distinct cases that both resolve to zero ancestors: + // - targetRef/targetRefs pointing at an object that does not exist (#8926) + // - targetSelectors that match no objects (#8927) + // In both cases we set Accepted=False with reason TargetNotFound and the current + // generation, so a policy that stops matching any target no longer keeps a stale + // Accepted=True condition with an out-of-date observedGeneration. + for i, currPolicy := range backendTrafficPolicies { + policyName := utils.NamespacedName(currPolicy) + policy, found := handledPolicies[policyName] + if !found { + // Never matched any target (e.g. targetSelectors matched nothing), so the + // policy was never added to res. Add it now so its status is published. + policy = policyCopies[i] + handledPolicies[policyName] = policy + res = append(res, policy) + } + // If the policy attached to at least one target, it already has ancestor + // status set by the processing loops above; leave it untouched. + if len(policy.Status.Ancestors) > 0 { + continue + } + ancestorRef := ancestorRefForUnattachedBackendTrafficPolicy(policy) + status.SetConditionForPolicyAncestor(&policy.Status, + &ancestorRef, + t.GatewayControllerName, + gwapiv1.PolicyConditionAccepted, + metav1.ConditionFalse, + gwapiv1.PolicyReasonTargetNotFound, + backendTrafficPolicyTargetNotFoundMessage(policy), + policy.Generation, + ) + } + for _, policy := range res { // Truncate Ancestor list of longer than 16 if len(policy.Status.Ancestors) > 16 { @@ -371,6 +405,47 @@ func (t *Translator) ProcessBackendTrafficPolicies( return res } +// ancestorRefForUnattachedBackendTrafficPolicy builds the ancestor reference used to +// report status for a BackendTrafficPolicy that could not be attached to any target. +// When the policy uses an explicit targetRef/targetRefs, the (unresolved) target is +// used as the ancestor so the status clearly points at the missing object. When the +// policy only uses targetSelectors, which name no specific object, the policy itself +// is used as the ancestor. +func ancestorRefForUnattachedBackendTrafficPolicy(policy *egv1a1.BackendTrafficPolicy) gwapiv1.ParentReference { + if refs := policy.Spec.GetTargetRefs(); len(refs) > 0 { + ref := refs[0] + group := ref.Group + kind := ref.Kind + ns := gwapiv1.Namespace(policy.Namespace) + return gwapiv1.ParentReference{ + Group: &group, + Kind: &kind, + Namespace: &ns, + Name: ref.Name, + SectionName: ref.SectionName, + } + } + + ns := gwapiv1.Namespace(policy.Namespace) + return gwapiv1.ParentReference{ + Group: GroupPtr(egv1a1.GroupName), + Kind: KindPtr(egv1a1.KindBackendTrafficPolicy), + Namespace: &ns, + Name: gwapiv1.ObjectName(policy.Name), + } +} + +// backendTrafficPolicyTargetNotFoundMessage returns a human-readable message explaining +// why an unattached BackendTrafficPolicy resolved to zero targets. +func backendTrafficPolicyTargetNotFoundMessage(policy *egv1a1.BackendTrafficPolicy) string { + if refs := policy.Spec.GetTargetRefs(); len(refs) > 0 { + ref := refs[0] + return fmt.Sprintf("The BackendTrafficPolicy target %s %s/%s was not found", + ref.Kind, policy.Namespace, ref.Name) + } + return "The BackendTrafficPolicy targetSelectors did not match any target" +} + func (t *Translator) buildGatewayPolicyMap( backendTrafficPolicies []*egv1a1.BackendTrafficPolicy, gateways []*GatewayContext, diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.in.yaml new file mode 100644 index 0000000000..60d4ca7fa7 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.in.yaml @@ -0,0 +1,50 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: listener-1 + protocol: HTTP + port: 8081 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + labels: + app: real-label + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: listener-1 + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-with-unmatched-selector + spec: + targetSelectors: + - group: gateway.networking.k8s.io + kind: HTTPRoute + matchLabels: + app: no-such-label + timeout: + tcp: + connectTimeout: 15s diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.out.yaml new file mode 100644 index 0000000000..4c2a983e63 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-selector-no-match.out.yaml @@ -0,0 +1,203 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-with-unmatched-selector + namespace: default + spec: + targetSelectors: + - group: gateway.networking.k8s.io + kind: HTTPRoute + matchLabels: + app: no-such-label + timeout: + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.envoyproxy.io + kind: BackendTrafficPolicy + name: policy-with-unmatched-selector + namespace: default + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy targetSelectors did not match any target + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: listener-1 + port: 8081 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: listener-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + labels: + app: real-label + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/listener-1 + ports: + - containerPort: 8081 + name: http-8081 + protocol: HTTP + servicePort: 8081 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 8081 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 + name: envoy-gateway/gateway-1/listener-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8081 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + name: "" + prefix: /foo + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-status-conditions.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-status-conditions.out.yaml index ce7c003177..b0b6c5dc1e 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-status-conditions.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-status-conditions.out.yaml @@ -93,7 +93,20 @@ backendTrafficPolicies: kind: HTTPRoute name: unknown-httproute status: - ancestors: null + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: unknown-httproute + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy target HTTPRoute envoy-gateway/unknown-httproute + was not found + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: @@ -105,7 +118,20 @@ backendTrafficPolicies: kind: HTTPRoute name: not-same-namespace-httproute status: - ancestors: null + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: not-same-namespace-httproute + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy target HTTPRoute envoy-gateway/not-same-namespace-httproute + was not found + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: @@ -177,7 +203,20 @@ backendTrafficPolicies: kind: Gateway name: unknown-gateway status: - ancestors: null + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: unknown-gateway + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy target Gateway envoy-gateway/unknown-gateway + was not found + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: @@ -189,7 +228,20 @@ backendTrafficPolicies: kind: Gateway name: not-same-namespace-gateway status: - ancestors: null + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: not-same-namespace-gateway + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy target Gateway envoy-gateway/not-same-namespace-gateway + was not found + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller gateways: - apiVersion: gateway.networking.k8s.io/v1beta1 kind: Gateway diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.in.yaml new file mode 100644 index 0000000000..341ccd2e12 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.in.yaml @@ -0,0 +1,47 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: listener-1 + protocol: HTTP + port: 8081 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: listener-1 + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-nonexistent-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: does-not-exist + timeout: + tcp: + connectTimeout: 15s diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.out.yaml new file mode 100644 index 0000000000..a1d3359138 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found.out.yaml @@ -0,0 +1,201 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-nonexistent-route + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: does-not-exist + timeout: + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: does-not-exist + namespace: default + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy target HTTPRoute default/does-not-exist + was not found + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: listener-1 + port: 8081 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: listener-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/listener-1 + ports: + - containerPort: 8081 + name: http-8081 + protocol: HTTP + servicePort: 8081 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 8081 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 + name: envoy-gateway/gateway-1/listener-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8081 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + name: "" + prefix: /foo + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-gateway.out.yaml b/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-gateway.out.yaml index 3a1c15c021..f9cc0a8993 100644 --- a/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-gateway.out.yaml +++ b/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-gateway.out.yaml @@ -1,3 +1,32 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: cross-ns-policy + namespace: policy-ns + spec: + targetSelectors: + - group: gateway.networking.k8s.io + kind: Gateway + matchLabels: + policy: selected + namespaces: + from: All + useClientProtocol: true + status: + ancestors: + - ancestorRef: + group: gateway.envoyproxy.io + kind: BackendTrafficPolicy + name: cross-ns-policy + namespace: policy-ns + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy targetSelectors did not match any target + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway diff --git a/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-route.out.yaml b/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-route.out.yaml index 129c29c879..3ce067dd4d 100644 --- a/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-route.out.yaml +++ b/internal/gatewayapi/testdata/policy-cross-namespace-targetselector-missing-referencegrant-route.out.yaml @@ -1,3 +1,32 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: cross-ns-policy + namespace: policy-ns + spec: + targetSelectors: + - group: gateway.networking.k8s.io + kind: HTTPRoute + matchLabels: + policy: selected + namespaces: + from: All + useClientProtocol: true + status: + ancestors: + - ancestorRef: + group: gateway.envoyproxy.io + kind: BackendTrafficPolicy + name: cross-ns-policy + namespace: policy-ns + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy targetSelectors did not match any target + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway diff --git a/release-notes/current/bug_fixes/8926-btp-status-target-not-found.md b/release-notes/current/bug_fixes/8926-btp-status-target-not-found.md new file mode 100644 index 0000000000..a12008f8e2 --- /dev/null +++ b/release-notes/current/bug_fixes/8926-btp-status-target-not-found.md @@ -0,0 +1 @@ +Fixed BackendTrafficPolicy status going stale when the policy stops attaching to any target: policies whose `targetRef`/`targetRefs` point at a nonexistent object (#8926) or whose `targetSelectors` match no objects (#8927) now report `Accepted=False` with reason `TargetNotFound` and an updated `observedGeneration`, instead of retaining a stale `Accepted=True` condition. From 751ff5e35c8a7d2bc90ef2af005238305bca7055 Mon Sep 17 00:00:00 2001 From: Vishal Date: Sat, 4 Jul 2026 15:00:08 -0400 Subject: [PATCH 2/2] fix: clear stale ancestors for unattached BackendTrafficPolicy Track whether each policy actually attached to a target during translation instead of inferring it from the presence of ancestor status. A policy that carries ancestor status from a previous generation (e.g. it used to match a target but its targetRef/targetSelectors were then changed to match nothing) would otherwise skip the TargetNotFound handling and keep its stale Accepted=True condition. Now any policy not attached to a target this pass has its carried-over ancestors cleared before TargetNotFound is set with the current generation. Adds a golden case exercising a policy with pre-existing stale status. Signed-off-by: Vishal Co-authored-by: Cursor --- internal/gatewayapi/backendtrafficpolicy.go | 60 ++++-- ...y-targetref-not-found-stale-status.in.yaml | 63 ++++++ ...-targetref-not-found-stale-status.out.yaml | 203 ++++++++++++++++++ 3 files changed, 305 insertions(+), 21 deletions(-) create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.out.yaml diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index eaf08750b1..676ff01209 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -263,6 +263,10 @@ func (t *Translator) ProcessBackendTrafficPolicies( handledPolicies := make(map[types.NamespacedName]*egv1a1.BackendTrafficPolicy, policyMapSize) + // Tracks policies that were successfully attached to at least one real target + // during this pass, so we can report the rest (zero targets) as TargetNotFound. + attachedPolicies := make(map[types.NamespacedName]bool, policyMapSize) + // Translate // 1. First translate Policies targeting RouteRules // 2. Next translate Policies targeting xRoutes @@ -286,8 +290,10 @@ func (t *Translator) ProcessBackendTrafficPolicies( res = append(res, policy) } - t.processBackendTrafficPolicyForRoute(xdsIR, - routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget) + if t.processBackendTrafficPolicyForRoute(xdsIR, + routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget) { + attachedPolicies[policyName] = true + } } } } @@ -312,8 +318,10 @@ func (t *Translator) ProcessBackendTrafficPolicies( res = append(res, policy) } - t.processBackendTrafficPolicyForRoute(xdsIR, - routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget) + if t.processBackendTrafficPolicyForRoute(xdsIR, + routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget) { + attachedPolicies[policyName] = true + } } } } @@ -331,8 +339,10 @@ func (t *Translator) ProcessBackendTrafficPolicies( handledPolicies[policyName] = policy res = append(res, policy) } - t.processBackendTrafficPolicyForGateway(xdsIR, - gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget) + if t.processBackendTrafficPolicyForGateway(xdsIR, + gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget) { + attachedPolicies[policyName] = true + } } } } @@ -356,14 +366,16 @@ func (t *Translator) ProcessBackendTrafficPolicies( handledPolicies[policyName] = policy res = append(res, policy) } - t.processBackendTrafficPolicyForGateway(xdsIR, - gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget) + if t.processBackendTrafficPolicyForGateway(xdsIR, + gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget) { + attachedPolicies[policyName] = true + } } } } // Report policies that did not attach to any target so their status does not - // go stale. This covers two distinct cases that both resolve to zero ancestors: + // go stale. This covers two distinct cases that both resolve to zero targets: // - targetRef/targetRefs pointing at an object that does not exist (#8926) // - targetSelectors that match no objects (#8927) // In both cases we set Accepted=False with reason TargetNotFound and the current @@ -371,6 +383,9 @@ func (t *Translator) ProcessBackendTrafficPolicies( // Accepted=True condition with an out-of-date observedGeneration. for i, currPolicy := range backendTrafficPolicies { policyName := utils.NamespacedName(currPolicy) + if attachedPolicies[policyName] { + continue + } policy, found := handledPolicies[policyName] if !found { // Never matched any target (e.g. targetSelectors matched nothing), so the @@ -379,11 +394,10 @@ func (t *Translator) ProcessBackendTrafficPolicies( handledPolicies[policyName] = policy res = append(res, policy) } - // If the policy attached to at least one target, it already has ancestor - // status set by the processing loops above; leave it untouched. - if len(policy.Status.Ancestors) > 0 { - continue - } + // The policy attached to no target. Drop any ancestor status carried over from + // a previous generation (which would otherwise remain stale) and report + // TargetNotFound with the current generation. + policy.Status.Ancestors = nil ancestorRef := ancestorRefForUnattachedBackendTrafficPolicy(policy) status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, @@ -506,7 +520,7 @@ func (t *Translator) processBackendTrafficPolicyForRoute( gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy, policy *egv1a1.BackendTrafficPolicy, currTarget policyTargetReferenceWithSectionName, -) { +) bool { var ( targetedRoute RouteContext resolveErr *status.PolicyResolveError @@ -518,7 +532,7 @@ func (t *Translator) processBackendTrafficPolicyForRoute( // reconciled by multiple controllers. And the other controller may // have the target route. if targetedRoute == nil { - return + return false } // Find the Gateway that the route belongs to and add it to the @@ -568,7 +582,7 @@ func (t *Translator) processBackendTrafficPolicyForRoute( policy.Generation, resolveErr, ) - return + return true } if policy.Spec.MergeType == nil { @@ -670,7 +684,7 @@ func (t *Translator) processBackendTrafficPolicyForRoute( // Check if this policy is overridden by other policies targeting at route rule levels // If policy target is route rule, we can skip the check if currTarget.SectionName != nil { - return + return true } key := policyTargetRouteKey{ @@ -690,6 +704,8 @@ func (t *Translator) processBackendTrafficPolicyForRoute( policy.Generation, ) } + + return true } func (t *Translator) processBackendTrafficPolicyForGateway( @@ -699,7 +715,7 @@ func (t *Translator) processBackendTrafficPolicyForGateway( gatewayPolicyMergedMap *GatewayPolicyRouteMap, policy *egv1a1.BackendTrafficPolicy, currTarget policyTargetReferenceWithSectionName, -) { +) bool { var ( targetedGateway *GatewayContext resolveErr *status.PolicyResolveError @@ -708,7 +724,7 @@ func (t *Translator) processBackendTrafficPolicyForGateway( // Negative statuses have already been assigned so it's safe to skip targetedGateway, resolveErr = resolveBackendTrafficPolicyGatewayTargetRef(currTarget, gatewayMap) if targetedGateway == nil { - return + return false } // Find its ancestor reference by resolved gateway, even with resolve error @@ -723,7 +739,7 @@ func (t *Translator) processBackendTrafficPolicyForGateway( policy.Generation, resolveErr, ) - return + return true } // Set conditions for translation error if it got any @@ -769,6 +785,8 @@ func (t *Translator) processBackendTrafficPolicyForGateway( policy.Generation, ) } + + return true } func resolveBackendTrafficPolicyGatewayTargetRef( diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.in.yaml new file mode 100644 index 0000000000..8e9424ae7c --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.in.yaml @@ -0,0 +1,63 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: listener-1 + protocol: HTTP + port: 8081 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: listener-1 + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-was-attached-now-missing + generation: 2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: does-not-exist + timeout: + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + observedGeneration: 1 + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.out.yaml new file mode 100644 index 0000000000..7f2a2ebac5 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-targetref-not-found-stale-status.out.yaml @@ -0,0 +1,203 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + generation: 2 + name: policy-was-attached-now-missing + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: does-not-exist + timeout: + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: does-not-exist + namespace: default + conditions: + - lastTransitionTime: null + message: The BackendTrafficPolicy target HTTPRoute default/does-not-exist + was not found + observedGeneration: 2 + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: listener-1 + port: 8081 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: listener-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/listener-1 + ports: + - containerPort: 8081 + name: http-8081 + protocol: HTTP + servicePort: 8081 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 8081 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: listener-1 + name: envoy-gateway/gateway-1/listener-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8081 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + name: "" + prefix: /foo + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003