Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion api/v1alpha1/envoyproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: |-
Expand Down
16 changes: 10 additions & 6 deletions internal/gatewayapi/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 30 additions & 7 deletions internal/gatewayapi/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1649,45 +1649,68 @@ 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",
res: &resource.Resources{
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))
})
}
}
Expand Down
66 changes: 61 additions & 5 deletions internal/gatewayapi/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down
Loading