Skip to content
9 changes: 5 additions & 4 deletions api/v1alpha1/envoygateway_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,11 @@ func (r *RateLimit) GetServiceSettings() *ClusterSettings {

// defaultRuntimeFlags are the default runtime flags for Envoy Gateway.
var defaultRuntimeFlags = map[RuntimeFlag]bool{
XDSNameSchemeV2: false,
EndpointSliceIndex: true,
EnvoyProxyPatch: true,
OIDCTokenCookieChunking: true,
XDSNameSchemeV2: false,
EndpointSliceIndex: true,
EnvoyProxyPatch: true,
OIDCTokenCookieChunking: true,
DropBackendRefsWithoutEndpoints: false,
}

// IsEnabled checks if an experimental Gateway API is enabled in the EnvoyGateway configuration.
Expand Down
10 changes: 9 additions & 1 deletion api/v1alpha1/envoygateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ type GatewayAPISettings struct {
// RuntimeFlag defines a runtime flag used to guard breaking changes or risky experimental features in new Envoy Gateway releases.
// A runtime flag may be enabled or disabled by default and can be toggled through the EnvoyGateway resource.
// +enum
// +kubebuilder:validation:Enum=XDSNameSchemeV2;EndpointSliceIndex;PerResourceSystemCASecret;EnvoyProxyPatch;ApplyClientTrafficPolicyHTTP1SettingsToClientsOnly;OIDCTokenCookieChunking
// +kubebuilder:validation:Enum=XDSNameSchemeV2;EndpointSliceIndex;PerResourceSystemCASecret;EnvoyProxyPatch;ApplyClientTrafficPolicyHTTP1SettingsToClientsOnly;OIDCTokenCookieChunking;DropBackendRefsWithoutEndpoints
type RuntimeFlag string

const (
Expand Down Expand Up @@ -218,6 +218,14 @@ const (
// Disable this flag while proxies of an earlier Envoy Gateway release serve the same
// hostnames, for example during an upgrade.
OIDCTokenCookieChunking RuntimeFlag = "OIDCTokenCookieChunking"

// DropBackendRefsWithoutEndpoints excludes backendRefs without ready endpoints from an HTTPRoute
// or GRPCRoute rule when at least one other backendRef in the same rule is available, so the
// remaining backends receive all the traffic per their relative weights instead of returning 503
// for the share of the excluded backendRefs.
// Rules that also have invalid backendRefs are unchanged.
// Disabled by default.
DropBackendRefsWithoutEndpoints RuntimeFlag = "DropBackendRefsWithoutEndpoints"
)

// RuntimeFlags provide a mechanism to guard breaking changes or risky experimental features in new Envoy Gateway releases.
Expand Down
34 changes: 34 additions & 0 deletions internal/gatewayapi/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"net"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -230,6 +231,14 @@ type routeBackendRefDestination struct {
backendClusterKey *BackendClusterKey
}

// removeNoEndpointsDestinations returns the destinations without the ones that have no ready endpoints.
// Invalid, dynamic resolver and custom backend destinations are kept as they are handled separately.
func removeNoEndpointsDestinations(destinations []routeBackendRefDestination) []routeBackendRefDestination {
return slices.DeleteFunc(destinations, func(d routeBackendRefDestination) bool {
return !d.ds.Invalid && !d.ds.IsDynamicResolver && !d.ds.IsCustomBackend && len(d.ds.Endpoints) == 0
Comment thread
kkk777-7 marked this conversation as resolved.
Resolved
Comment thread
kkk777-7 marked this conversation as resolved.
Resolved
})
}

// httpRouteWithBackendDestinations pairs one rule-match's ir.HTTPRoute with its rule's not-yet-resolved
// routeBackendDestinations, deferring the final Settings/BackendClusterRefs split - and thus whether a
// merge-eligible backend actually gets a shared cluster - to routeDestinationForListener, once a
Expand Down Expand Up @@ -385,6 +394,18 @@ func (t *Translator) processHTTPRouteRules(httpRoute *HTTPRouteContext, parentRe
routeBackendDestinations = append(routeBackendDestinations, backendDest)
}

// When DropBackendRefsWithoutEndpoints is enabled and some backendRefs in the rule are available, drop the
// ones without ready endpoints so that traffic is redistributed to the available backends instead of
// returning 503 for their share.
// The BackendsAvailable condition is still set for the dropped backendRefs.
// Skip this when the rule has invalid backendRefs, as the dropped share would also go to the invalid ones
// and change their configured fraction.
if t.DropBackendRefsWithoutEndpoints &&
backendWeights.Valid > 0 && backendWeights.NoEndpoints > 0 && backendWeights.Invalid == 0 {
routeBackendDestinations = removeNoEndpointsDestinations(routeBackendDestinations)
backendWeights.NoEndpoints = 0
}

switch {
// return 500 if any filter processing error occurred
case processFilterError != nil:
Expand Down Expand Up @@ -1582,6 +1603,7 @@ func (t *Translator) processGRPCRouteRules(grpcRoute *GRPCRouteContext, parentRe
fmt.Errorf("failed to process route rule %d backendRef %d: %w", ruleIdx, i, err),
err.Reason(),
))
backendDest.ds.Invalid = true
Comment thread
kkk777-7 marked this conversation as resolved.
Resolved
processDestinationError = err
}
}
Expand All @@ -1602,6 +1624,18 @@ func (t *Translator) processGRPCRouteRules(grpcRoute *GRPCRouteContext, parentRe
routeBackendDestinations = append(routeBackendDestinations, backendDest)
}

// When DropBackendRefsWithoutEndpoints is enabled and some backendRefs in the rule are available, drop the
// ones without ready endpoints so that traffic is redistributed to the available backends instead of
// returning 503 for their share.
// The BackendsAvailable condition is still set for the dropped backendRefs.
// Skip this when the rule has invalid backendRefs, as the dropped share would also go to the invalid ones
// and change their configured fraction.
if t.DropBackendRefsWithoutEndpoints &&
backendWeights.Valid > 0 && backendWeights.NoEndpoints > 0 && backendWeights.Invalid == 0 {
routeBackendDestinations = removeNoEndpointsDestinations(routeBackendDestinations)
backendWeights.NoEndpoints = 0
}

switch {
// return 500 if any filter processing error occurred
case processFilterError != nil:
Expand Down
1 change: 1 addition & 0 deletions internal/gatewayapi/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ func (r *Runner) subscribeAndTranslate(sub <-chan watchable.Snapshot[string, *re
MergeGateways: gatewayapi.IsMergeGatewaysEnabled(resources),
MergeBackends: gatewayapi.ResolveMergeBackendsConfig(resources),
PerResourceSystemCASecret: r.EnvoyGateway.RuntimeFlags.IsEnabled(egv1a1.PerResourceSystemCASecret),
DropBackendRefsWithoutEndpoints: r.EnvoyGateway.RuntimeFlags.IsEnabled(egv1a1.DropBackendRefsWithoutEndpoints),
WasmCache: r.wasmCache,
RunningOnHost: r.EnvoyGateway.Provider != nil && r.EnvoyGateway.Provider.IsRunningOnHost(),
InfraRemotelyManaged: r.EnvoyGateway.Provider != nil && r.EnvoyGateway.Provider.IsInfraManagedRemotely(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ xdsIR:
name: grpcroute/default/grpcroute-1/rule/0/backend/0
protocol: GRPC
weight: 1
- name: grpcroute/default/grpcroute-1/rule/0/backend/1
- invalid: true
name: grpcroute/default/grpcroute-1/rule/0/backend/1
weight: 1
headerMatches:
- distinct: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,8 @@ xdsIR:
name: grpcroute/default/grpc-route-1/rule/0/backend/0
protocol: GRPC
weight: 1
- name: grpcroute/default/grpc-route-1/rule/0/backend/1
- invalid: true
name: grpcroute/default/grpc-route-1/rule/0/backend/1
weight: 1
hostname: '*'
isHTTP2: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
gateways:
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
namespace: envoy-gateway
name: gateway-1
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
httpRoutes:
- apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: default
name: httproute-1
spec:
parentRefs:
- namespace: envoy-gateway
name: gateway-1
rules:
# the backendRef without endpoints is dropped, service-1 receives all the traffic
- matches:
- path:
type: PathPrefix
value: "/some-without-endpoints"
backendRefs:
- name: service-1
port: 8080
- name: service-with-no-endpoints
port: 8080
# the backendRef without endpoints is kept to preserve the invalid fraction, the invalid and no endpoints share returns 503
- matches:
- path:
type: PathPrefix
value: "/mixed-invalid-and-without-endpoints"
backendRefs:
- name: service-1
port: 8080
- name: service-with-no-endpoints
port: 8080
- name: service-not-exist
port: 8080
# all backendRefs have no endpoints, 503 direct response is returned
- matches:
- path:
type: PathPrefix
value: "/all-without-endpoints"
backendRefs:
- name: service-with-no-endpoints
port: 8080
- name: service-with-no-endpoints
port: 8080
grpcRoutes:
- apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
namespace: default
name: grpcroute-1
spec:
parentRefs:
- namespace: envoy-gateway
name: gateway-1
rules:
# the backendRef without endpoints is dropped, service-1 receives all the traffic
- matches:
- method:
service: com.example
method: Example
backendRefs:
- name: service-1
port: 8080
- name: service-with-no-endpoints
port: 8080
services:
- apiVersion: v1
kind: Service
metadata:
namespace: default
name: service-with-no-endpoints
spec:
clusterIP: 1.1.1.1
ports:
- name: http
port: 8080
protocol: TCP
targetPort: 8080
endpointSlices:
- apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
namespace: default
name: endpointslice-service-with-no-endpoints
labels:
kubernetes.io/service-name: service-with-no-endpoints
addressType: IPv4
ports:
- name: http
port: 8080
protocol: TCP
Loading
Loading