diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go
index 5d688099a6..3b1e59206d 100644
--- a/api/v1alpha1/envoyproxy_types.go
+++ b/api/v1alpha1/envoyproxy_types.go
@@ -235,7 +235,15 @@ type EnvoyProxySpec struct {
// MergeBackendsConfig configures backend cluster deduplication (MergeBackends). Its mere
// presence on EnvoyProxySpec enables it; a backendRef is only merged into a shared cluster when
// safe to do so, otherwise it falls back to a dedicated per-route cluster.
-type MergeBackendsConfig struct{}
+type MergeBackendsConfig struct {
+ // Selector restricts cluster deduplication to backends whose target Service, ServiceImport,
+ // or Backend resource matches this label selector. When unset, every otherwise-eligible
+ // backend is merged. Use this to opt individual backends into deduplication gradually
+ // instead of enabling it for every backend at once.
+ //
+ // +optional
+ Selector *metav1.LabelSelector `json:"selector,omitempty"`
+}
// EnvoyProxyGeoIP defines shared GeoIP provider settings for EnvoyProxy.
type EnvoyProxyGeoIP struct {
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 7730ebed2b..ac28d51ed7 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -3368,7 +3368,7 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) {
if in.MergeBackends != nil {
in, out := &in.MergeBackends, &out.MergeBackends
*out = new(MergeBackendsConfig)
- **out = **in
+ (*in).DeepCopyInto(*out)
}
if in.Shutdown != nil {
in, out := &in.Shutdown, &out.Shutdown
@@ -6137,6 +6137,11 @@ func (in *Lua) DeepCopy() *Lua {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MergeBackendsConfig) DeepCopyInto(out *MergeBackendsConfig) {
*out = *in
+ if in.Selector != nil {
+ in, out := &in.Selector, &out.Selector
+ *out = new(metav1.LabelSelector)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeBackendsConfig.
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml
index 9e52118274..2cc16cdc00 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -790,6 +790,57 @@ spec:
Disabled when unset; specifying this field at all (even without further configuration)
enables it. Mutually exclusive with MergeGateways.
+ properties:
+ selector:
+ description: |-
+ Selector restricts cluster deduplication to backends whose target Service, ServiceImport,
+ or Backend resource matches this label selector. When unset, every otherwise-eligible
+ backend is merged. Use this to opt individual backends into deduplication gradually
+ instead of enabling it for every backend at once.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
type: object
mergeGateways:
description: |-
diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
index 256ae54218..01be40b694 100644
--- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -789,6 +789,57 @@ spec:
Disabled when unset; specifying this field at all (even without further configuration)
enables it. Mutually exclusive with MergeGateways.
+ properties:
+ selector:
+ description: |-
+ Selector restricts cluster deduplication to backends whose target Service, ServiceImport,
+ or Backend resource matches this label selector. When unset, every otherwise-eligible
+ backend is merged. Use this to opt individual backends into deduplication gradually
+ instead of enabling it for every backend at once.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
type: object
mergeGateways:
description: |-
diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go
index 57602ac2d0..42c5306f60 100644
--- a/internal/gatewayapi/helpers.go
+++ b/internal/gatewayapi/helpers.go
@@ -699,19 +699,23 @@ func IsMergeGatewaysEnabled(resources *resource.Resources) bool {
return false
}
-func IsMergeBackendsEnabled(resources *resource.Resources) bool {
- // Check GatewayClass-level EnvoyProxy first (higher priority)
+// ResolveMergeBackendsConfig resolves MergeBackends config, preferring the GatewayClass-level
+// EnvoyProxy over the global default. Returns nil when MergeBackends is unset in both.
+func ResolveMergeBackendsConfig(resources *resource.Resources) *MergeBackendsConfig {
if resources.EnvoyProxyForGatewayClass != nil &&
resources.EnvoyProxyForGatewayClass.Spec.MergeBackends != nil {
- return true
+ cfg := resources.EnvoyProxyForGatewayClass.Spec.MergeBackends
+ return &MergeBackendsConfig{Selector: cfg.Selector}
}
// Fall back to default EnvoyProxySpec from EnvoyGateway configuration
- if resources.EnvoyProxyDefaultSpec != nil {
- return resources.EnvoyProxyDefaultSpec.MergeBackends != nil
+ if resources.EnvoyProxyDefaultSpec != nil &&
+ resources.EnvoyProxyDefaultSpec.MergeBackends != nil {
+ cfg := resources.EnvoyProxyDefaultSpec.MergeBackends
+ return &MergeBackendsConfig{Selector: cfg.Selector}
}
- return false
+ return nil
}
func protocolSliceToStringSlice(protocols []gwapiv1.ProtocolType) []string {
diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go
index 8df6d44e78..ce8a26133a 100644
--- a/internal/gatewayapi/helpers_test.go
+++ b/internal/gatewayapi/helpers_test.go
@@ -1649,27 +1649,29 @@ func TestIrBackendClusterName(t *testing.T) {
}
}
-func TestIsMergeBackendsEnabled(t *testing.T) {
+func TestResolveMergeBackendsConfig(t *testing.T) {
+ fooSelector := &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}
+ barSelector := &metav1.LabelSelector{MatchLabels: map[string]string{"baz": "qux"}}
enabled := &egv1a1.MergeBackendsConfig{}
tests := []struct {
name string
res *resource.Resources
- want bool
+ want *MergeBackendsConfig
}{
{
name: "gatewayclass envoyproxy set",
res: &resource.Resources{
EnvoyProxyForGatewayClass: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{MergeBackends: enabled}},
},
- want: true,
+ want: &MergeBackendsConfig{},
},
{
name: "default spec set",
res: &resource.Resources{
EnvoyProxyDefaultSpec: &egv1a1.EnvoyProxySpec{MergeBackends: enabled},
},
- want: true,
+ want: &MergeBackendsConfig{},
},
{
name: "gatewayclass envoyproxy set but MergeBackends nil falls back to default spec",
@@ -1677,17 +1679,38 @@ func TestIsMergeBackendsEnabled(t *testing.T) {
EnvoyProxyForGatewayClass: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{}},
EnvoyProxyDefaultSpec: &egv1a1.EnvoyProxySpec{MergeBackends: enabled},
},
- want: true,
+ want: &MergeBackendsConfig{},
},
{
name: "unset",
res: &resource.Resources{},
- want: false,
+ want: nil,
+ },
+ {
+ name: "gatewayclass-level selector wins over global default's selector",
+ res: &resource.Resources{
+ EnvoyProxyForGatewayClass: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{
+ MergeBackends: &egv1a1.MergeBackendsConfig{Selector: fooSelector},
+ }},
+ EnvoyProxyDefaultSpec: &egv1a1.EnvoyProxySpec{
+ MergeBackends: &egv1a1.MergeBackendsConfig{Selector: barSelector},
+ },
+ },
+ want: &MergeBackendsConfig{Selector: fooSelector},
+ },
+ {
+ name: "falls back to global default's selector when gatewayclass-level MergeBackends is unset",
+ res: &resource.Resources{
+ EnvoyProxyDefaultSpec: &egv1a1.EnvoyProxySpec{
+ MergeBackends: &egv1a1.MergeBackendsConfig{Selector: barSelector},
+ },
+ },
+ want: &MergeBackendsConfig{Selector: barSelector},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- require.Equal(t, tc.want, IsMergeBackendsEnabled(tc.res))
+ require.Equal(t, tc.want, ResolveMergeBackendsConfig(tc.res))
})
}
}
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index ac5d3fccbb..5508798240 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -28,6 +28,7 @@ import (
"github.com/envoyproxy/gateway/internal/gatewayapi/status"
"github.com/envoyproxy/gateway/internal/ir"
"github.com/envoyproxy/gateway/internal/utils"
+ labelsutil "github.com/envoyproxy/gateway/internal/utils/labels"
"github.com/envoyproxy/gateway/internal/utils/regex"
)
@@ -615,7 +616,8 @@ func (t *Translator) shouldMergeBackend(
}
// Cheapest check first: skip all the more expensive eligibility work below when merging is
// off for this Gateway.
- if !t.isMergeBackendsEnabledForGateway(gatewayCtx) {
+ cfg := t.mergeBackendsConfigForGateway(gatewayCtx)
+ if cfg == nil {
return false
}
// Custom/extension-provided and dynamic-resolver backends can never safely share a cluster.
@@ -633,6 +635,10 @@ func (t *Translator) shouldMergeBackend(
if ds.Filters != nil {
return false
}
+ // The backend's target object must match the configured Selector, if any.
+ if cfg.Selector != nil && !t.mergeBackendsSelectorMatches(cfg.Selector, backendRef, backendNamespace) {
+ return false
+ }
// A rule whose effective RoutingType diverges from the gateway's baseline would leak that
// divergence into a cluster shared with rules that don't diverge.
if t.routingTypeDivergesForRule(gatewayCtx, btpRoutingType) {
@@ -642,15 +648,65 @@ func (t *Translator) shouldMergeBackend(
return true
}
-// isMergeBackendsEnabledForGateway resolves MergeBackends for gatewayCtx, letting a Gateway-level
-// override (via gatewayCtx.envoyProxy) win over t.MergeBackends' GatewayClass/default value.
-func (t *Translator) isMergeBackendsEnabledForGateway(gatewayCtx *GatewayContext) bool {
+// mergeBackendsConfigForGateway resolves the effective MergeBackendsConfig for gatewayCtx,
+// preferring a Gateway-level override over the GatewayClass/default value. Returns nil when
+// disabled.
+func (t *Translator) mergeBackendsConfigForGateway(gatewayCtx *GatewayContext) *MergeBackendsConfig {
if gatewayCtx != nil && gatewayCtx.envoyProxy != nil && gatewayCtx.envoyProxy.Spec.MergeBackends != nil {
- return true
+ cfg := gatewayCtx.envoyProxy.Spec.MergeBackends
+ return &MergeBackendsConfig{Selector: cfg.Selector}
}
return t.MergeBackends
}
+// isMergeBackendsEnabledForGateway resolves whether MergeBackends is enabled for gatewayCtx.
+func (t *Translator) isMergeBackendsEnabledForGateway(gatewayCtx *GatewayContext) bool {
+ return t.mergeBackendsConfigForGateway(gatewayCtx) != nil
+}
+
+// mergeBackendsSelectorMatches reports whether backendRef's target object matches selector. An
+// unresolvable target or an unparsable selector does not match.
+func (t *Translator) mergeBackendsSelectorMatches(selector *metav1.LabelSelector, backendRef gwapiv1.BackendObjectReference, backendNamespace string) bool {
+ backendLabels, found := t.backendLabelsFor(backendRef, backendNamespace)
+ if !found {
+ return false
+ }
+ matches, err := labelsutil.SelectorMatch(selector, backendLabels)
+ if err != nil {
+ t.Logger.Error(err, "invalid mergeBackends selector, excluding backend from deduplication",
+ "backendRef", backendRef.Name, "namespace", backendNamespace)
+ return false
+ }
+ return matches
+}
+
+// backendLabelsFor returns the labels of the Service, ServiceImport, or Backend object backendRef
+// resolves to, and whether it was found.
+func (t *Translator) backendLabelsFor(backendRef gwapiv1.BackendObjectReference, backendNamespace string) (map[string]string, bool) {
+ switch KindDerefOr(backendRef.Kind, resource.KindService) {
+ case resource.KindServiceImport:
+ svcImport := t.GetServiceImport(backendNamespace, string(backendRef.Name))
+ if svcImport == nil {
+ return nil, false
+ }
+ return svcImport.Labels, true
+ case resource.KindService:
+ svc := t.GetService(backendNamespace, string(backendRef.Name))
+ if svc == nil {
+ return nil, false
+ }
+ return svc.Labels, true
+ case egv1a1.KindBackend:
+ backend := t.GetBackend(backendNamespace, string(backendRef.Name))
+ if backend == nil {
+ return nil, false
+ }
+ return backend.Labels, true
+ default:
+ return nil, false
+ }
+}
+
// anyGatewayHasMergeBackendsEnabled reports whether MergeBackends is enabled for at least one of
// gateways, so callers can skip merge-only precomputation entirely when none of them merge.
func (t *Translator) anyGatewayHasMergeBackendsEnabled(gateways []*GatewayContext) bool {
diff --git a/internal/gatewayapi/route_test.go b/internal/gatewayapi/route_test.go
index eb62a18a34..e21a507015 100644
--- a/internal/gatewayapi/route_test.go
+++ b/internal/gatewayapi/route_test.go
@@ -16,6 +16,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+ mcsapiv1a1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1"
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
@@ -591,12 +592,15 @@ func TestShouldMergeBackend(t *testing.T) {
tests := []struct {
name string
mergeEnabled bool
+ mergeSelector *metav1.LabelSelector
gatewayEnvoyProxy *egv1a1.EnvoyProxy
gatewayBaselineRT *egv1a1.RoutingType
effectiveRT *egv1a1.RoutingType
mergeIncompatible bool
backendRef gwapiv1.BackendObjectReference
backend *egv1a1.Backend
+ service *corev1.Service
+ serviceImport *mcsapiv1a1.ServiceImport
filters *ir.DestinationFilters
want bool
}{
@@ -665,6 +669,94 @@ func TestShouldMergeBackend(t *testing.T) {
filters: &ir.DestinationFilters{AddRequestHeaders: []ir.AddHeader{{Name: "x-foo", Value: []string{"bar"}}}},
want: false,
},
+ {
+ name: "nil default-level selector: matches everything",
+ mergeEnabled: true,
+ backendRef: serviceBackendRef,
+ want: true,
+ },
+ {
+ name: "default-level selector matches the Service's labels: merges",
+ mergeEnabled: true,
+ mergeSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "dedup"}},
+ backendRef: serviceBackendRef,
+ service: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "service-1", Namespace: "default", Labels: map[string]string{"tier": "dedup"}},
+ },
+ want: true,
+ },
+ {
+ name: "default-level selector does not match the Service's labels: excluded",
+ mergeEnabled: true,
+ mergeSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "dedup"}},
+ backendRef: serviceBackendRef,
+ service: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "service-1", Namespace: "default", Labels: map[string]string{"tier": "other"}},
+ },
+ want: false,
+ },
+ {
+ name: "selector configured but target Service not found: excluded (fail-closed)",
+ mergeEnabled: true,
+ mergeSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "dedup"}},
+ backendRef: serviceBackendRef,
+ want: false,
+ },
+ {
+ name: "selector fails to parse (In operator with no values): excluded (fail-closed)",
+ mergeEnabled: true,
+ mergeSelector: &metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{
+ {Key: "tier", Operator: metav1.LabelSelectorOpIn, Values: nil},
+ }},
+ backendRef: serviceBackendRef,
+ service: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "service-1", Namespace: "default", Labels: map[string]string{"tier": "dedup"}},
+ },
+ want: false,
+ },
+ {
+ name: "selector matches a Backend CR's labels, not just Service",
+ mergeEnabled: true,
+ mergeSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "dedup"}},
+ backendRef: dynamicResolverBackendRef,
+ backend: &egv1a1.Backend{
+ ObjectMeta: metav1.ObjectMeta{Name: "be-dynamic", Namespace: "default", Labels: map[string]string{"tier": "dedup"}},
+ // deliberately NOT a dynamic-resolver type here, so isMergeableBackendKind doesn't
+ // exclude it first and this case actually exercises the selector path; reuse
+ // dynamicResolverBackendRef only for its Kind=Backend, with a plain Spec.
+ Spec: egv1a1.BackendSpec{},
+ },
+ want: true,
+ },
+ {
+ name: "selector matches a ServiceImport's labels",
+ mergeEnabled: true,
+ mergeSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "dedup"}},
+ backendRef: gwapiv1.BackendObjectReference{
+ Group: GroupPtr(mcsapiv1a1.GroupName),
+ Kind: KindPtr(resource.KindServiceImport),
+ Name: "service-import-1",
+ },
+ serviceImport: &mcsapiv1a1.ServiceImport{
+ ObjectMeta: metav1.ObjectMeta{Name: "service-import-1", Namespace: "default", Labels: map[string]string{"tier": "dedup"}},
+ },
+ want: true,
+ },
+ {
+ name: "Gateway-level EnvoyProxy selector overrides the default-level selector entirely",
+ mergeEnabled: true,
+ mergeSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "dedup"}},
+ gatewayEnvoyProxy: &egv1a1.EnvoyProxy{
+ Spec: egv1a1.EnvoyProxySpec{MergeBackends: &egv1a1.MergeBackendsConfig{
+ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"tier": "other"}},
+ }},
+ },
+ backendRef: serviceBackendRef,
+ service: &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "service-1", Namespace: "default", Labels: map[string]string{"tier": "dedup"}},
+ },
+ want: false, // matches the DEFAULT-level selector's labels, not the Gateway-level override's — must be excluded, proving the override, not the default, was actually applied
+ },
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
@@ -672,10 +764,24 @@ func TestShouldMergeBackend(t *testing.T) {
if tc.backend != nil {
backendMap[types.NamespacedName{Namespace: tc.backend.Namespace, Name: tc.backend.Name}] = tc.backend
}
+ serviceMap := map[types.NamespacedName]*corev1.Service{}
+ if tc.service != nil {
+ serviceMap[types.NamespacedName{Namespace: tc.service.Namespace, Name: tc.service.Name}] = tc.service
+ }
+ serviceImportMap := map[types.NamespacedName]*mcsapiv1a1.ServiceImport{}
+ if tc.serviceImport != nil {
+ serviceImportMap[types.NamespacedName{Namespace: tc.serviceImport.Namespace, Name: tc.serviceImport.Name}] = tc.serviceImport
+ }
+ var mergeBackends *MergeBackendsConfig
+ if tc.mergeEnabled {
+ mergeBackends = &MergeBackendsConfig{Selector: tc.mergeSelector}
+ }
tr := &Translator{
- MergeBackends: tc.mergeEnabled,
+ MergeBackends: mergeBackends,
TranslatorContext: &TranslatorContext{
- BackendMap: backendMap,
+ BackendMap: backendMap,
+ ServiceMap: serviceMap,
+ ServiceImportMap: serviceImportMap,
BTPRoutingTypeIndex: func() *BTPRoutingTypeIndex {
idx := newBTPRoutingTypeIndex()
idx.setGatewayLevel(gwNN, tc.gatewayBaselineRT)
diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go
index 810a744a82..637d9a219f 100644
--- a/internal/gatewayapi/runner/runner.go
+++ b/internal/gatewayapi/runner/runner.go
@@ -299,7 +299,7 @@ func (r *Runner) subscribeAndTranslate(sub <-chan watchable.Snapshot[string, *re
ControllerNamespace: r.ControllerNamespace,
GatewayNamespaceMode: r.EnvoyGateway.GatewayNamespaceMode(),
MergeGateways: gatewayapi.IsMergeGatewaysEnabled(resources),
- MergeBackends: gatewayapi.IsMergeBackendsEnabled(resources),
+ MergeBackends: gatewayapi.ResolveMergeBackendsConfig(resources),
PerResourceSystemCASecret: r.EnvoyGateway.RuntimeFlags.IsEnabled(egv1a1.PerResourceSystemCASecret),
WasmCache: r.wasmCache,
RunningOnHost: r.EnvoyGateway.Provider != nil && r.EnvoyGateway.Provider.IsRunningOnHost(),
diff --git a/internal/gatewayapi/testdata/mergebackends-selector-mixed-match.in.yaml b/internal/gatewayapi/testdata/mergebackends-selector-mixed-match.in.yaml
new file mode 100644
index 0000000000..115c946087
--- /dev/null
+++ b/internal/gatewayapi/testdata/mergebackends-selector-mixed-match.in.yaml
@@ -0,0 +1,111 @@
+envoyProxyForGatewayClass:
+ apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyProxy
+ metadata:
+ namespace: envoy-gateway-system
+ name: test
+ spec:
+ mergeBackends:
+ selector:
+ matchLabels:
+ mergebackends-selector: enabled
+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: http-route-1
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ rules:
+ - matches:
+ - path:
+ value: /selected
+ backendRefs:
+ - name: service-selected
+ port: 8080
+ - matches:
+ - path:
+ value: /not-selected
+ backendRefs:
+ - name: service-not-selected
+ port: 8080
+services:
+ - apiVersion: v1
+ kind: Service
+ metadata:
+ name: service-selected
+ namespace: default
+ labels:
+ mergebackends-selector: enabled
+ spec:
+ clusterIP: 7.7.7.10
+ ports:
+ - name: http
+ protocol: TCP
+ port: 8080
+ targetPort: 8080
+ - apiVersion: v1
+ kind: Service
+ metadata:
+ name: service-not-selected
+ namespace: default
+ spec:
+ clusterIP: 7.7.7.11
+ ports:
+ - name: http
+ protocol: TCP
+ port: 8080
+ targetPort: 8080
+endpointSlices:
+ - apiVersion: discovery.k8s.io/v1
+ kind: EndpointSlice
+ metadata:
+ name: endpointslice-service-selected
+ namespace: default
+ labels:
+ kubernetes.io/service-name: service-selected
+ addressType: IPv4
+ ports:
+ - name: http
+ protocol: TCP
+ port: 8080
+ endpoints:
+ - addresses:
+ - 7.7.7.10
+ conditions:
+ ready: true
+ - apiVersion: discovery.k8s.io/v1
+ kind: EndpointSlice
+ metadata:
+ name: endpointslice-service-not-selected
+ namespace: default
+ labels:
+ kubernetes.io/service-name: service-not-selected
+ addressType: IPv4
+ ports:
+ - name: http
+ protocol: TCP
+ port: 8080
+ endpoints:
+ - addresses:
+ - 7.7.7.11
+ conditions:
+ ready: true
diff --git a/internal/gatewayapi/testdata/mergebackends-selector-mixed-match.out.yaml b/internal/gatewayapi/testdata/mergebackends-selector-mixed-match.out.yaml
new file mode 100644
index 0000000000..908287a1f4
--- /dev/null
+++ b/internal/gatewayapi/testdata/mergebackends-selector-mixed-match.out.yaml
@@ -0,0 +1,228 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: http
+ port: 80
+ protocol: HTTP
+ status:
+ listeners:
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: http
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: http-route-1
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ rules:
+ - backendRefs:
+ - name: service-selected
+ port: 8080
+ matches:
+ - path:
+ value: /selected
+ - backendRefs:
+ - name: service-not-selected
+ port: 8080
+ matches:
+ - path:
+ value: /not-selected
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ config:
+ apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyProxy
+ metadata:
+ name: test
+ namespace: envoy-gateway-system
+ spec:
+ logging: {}
+ mergeBackends:
+ selector:
+ matchLabels:
+ mergebackends-selector: enabled
+ status: {}
+ listeners:
+ - name: envoy-gateway/gateway-1/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ backendClusters:
+ - metadata:
+ kind: Service
+ name: service-selected
+ namespace: default
+ sectionName: "8080"
+ name: service/default/service-selected/8080/http
+ setting:
+ addressType: IP
+ endpoints:
+ - host: 7.7.7.10
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-selected
+ namespace: default
+ sectionName: "8080"
+ name: service/default/service-selected/8080/http
+ protocol: HTTP
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.11
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-not-selected
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/http-route-1/rule/1/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/1/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /not-selected
+ - destination:
+ backendClusterRefs:
+ - name: service/default/service-selected/8080/http
+ weight: 1
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/0
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/0/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /selected
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go
index 88ebd0177e..489b67b8fa 100644
--- a/internal/gatewayapi/translator.go
+++ b/internal/gatewayapi/translator.go
@@ -11,6 +11,7 @@ import (
"fmt"
"maps"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
@@ -61,6 +62,13 @@ type TranslatorManager interface {
FiltersTranslator
}
+// MergeBackendsConfig is the resolved MergeBackends config. A nil *MergeBackendsConfig means
+// disabled; any non-nil value means enabled, mirroring egv1a1.MergeBackendsConfig's own
+// mere-presence-enables convention.
+type MergeBackendsConfig struct {
+ Selector *metav1.LabelSelector
+}
+
// Translator translates Gateway API resources to IRs and computes status
// for Gateway API resources.
type Translator struct {
@@ -88,9 +96,8 @@ type Translator struct {
// should be merged under the parent GatewayClass.
MergeGateways bool
- // MergeBackends is true when cluster deduplication is enabled: routes referencing the same
- // backend reuse a single BackendCluster instead of each getting their own.
- MergeBackends bool
+ // MergeBackends is the resolved MergeBackends config, set via ResolveMergeBackendsConfig.
+ MergeBackends *MergeBackendsConfig
// PerResourceSystemCASecret restores the old behavior of emitting one SDS secret per
// BackendTLSPolicy or Backend resource using WellKnownCACertificates: System, instead of
diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go
index f17ee67748..6752c5d744 100644
--- a/internal/gatewayapi/translator_test.go
+++ b/internal/gatewayapi/translator_test.go
@@ -173,7 +173,7 @@ func TestTranslate(t *testing.T) {
PerResourceSystemCASecret: perResourceSystemCASecret,
ControllerNamespace: "envoy-gateway-system",
MergeGateways: IsMergeGatewaysEnabled(resources),
- MergeBackends: IsMergeBackendsEnabled(resources),
+ MergeBackends: ResolveMergeBackendsConfig(resources),
GatewayNamespaceMode: gatewayNamespaceMode,
WasmCache: &mockWasmCache{},
RunningOnHost: runningOnHost,
diff --git a/internal/xds/translator/testdata/in/xds-ir/merge-backends-selector-mixed-match.yaml b/internal/xds/translator/testdata/in/xds-ir/merge-backends-selector-mixed-match.yaml
new file mode 100644
index 0000000000..2543ce98bd
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/merge-backends-selector-mixed-match.yaml
@@ -0,0 +1,88 @@
+# The Selector-matched backend (service-selected) gets an identity-scoped merged Cluster, while
+# the Selector-excluded backend (service-not-selected) keeps its own route-scoped Cluster. The two
+# backends must render as two distinct Envoy Clusters, not collapse into one.
+backendClusters:
+- metadata:
+ kind: Service
+ name: service-selected
+ namespace: default
+ sectionName: "8080"
+ name: service/default/service-selected/8080/http
+ setting:
+ addressType: IP
+ endpoints:
+ - host: 7.7.7.10
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-selected
+ namespace: default
+ sectionName: "8080"
+ name: service/default/service-selected/8080/http
+ protocol: HTTP
+http:
+- address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.11
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-not-selected
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/http-route-1/rule/1/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/1/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /not-selected
+ - destination:
+ backendClusterRefs:
+ - name: service/default/service-selected/8080/http
+ weight: 1
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/0
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/0/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /selected
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.clusters.yaml
new file mode 100644
index 0000000000..0d40a65cc6
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.clusters.yaml
@@ -0,0 +1,61 @@
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: httproute/default/http-route-1/rule/1
+ ignoreHealthOnHostRemoval: true
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/1
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: service/default/service-selected/8080/http
+ ignoreHealthOnHostRemoval: true
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: Service
+ name: service-selected
+ namespace: default
+ sectionName: "8080"
+ name: service/default/service-selected/8080/http
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.endpoints.yaml
new file mode 100644
index 0000000000..3073bc21b4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.endpoints.yaml
@@ -0,0 +1,40 @@
+- clusterName: httproute/default/http-route-1/rule/1
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 7.7.7.11
+ portValue: 8080
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: httproute/default/http-route-1/rule/1/backend/0
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: Service
+ name: service-not-selected
+ namespace: default
+ sectionName: "8080"
+- clusterName: service/default/service-selected/8080/http
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 7.7.7.10
+ portValue: 8080
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: service/default/service-selected/8080/http
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: Service
+ name: service-selected
+ namespace: default
+ sectionName: "8080"
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.listeners.yaml
new file mode 100644
index 0000000000..baf12bce7d
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.listeners.yaml
@@ -0,0 +1,36 @@
+- address:
+ socketAddress:
+ address: 0.0.0.0
+ portValue: 10080
+ defaultFilterChain:
+ filters:
+ - name: envoy.filters.network.http_connection_manager
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ commonHttpProtocolOptions:
+ headersWithUnderscoresAction: REJECT_REQUEST
+ http2ProtocolOptions:
+ initialConnectionWindowSize: 1048576
+ initialStreamWindowSize: 65536
+ maxConcurrentStreams: 100
+ httpFilters:
+ - name: envoy.filters.http.router
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ suppressEnvoyHeaders: true
+ mergeSlashes: true
+ normalizePath: true
+ pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+ rds:
+ configSource:
+ ads: {}
+ initialFetchTimeout: 0s
+ resourceApiVersion: V3
+ routeConfigName: envoy-gateway/gateway-1/http
+ serverHeaderTransformation: PASS_THROUGH
+ statPrefix: http-10080
+ useRemoteAddress: true
+ name: envoy-gateway/gateway-1/http
+ maxConnectionsToAcceptPerSocketEvent: 1
+ name: envoy-gateway/gateway-1/http
+ perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.routes.yaml
new file mode 100644
index 0000000000..64484792c3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-selector-mixed-match.routes.yaml
@@ -0,0 +1,43 @@
+- ignorePortInHostMatching: true
+ name: envoy-gateway/gateway-1/http
+ virtualHosts:
+ - domains:
+ - '*'
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http/*
+ routes:
+ - match:
+ pathSeparatedPrefix: /not-selected
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/1/match/0/*
+ route:
+ cluster: httproute/default/http-route-1/rule/1
+ upgradeConfigs:
+ - upgradeType: websocket
+ - match:
+ pathSeparatedPrefix: /selected
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: HTTPRoute
+ name: http-route-1
+ namespace: default
+ name: httproute/default/http-route-1/rule/0/match/0/*
+ route:
+ cluster: service/default/service-selected/8080/http
+ upgradeConfigs:
+ - upgradeType: websocket
diff --git a/release-notes/current/new_features/9624-mergebackends-selector.md b/release-notes/current/new_features/9624-mergebackends-selector.md
new file mode 100644
index 0000000000..5120ee88a9
--- /dev/null
+++ b/release-notes/current/new_features/9624-mergebackends-selector.md
@@ -0,0 +1 @@
+Added a `selector` field to `EnvoyProxy.spec.mergeBackends` to restrict cluster deduplication to backends whose Service, ServiceImport, or Backend resource matches a label selector, so operators can opt individual backends in gradually instead of enabling deduplication for every backend at once.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 20d3157df6..64c9eebb2f 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -4324,6 +4324,9 @@ safe to do so, otherwise it falls back to a dedicated per-route cluster.
_Appears in:_
- [EnvoyProxySpec](#envoyproxyspec)
+| Field | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| `selector` | _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#labelselector-v1-meta)_ | false | | Selector restricts cluster deduplication to backends whose target Service, ServiceImport,
or Backend resource matches this label selector. When unset, every otherwise-eligible
backend is merged. Use this to opt individual backends into deduplication gradually
instead of enabling it for every backend at once. |
#### MergeType
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index e8ae71f7e4..102af87d5c 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -34633,6 +34633,57 @@ spec:
Disabled when unset; specifying this field at all (even without further configuration)
enables it. Mutually exclusive with MergeGateways.
+ properties:
+ selector:
+ description: |-
+ Selector restricts cluster deduplication to backends whose target Service, ServiceImport,
+ or Backend resource matches this label selector. When unset, every otherwise-eligible
+ backend is merged. Use this to opt individual backends into deduplication gradually
+ instead of enabling it for every backend at once.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
type: object
mergeGateways:
description: |-
diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml
index 8d51ce19a8..9706ecded9 100644
--- a/test/helm/gateway-crds-helm/e2e.out.yaml
+++ b/test/helm/gateway-crds-helm/e2e.out.yaml
@@ -10571,6 +10571,57 @@ spec:
Disabled when unset; specifying this field at all (even without further configuration)
enables it. Mutually exclusive with MergeGateways.
+ properties:
+ selector:
+ description: |-
+ Selector restricts cluster deduplication to backends whose target Service, ServiceImport,
+ or Backend resource matches this label selector. When unset, every otherwise-eligible
+ backend is merged. Use this to opt individual backends into deduplication gradually
+ instead of enabling it for every backend at once.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
type: object
mergeGateways:
description: |-
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index a8dccebf69..5f171bbe73 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -10571,6 +10571,57 @@ spec:
Disabled when unset; specifying this field at all (even without further configuration)
enables it. Mutually exclusive with MergeGateways.
+ properties:
+ selector:
+ description: |-
+ Selector restricts cluster deduplication to backends whose target Service, ServiceImport,
+ or Backend resource matches this label selector. When unset, every otherwise-eligible
+ backend is merged. Use this to opt individual backends into deduplication gradually
+ instead of enabling it for every backend at once.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
type: object
mergeGateways:
description: |-