Skip to content

BackendTrafficPolicy status becomes stale when targetSelectors change from matching routes to matching no routes #8927

Description

@zhaohuabing

Description

A BackendTrafficPolicy using targetSelectors can become stale in status when the selector is changed from matching an existing route to matching no routes.

After the selector update:

  • the policy metadata.generation increments
  • status still shows the old Accepted=True condition
  • observedGeneration remains stale
  • no condition indicates that the selector now matches zero resources

This makes the policy appear attached/valid even after it no longer selects any targets.

Environment

  • Envoy Gateway repo: main
  • Reproduced at commit: 69b1dde32fdb44cca7eb3ee880064e36f90492f4

Reproducer

  1. Install Envoy Gateway from current main
  2. Apply the quickstart example
  3. Label the quickstart HTTPRoute
  4. Create a BackendTrafficPolicy using targetSelectors that matches that route
  5. Update the same policy so the selector matches no routes

Route setup

Label the quickstart route:

kubectl -n default label httproute backend app=selector-demo --overwrite

Initial policy that matches

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: quickstart-btp-selector-flip
  namespace: default
spec:
  targetSelectors:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    matchLabels:
      app: selector-demo
  retry:
    numRetries: 3
    perRetry:
      backOff:
        baseInterval: 100ms
        maxInterval: 1s
      timeout: 250ms
    retryOn:
      triggers:
      - 5xx
      - gateway-error
      - connect-failure

Observed status after create:

status:
  ancestors:
  - ancestorRef:
      group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
      namespace: default
    conditions:
    - type: Accepted
      status: "True"
      reason: Accepted
      message: Policy has been accepted.
      observedGeneration: 1

Updated policy that matches nothing

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: quickstart-btp-selector-flip
  namespace: default
spec:
  targetSelectors:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    matchLabels:
      app: selector-demo-no-match
  retry:
    numRetries: 3
    perRetry:
      backOff:
        baseInterval: 100ms
        maxInterval: 1s
      timeout: 250ms
    retryOn:
      triggers:
      - 5xx
      - gateway-error
      - connect-failure

The route labels remain:

kubectl -n default get httproute backend --show-labels
# NAME      HOSTNAMES             AGE   LABELS
# backend   ["www.example.com"]   ...   app=selector-demo

So the updated selector no longer matches any route.

Observed behavior

After the update:

  • metadata.generation increments from 1 to 2
  • status still shows:
    • Accepted=True
    • observedGeneration=1
  • no condition indicates:
    • zero selected resources
    • detachment / unattached policy
    • selector resolution result for the new generation

Example:

metadata:
  generation: 2
status:
  ancestors:
  - ancestorRef:
      group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
      namespace: default
    conditions:
    - type: Accepted
      status: "True"
      reason: Accepted
      message: Policy has been accepted.
      observedGeneration: 1

Expected behavior

When a selector-based policy changes from matching resources to matching none, I’d expect status to reflect the new state, for example by:

  • updating observedGeneration to the latest generation
  • surfacing a condition that zero resources are selected / attached
  • or otherwise making it clear that the policy is no longer effectively attached

Why this matters

This is misleading operationally:

  • the initial status looks correct when the selector matches
  • after the update, the policy still appears accepted and attached
  • but it no longer selects any resources
  • stale observedGeneration makes it unclear whether reconciliation completed

This makes selector-based policy debugging harder and can hide unintended detachments.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions