diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go index c31a322de5..5eed95aab6 100644 --- a/internal/gatewayapi/filters.go +++ b/internal/gatewayapi/filters.go @@ -210,6 +210,16 @@ func (t *Translator) ProcessGRPCFilters( } } + if httpFiltersContext.DirectResponse != nil && len(httpFiltersContext.Mirrors) > 0 { + httpFiltersContext.DirectResponse = nil + httpFiltersContext.Mirrors = nil + + errs.Add(status.NewRouteStatusError( + errors.New(requestMirrorDirectResponseConflictMsg), + gwapiv1.RouteReasonIncompatibleFilters, + ).WithType(gwapiv1.RouteConditionAccepted)) + } + return httpFiltersContext, errs.GetAllErrors() } diff --git a/internal/gatewayapi/testdata/grpcroute-with-conflicting-filters.in.yaml b/internal/gatewayapi/testdata/grpcroute-with-conflicting-filters.in.yaml new file mode 100644 index 0000000000..155d42ae5e --- /dev/null +++ b/internal/gatewayapi/testdata/grpcroute-with-conflicting-filters.in.yaml @@ -0,0 +1,58 @@ +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 +grpcRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: GRPCRoute + metadata: + namespace: default + name: mirror-direct-response + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - type: RequestMirror + requestMirror: + backendRef: + kind: Service + name: service-1 + port: 8080 + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: mirror-direct-response + - backendRefs: + - name: service-2 + port: 8080 +httpFilters: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: HTTPRouteFilter + metadata: + name: mirror-direct-response + namespace: default + spec: + directResponse: + statusCode: 404 + contentType: text/plain + body: + type: Inline + inline: "blocked" diff --git a/internal/gatewayapi/testdata/grpcroute-with-conflicting-filters.out.yaml b/internal/gatewayapi/testdata/grpcroute-with-conflicting-filters.out.yaml new file mode 100644 index 0000000000..798bd83c26 --- /dev/null +++ b/internal/gatewayapi/testdata/grpcroute-with-conflicting-filters.out.yaml @@ -0,0 +1,199 @@ +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 +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: GRPCRoute + metadata: + name: mirror-direct-response + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - requestMirror: + backendRef: + kind: Service + name: service-1 + port: 8080 + type: RequestMirror + - extensionRef: + group: gateway.envoyproxy.io + kind: HTTPRouteFilter + name: mirror-direct-response + type: ExtensionRef + - backendRefs: + - name: service-2 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: |- + Dropped Rule(s) [0]: Invalid filter ExtensionRef: unknown kind gateway.envoyproxy.io/HTTPRouteFilter. + RequestMirror filter cannot be used when the rule also configures a DirectResponse filter. + reason: IncompatibleFilters, UnsupportedValue + status: "True" + type: PartiallyInvalid + - 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 + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + 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 + 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 + grpc: + enableGRPCStats: true + enableGRPCWeb: true + 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: + - directResponse: + statusCode: 500 + hostname: '*' + isHTTP2: true + metadata: + kind: GRPCRoute + name: mirror-direct-response + namespace: default + name: grpcroute/default/mirror-direct-response/rule/0/match/-1/* + - destination: + metadata: + kind: GRPCRoute + name: mirror-direct-response + namespace: default + name: grpcroute/default/mirror-direct-response/rule/1 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-2 + namespace: default + sectionName: "8080" + name: grpcroute/default/mirror-direct-response/rule/1/backend/0 + protocol: GRPC + weight: 1 + hostname: '*' + isHTTP2: true + metadata: + kind: GRPCRoute + name: mirror-direct-response + namespace: default + name: grpcroute/default/mirror-direct-response/rule/1/match/-1/* + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/provider/kubernetes/indexers.go b/internal/provider/kubernetes/indexers.go index 52e50d459e..ef3cedb017 100644 --- a/internal/provider/kubernetes/indexers.go +++ b/internal/provider/kubernetes/indexers.go @@ -466,6 +466,21 @@ func backendGRPCRouteIndexFunc(rawObj client.Object) []string { ) } } + + for _, filter := range rule.Filters { + if filter.Type != gwapiv1.GRPCRouteFilterRequestMirror { + continue + } + + mirrorBackendRef := filter.RequestMirror.BackendRef + + backendRefs = append(backendRefs, + types.NamespacedName{ + Namespace: gatewayapi.NamespaceDerefOr(mirrorBackendRef.Namespace, grpcroute.Namespace), + Name: string(mirrorBackendRef.Name), + }.String(), + ) + } } return backendRefs } diff --git a/internal/provider/kubernetes/indexers_test.go b/internal/provider/kubernetes/indexers_test.go new file mode 100644 index 0000000000..a09b9f1f64 --- /dev/null +++ b/internal/provider/kubernetes/indexers_test.go @@ -0,0 +1,247 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func TestBackendGRPCRouteIndexFunc(t *testing.T) { + testCases := []struct { + name string + route *gwapiv1.GRPCRoute + expected []string + }{ + { + name: "no filters, single backend", + route: &gwapiv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grpcroute-1", + Namespace: "default", + }, + Spec: gwapiv1.GRPCRouteSpec{ + Rules: []gwapiv1.GRPCRouteRule{ + { + BackendRefs: []gwapiv1.GRPCBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Name: "service-1", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{"default/service-1"}, + }, + { + name: "request mirror filter includes mirror backendRef", + route: &gwapiv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grpcroute-mirror", + Namespace: "default", + }, + Spec: gwapiv1.GRPCRouteSpec{ + Rules: []gwapiv1.GRPCRouteRule{ + { + BackendRefs: []gwapiv1.GRPCBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Name: "service-1", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + Filters: []gwapiv1.GRPCRouteFilter{ + { + Type: gwapiv1.GRPCRouteFilterRequestMirror, + RequestMirror: &gwapiv1.HTTPRequestMirrorFilter{ + BackendRef: gwapiv1.BackendObjectReference{ + Kind: ptr.To(gwapiv1.Kind("Service")), + Name: "mirror-service", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{"default/service-1", "default/mirror-service"}, + }, + { + name: "mirror filter with cross-namespace backendRef", + route: &gwapiv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grpcroute-cross-ns", + Namespace: "default", + }, + Spec: gwapiv1.GRPCRouteSpec{ + Rules: []gwapiv1.GRPCRouteRule{ + { + BackendRefs: []gwapiv1.GRPCBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Name: "service-1", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + Filters: []gwapiv1.GRPCRouteFilter{ + { + Type: gwapiv1.GRPCRouteFilterRequestMirror, + RequestMirror: &gwapiv1.HTTPRequestMirrorFilter{ + BackendRef: gwapiv1.BackendObjectReference{ + Kind: ptr.To(gwapiv1.Kind("Service")), + Namespace: ptr.To(gwapiv1.Namespace("other-ns")), + Name: "mirror-service", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{"default/service-1", "other-ns/mirror-service"}, + }, + { + name: "multiple mirror filters in same rule", + route: &gwapiv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grpcroute-multi-mirror", + Namespace: "default", + }, + Spec: gwapiv1.GRPCRouteSpec{ + Rules: []gwapiv1.GRPCRouteRule{ + { + BackendRefs: []gwapiv1.GRPCBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Name: "service-1", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + Filters: []gwapiv1.GRPCRouteFilter{ + { + Type: gwapiv1.GRPCRouteFilterRequestMirror, + RequestMirror: &gwapiv1.HTTPRequestMirrorFilter{ + BackendRef: gwapiv1.BackendObjectReference{ + Kind: ptr.To(gwapiv1.Kind("Service")), + Name: "mirror-1", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + { + Type: gwapiv1.GRPCRouteFilterRequestMirror, + RequestMirror: &gwapiv1.HTTPRequestMirrorFilter{ + BackendRef: gwapiv1.BackendObjectReference{ + Kind: ptr.To(gwapiv1.Kind("Service")), + Name: "mirror-2", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{"default/service-1", "default/mirror-1", "default/mirror-2"}, + }, + { + name: "non-mirror filter is ignored", + route: &gwapiv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grpcroute-header-filter", + Namespace: "default", + }, + Spec: gwapiv1.GRPCRouteSpec{ + Rules: []gwapiv1.GRPCRouteRule{ + { + BackendRefs: []gwapiv1.GRPCBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Name: "service-1", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + Filters: []gwapiv1.GRPCRouteFilter{ + { + Type: gwapiv1.GRPCRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gwapiv1.HTTPHeaderFilter{ + Set: []gwapiv1.HTTPHeader{ + {Name: "x-custom", Value: "value"}, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{"default/service-1"}, + }, + { + name: "backend with Backend kind is indexed", + route: &gwapiv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grpcroute-backend-kind", + Namespace: "default", + }, + Spec: gwapiv1.GRPCRouteSpec{ + Rules: []gwapiv1.GRPCRouteRule{ + { + BackendRefs: []gwapiv1.GRPCBackendRef{ + { + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: gwapiv1.BackendObjectReference{ + Kind: ptr.To(gwapiv1.Kind(egv1a1.KindBackend)), + Name: "backend-1", + Port: ptr.To(gwapiv1.PortNumber(8080)), + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{"default/backend-1"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := backendGRPCRouteIndexFunc(tc.route) + require.ElementsMatch(t, tc.expected, result) + }) + } +} diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 871764e8e4..4a5d170f29 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -50,6 +50,8 @@ bug fixes: | Made ConnectionLimit.Value optional so users can configure MaxConnectionDuration, MaxRequestsPerConnection, or MaxStreamDuration without setting a max connections value. Fixed endpoint hostname is not respected when doing active health check. Fixed ratelimit deployment missing metrics container port (19001), which prevented PodMonitor/ServiceMonitor from targeting the metrics endpoint. + Fixed GRPCRoute RequestMirror filter backend not being indexed, causing "service not found" errors for mirror targets that exist in the cluster. + Fixed GRPCRoute not detecting conflicting RequestMirror and DirectResponse filters, which caused the mirror to be silently dropped. Fixed per-endpoint hostname override not working because the auto-generated wildcard hostname. # Enhancements that improve performance.