feat: add MergeBackends option for backend cluster deduplication#9477
feat: add MergeBackends option for backend cluster deduplication#9477muwaqar-cflt wants to merge 85 commits into
Conversation
✅ Deploy Preview for cerulean-figolla-1f9435 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
|
/retest |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9477 +/- ##
==========================================
+ Coverage 75.41% 75.60% +0.18%
==========================================
Files 252 253 +1
Lines 41660 42116 +456
==========================================
+ Hits 31417 31840 +423
- Misses 8113 8139 +26
- Partials 2130 2137 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
d5cbd23 to
0f5efab
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6011972ddb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31fbfa0fa5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // | ||
| // +optional | ||
| // +kubebuilder:default=false | ||
| Enabled *bool `json:"enabled,omitempty"` |
There was a problem hiding this comment.
We may want to default this to true eventually. The fallback approach won't break current user-facing routing behaviors, and it helps with the Envoy memory issue.
It may be too risky to turn this on by default in 1.9, as it does break EPPs and extension servers, but we could keep it opt-in for the first release and consider flipping the default in 1.10 after we have more coverage and user feedback.
There was a problem hiding this comment.
resolving because not seeing an action item for now.
There was a problem hiding this comment.
is this flag needed ? mergeBackends: {} should imply we are turning on the feature
There was a problem hiding this comment.
yeah, not necessarily. A bool flag is more explicit than mergeBackends: {}, I'm fine with both.
|
lint fails :( |
|
This PR looks good. Great work! I left a few minor comments. @muwaqar-cflt Could you please fix the link and check whether all e2e tests pass? I’d like to get this merged before 1.9.0 if possible. Thanks! |
Per maintainer feedback on PR envoyproxy#9477, ship only the Enabled boolean for the first release. Fallback (the only supported behavior) is now unconditional; Force was already rejected by CEL validation and had no implementation, so there was no user-visible behavior to preserve by keeping the enum. A Mode/MergeBackendsMode field can be reintroduced later if a concrete Force use case emerges. Regenerated deepcopy, CRDs, and docs; dropped mode: Fallback from all mergebackends-* input fixtures and their derived golden output; replaced the CEL validation cases exercising Mode with Enabled-only equivalents. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
A merged cluster is already scoped to one gateway, so a gateway-level BTP setting applies uniformly to every route sharing it and can never cause the divergence this check exists to catch — worth a one-line note for future readers, per review feedback on PR envoyproxy#9477. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
| @@ -133,24 +160,14 @@ xdsIR: | |||
| port: 10090 | |||
| routes: | |||
| - destination: | |||
| backendClusterRefs: | |||
There was a problem hiding this comment.
can we avoid testdata changes to the default case
Add mergebackends-http-demerge-routing-type-divergence covering the scenario where a route-level BackendTrafficPolicy overrides RoutingType away from the gateway's baseline: two HTTPRoutes share one backend identity, a gateway-level BTP sets a baseline RoutingType of Service, and a route-level BTP on one route overrides it to Endpoint. This proves routingTypeDivergesForRule correctly keeps the diverging route on its own route-scoped cluster while the other route still shares the merged backend-identity-scoped cluster. No production code changes; this closes a coverage gap identified against a competing PR's fixture (mergebackends-http-service-routing-not-merged), which uses a different EnvoyProxy.spec.mergeBackends field shape than this branch. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
Task 5 replaced the two Cluster: backendWeights.Name references (buildXdsRouteAction, buildXdsURLRewriteAction) with singleResolvedClusterName(route.Destination), since backendWeights.Name was the rule-scoped destination name rather than the resolved BackendCluster name and produced dangling cluster references for merged backends. That left BackendWeights.Name populated but never read anywhere (the weighted-clusters path only ever consumes UnavailableWeight()) - remove it so it can't be mistaken for authoritative and reintroduce the same bug. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…age and link merge-backends fixtures to real gatewayapi IR Adds dedicated xds/translator-layer fixtures proving the merged-cluster cluster-naming fix (from the prior commit) holds for TLS and GRPC, not just plain HTTP/TCP/UDP, and for the weighted-clusters route-action path (used when a route's destination needs a per-setting cluster breakdown), not just the single-cluster path. Also closes a realism gap in all five protocols' xds/translator fixtures: they previously had only one route/endpoint each, which didn't exercise "merging" (multiple routes collapsing onto one cluster) or a realistic multi-endpoint backend. For each of HTTP/GRPC/TCP/UDP/TLS, adds (or enriches an existing) gatewayapi-layer mergebackends-<proto>-shared-cluster fixture - a real Gateway API resource setup proving the IR-building layer merges correctly - then replaces the corresponding xds/translator fixture's hand-authored content with the actual IR extracted from that gatewayapi fixture's real output. This gives a genuine, traceable, end-to-end proof chain (real K8s-shaped input -> real IR -> real Envoy config) instead of two independently hand-authored, disconnected layers. Also adds a mixed-merge regression test pair, at both layers, for a subtle case: a single rule with backendRefs that have different merge outcomes (one merges, one or more don't, e.g. because of a per-backendRef filter). resolveBackendCluster's non-merge branch keys all of a rule's non-merged backendRefs under the same rule-scoped cache entry, so they land in one shared gatewayapi-layer BackendCluster "container" - but each retains its own DestinationSetting.Name, and the xds/translator's cluster-per-setting splitting (triggered by the per-backendRef filters) correctly gives each its own real Envoy Cluster and its own entry in the weighted route action, never collapsing them under the shared container's name. Verified with both a 2-backendRef (1 merged, 1 filtered) and 3-backendRef (1 merged, 2 differently-filtered) case. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…view - Rename btpClusterSettingsHasSettings to btpSpecHasClusterScopedFields (and its test) - the old name read as testing "ClusterSettings" the struct, when it actually checks the whole BackendTrafficPolicySpec for any CDS-scoped field. - Trim ir.BackendCluster.Merged's doc comment to a one-liner, consistent with the struct's other fields. - Rewrite singleResolvedClusterName's comment: frame the registry-lookup indirection as xds/translator defensively protecting itself against not-necessarily-well-formed data from an earlier layer, rather than tracing specific (and driftable) legit code paths that could produce a ref-without-registration. - Rename mergeIncompatibleForRule/mergeIncompatibleForNonWeightedRule to mergeIncompatibleForWeightedRule/mergeIncompatibleForSingleClusterRule - the old names read as a general case and a negated special case of it, when they're actually independent implementations for two route-type families distinguished by whether their route action supports weighted clusters. Split mergeIncompatibleForSingleClusterRule's combined boolean into guard clauses with a comment per condition, matching the sibling function's established style. - mergeIncompatibleForWeightedRule: add a comment to the len(backendRefs) <= 1 guard, and restructure the final ConsistentHash condition into an explicit if/return true with a trailing return false, for consistency with the other guard clauses. Its position (checked before session persistence, fallback backend, and ConsistentHash) is intentional and unchanged: those three checks protect a multi-backend pool from fragmenting, which doesn't apply when there's nothing to fragment. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…ure to BackendClusterRefs schema This fixture arrived via the sync with upstream main (PR envoyproxy#9199) after this branch's earlier fixture-migration commits already ran, so it still used the old inline destination.settings shape instead of backendClusterRefs/backends. Migrate it the same way; golden output is byte-for-byte identical, confirming this is a pure schema migration with no behavior change. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
Found while comparing test coverage against the competing PR envoyproxy#9459, which has its own TestIsMergeBackendsEnabled - we had none, only indirect coverage via translator_test.go's fixture harness (which exercises the function but doesn't specifically pin its precedence behavior). Add direct coverage for GatewayClass-level vs. default-spec precedence, including two cases the competing PR's version didn't cover: GatewayClass EnvoyProxy explicitly disabling MergeBackends still wins over a default spec that enables it, and a GatewayClass EnvoyProxy with no MergeBackends field at all correctly falls back to the default spec. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
Cluster.AltStatName (customized via EnvoyProxy.Spec.Telemetry.Metrics.ClusterStatName) is derived from a route/rule's own identity (route name, rule name/index, backendRef names) and set on ir.RouteDestination.StatName. asClusterArgs applied it unconditionally, so a merged BackendCluster - shared by routes whose ExtraArgs.statName differs - got whichever route's value happened to register the cluster first, misattributing the other routes' stats. Same fix shape as the earlier routeHostname leak: fall back to no AltStatName override for a merged cluster, so Envoy reports stats under the cluster's own backend-identity name instead of an arbitrary route's. This is preserved unchanged for the non-merged case, where the route-derived name remains exactly as before - the field itself stays on RouteDestination rather than moving to BackendCluster, since a stat name is inherently rule-scoped (one rule can resolve to several distinct clusters that already share an identical name today, by design) rather than tied to any single cluster. New xds-ir fixture (merge-backends-statname) proves both directions: two routes with different stat names sharing one merged cluster produce no AltStatName override, while the existing non-merged stat-name fixture is unaffected. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
btpSpecHasClusterScopedFields missed AdmissionControl, so a BackendTrafficPolicy that only sets admissionControl was wrongly treated as CDS-compatible with a shared/merged cluster, letting its filter leak onto every route sharing that cluster instead of forcing its own dedicated one. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…usters A merged BackendCluster's single Setting is shared by every route referencing it, so its Weight only ever reflects whichever route first registered the cluster. buildXdsWeightedRouteAction and backendWeights read that cached, stale Weight directly, so all but the first route to reference a merged cluster got the wrong weight in weighted route actions and error-response proportioning. Resolve weight through the route's own BackendClusterRef instead: resolvedBackendRef.ResolvedWeight uses ref.Weight for merged clusters and the setting's own Weight otherwise, and ToBackendWeights aggregates over that. Also clear the merged cluster's stored Setting.Weight so nothing can read the stale value directly by mistake. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…namespace BuildBTPClusterSettingsIndex and BuildBTPLoadBalancerIndex keyed their btpRoutingKey entries by the policy's own namespace instead of the resolved target's namespace. For a BTP using cross-namespace targetSelectors (allowed via a ReferenceGrant), this stored the entry under the wrong namespace, so a later lookup for the target's actual namespace missed and MergeBackends wrongly shared a cluster with routes that don't carry that cluster-scoped policy. Key by ref.Namespace instead, matching BuildBTPRoutingTypeIndex, which already does this correctly. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
shouldMergeBackend only checked t.MergeBackends, which IsMergeBackendsEnabled derives from the GatewayClass-attached EnvoyProxy (or the EnvoyGateway default) — never from a Gateway-level EnvoyProxy attached via Gateway.spec.infrastructure.parametersRef. A Gateway wanting to opt in (or out) of backend cluster deduplication on its own, without changing the GatewayClass-wide default, had no way to do so. gatewayCtx.envoyProxy is already the correctly-merged config for this Gateway (Gateway overriding GatewayClass overriding default, see GatewayContext.attachEnvoyProxy), and shouldMergeBackend already receives gatewayCtx, so resolve MergeBackends from there when set, falling back to t.MergeBackends otherwise. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
Covers EnvoyProxy.spec.mergeBackends end-to-end: two HTTPRoutes referencing the same backend both route correctly, and Envoy generates a single deduplicated Cluster for that backend instead of one per route. The dedup check fetches Envoy's admin /clusters endpoint via port-forward (collect.RequestWithPortForwarder, already used the same way in test/benchmark/suite/report.go for pprof). Uses a Gateway-level EnvoyProxy attachment to enable mergeBackends, per the Gateway-level override support just added in shouldMergeBackend. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
AddWeighted used receiver w while UnavailableWeight (same type) uses b, which CI's lint job (staticcheck) flags. Rename to b for consistency. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
Per maintainer feedback on PR envoyproxy#9477, ship only the Enabled boolean for the first release. Fallback (the only supported behavior) is now unconditional; Force was already rejected by CEL validation and had no implementation, so there was no user-visible behavior to preserve by keeping the enum. A Mode/MergeBackendsMode field can be reintroduced later if a concrete Force use case emerges. Regenerated deepcopy, CRDs, and docs; dropped mode: Fallback from all mergebackends-* input fixtures and their derived golden output; replaced the CEL validation cases exercising Mode with Enabled-only equivalents. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
A merged cluster is already scoped to one gateway, so a gateway-level BTP setting applies uniformly to every route sharing it and can never cause the divergence this check exists to catch — worth a one-line note for future readers, per review feedback on PR envoyproxy#9477. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
processClientCertificates ran once per route referencing a backend and added a secret via tCtx.AddXdsResource directly, which has no dedup by name (unlike addXdsSecret). Multiple routes sharing one backend's client certificate (e.g. a SecurityPolicy's extAuth destination attached to more than one route, or a BackendTLS client cert shared across routes) produced one duplicate secret resource per extra route referencing it. This was previously masked in the ext-auth-backend fixture by two routes' extAuth destinations having artificially different, hand-authored settings under the old inline-settings IR schema despite sharing a name; migrating to the BackendClusterRefs registry made both routes correctly resolve to the one real shared backend, surfacing the latent duplicate-add bug. Route to addXdsSecret, which already dedupes by name. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…adata getOrCreateBackendCluster set a merged cluster's Metadata to whichever route's routeRuleMetadata happened to register the cluster first - the same first-registered-wins hazard already fixed for StatName, Hostname, and Weight. Since the cluster is shared across every route referencing that backend, this metadata is both semantically wrong (implies the cluster belongs to one specific route) and a source of unnecessary xDS churn (it can flip to a different route's identity as routes are added, removed, or reordered, despite the backend itself never changing). ds.Metadata already holds the backend's own resource identity (Service or Backend CR, built independent of any calling route in processServiceDestinationSetting/processBackendDestinationSetting), so use that for merged clusters instead - a stable value tied to the backend, not to whichever route asks first. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…-direct-source-ip fixture Migrating this fixture to the BackendClusterRefs registry schema dropped the route's Metadata (previously set directly on RouteDestination) since the new top-level backends: entry never had it added. This is a route-scoped (non- merged) cluster, so its Cluster metadata should identify the owning route, same as every other non-merged cluster - restore it to match the pre-migration baseline exactly. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
IsConsistentHash has exactly one caller (mergeIncompatibleForWeightedRule), which only reaches it after hasRouteLevelClusterSettings is already false for the same target. Since btpSpecHasClusterScopedFields treats any LoadBalancer as a cluster-scoped field (not just ConsistentHash), that precondition already guarantees no route-rule/route/listener-level BTP sets LoadBalancer at all - making those three levels of BTPLoadBalancerIndex dead code at its one call site. Collapse the index to a single gateway-level map, precomputed as a bool (is-ConsistentHash) rather than storing the full *LoadBalancer and re-checking .Type on every lookup. Shrink IsConsistentHash's signature to just the gateway identity it now needs, and drop the parentRef/ruleName params from mergeIncompatibleForWeightedRule that existed solely to build the old signature's route-rule key. Rewrite TestBTPLoadBalancerIndexIsConsistentHash: drop the two cases that tested now-removed route-rule/route precedence behavior, and re-point the cross-namespace case at a Gateway target (the only level left) instead of a Route. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
mergeIncompatibleForWeightedRule mixed the len(backendRefs) <= 1 guard with the session-persistence/fallback/ConsistentHash checks it guards. Extract the latter into weightedRuleBackendsMustBeInOneCluster (named for the HTTP/GRPC-only weighted-rule path, mirroring mergeIncompatibleForSingleClusterRule's naming for the TCP/UDP/TLS path) so the guard and the checks it protects read as separate concerns. Pure extraction, no behavior change. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
…dentity resolveBackendCluster used t.getIRKey(gatewayCtx.Gateway) for a merged cluster's key and name, which collapses to the GatewayClass-wide identity when MergeGateways is enabled. Two Gateways sharing one Envoy deployment via MergeGateways can have different gateway-scoped BackendTrafficPolicy settings; if they also reference the same backend under MergeBackends, they were incorrectly merged into one shared cluster, and the CDS cluster name collision meant whichever route was processed first silently won - addXdsCluster skips rebuilding a cluster that already exists by name, so the other Gateway's settings (health check, circuit breaker, LB policy, etc.) were dropped rather than applied. Always key/name a merged cluster by the Gateway's own raw namespace/name, never the MergeGateways-collapsed one. When MergeGateways is disabled this is exactly what getIRKey already returned, so no existing fixture changes; irBackendClusterName only appends the Gateway identity when MergeGateways is enabled, keeping cluster names unchanged for the common case. Adds coverage at three levels: a resolveBackendCluster unit test proving two Gateways get different keys and names, a gatewayapi Translate() fixture with two Gateways carrying different gateway-scoped BackendTrafficPolicy CircuitBreaker settings on the same backend, and an xds/translator fixture proving both Clusters build with their own distinct settings rather than one silently winning. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
Cover the MergeGateways+MergeBackends combination: two Gateways sharing one Envoy deployment, each with its own gateway-scoped BackendTrafficPolicy, routing to the same backend. Verifies each Gateway keeps its own Cluster with its own settings instead of colliding into one shared Cluster. Also extract the admin /clusters port-forward/fetch logic shared by MergeBackendsTest and the new test into fetchEnvoyClustersOutput. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
Rebasing onto main pulled in new tests whose golden fixtures still used the pre-BackendClusterRefs-migration schema (inline destination settings instead of backendClusterRefs + a top-level backends registry). Regenerated the gatewayapi-layer fixtures via -override-testdata, and hand-migrated the one xds-ir input fixture (host-normalization.yaml) that -override-testdata can't rewrite itself. Signed-off-by: Muhammad Waqar <waqar.hameed08@gmail.com>
f3bc58b to
fff4d8f
Compare
| for _, rule := range tlsRoute.Spec.Rules { | ||
| gatewayCtx, btpRoutingType, hasRouteLevelClusterSettings := t.resolveRoutingContext(tlsRoute, parentRef, rule.Name) | ||
| gwIR := t.gatewayXdsIR(gatewayCtx, xdsIR) | ||
| mergeIncompatible := mergeIncompatibleForSingleClusterRule(hasRouteLevelClusterSettings, len(rule.BackendRefs)) | ||
| for i := range rule.BackendRefs { | ||
| settingName := irDestinationSettingName(destName, i) | ||
| backendRefCtx := DirectBackendRef{BackendRef: &rule.BackendRefs[i]} | ||
| // ds will never be nil here because processDestination returns an empty DestinationSetting for invalid backendRefs. | ||
| ds, _, err := t.processDestination(settingName, backendRefCtx, parentRef, tlsRoute, resources, rule.Name) | ||
| ds, cluster, _, err := t.processBackendRef(destName, i, backendRefCtx, parentRef, tlsRoute, resources, gatewayCtx, btpRoutingType, xdsIR, mergeIncompatible) | ||
| if err != nil { | ||
| resolveErrs.Add(err) | ||
| continue | ||
| } | ||
| // skip backendRefs with weight 0 as they do not affect the traffic distribution | ||
| if ds.Weight != nil && *ds.Weight > 0 { | ||
| destSettings = append(destSettings, ds) | ||
| backendCluster := t.getOrCreateBackendCluster(gwIR, cluster.Key, cluster.Name, cluster.Merge, ds, routeRuleMetadata) |
There was a problem hiding this comment.
@arkodg Splitting the Codex findings into individual threads for tracking, so each can be resolved independently as it's fixed.
Multi-rule TLSRoute can reference a nonexistent cluster
Merge safety is decided independently per rule here, but a TLSRoute can have up to 16 rules — two rules that each have a single backend can each look mergeable on their own. That produces one TCP proxy route referencing multiple backend-cluster names, and singleResolvedClusterName() falls back to a route-scoped name that was never actually created as a Cluster — so the listener ends up referencing a missing cluster.
Merge compatibility needs to be computed across the whole TLSRoute, not independently per rule.
| func (t *Translator) shouldMergeBackend( | ||
| gatewayCtx *GatewayContext, | ||
| btpRoutingType *egv1a1.RoutingType, | ||
| mergeIncompatible bool, | ||
| backendRef gwapiv1.BackendObjectReference, | ||
| backendNamespace string, | ||
| ds *ir.DestinationSetting, | ||
| ) bool { | ||
| // Custom/extension-provided and dynamic-resolver backends can never safely share a merged cluster. | ||
| if !t.isMergeableBackendKind(backendRef, backendNamespace) { | ||
| return false | ||
| } | ||
| // A rule-wide reason (route-level cluster settings, session persistence, fallback backend, | ||
| // ConsistentHash) already makes merging unsafe for every backendRef in this rule. | ||
| if mergeIncompatible { | ||
| return false | ||
| } | ||
| // A merged cluster keeps only the first-registered backendRef's Filters (getOrCreateBackendCluster | ||
| // never appends a second Settings entry when merge=true), so any backendRef carrying filters that | ||
| // could legitimately differ per-backendRef (header modification, URL rewrite, CredentialInjection, | ||
| // etc.) must never share a cluster with a differently-configured one. | ||
| if ds.Filters != nil { | ||
| return false | ||
| } | ||
| if !t.isMergeBackendsEnabledForGateway(gatewayCtx) { | ||
| return false | ||
| } | ||
|
|
||
| return !t.routingTypeDivergesForRule(gatewayCtx, btpRoutingType) | ||
| } | ||
|
|
There was a problem hiding this comment.
Listener-specific ClientTrafficPolicy settings leak or disappear
shouldMergeBackend doesn't account for listener-specific HTTP settings (e.g. preserveHeaderCase). If two listeners with different listener-level HTTP settings both route to the same backend, they end up sharing one merged cluster; whichever listener's route gets processed first wins (addXdsCluster's existing-cluster-name skip), silently dropping or leaking the other listener's settings depending on processing order.
Effective listener HTTP settings need to either be folded into the merge identity, or make the backend ineligible for merging.
| func (httpRoute *HTTPRouteTranslator) asClusterArgs(bc *ir.BackendCluster, extra *ExtraArgs) *xdsClusterArgs { | ||
| // A merged cluster may be shared by routes with different hostnames or stat names, so no | ||
| // single route's value reliably describes it — fall back to none rather than an arbitrary one. | ||
| routeHostname := httpRoute.Hostname | ||
| statName := extra.statName | ||
| if bc.Merged { | ||
| routeHostname = "" | ||
| statName = nil |
There was a problem hiding this comment.
Merging can break active HTTP health checks
For every merged cluster, this clears the route hostname unconditionally (routeHostname = ""), even when only one route currently references the cluster. An HTTP health check without an explicit Host previously used that route hostname; once merged, no Host is emitted, so Envoy falls back to the cluster name instead — a virtual-host-sensitive health endpoint can return 404 and mark the backend unhealthy.
Should only merge when the health-check Host is explicit/invariant across the merged routes, or keep separate clusters when route hostname affects health checking.
| func (httpRoute *HTTPRouteTranslator) asClusterArgs(bc *ir.BackendCluster, extra *ExtraArgs) *xdsClusterArgs { | ||
| // A merged cluster may be shared by routes with different hostnames or stat names, so no | ||
| // single route's value reliably describes it — fall back to none rather than an arbitrary one. | ||
| routeHostname := httpRoute.Hostname | ||
| statName := extra.statName | ||
| if bc.Merged { | ||
| routeHostname = "" | ||
| statName = nil |
There was a problem hiding this comment.
clusterStatName is silently ignored for merged clusters
Same block also clears statName unconditionally for merged clusters, so a configured EnvoyProxy.spec.telemetry.metrics.clusterStatName stops producing alt_stat_name once merging is enabled.
Should either prevent merging when a stat name is configured, use a documented backend-based stat name, or explicitly reject/document this as an incompatible combination rather than silently discarding it.
|
The current shape makes the backend registry part of every translation, even when I think we can keep the universal IR registry while making merge-specific work pay for itself:
The first two changes are the ones I would prioritize in this PR because they directly address the measured cost when the feature is not enabled. The remaining items could be follow-ups if keeping this change smaller is preferable. |
| // and its endpoint configuration. Multiple routes sharing the same backend can reference the | ||
| // same BackendCluster. | ||
| // +kubebuilder:object:generate=true | ||
| type BackendCluster struct { |
There was a problem hiding this comment.
Consider using BackendCluster only for the merged case, and keeping inline Settings on RouteDestination for non-merged.
Right now BackendCluster serves double duty — merged (shared, in Xds.Backends) and non-merged (route-scoped) — carried by a Merged flag. That makes the xDS translation layer harder to follow and it churns every non-merged golden fixture.
I think we can confine the new machinery to the merged path:
type RouteDestination struct {
Name string `json:"name" yaml:"name"`
StatName *string `json:"statName,omitempty" yaml:"statName,omitempty"`
// A destination is either route-local (Settings) or merged (BackendClusterRefs) — never
// both. Both empty means it has no resolvable backend.
//
// Settings holds the endpoints of this route's OWN, non-merged cluster (the default case).
// A rule's backendRefs collapse into this one route-scoped cluster, so it can hold multiple
// settings. It is never shared with another route.
Settings []*DestinationSetting `json:"settings,omitempty" yaml:"settings,omitempty"`
// BackendClusterRefs references shared, identity-deduplicated clusters in Xds.Backends (the
// merged case) — one ref per backend. Each referenced cluster is a single backend identity
// and may be shared across routes; its data lives in the registry.
BackendClusterRefs []*BackendClusterRef `json:"backendClusterRefs,omitempty" yaml:"backendClusterRefs,omitempty"`
IsDynamicResolver bool `json:"isDynamicResolver,omitempty" yaml:"isDynamicResolver,omitempty"`
Metadata *ResourceMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// A shared, identity-deduplicated backend cluster: one backend identity, one setting.
type BackendCluster struct {
Name string
Setting *DestinationSetting // singular — holding the endpoints for a shared backend
Metadata *ResourceMetadata
}
Benefits:
- Non-merged IR and xDS output revert to their previous shape → non-merged fixtures stay untouched, so the diff shows only the merged feature.
- Merged becomes structural ("in the registry ⇔ merged") rather than a flag, so the type stops doing double duty.
- len(rd.BackendClusterRefs) > 1 becomes unambiguous — non-merged has zero refs, so > 1 cleanly means "merged multi-backend."
- Easier to review since the number of changed files would be much less.
WDYT? @muwaqar-cflt @arkodg ?
What this PR does / why we need it:
Adds an opt-in
EnvoyProxy.spec.mergeBackendsoption: routes referencing the identical backend can share a single Envoy cluster instead of Envoy Gateway generating one cluster per route rule. This reduces xDS size, active health-check traffic, and stats cardinality on gateways with many routes fanning out to a small set of shared backends. Disabled by default and marked experimental.Cluster sharing is skipped wherever it would be unsafe:
UseClientProtocoloverride, both of which are baked into the shared cluster rather than the per-route destinationApproach: enabling this required restructuring how a route's backend data is stored. Previously, every
RouteDestinationembedded its own copy of the resolvedDestinationSettings, so two routes sharing a backend could never share anything at the IR level - deduplication had nowhere to attach. This PR introduces a per-gateway backend cluster registry (ir.Xds.Backends,ir.BackendCluster): each distinct backend is resolved once, and everyRouteDestinationholds only aBackendClusterRef(a name) into that registry.RouteDestination.Settingsis removed onceBackendClusterRefsis the sole source.Beyond enabling MergeBackends, this registry is what makes a future backend-level BackendTrafficPolicy straightforward: since a backend's settings now live in exactly one place instead of being copied into every route that references it, a policy targeting the backend directly can be applied once at the registry entry, rather than needing to be threaded through and reconciled across every route-level copy.
Reviewer notes: this is a large diff; start here:
api/v1alpha1/envoyproxy_types.go- the new user-facingMergeBackends/MergeBackendsConfigfieldinternal/ir/xds.go- theBackendCluster/BackendClusterReftypes and theXds.Backendsregistryinternal/gatewayapi/route.go-processBackendRefis the single entry point every route type (and the RequestMirror filter) uses to resolve a backendRef's destination and cluster assignment together. It wrapsresolveBackendCluster(decides the cache key/name/merge outcome),shouldMergeBackend(owns the full eligibility decision),mergeIncompatibleForWeightedRule/mergeIncompatibleForSingleClusterRule(the per-rule-level exclusions, split by whether the route type supports weighted-cluster route actions), andgetOrCreateBackendCluster(the find-or-create registry lookup)internal/xds/translator/backend_registry.go- registry resolution (getBackendClusters,singleResolvedClusterName) consumed by the HTTP/GRPC route-action and TCP/UDP listener builders, andcluster.go's cluster-per-setting/weight logicA good portion of the remaining diff is golden testdata:
internal/gatewayapi/testdata/*.out.yamlfixtures re-derived to reflect destinations serializing asbackendClusterRefs+ a top-levelbackends:registry instead of inlinesettings:, plus new fixtures (mergebackends-*ininternal/gatewayapi/testdata,merge-backends-*ininternal/xds/translator/testdata) covering the merge-eligibility rules above at both the IR-building layer and the xDS translation layer - several of the latter are derived directly from the former's real output, tracing a single Gateway API resource all the way through to the generated Envoy config.Which issue(s) this PR fixes:
Fixes #5307