From b2b2b358a08b225eddf0f460907b32ed4feb3219 Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:05:07 +0800 Subject: [PATCH 01/13] fix: stale HTTPRoute/GRPCRoute/TLSRoute/TCPRoute/UDPRoute status after gateway-only spec changes Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- internal/gatewayapi/helpers.go | 9 +- internal/gatewayapi/route.go | 78 +++- internal/gatewayapi/runner/runner_test.go | 425 ++++++++++++++++++++++ 3 files changed, 494 insertions(+), 18 deletions(-) diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 03736100c3..67436e6713 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -147,7 +147,8 @@ func IsRefToGateway(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentRe // in the given list, and if so, a list of the Listeners within that Gateway or ListenerSet that // are included by the parent ref (either one specific Listener, or all Listeners // in the Gateway or ListenerSet, depending on whether section name is specified or not). -func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentReference, gateways []*GatewayContext) (bool, []*ListenerContext) { +// The second return value is the matched GatewayContext when the parentRef points to a Gateway +func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentReference, gateways []*GatewayContext) (bool, *GatewayContext, []*ListenerContext) { var referencedListeners []*ListenerContext // The parentRef is an ListenerSet @@ -173,7 +174,7 @@ func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1. } } } - return matchedListenerSet, referencedListeners + return matchedListenerSet, nil, referencedListeners } // The parentRef is a Gateway @@ -188,11 +189,11 @@ func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1. referencedListeners = append(referencedListeners, listener) } } - return true, referencedListeners + return true, gateway, referencedListeners } } - return false, referencedListeners + return false, nil, referencedListeners } func isRefToListenerSet(parentRef gwapiv1.ParentReference) bool { diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 3ea595a13a..2a45a4d7ff 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -116,6 +116,15 @@ func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, res // any conditions that come out of it have to go on each RouteParentStatus, // not on the Route as a whole. routeRoutes, errs, unacceptedRules := t.processHTTPRouteRules(httpRoute, parentRef, resources) + + // observedGeneration is the max of the route's own generation and the matched + // Gateway's generation. Gateway-only edit produces a condition value that differs + // from the stale stored one, unblocking the watchable DeepEqual gate. + observedGeneration := httpRoute.GetGeneration() + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + observedGeneration = gw.GetGeneration() + } + if len(errs) > 0 { routeStatus := GetRouteStatus(httpRoute) // errs are already grouped by condition type in TypedErrorCollector @@ -186,7 +195,7 @@ func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, res routeStatus := GetRouteStatus(httpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - httpRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingListenerHostname, @@ -205,7 +214,7 @@ func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, res routeStatus := GetRouteStatus(httpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - httpRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, @@ -846,6 +855,15 @@ func (t *Translator) processGRPCRouteParentRefs(grpcRoute *GRPCRouteContext, res // any conditions that come out of it have to go on each RouteParentStatus, // not on the Route as a whole. routeRoutes, errs, unacceptedRules := t.processGRPCRouteRules(grpcRoute, parentRef, resources) + + // observedGeneration is the max of the route's own generation and the matched + // Gateway's generation. Gateway-only edit produces a condition value that differs + // from the stale stored one, unblocking the watchable DeepEqual gate. + observedGeneration := grpcRoute.GetGeneration() + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + observedGeneration = gw.GetGeneration() + } + if len(errs) > 0 { routeStatus := GetRouteStatus(grpcRoute) // errs are already grouped by condition type in TypedErrorCollector @@ -920,7 +938,7 @@ func (t *Translator) processGRPCRouteParentRefs(grpcRoute *GRPCRouteContext, res routeStatus := GetRouteStatus(grpcRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - grpcRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingListenerHostname, @@ -934,7 +952,7 @@ func (t *Translator) processGRPCRouteParentRefs(grpcRoute *GRPCRouteContext, res routeStatus := GetRouteStatus(grpcRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - grpcRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, @@ -1395,6 +1413,14 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour destName = irRouteDestinationName(tlsRoute, -1 /*rule index*/) ) + // observedGeneration is the max of the route's own generation and the matched + // Gateway's generation. Gateway-only edit produces a condition value that differs + // from the stale stored one, unblocking the watchable DeepEqual gate. + observedGeneration := tlsRoute.GetGeneration() + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + observedGeneration = gw.GetGeneration() + } + // compute backends for _, rule := range tlsRoute.Spec.Rules { for i := range rule.BackendRefs { @@ -1495,7 +1521,7 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour routeStatus := GetRouteStatus(tlsRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - tlsRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingListenerHostname, @@ -1514,7 +1540,7 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour routeStatus := GetRouteStatus(tlsRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - tlsRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, @@ -1578,6 +1604,14 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour destName = irRouteDestinationName(udpRoute, -1 /*rule index*/) ) + // observedGeneration is the max of the route's own generation and the matched + // Gateway's generation. Gateway-only edit produces a condition value that differs + // from the stale stored one, unblocking the watchable DeepEqual gate. + observedGeneration := udpRoute.GetGeneration() + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + observedGeneration = gw.GetGeneration() + } + for i := range udpRoute.Spec.Rules[0].BackendRefs { settingName := irDestinationSettingName(destName, i) backendRefCtx := DirectBackendRef{BackendRef: &udpRoute.Spec.Rules[0].BackendRefs[i]} @@ -1655,7 +1689,7 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour routeStatus := GetRouteStatus(udpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - udpRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, @@ -1667,7 +1701,7 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour routeStatus := GetRouteStatus(udpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - udpRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonUnsupportedValue, @@ -1731,6 +1765,14 @@ func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resour destName = irRouteDestinationName(tcpRoute, -1 /*rule index*/) ) + // observedGeneration is the max of the route's own generation and the matched + // Gateway's generation. Gateway-only edit produces a condition value that differs + // from the stale stored one, unblocking the watchable DeepEqual gate. + observedGeneration := tcpRoute.GetGeneration() + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + observedGeneration = gw.GetGeneration() + } + for i := range tcpRoute.Spec.Rules[0].BackendRefs { settingName := irDestinationSettingName(destName, i) backendRefCtx := DirectBackendRef{BackendRef: &tcpRoute.Spec.Rules[0].BackendRefs[i]} @@ -1817,7 +1859,7 @@ func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resour routeStatus := GetRouteStatus(tcpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - tcpRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, @@ -1828,7 +1870,7 @@ func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resour routeStatus := GetRouteStatus(tcpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - tcpRoute.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonUnsupportedValue, @@ -2268,7 +2310,7 @@ func (t *Translator) processAllowedListenersForParentRefs( var relevantRoute bool ns := gwapiv1.Namespace(routeContext.GetNamespace()) for _, parentRef := range GetParentReferences(routeContext) { - isRelevantParentRef, selectedListeners := GetReferencedListeners(ns, parentRef, gateways) + isRelevantParentRef, matchedGateway, selectedListeners := GetReferencedListeners(ns, parentRef, gateways) // Parent ref is not to a Gateway that we control: skip it if !isRelevantParentRef { @@ -2280,11 +2322,19 @@ func (t *Translator) processAllowedListenersForParentRefs( // Reset conditions since they will be recomputed during translation parentRefCtx.ResetConditions(routeContext) + // observedGeneration is the max of the route's own generation and the matched + // Gateway's generation. Gateway-only edit produces a condition value that differs + // from the stale stored one, unblocking the watchable DeepEqual gate. + observedGeneration := routeContext.GetGeneration() + if matchedGateway != nil && matchedGateway.GetGeneration() > observedGeneration { + observedGeneration = matchedGateway.GetGeneration() + } + if len(selectedListeners) == 0 { routeStatus := GetRouteStatus(routeContext) status.SetRouteStatusCondition(routeStatus, parentRefCtx.routeParentStatusIdx, - routeContext.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingParent, @@ -2306,7 +2356,7 @@ func (t *Translator) processAllowedListenersForParentRefs( routeStatus := GetRouteStatus(routeContext) status.SetRouteStatusCondition(routeStatus, parentRefCtx.routeParentStatusIdx, - routeContext.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNotAllowedByListeners, @@ -2319,7 +2369,7 @@ func (t *Translator) processAllowedListenersForParentRefs( routeStatus := GetRouteStatus(routeContext) status.SetRouteStatusCondition(routeStatus, parentRefCtx.routeParentStatusIdx, - routeContext.GetGeneration(), + observedGeneration, gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, diff --git a/internal/gatewayapi/runner/runner_test.go b/internal/gatewayapi/runner/runner_test.go index b85eb102a3..12b34ca93b 100644 --- a/internal/gatewayapi/runner/runner_test.go +++ b/internal/gatewayapi/runner/runner_test.go @@ -15,7 +15,11 @@ import ( "time" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -23,6 +27,7 @@ import ( "github.com/envoyproxy/gateway/internal/crypto" "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/extension/registry" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/message" pb "github.com/envoyproxy/gateway/proto/extension" @@ -522,3 +527,423 @@ func TestLoadTLSConfig_HostMode(t *testing.T) { require.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth) require.Equal(t, uint16(tls.VersionTLS13), tlsConfig.MinVersion) } + +func TestHTTPRouteStatusStaleAfterListenerHostnameChange(t *testing.T) { + pResources := new(message.ProviderResources) + xdsIR := new(message.XdsIR) + infraIR := new(message.InfraIR) + cfg, err := config.New(os.Stdout, os.Stderr) + require.NoError(t, err) + extMgr, closeFunc, err := registry.NewInMemoryManager(&egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{}) + require.NoError(t, err) + defer closeFunc() + + r := New(&Config{ + Server: *cfg, + ProviderResources: pResources, + RunnerErrors: new(message.RunnerErrors), + XdsIR: xdsIR, + InfraIR: infraIR, + ExtensionManager: extMgr, + }) + err = r.Start(t.Context()) + require.NoError(t, err) + + routeKey := types.NamespacedName{Namespace: "default", Name: "httproute-1"} + + // buildResources constructs a minimal ControllerResources snapshot. + // The HTTPRoute's generation is fixed at 1 throughout, only the Gateway changes. + buildResources := func(listenerHostname string, gatewayGeneration int64) *resource.ControllerResources { + listenerHostnameTyped := gwapiv1.Hostname(listenerHostname) + res := resource.NewResources() + res.GatewayClass = &gwapiv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{Name: "envoy-gateway-class"}, + Spec: gwapiv1.GatewayClassSpec{ + ControllerName: egv1a1.GatewayControllerName, + }, + } + res.Gateways = []*gwapiv1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "envoy-gateway", + Name: "gateway-1", + Generation: gatewayGeneration, + }, + Spec: gwapiv1.GatewaySpec{ + GatewayClassName: "envoy-gateway-class", + Listeners: []gwapiv1.Listener{ + { + Name: "http", + Protocol: gwapiv1.HTTPProtocolType, + Port: 80, + Hostname: &listenerHostnameTyped, + AllowedRoutes: &gwapiv1.AllowedRoutes{ + Namespaces: &gwapiv1.RouteNamespaces{ + From: func() *gwapiv1.FromNamespaces { + f := gwapiv1.NamespacesFromAll + return &f + }(), + }, + }, + }, + }, + }, + }, + } + routeHostname := gwapiv1.Hostname("app.example.com") + res.HTTPRoutes = []*gwapiv1.HTTPRoute{ + { + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "httproute-1", + Generation: 1, // intentionally fixed — route is never edited + }, + Spec: gwapiv1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1.CommonRouteSpec{ + ParentRefs: []gwapiv1.ParentReference{ + { + Namespace: func() *gwapiv1.Namespace { + ns := gwapiv1.Namespace("envoy-gateway") + return &ns + }(), + Name: "gateway-1", + }, + }, + }, + Hostnames: []gwapiv1.Hostname{routeHostname}, + Rules: []gwapiv1.HTTPRouteRule{ + { + BackendRefs: []gwapiv1.HTTPBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Name: "service-1", + Port: func() *gwapiv1.PortNumber { + p := gwapiv1.PortNumber(8080) + return &p + }(), + }, + }, + }, + }, + }, + }, + }, + }, + } + res.Namespaces = []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "envoy-gateway"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "default"}}, + } + res.Services = []*corev1.Service{ + { + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "service-1"}, + Spec: corev1.ServiceSpec{ + ClusterIP: "1.1.1.1", + Ports: []corev1.ServicePort{ + {Name: "http", Port: 8080, TargetPort: intstr.FromInt32(8080), Protocol: corev1.ProtocolTCP}, + }, + }, + }, + } + res.EndpointSlices = []*discoveryv1.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "endpointslice-1", + Labels: map[string]string{discoveryv1.LabelServiceName: "service-1"}, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Ports: []discoveryv1.EndpointPort{ + {Name: func() *string { s := "http"; return &s }(), Port: func() *int32 { p := int32(8080); return &p }(), Protocol: func() *corev1.Protocol { p := corev1.ProtocolTCP; return &p }()}, + }, + Endpoints: []discoveryv1.Endpoint{ + {Addresses: []string{"7.7.7.7"}, Conditions: discoveryv1.EndpointConditions{Ready: func() *bool { b := true; return &b }()}}, + }, + }, + } + cr := resource.ControllerResources{res} + return &cr + } + + // Cycle 1: Gateway gen=1, listener hostname does NOT match the route hostname + // Route: "app.example.com", Listener: "other.example.com", Accepted=False/NoMatchingListenerHostname + pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ + Resources: buildResources("other.example.com", 1), + }) + + require.Eventually(t, func() bool { + status, ok := pResources.HTTPRouteStatuses.Load(routeKey) + if !ok || status == nil || len(status.Parents) == 0 { + return false + } + for _, cond := range status.Parents[0].Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + return cond.Status == metav1.ConditionFalse && + cond.Reason == string(gwapiv1.RouteReasonNoMatchingListenerHostname) && + cond.ObservedGeneration == 1 + } + } + return false + }, 5*time.Second, 50*time.Millisecond, + "cycle 1: expected Accepted=False/NoMatchingListenerHostname with ObservedGeneration=1") + + // Cycle 2: Gateway gen=2, still mismatching hostname + // If the route did not change, gen still 1. And neither did the condition outcome, still Accepted=False/NoMatchingListenerHostname. + // The new status value is byte-for-byte identical to cycle 1's value. So the subscriber goroutine never fires and Kubernetes status is never patched. + pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ + Resources: buildResources("other.example.com", 2), + }) + + require.Eventually(t, func() bool { + status, ok := pResources.HTTPRouteStatuses.Load(routeKey) + if !ok || status == nil || len(status.Parents) == 0 { + return false + } + for _, cond := range status.Parents[0].Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + // must have bumped ObservedGeneration to 2 even though + // the condition outcome (False/NoMatchingListenerHostname) is unchanged. + return cond.Status == metav1.ConditionFalse && + cond.Reason == string(gwapiv1.RouteReasonNoMatchingListenerHostname) && + cond.ObservedGeneration == 2 + } + } + return false + }, 5*time.Second, 50*time.Millisecond, + "cycle 2: expected ObservedGeneration to be bumped to 2 when Gateway generation increased, "+ + "even though the condition outcome (Accepted=False/NoMatchingListenerHostname) is unchanged. "+ + "Without the fix, ObservedGeneration stays at 1 and the watchable DeepEqual gate suppresses the update.") + + // Cycle 3: Gateway gen=3, hostname is fixed to match the route + // Now both hostnames agree, Accepted=True, ObservedGeneration=3 + pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ + Resources: buildResources("app.example.com", 3), + }) + + require.Eventually(t, func() bool { + status, ok := pResources.HTTPRouteStatuses.Load(routeKey) + if !ok || status == nil || len(status.Parents) == 0 { + return false + } + for _, cond := range status.Parents[0].Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + return cond.Status == metav1.ConditionTrue + } + } + return false + }, 5*time.Second, 50*time.Millisecond, + "cycle 3: expected Accepted=True after Gateway listener hostname was fixed to match the route") +} + +func TestHTTPRouteStatusStaleAfterAllowedRoutesChange(t *testing.T) { + pResources := new(message.ProviderResources) + xdsIR := new(message.XdsIR) + infraIR := new(message.InfraIR) + cfg, err := config.New(os.Stdout, os.Stderr) + require.NoError(t, err) + extMgr, closeFunc, err := registry.NewInMemoryManager(&egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{}) + require.NoError(t, err) + defer closeFunc() + + r := New(&Config{ + Server: *cfg, + ProviderResources: pResources, + RunnerErrors: new(message.RunnerErrors), + XdsIR: xdsIR, + InfraIR: infraIR, + ExtensionManager: extMgr, + }) + err = r.Start(t.Context()) + require.NoError(t, err) + + routeKey := types.NamespacedName{Namespace: "default", Name: "httproute-2"} + + // buildResources constructs a snapshot where the listener's AllowedRoutes is controlled + // The HTTPRoute's generation is fixed at 1 throughout. + buildResources := func(allowAll bool, gatewayGeneration int64) *resource.ControllerResources { + from := gwapiv1.NamespacesFromSame + if allowAll { + from = gwapiv1.NamespacesFromAll + } + res := resource.NewResources() + res.GatewayClass = &gwapiv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{Name: "envoy-gateway-class"}, + Spec: gwapiv1.GatewayClassSpec{ + ControllerName: egv1a1.GatewayControllerName, + }, + } + hostname := gwapiv1.Hostname("app.example.com") + res.Gateways = []*gwapiv1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "envoy-gateway", + Name: "gateway-2", + Generation: gatewayGeneration, + }, + Spec: gwapiv1.GatewaySpec{ + GatewayClassName: "envoy-gateway-class", + Listeners: []gwapiv1.Listener{ + { + Name: "http", + Protocol: gwapiv1.HTTPProtocolType, + Port: 80, + Hostname: &hostname, + AllowedRoutes: &gwapiv1.AllowedRoutes{ + Namespaces: &gwapiv1.RouteNamespaces{ + From: &from, + }, + }, + }, + }, + }, + }, + } + routeHostname := gwapiv1.Hostname("app.example.com") + res.HTTPRoutes = []*gwapiv1.HTTPRoute{ + { + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "httproute-2", + Generation: 1, + }, + Spec: gwapiv1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1.CommonRouteSpec{ + ParentRefs: []gwapiv1.ParentReference{ + { + Namespace: func() *gwapiv1.Namespace { + ns := gwapiv1.Namespace("envoy-gateway") + return &ns + }(), + Name: "gateway-2", + }, + }, + }, + Hostnames: []gwapiv1.Hostname{routeHostname}, + Rules: []gwapiv1.HTTPRouteRule{ + { + BackendRefs: []gwapiv1.HTTPBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Name: "service-2", + Port: func() *gwapiv1.PortNumber { + p := gwapiv1.PortNumber(8080) + return &p + }(), + }, + }, + }, + }, + }, + }, + }, + }, + } + res.Namespaces = []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "envoy-gateway"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "default"}}, + } + res.Services = []*corev1.Service{ + { + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "service-2"}, + Spec: corev1.ServiceSpec{ + ClusterIP: "2.2.2.2", + Ports: []corev1.ServicePort{ + {Name: "http", Port: 8080, TargetPort: intstr.FromInt32(8080), Protocol: corev1.ProtocolTCP}, + }, + }, + }, + } + res.EndpointSlices = []*discoveryv1.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "endpointslice-2", + Labels: map[string]string{discoveryv1.LabelServiceName: "service-2"}, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Ports: []discoveryv1.EndpointPort{ + {Name: func() *string { s := "http"; return &s }(), Port: func() *int32 { p := int32(8080); return &p }(), Protocol: func() *corev1.Protocol { p := corev1.ProtocolTCP; return &p }()}, + }, + Endpoints: []discoveryv1.Endpoint{ + {Addresses: []string{"8.8.8.8"}, Conditions: discoveryv1.EndpointConditions{Ready: func() *bool { b := true; return &b }()}}, + }, + }, + } + cr := resource.ControllerResources{res} + return &cr + } + + // Cycle 1: Gateway gen=1, AllowedRoutes.From=Same, route in "default" is not allowed + pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ + Resources: buildResources(false, 1), + }) + + require.Eventually(t, func() bool { + status, ok := pResources.HTTPRouteStatuses.Load(routeKey) + if !ok || status == nil || len(status.Parents) == 0 { + return false + } + for _, cond := range status.Parents[0].Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + return cond.Status == metav1.ConditionFalse && + cond.Reason == string(gwapiv1.RouteReasonNotAllowedByListeners) && + cond.ObservedGeneration == 1 + } + } + return false + }, 5*time.Second, 50*time.Millisecond, + "cycle 1: expected Accepted=False/NotAllowedByListeners with ObservedGeneration=1") + + // Cycle 2: Gateway gen=2, still From=Same restriction, unrelated Gateway change + pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ + Resources: buildResources(false, 2), + }) + + require.Eventually(t, func() bool { + status, ok := pResources.HTTPRouteStatuses.Load(routeKey) + if !ok || status == nil || len(status.Parents) == 0 { + return false + } + for _, cond := range status.Parents[0].Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + return cond.Status == metav1.ConditionFalse && + cond.Reason == string(gwapiv1.RouteReasonNotAllowedByListeners) && + cond.ObservedGeneration == 2 + } + } + return false + }, 5*time.Second, 50*time.Millisecond, + "cycle 2: expected ObservedGeneration to be bumped to 2 when Gateway generation increased, "+ + "even though the condition outcome (Accepted=False/NotAllowedByListeners) is unchanged. "+ + "Without the fix in processAllowedListenersForParentRefs, ObservedGeneration stays at 1 "+ + "and the watchable DeepEqual gate suppresses the update.") + + // Cycle 3: Gateway gen=3, AllowedRoutes.From=All, route is now allowed + pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ + Resources: buildResources(true, 3), + }) + + require.Eventually(t, func() bool { + status, ok := pResources.HTTPRouteStatuses.Load(routeKey) + if !ok || status == nil || len(status.Parents) == 0 { + return false + } + for _, cond := range status.Parents[0].Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == 3 + } + } + return false + }, 5*time.Second, 50*time.Millisecond, + "cycle 3: expected Accepted=True after Gateway AllowedRoutes was updated to allow all namespaces") +} From d1e362aa4688a83c511e6e2a1bb456a90dfea902 Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:56:42 +0800 Subject: [PATCH 02/13] For gateway-driven conditions, use the Gateway's generation as observedGeneration Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- internal/gatewayapi/route.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 2a45a4d7ff..5596307401 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -117,11 +117,11 @@ func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, res // not on the Route as a whole. routeRoutes, errs, unacceptedRules := t.processHTTPRouteRules(httpRoute, parentRef, resources) - // observedGeneration is the max of the route's own generation and the matched - // Gateway's generation. Gateway-only edit produces a condition value that differs + // For gateway-driven conditions, use the Gateway's generation as observedGeneration + // Gateway-only edit produces a condition value that differs // from the stale stored one, unblocking the watchable DeepEqual gate. observedGeneration := httpRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { observedGeneration = gw.GetGeneration() } @@ -856,11 +856,11 @@ func (t *Translator) processGRPCRouteParentRefs(grpcRoute *GRPCRouteContext, res // not on the Route as a whole. routeRoutes, errs, unacceptedRules := t.processGRPCRouteRules(grpcRoute, parentRef, resources) - // observedGeneration is the max of the route's own generation and the matched - // Gateway's generation. Gateway-only edit produces a condition value that differs + // For gateway-driven conditions, use the Gateway's generation as observedGeneration + // Gateway-only edit produces a condition value that differs // from the stale stored one, unblocking the watchable DeepEqual gate. observedGeneration := grpcRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { observedGeneration = gw.GetGeneration() } @@ -1413,11 +1413,11 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour destName = irRouteDestinationName(tlsRoute, -1 /*rule index*/) ) - // observedGeneration is the max of the route's own generation and the matched - // Gateway's generation. Gateway-only edit produces a condition value that differs + // For gateway-driven conditions, use the Gateway's generation as observedGeneration + // Gateway-only edit produces a condition value that differs // from the stale stored one, unblocking the watchable DeepEqual gate. observedGeneration := tlsRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { observedGeneration = gw.GetGeneration() } @@ -1604,11 +1604,11 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour destName = irRouteDestinationName(udpRoute, -1 /*rule index*/) ) - // observedGeneration is the max of the route's own generation and the matched - // Gateway's generation. Gateway-only edit produces a condition value that differs + // For gateway-driven conditions, use the Gateway's generation as observedGeneration + // Gateway-only edit produces a condition value that differs // from the stale stored one, unblocking the watchable DeepEqual gate. observedGeneration := udpRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { observedGeneration = gw.GetGeneration() } @@ -1765,11 +1765,11 @@ func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resour destName = irRouteDestinationName(tcpRoute, -1 /*rule index*/) ) - // observedGeneration is the max of the route's own generation and the matched - // Gateway's generation. Gateway-only edit produces a condition value that differs + // For gateway-driven conditions, use the Gateway's generation as observedGeneration + // Gateway-only edit produces a condition value that differs // from the stale stored one, unblocking the watchable DeepEqual gate. observedGeneration := tcpRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > observedGeneration { + if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { observedGeneration = gw.GetGeneration() } @@ -2322,11 +2322,11 @@ func (t *Translator) processAllowedListenersForParentRefs( // Reset conditions since they will be recomputed during translation parentRefCtx.ResetConditions(routeContext) - // observedGeneration is the max of the route's own generation and the matched - // Gateway's generation. Gateway-only edit produces a condition value that differs + // For gateway-driven conditions, use the Gateway's generation as observedGeneration + // Gateway-only edit produces a condition value that differs // from the stale stored one, unblocking the watchable DeepEqual gate. observedGeneration := routeContext.GetGeneration() - if matchedGateway != nil && matchedGateway.GetGeneration() > observedGeneration { + if matchedGateway != nil && matchedGateway.GetGeneration() > 0 { observedGeneration = matchedGateway.GetGeneration() } From ef844dad5d2fd0b281178e33d9ca9a7eacd4b9ce Mon Sep 17 00:00:00 2001 From: Marc Navarro Date: Tue, 28 Apr 2026 06:50:39 +0200 Subject: [PATCH 03/13] feat(extensionManager): add support for multiple ExtensionManagers with sequential chaining (#8458) * feat: add support for multiple ExtensionManagers with sequential chaining Add a new `extensionManagers` plural field to `EnvoyGatewaySpec` that allows registering multiple extension managers with sequential chaining semantics. Each extension's output becomes the next extension's input. Key changes: - Add `Name` field to `ExtensionManager` and `ExtensionManagers` list to `EnvoyGatewaySpec` - Add `GetExtensionManagers()` helper to normalize singular/plural fields - Add mutual exclusivity validation between singular and plural fields - Implement `CompositeManager` wrapping multiple managers behind the `Manager` interface - Implement `compositeXDSHookClient` with per-extension policy filtering and per-extension resource-type gating in `PostTranslateModifyHook` - Merge `TranslationConfig` using OR semantics across all managers - Add `CleanupHookConns()` to the `Manager` interface - Unify `NewManager` factory to handle 0, 1, and N extensions Signed-off-by: Marc Navarro Sonnenfeld * Fix lint issues Signed-off-by: Marc Navarro Sonnenfeld * Add more test cases Signed-off-by: Marc Navarro Sonnenfeld * Per-extension manager filter Resources, BackendResources, and PolicyResources accordingly Signed-off-by: Marc Navarro Sonnenfeld * Implement new hook method PostEndpointsModifyHook Signed-off-by: Marc Navarro Sonnenfeld * Respect failOpen when GetPre/PostXDSHookClient errors in CompositeManager Signed-off-by: Marc Navarro Sonnenfeld * Simplified and unified client getter and tests Signed-off-by: Marc Navarro Sonnenfeld * Add resilience tests for multiple ExtensionManagers chaining and resource isolation Enhance the simple-extension-server with a configurable --suffix flag and resource-aware PostRouteModify to support testing multiple extension managers. Add resilience tests that verify sequential chaining of VirtualHost mutations and per-extension resource isolation via extensionRef filters. Signed-off-by: Marc Navarro Sonnenfeld * Add CompositeManager coverage to xDS translator tests and document plural ExtensionManagers Mirror the existing extension-manager translator error-handling tests through CompositeManager via a new NewInMemoryCompositeManager helper: a 1-entry composite verifies child errors are swallowed when failOpen is true, and a 2-entry composite verifies errors are propagated with the 'extension "":' prefix when failOpen is false. Extract a shared buildManagerGVKSets helper to avoid duplication between NewManager and the new in-memory constructor. Also extend the extension-server docs with a "Multiple extension servers" subsection covering sequential chaining, per-server resource isolation, per-server failOpen, the unique name requirement, and mutual exclusivity with the singular extensionManager field. Signed-off-by: Marc Navarro Sonnenfeld * Fix lint errors Signed-off-by: Marc Navarro Sonnenfeld * Match extension resources by group/kind, and wire cleanup for in-memory composite Two fixes in the composite-extension path: 1. CompositeManager filter now matches by group/kind only, aligning with runner.ExtensionGroupKinds, Manager.HasExtension, and Gateway API extensionRef. Exact-GVK matching could silently drop resources whose served version differed from the one declared in ExtensionManager.Resources, diverging from single-manager behavior when CRDs serve multiple versions. 2. NewInMemoryCompositeManager wires a sync.Once-guarded cleanup into every entry's cleanupHookConn, so CompositeManager.CleanupHookConns() (from the Manager interface) tears down the shared bufconn/server. The separate cleanup func return is removed; callers use CleanupHookConns() on the returned Manager. Signed-off-by: Marc Navarro Sonnenfeld * Add MinItems=1 and mutual-exclusion validation on extensionManagers * +kubebuilder:validation:MinItems=1 on EnvoyGatewaySpec.ExtensionManagers, mirrored by a runtime check that rejects an explicitly-set-but-empty list (a nil slice still means "omitted"). * +kubebuilder:validation:XValidation on EnvoyGatewaySpec declaring that extensionManager and extensionManagers are mutually exclusive. The same constraint is already enforced at runtime in validateEnvoyGatewayExtensionManagers; the marker documents the schema and is wired up for consumers that validate against the generated OpenAPI. Signed-off-by: Marc Navarro Sonnenfeld * Fix MultiextensionManagers tests Signed-off-by: Marc Navarro Sonnenfeld --------- Signed-off-by: Marc Navarro Sonnenfeld Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- api/v1alpha1/validation/envoygateway_validate_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/v1alpha1/validation/envoygateway_validate_test.go b/api/v1alpha1/validation/envoygateway_validate_test.go index de79017ce8..475b54c09e 100644 --- a/api/v1alpha1/validation/envoygateway_validate_test.go +++ b/api/v1alpha1/validation/envoygateway_validate_test.go @@ -1363,6 +1363,7 @@ func TestGetExtensionManagers(t *testing.T) { assert.Equal(t, "plural2", result[1].Name) }) } +<<<<<<< HEAD func TestWarnEnvoyGateway(t *testing.T) { eg := egv1a1.DefaultEnvoyGateway() @@ -1462,3 +1463,5 @@ func TestLuaDisabled(t *testing.T) { }) } } +======= +>>>>>>> c68d38ca0 (feat(extensionManager): add support for multiple ExtensionManagers with sequential chaining (#8458)) From 8051607af4adb8e66eb695078e38b254f66187d2 Mon Sep 17 00:00:00 2001 From: Kota Kimura <86363983+kkk777-7@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:36:39 +0900 Subject: [PATCH 04/13] feat: bandwidth limit (#8862) * feat: bandwidth limit Signed-off-by: kkk777-7 * add: crd validation Signed-off-by: kkk777-7 --------- Signed-off-by: kkk777-7 Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- internal/ir/xds.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 5987fb8706..306a02b7d4 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -2782,6 +2782,33 @@ type BandwidthLimitResponseTrailers struct { Prefix *string `json:"prefix,omitempty" yaml:"prefix,omitempty"` } +// BandwidthLimit holds bandwidth limiting configuration for request and/or response directions. +// At least one of Request or Response must be non-nil. +// +k8s:deepcopy-gen=true +type BandwidthLimit struct { + // Request configures bandwidth limiting for the request direction. + Request *BandwidthLimitConfig `json:"request,omitempty" yaml:"request,omitempty"` + // Response configures bandwidth limiting for the response direction. + Response *BandwidthLimitConfig `json:"response,omitempty" yaml:"response,omitempty"` +} + +// BandwidthLimitConfig holds the bandwidth limit configuration for one direction. +// +k8s:deepcopy-gen=true +type BandwidthLimitConfig struct { + // LimitKibps specifies the bandwidth in kibibytes per second (KiB/s). + LimitKibps uint64 `json:"limitKibps" yaml:"limitKibps"` + // ResponseTrailers configures trailer headers appended when bandwidth limiting introduces delays. + // Only applicable to the response direction. + ResponseTrailers *BandwidthLimitResponseTrailers `json:"responseTrailers,omitempty" yaml:"responseTrailers,omitempty"` +} + +// BandwidthLimitResponseTrailers holds trailer prefix configuration for bandwidth limiting. +// +k8s:deepcopy-gen=true +type BandwidthLimitResponseTrailers struct { + // Prefix is prepended to each trailer header name. + Prefix *string `json:"prefix,omitempty" yaml:"prefix,omitempty"` +} + type ProxyAccessLogType egv1a1.ProxyAccessLogType const ( From bbb27a63b22d7782cc768bb6933d915c8892022e Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Fri, 8 May 2026 23:01:03 +0800 Subject: [PATCH 05/13] fix conformance test failure, replace generation with message. Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- internal/gatewayapi/helpers.go | 9 +- internal/gatewayapi/route.go | 132 +++++++++------------- internal/gatewayapi/runner/runner_test.go | 23 ++-- 3 files changed, 70 insertions(+), 94 deletions(-) diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 67436e6713..03736100c3 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -147,8 +147,7 @@ func IsRefToGateway(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentRe // in the given list, and if so, a list of the Listeners within that Gateway or ListenerSet that // are included by the parent ref (either one specific Listener, or all Listeners // in the Gateway or ListenerSet, depending on whether section name is specified or not). -// The second return value is the matched GatewayContext when the parentRef points to a Gateway -func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentReference, gateways []*GatewayContext) (bool, *GatewayContext, []*ListenerContext) { +func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1.ParentReference, gateways []*GatewayContext) (bool, []*ListenerContext) { var referencedListeners []*ListenerContext // The parentRef is an ListenerSet @@ -174,7 +173,7 @@ func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1. } } } - return matchedListenerSet, nil, referencedListeners + return matchedListenerSet, referencedListeners } // The parentRef is a Gateway @@ -189,11 +188,11 @@ func GetReferencedListeners(routeNamespace gwapiv1.Namespace, parentRef gwapiv1. referencedListeners = append(referencedListeners, listener) } } - return true, gateway, referencedListeners + return true, referencedListeners } } - return false, nil, referencedListeners + return false, referencedListeners } func isRefToListenerSet(parentRef gwapiv1.ParentReference) bool { diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 5596307401..d2d8fc7280 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -110,21 +110,22 @@ func (t *Translator) ProcessGRPCRoutes(grpcRoutes []*gwapiv1.GRPCRoute, gateways return relevantGRPCRoutes } +// gatewayAcceptedMsg returns the message embedding the Gateway's generation +// so that a gateway-only update changes the message text and breaks the watchable DeepEqual gate +// observedGeneration stays as the route's own generation to remain spec-compliant. +func (t *Translator) gatewayMsg(gw *GatewayContext, base string) string { + if gw != nil && gw.GetGeneration() > 0 { + return fmt.Sprintf("%s (gateway generation: %d)", base, gw.GetGeneration()) + } + return base +} + func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, resources *resource.Resources, xdsIR resource.XdsIRMap) { for _, parentRef := range httpRoute.ParentRefs { // Need to compute Route rules within the parentRef loop because // any conditions that come out of it have to go on each RouteParentStatus, // not on the Route as a whole. routeRoutes, errs, unacceptedRules := t.processHTTPRouteRules(httpRoute, parentRef, resources) - - // For gateway-driven conditions, use the Gateway's generation as observedGeneration - // Gateway-only edit produces a condition value that differs - // from the stale stored one, unblocking the watchable DeepEqual gate. - observedGeneration := httpRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { - observedGeneration = gw.GetGeneration() - } - if len(errs) > 0 { routeStatus := GetRouteStatus(httpRoute) // errs are already grouped by condition type in TypedErrorCollector @@ -195,11 +196,11 @@ func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, res routeStatus := GetRouteStatus(httpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + httpRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingListenerHostname, - "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", + t.gatewayMsg(parentRef.GetGateway(), "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s)."), ) } @@ -214,11 +215,11 @@ func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, res routeStatus := GetRouteStatus(httpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + httpRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, - "Route is accepted", + t.gatewayMsg(parentRef.GetGateway(), "Route is accepted"), ) } } @@ -855,15 +856,6 @@ func (t *Translator) processGRPCRouteParentRefs(grpcRoute *GRPCRouteContext, res // any conditions that come out of it have to go on each RouteParentStatus, // not on the Route as a whole. routeRoutes, errs, unacceptedRules := t.processGRPCRouteRules(grpcRoute, parentRef, resources) - - // For gateway-driven conditions, use the Gateway's generation as observedGeneration - // Gateway-only edit produces a condition value that differs - // from the stale stored one, unblocking the watchable DeepEqual gate. - observedGeneration := grpcRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { - observedGeneration = gw.GetGeneration() - } - if len(errs) > 0 { routeStatus := GetRouteStatus(grpcRoute) // errs are already grouped by condition type in TypedErrorCollector @@ -938,11 +930,11 @@ func (t *Translator) processGRPCRouteParentRefs(grpcRoute *GRPCRouteContext, res routeStatus := GetRouteStatus(grpcRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + grpcRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingListenerHostname, - "There were no hostname intersections between the GRPCRoute and this parent ref's Listener(s).", + t.gatewayMsg(parentRef.GetGateway(), "There were no hostname intersections between the GRPCRoute and this parent ref's Listener(s)."), ) } @@ -952,11 +944,11 @@ func (t *Translator) processGRPCRouteParentRefs(grpcRoute *GRPCRouteContext, res routeStatus := GetRouteStatus(grpcRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + grpcRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, - "Route is accepted", + t.gatewayMsg(parentRef.GetGateway(), "Route is accepted"), ) } @@ -1413,14 +1405,6 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour destName = irRouteDestinationName(tlsRoute, -1 /*rule index*/) ) - // For gateway-driven conditions, use the Gateway's generation as observedGeneration - // Gateway-only edit produces a condition value that differs - // from the stale stored one, unblocking the watchable DeepEqual gate. - observedGeneration := tlsRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { - observedGeneration = gw.GetGeneration() - } - // compute backends for _, rule := range tlsRoute.Spec.Rules { for i := range rule.BackendRefs { @@ -1521,11 +1505,11 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour routeStatus := GetRouteStatus(tlsRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + tlsRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingListenerHostname, - "There were no hostname intersections between the TLSRoute and this parent ref's Listener(s).", + t.gatewayMsg(parentRef.GetGateway(), "There were no hostname intersections between the TLSRoute and this parent ref's Listener(s)."), ) } @@ -1540,11 +1524,11 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour routeStatus := GetRouteStatus(tlsRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + tlsRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, - "Route is accepted", + t.gatewayMsg(parentRef.GetGateway(), "Route is accepted"), ) } } @@ -1604,14 +1588,6 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour destName = irRouteDestinationName(udpRoute, -1 /*rule index*/) ) - // For gateway-driven conditions, use the Gateway's generation as observedGeneration - // Gateway-only edit produces a condition value that differs - // from the stale stored one, unblocking the watchable DeepEqual gate. - observedGeneration := udpRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { - observedGeneration = gw.GetGeneration() - } - for i := range udpRoute.Spec.Rules[0].BackendRefs { settingName := irDestinationSettingName(destName, i) backendRefCtx := DirectBackendRef{BackendRef: &udpRoute.Spec.Rules[0].BackendRefs[i]} @@ -1689,11 +1665,11 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour routeStatus := GetRouteStatus(udpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + udpRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, - "Route is accepted", + t.gatewayMsg(parentRef.GetGateway(), "Route is accepted"), ) } @@ -1701,11 +1677,11 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour routeStatus := GetRouteStatus(udpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + udpRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonUnsupportedValue, - "Multiple routes on the same UDP listener", + t.gatewayMsg(parentRef.GetGateway(), "Multiple routes on the same UDP listener"), ) } } @@ -1765,14 +1741,6 @@ func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resour destName = irRouteDestinationName(tcpRoute, -1 /*rule index*/) ) - // For gateway-driven conditions, use the Gateway's generation as observedGeneration - // Gateway-only edit produces a condition value that differs - // from the stale stored one, unblocking the watchable DeepEqual gate. - observedGeneration := tcpRoute.GetGeneration() - if gw := parentRef.GetGateway(); gw != nil && gw.GetGeneration() > 0 { - observedGeneration = gw.GetGeneration() - } - for i := range tcpRoute.Spec.Rules[0].BackendRefs { settingName := irDestinationSettingName(destName, i) backendRefCtx := DirectBackendRef{BackendRef: &tcpRoute.Spec.Rules[0].BackendRefs[i]} @@ -1859,22 +1827,22 @@ func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resour routeStatus := GetRouteStatus(tcpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + tcpRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, - "Route is accepted", + t.gatewayMsg(parentRef.GetGateway(), "Route is accepted"), ) } if !accepted { routeStatus := GetRouteStatus(tcpRoute) status.SetRouteStatusCondition(routeStatus, parentRef.routeParentStatusIdx, - observedGeneration, + tcpRoute.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonUnsupportedValue, - "Multiple routes on the same TCP listener", + t.gatewayMsg(parentRef.GetGateway(), "Multiple routes on the same TCP listener"), ) } @@ -2310,7 +2278,7 @@ func (t *Translator) processAllowedListenersForParentRefs( var relevantRoute bool ns := gwapiv1.Namespace(routeContext.GetNamespace()) for _, parentRef := range GetParentReferences(routeContext) { - isRelevantParentRef, matchedGateway, selectedListeners := GetReferencedListeners(ns, parentRef, gateways) + isRelevantParentRef, selectedListeners := GetReferencedListeners(ns, parentRef, gateways) // Parent ref is not to a Gateway that we control: skip it if !isRelevantParentRef { @@ -2322,23 +2290,15 @@ func (t *Translator) processAllowedListenersForParentRefs( // Reset conditions since they will be recomputed during translation parentRefCtx.ResetConditions(routeContext) - // For gateway-driven conditions, use the Gateway's generation as observedGeneration - // Gateway-only edit produces a condition value that differs - // from the stale stored one, unblocking the watchable DeepEqual gate. - observedGeneration := routeContext.GetGeneration() - if matchedGateway != nil && matchedGateway.GetGeneration() > 0 { - observedGeneration = matchedGateway.GetGeneration() - } - if len(selectedListeners) == 0 { routeStatus := GetRouteStatus(routeContext) status.SetRouteStatusCondition(routeStatus, parentRefCtx.routeParentStatusIdx, - observedGeneration, + routeContext.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNoMatchingParent, - "No listeners match this parent ref", + t.gatewayMsg(parentRefCtx.GetGateway(), "No listeners match this parent ref"), ) continue } @@ -2352,28 +2312,46 @@ func (t *Translator) processAllowedListenersForParentRefs( } } + var selectedGateway *GatewayContext + if len(selectedListeners) > 0 { + selectedGateway = selectedListeners[0].gateway + } + if len(allowedListeners) == 0 { routeStatus := GetRouteStatus(routeContext) status.SetRouteStatusCondition(routeStatus, parentRefCtx.routeParentStatusIdx, - observedGeneration, + routeContext.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionFalse, gwapiv1.RouteReasonNotAllowedByListeners, - "No listeners included by this parent ref allowed this attachment.", + t.gatewayMsg(selectedGateway, "No listeners included by this parent ref allowed this attachment."), ) continue } parentRefCtx.SetListeners(allowedListeners...) + if !HasReadyListener(selectedListeners) { + routeStatus := GetRouteStatus(routeContext) + status.SetRouteStatusCondition(routeStatus, + parentRefCtx.routeParentStatusIdx, + routeContext.GetGeneration(), + gwapiv1.RouteConditionAccepted, + metav1.ConditionFalse, + "NoReadyListeners", + t.gatewayMsg(parentRefCtx.GetGateway(), "There are no ready listeners for this parent ref"), + ) + continue + } + routeStatus := GetRouteStatus(routeContext) status.SetRouteStatusCondition(routeStatus, parentRefCtx.routeParentStatusIdx, - observedGeneration, + routeContext.GetGeneration(), gwapiv1.RouteConditionAccepted, metav1.ConditionTrue, gwapiv1.RouteReasonAccepted, - "Route is accepted", + t.gatewayMsg(parentRefCtx.GetGateway(), "Route is accepted"), ) } return relevantRoute diff --git a/internal/gatewayapi/runner/runner_test.go b/internal/gatewayapi/runner/runner_test.go index 12b34ca93b..041a601e7f 100644 --- a/internal/gatewayapi/runner/runner_test.go +++ b/internal/gatewayapi/runner/runner_test.go @@ -11,6 +11,7 @@ import ( "maps" "os" "path/filepath" + "strings" "testing" "time" @@ -693,8 +694,6 @@ func TestHTTPRouteStatusStaleAfterListenerHostnameChange(t *testing.T) { "cycle 1: expected Accepted=False/NoMatchingListenerHostname with ObservedGeneration=1") // Cycle 2: Gateway gen=2, still mismatching hostname - // If the route did not change, gen still 1. And neither did the condition outcome, still Accepted=False/NoMatchingListenerHostname. - // The new status value is byte-for-byte identical to cycle 1's value. So the subscriber goroutine never fires and Kubernetes status is never patched. pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ Resources: buildResources("other.example.com", 2), }) @@ -706,21 +705,19 @@ func TestHTTPRouteStatusStaleAfterListenerHostnameChange(t *testing.T) { } for _, cond := range status.Parents[0].Conditions { if cond.Type == string(gwapiv1.RouteConditionAccepted) { - // must have bumped ObservedGeneration to 2 even though - // the condition outcome (False/NoMatchingListenerHostname) is unchanged. return cond.Status == metav1.ConditionFalse && cond.Reason == string(gwapiv1.RouteReasonNoMatchingListenerHostname) && - cond.ObservedGeneration == 2 + cond.ObservedGeneration == 1 && + strings.Contains(cond.Message, "gateway generation: 2") } } return false }, 5*time.Second, 50*time.Millisecond, - "cycle 2: expected ObservedGeneration to be bumped to 2 when Gateway generation increased, "+ + "cycle 2: expected message to contain 'gateway generation: 2' when Gateway generation increased, "+ "even though the condition outcome (Accepted=False/NoMatchingListenerHostname) is unchanged. "+ - "Without the fix, ObservedGeneration stays at 1 and the watchable DeepEqual gate suppresses the update.") + "Without the fix, the message is identical to cycle 1 and the watchable DeepEqual gate suppresses the update.") // Cycle 3: Gateway gen=3, hostname is fixed to match the route - // Now both hostnames agree, Accepted=True, ObservedGeneration=3 pResources.GatewayAPIResources.Store(egv1a1.GatewayControllerName, &resource.ControllerResourcesContext{ Resources: buildResources("app.example.com", 3), }) @@ -918,14 +915,15 @@ func TestHTTPRouteStatusStaleAfterAllowedRoutesChange(t *testing.T) { if cond.Type == string(gwapiv1.RouteConditionAccepted) { return cond.Status == metav1.ConditionFalse && cond.Reason == string(gwapiv1.RouteReasonNotAllowedByListeners) && - cond.ObservedGeneration == 2 + cond.ObservedGeneration == 1 && + strings.Contains(cond.Message, "gateway generation: 2") } } return false }, 5*time.Second, 50*time.Millisecond, - "cycle 2: expected ObservedGeneration to be bumped to 2 when Gateway generation increased, "+ + "cycle 2: expected message to contain 'gateway generation: 2' when Gateway generation increased, "+ "even though the condition outcome (Accepted=False/NotAllowedByListeners) is unchanged. "+ - "Without the fix in processAllowedListenersForParentRefs, ObservedGeneration stays at 1 "+ + "Without the fix in processAllowedListenersForParentRefs, the message is identical to cycle 1 "+ "and the watchable DeepEqual gate suppresses the update.") // Cycle 3: Gateway gen=3, AllowedRoutes.From=All, route is now allowed @@ -940,7 +938,8 @@ func TestHTTPRouteStatusStaleAfterAllowedRoutesChange(t *testing.T) { } for _, cond := range status.Parents[0].Conditions { if cond.Type == string(gwapiv1.RouteConditionAccepted) { - return cond.Status == metav1.ConditionTrue && cond.ObservedGeneration == 3 + return cond.Status == metav1.ConditionTrue && + strings.Contains(cond.Message, "gateway generation: 3") } } return false From 7cf2c555bb9b5de923eb85b5adb556ab813f71e0 Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Tue, 12 May 2026 22:23:21 +0800 Subject: [PATCH 06/13] add e2e test case Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- test/e2e/testdata/httproute-stale-status.yaml | 31 ++++ test/e2e/tests/httproute_stale_status.go | 143 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 test/e2e/testdata/httproute-stale-status.yaml create mode 100644 test/e2e/tests/httproute_stale_status.go diff --git a/test/e2e/testdata/httproute-stale-status.yaml b/test/e2e/testdata/httproute-stale-status.yaml new file mode 100644 index 0000000000..08fab0d6f4 --- /dev/null +++ b/test/e2e/testdata/httproute-stale-status.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: stale-status-gw + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http-first + port: 8888 + protocol: HTTP + hostname: "other.example.com" + allowedRoutes: + namespaces: + from: Same +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: stale-status-route + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: stale-status-gw + hostnames: + - "stale.example.com" + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/test/e2e/tests/httproute_stale_status.go b/test/e2e/tests/httproute_stale_status.go new file mode 100644 index 0000000000..51a13a8614 --- /dev/null +++ b/test/e2e/tests/httproute_stale_status.go @@ -0,0 +1,143 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e + +package tests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/conformance/utils/tlog" +) + +func init() { + ConformanceTests = append(ConformanceTests, HTTPRouteStaleStatusTest) +} + +var HTTPRouteStaleStatusTest = suite.ConformanceTest{ + ShortName: "HTTPRouteStaleStatus", + Description: "Stale Accepted=False/NoMatchingListenerHostname condition must be cleared after a matching listener is added to the Gateway", + Manifests: []string{"testdata/httproute-stale-status.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + gwNN := types.NamespacedName{Name: "stale-status-gw", Namespace: ns} + routeNN := types.NamespacedName{Name: "stale-status-route", Namespace: ns} + + // Phase 1: Gateway listener hostname "other.example.com" does not match the + // route's hostname "stale.example.com". + // Expect Accepted=False / NoMatchingListenerHostname. + t.Run("route rejected with NoMatchingListenerHostname when hostnames differ", func(t *testing.T) { + tlog.Logf(t, "Waiting for HTTPRoute %s to be Accepted=False/NoMatchingListenerHostname", routeNN) + require.NoError(t, wait.PollUntilContextTimeout( + t.Context(), time.Second, suite.TimeoutConfig.MaxTimeToConsistency, true, + func(ctx context.Context) (bool, error) { + route := &gwapiv1.HTTPRoute{} + if err := suite.Client.Get(ctx, routeNN, route); err != nil { + tlog.Logf(t, "failed to get HTTPRoute: %v", err) + return false, nil + } + for _, parent := range route.Status.Parents { + for _, cond := range parent.Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + if cond.Status == metav1.ConditionFalse && + cond.Reason == string(gwapiv1.RouteReasonNoMatchingListenerHostname) { + tlog.Logf(t, "HTTPRoute correctly Accepted=False/NoMatchingListenerHostname: %s", cond.Message) + return true, nil + } + tlog.Logf(t, "Accepted condition not yet expected (status=%s reason=%s)", cond.Status, cond.Reason) + } + } + } + return false, nil + }, + )) + }) + + // Phase 2: Add a second listener whose hostname matches the route's hostname. + // The original non-matching listener is kept. + t.Run("matching listener added to gateway", func(t *testing.T) { + tlog.Logf(t, "Adding matching listener (stale.example.com) to gateway %s", gwNN) + require.NoError(t, wait.PollUntilContextTimeout( + t.Context(), time.Second, suite.TimeoutConfig.MaxTimeToConsistency, true, + func(ctx context.Context) (bool, error) { + gw := &gwapiv1.Gateway{} + if err := suite.Client.Get(ctx, gwNN, gw); err != nil { + tlog.Logf(t, "failed to get Gateway: %v", err) + return false, nil + } + gw.Spec.Listeners = []gwapiv1.Listener{ + { + Name: "http-first", + Port: 8888, + Protocol: gwapiv1.HTTPProtocolType, + Hostname: ptrTo(gwapiv1.Hostname("other.example.com")), + AllowedRoutes: &gwapiv1.AllowedRoutes{ + Namespaces: &gwapiv1.RouteNamespaces{From: ptrTo(gwapiv1.NamespacesFromSame)}, + }, + }, + { + Name: "http-match", + Port: 8889, + Protocol: gwapiv1.HTTPProtocolType, + Hostname: ptrTo(gwapiv1.Hostname("stale.example.com")), + AllowedRoutes: &gwapiv1.AllowedRoutes{ + Namespaces: &gwapiv1.RouteNamespaces{From: ptrTo(gwapiv1.NamespacesFromSame)}, + }, + }, + } + if err := suite.Client.Update(ctx, gw, &client.UpdateOptions{}); err != nil { + tlog.Logf(t, "failed to update Gateway: %v", err) + return false, nil + } + tlog.Logf(t, "Gateway updated, new generation: %d", gw.Generation) + return true, nil + }, + )) + }) + + // Phase 3: The NoMatchingListenerHostname condition must now be gone. + // The route must be Accepted=True / Accepted. + t.Run("NoMatchingListenerHostname cleared and route accepted after matching listener added", func(t *testing.T) { + tlog.Logf(t, "Waiting for HTTPRoute %s to be Accepted=True/Accepted", routeNN) + require.NoError(t, wait.PollUntilContextTimeout( + t.Context(), time.Second, suite.TimeoutConfig.MaxTimeToConsistency, true, + func(ctx context.Context) (bool, error) { + route := &gwapiv1.HTTPRoute{} + if err := suite.Client.Get(ctx, routeNN, route); err != nil { + tlog.Logf(t, "failed to get HTTPRoute: %v", err) + return false, nil + } + for _, parent := range route.Status.Parents { + for _, cond := range parent.Conditions { + if cond.Type == string(gwapiv1.RouteConditionAccepted) { + if cond.Status == metav1.ConditionTrue && + cond.Reason == string(gwapiv1.RouteReasonAccepted) { + tlog.Logf(t, "NoMatchingListenerHostname cleared, route is Accepted=True/Accepted: %s", cond.Message) + return true, nil + } + tlog.Logf(t, "Condition not yet updated (status=%s reason=%s msg=%s)", cond.Status, cond.Reason, cond.Message) + } + } + } + return false, nil + }, + )) + }) + }, +} + +func ptrTo[T any](v T) *T { + return &v +} From b974462e569474a34a866d2c28d323e8ec79cdc8 Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:05:18 +0800 Subject: [PATCH 07/13] reverse incorrect merge during rebase Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- .../validation/envoygateway_validate_test.go | 5 +--- internal/ir/xds.go | 27 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/api/v1alpha1/validation/envoygateway_validate_test.go b/api/v1alpha1/validation/envoygateway_validate_test.go index 475b54c09e..b6bd6ace4d 100644 --- a/api/v1alpha1/validation/envoygateway_validate_test.go +++ b/api/v1alpha1/validation/envoygateway_validate_test.go @@ -1363,7 +1363,6 @@ func TestGetExtensionManagers(t *testing.T) { assert.Equal(t, "plural2", result[1].Name) }) } -<<<<<<< HEAD func TestWarnEnvoyGateway(t *testing.T) { eg := egv1a1.DefaultEnvoyGateway() @@ -1462,6 +1461,4 @@ func TestLuaDisabled(t *testing.T) { assert.Equal(t, tc.expected, tc.ext.LuaDisabled()) }) } -} -======= ->>>>>>> c68d38ca0 (feat(extensionManager): add support for multiple ExtensionManagers with sequential chaining (#8458)) +} \ No newline at end of file diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 306a02b7d4..5987fb8706 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -2782,33 +2782,6 @@ type BandwidthLimitResponseTrailers struct { Prefix *string `json:"prefix,omitempty" yaml:"prefix,omitempty"` } -// BandwidthLimit holds bandwidth limiting configuration for request and/or response directions. -// At least one of Request or Response must be non-nil. -// +k8s:deepcopy-gen=true -type BandwidthLimit struct { - // Request configures bandwidth limiting for the request direction. - Request *BandwidthLimitConfig `json:"request,omitempty" yaml:"request,omitempty"` - // Response configures bandwidth limiting for the response direction. - Response *BandwidthLimitConfig `json:"response,omitempty" yaml:"response,omitempty"` -} - -// BandwidthLimitConfig holds the bandwidth limit configuration for one direction. -// +k8s:deepcopy-gen=true -type BandwidthLimitConfig struct { - // LimitKibps specifies the bandwidth in kibibytes per second (KiB/s). - LimitKibps uint64 `json:"limitKibps" yaml:"limitKibps"` - // ResponseTrailers configures trailer headers appended when bandwidth limiting introduces delays. - // Only applicable to the response direction. - ResponseTrailers *BandwidthLimitResponseTrailers `json:"responseTrailers,omitempty" yaml:"responseTrailers,omitempty"` -} - -// BandwidthLimitResponseTrailers holds trailer prefix configuration for bandwidth limiting. -// +k8s:deepcopy-gen=true -type BandwidthLimitResponseTrailers struct { - // Prefix is prepended to each trailer header name. - Prefix *string `json:"prefix,omitempty" yaml:"prefix,omitempty"` -} - type ProxyAccessLogType egv1a1.ProxyAccessLogType const ( From 5f15b0346e2b4317bda5e622fe014cd4badcc553 Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:09:21 +0800 Subject: [PATCH 08/13] add back the missing new line at the end of file Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- api/v1alpha1/validation/envoygateway_validate_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/validation/envoygateway_validate_test.go b/api/v1alpha1/validation/envoygateway_validate_test.go index b6bd6ace4d..de79017ce8 100644 --- a/api/v1alpha1/validation/envoygateway_validate_test.go +++ b/api/v1alpha1/validation/envoygateway_validate_test.go @@ -1461,4 +1461,4 @@ func TestLuaDisabled(t *testing.T) { assert.Equal(t, tc.expected, tc.ext.LuaDisabled()) }) } -} \ No newline at end of file +} From d3d207f605633e3f97025fa9c39276bb85aa1e2c Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:44:09 +0800 Subject: [PATCH 09/13] add back the missing function Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- internal/gatewayapi/helpers.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 03736100c3..b90b00c495 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -203,6 +203,15 @@ func isRefToListenerSet(parentRef gwapiv1.ParentReference) bool { return false } +func HasReadyListener(listeners []*ListenerContext) bool { + for _, listener := range listeners { + if listener.IsReady() { + return true + } + } + return false +} + // ValidateHTTPRouteFilter validates the provided filter within HTTPRoute. func ValidateHTTPRouteFilter(filter *gwapiv1.HTTPRouteFilter, extGKs ...schema.GroupKind) error { switch { From 33feff7c170c7d9885c5f2a49c629c1c6eb69ffd Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:26:45 +0800 Subject: [PATCH 10/13] update listener test cases' expected output since HasReadyListener would check the 'no ready listeners' status. Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- ...afficpolicy-with-routing-type-listener.out.yaml | 6 +++--- .../testdata/gateway-with-attached-routes.out.yaml | 6 +++--- ...alid-allowed-routes-kind-and-supported.out.yaml | 6 +++--- ...invalid-tls-configuration-invalid-mode.out.yaml | 6 +++--- ...-tls-configuration-no-certificate-refs.out.yaml | 6 +++--- ...ls-configuration-secret-does-not-exist.out.yaml | 6 +++--- ...onfiguration-secret-in-other-namespace.out.yaml | 6 +++--- ...-tls-configuration-secret-is-not-valid.out.yaml | 6 +++--- ...tp-and-tlsroute-same-hostname-and-port.out.yaml | 12 ++++++------ ...-listeners-with-same-port-and-hostname.out.yaml | 6 +++--- ...th-same-port-and-incompatible-protocol.out.yaml | 6 +++--- ...eners-with-same-port-http-tcp-protocol.out.yaml | 14 +++++++------- ...et-https-tls-misuses-gateway-namespace.out.yaml | 6 +++--- ...tps-tls-mixed-valid-and-missing-secret.out.yaml | 6 +++--- ...nerset-https-tls-secret-does-not-exist.out.yaml | 6 +++--- ...istener-both-passthrough-and-cert-data.out.yaml | 6 +++--- ...lsroute-with-tls-listener-missing-mode.out.yaml | 6 +++--- test/e2e/tests/httproute_stale_status.go | 2 +- 18 files changed, 59 insertions(+), 59 deletions(-) diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml index 7bd937bb8f..04c0af5b6d 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml @@ -199,9 +199,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml b/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml index dd21926cc5..430bf42173 100644 --- a/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml @@ -276,9 +276,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: 'Failed to process route rule 0 backendRef 0: service envoy-gateway/does-not-exist diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml index 2ee6fe1a50..ad9f652f5d 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml @@ -55,9 +55,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml index 4c690433ae..d3b527cf81 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml @@ -59,9 +59,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml index e0e7f2510c..08b2c9ae80 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml @@ -56,9 +56,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml index 678fbdfa85..e71962d4a8 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml @@ -64,9 +64,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml index 96f846ddaa..be22741fa9 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml @@ -60,9 +60,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml index 8ed614e3c6..84a07d67f8 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml @@ -64,9 +64,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml index d57234d81b..5e3514635d 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml @@ -92,9 +92,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route @@ -135,9 +135,9 @@ tlsRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml index 3dc2137be3..e62d2a2dc7 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml @@ -92,9 +92,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml index f397776a61..4af2173f3d 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml @@ -94,9 +94,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml index 45a9719831..1a31d49847 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml @@ -45,7 +45,7 @@ gateways: kind: HTTPRoute - group: gateway.networking.k8s.io kind: GRPCRoute - - attachedRoutes: 1 + - attachedRoutes: 0 conditions: - lastTransitionTime: null message: All listeners for a given port must use a compatible protocol @@ -87,9 +87,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route @@ -130,9 +130,9 @@ tcpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml b/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml index 4f2647a360..be176e0d7f 100644 --- a/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml @@ -65,9 +65,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml b/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml index 871009c0cc..9526df4a3f 100644 --- a/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml @@ -102,9 +102,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml b/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml index c16e656624..bf0b1fa843 100644 --- a/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml @@ -66,9 +66,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml index 13070b28ec..e1f898a957 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml @@ -66,9 +66,9 @@ tlsRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml index 5f5685a26f..5a7b2c7289 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml @@ -63,9 +63,9 @@ tlsRoutes: parents: - conditions: - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" + message: There are no ready listeners for this parent ref + reason: NoReadyListeners + status: "False" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/test/e2e/tests/httproute_stale_status.go b/test/e2e/tests/httproute_stale_status.go index 51a13a8614..0557df907f 100644 --- a/test/e2e/tests/httproute_stale_status.go +++ b/test/e2e/tests/httproute_stale_status.go @@ -66,7 +66,7 @@ var HTTPRouteStaleStatusTest = suite.ConformanceTest{ }) // Phase 2: Add a second listener whose hostname matches the route's hostname. - // The original non-matching listener is kept. + // The original non-matching listener is kept. t.Run("matching listener added to gateway", func(t *testing.T) { tlog.Logf(t, "Adding matching listener (stale.example.com) to gateway %s", gwNN) require.NoError(t, wait.PollUntilContextTimeout( From e7f3e10b08077c49cf256ca6b9df85c6413b2411 Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:59:40 +0800 Subject: [PATCH 11/13] update apply_a_gateway_with_conflicted_listener_and_a_httproute e2e test case with NoReadyListeners reason Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- test/e2e/tests/merge_gateways.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/merge_gateways.go b/test/e2e/tests/merge_gateways.go index eee85fd57b..b7ed4c0c6e 100644 --- a/test/e2e/tests/merge_gateways.go +++ b/test/e2e/tests/merge_gateways.go @@ -210,8 +210,8 @@ var MergeGatewaysTest = suite.ConformanceTest{ expectedHTTPRouteCondition := metav1.Condition{ Type: string(gwapiv1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - Reason: string(gwapiv1.RouteReasonAccepted), + Status: metav1.ConditionFalse, + Reason: "NoReadyListeners", } kubernetes.HTTPRouteMustHaveCondition(t, suite.Client, suite.TimeoutConfig, route4NN, gw4NN, expectedHTTPRouteCondition) }) From 1b967e1495ecafadd10e0169ac7bb32a2a1a1673 Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:43:22 +0800 Subject: [PATCH 12/13] revert no ready listers status and test cases changes Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- internal/gatewayapi/route.go | 13 ------------- ...afficpolicy-with-routing-type-listener.out.yaml | 6 +++--- .../testdata/gateway-with-attached-routes.out.yaml | 6 +++--- ...alid-allowed-routes-kind-and-supported.out.yaml | 6 +++--- ...invalid-tls-configuration-invalid-mode.out.yaml | 6 +++--- ...-tls-configuration-no-certificate-refs.out.yaml | 6 +++--- ...ls-configuration-secret-does-not-exist.out.yaml | 6 +++--- ...onfiguration-secret-in-other-namespace.out.yaml | 6 +++--- ...-tls-configuration-secret-is-not-valid.out.yaml | 6 +++--- ...tp-and-tlsroute-same-hostname-and-port.out.yaml | 12 ++++++------ ...-listeners-with-same-port-and-hostname.out.yaml | 6 +++--- ...th-same-port-and-incompatible-protocol.out.yaml | 6 +++--- ...eners-with-same-port-http-tcp-protocol.out.yaml | 14 +++++++------- ...et-https-tls-misuses-gateway-namespace.out.yaml | 6 +++--- ...tps-tls-mixed-valid-and-missing-secret.out.yaml | 6 +++--- ...nerset-https-tls-secret-does-not-exist.out.yaml | 6 +++--- ...istener-both-passthrough-and-cert-data.out.yaml | 6 +++--- ...lsroute-with-tls-listener-missing-mode.out.yaml | 6 +++--- 18 files changed, 58 insertions(+), 71 deletions(-) diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index dac9956c2f..2dac879ac8 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -2335,19 +2335,6 @@ func (t *Translator) processAllowedListenersForParentRefs( } parentRefCtx.SetListeners(allowedListeners...) - if !HasReadyListener(selectedListeners) { - routeStatus := GetRouteStatus(routeContext) - status.SetRouteStatusCondition(routeStatus, - parentRefCtx.routeParentStatusIdx, - routeContext.GetGeneration(), - gwapiv1.RouteConditionAccepted, - metav1.ConditionFalse, - "NoReadyListeners", - t.gatewayMsg(parentRefCtx.GetGateway(), "There are no ready listeners for this parent ref"), - ) - continue - } - routeStatus := GetRouteStatus(routeContext) status.SetRouteStatusCondition(routeStatus, parentRefCtx.routeParentStatusIdx, diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml index 04c0af5b6d..7bd937bb8f 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-routing-type-listener.out.yaml @@ -199,9 +199,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml b/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml index 430bf42173..dd21926cc5 100644 --- a/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-attached-routes.out.yaml @@ -276,9 +276,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: 'Failed to process route rule 0 backendRef 0: service envoy-gateway/does-not-exist diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml index ad9f652f5d..2ee6fe1a50 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-routes-kind-and-supported.out.yaml @@ -55,9 +55,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml index d3b527cf81..4c690433ae 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml @@ -59,9 +59,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml index 08b2c9ae80..e0e7f2510c 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-no-certificate-refs.out.yaml @@ -56,9 +56,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml index e71962d4a8..678fbdfa85 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-does-not-exist.out.yaml @@ -64,9 +64,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml index be22741fa9..96f846ddaa 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-in-other-namespace.out.yaml @@ -60,9 +60,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml index 84a07d67f8..8ed614e3c6 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-secret-is-not-valid.out.yaml @@ -64,9 +64,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml index 5e3514635d..d57234d81b 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml @@ -92,9 +92,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route @@ -135,9 +135,9 @@ tlsRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml index e62d2a2dc7..3dc2137be3 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-hostname.out.yaml @@ -92,9 +92,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml index 4af2173f3d..f397776a61 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-and-incompatible-protocol.out.yaml @@ -94,9 +94,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml index 1a31d49847..45a9719831 100644 --- a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml @@ -45,7 +45,7 @@ gateways: kind: HTTPRoute - group: gateway.networking.k8s.io kind: GRPCRoute - - attachedRoutes: 0 + - attachedRoutes: 1 conditions: - lastTransitionTime: null message: All listeners for a given port must use a compatible protocol @@ -87,9 +87,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route @@ -130,9 +130,9 @@ tcpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml b/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml index be176e0d7f..4f2647a360 100644 --- a/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-https-tls-misuses-gateway-namespace.out.yaml @@ -65,9 +65,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml b/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml index 9526df4a3f..871009c0cc 100644 --- a/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-https-tls-mixed-valid-and-missing-secret.out.yaml @@ -102,9 +102,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml b/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml index bf0b1fa843..c16e656624 100644 --- a/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml +++ b/internal/gatewayapi/testdata/listenerset-https-tls-secret-does-not-exist.out.yaml @@ -66,9 +66,9 @@ httpRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml index e1f898a957..13070b28ec 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml @@ -66,9 +66,9 @@ tlsRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route diff --git a/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml index 5a7b2c7289..5f5685a26f 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-tls-listener-missing-mode.out.yaml @@ -63,9 +63,9 @@ tlsRoutes: parents: - conditions: - lastTransitionTime: null - message: There are no ready listeners for this parent ref - reason: NoReadyListeners - status: "False" + message: Route is accepted + reason: Accepted + status: "True" type: Accepted - lastTransitionTime: null message: Resolved all the Object references for the Route From 4f4d7bf15214236c8c1e132bc27e1497219a636d Mon Sep 17 00:00:00 2001 From: "hai.yue" <20416005+yuehaii@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:40:51 +0800 Subject: [PATCH 13/13] restore no ready listeners reason. Signed-off-by: hai.yue <20416005+yuehaii@users.noreply.github.com> --- test/e2e/tests/merge_gateways.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/merge_gateways.go b/test/e2e/tests/merge_gateways.go index b7ed4c0c6e..eee85fd57b 100644 --- a/test/e2e/tests/merge_gateways.go +++ b/test/e2e/tests/merge_gateways.go @@ -210,8 +210,8 @@ var MergeGatewaysTest = suite.ConformanceTest{ expectedHTTPRouteCondition := metav1.Condition{ Type: string(gwapiv1.RouteConditionAccepted), - Status: metav1.ConditionFalse, - Reason: "NoReadyListeners", + Status: metav1.ConditionTrue, + Reason: string(gwapiv1.RouteReasonAccepted), } kubernetes.HTTPRouteMustHaveCondition(t, suite.Client, suite.TimeoutConfig, route4NN, gw4NN, expectedHTTPRouteCondition) })