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: |-