From 2c416219bc5e509574b22629eb21eda495eaeecc Mon Sep 17 00:00:00 2001 From: salonichf5 <146118978+salonichf5@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:41:32 -0700 Subject: [PATCH] Add conformance tests for GRPCRouteFilterRequestHeaderModifier --- .../grpcroute-request-header-modifier.go | 197 ++++++++++++++++++ .../grpcroute-request-header-modifier.yaml | 70 +++++++ .../mesh/grpcroute-request-header-modifier.go | 195 +++++++++++++++++ .../grpcroute-request-header-modifier.yaml | 73 +++++++ pkg/features/grpcroute.go | 10 + 5 files changed, 545 insertions(+) create mode 100644 conformance/tests/grpcroute-request-header-modifier.go create mode 100644 conformance/tests/grpcroute-request-header-modifier.yaml create mode 100644 conformance/tests/mesh/grpcroute-request-header-modifier.go create mode 100644 conformance/tests/mesh/grpcroute-request-header-modifier.yaml diff --git a/conformance/tests/grpcroute-request-header-modifier.go b/conformance/tests/grpcroute-request-header-modifier.go new file mode 100644 index 0000000000..c29fba058b --- /dev/null +++ b/conformance/tests/grpcroute-request-header-modifier.go @@ -0,0 +1,197 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tests + +import ( + "testing" + + "google.golang.org/grpc/metadata" + "k8s.io/apimachinery/pkg/types" + + v1 "sigs.k8s.io/gateway-api/apis/v1" + pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver" + "sigs.k8s.io/gateway-api/conformance/utils/grpc" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/pkg/features" +) + +func init() { + ConformanceTests = append(ConformanceTests, GRPCRouteRequestHeaderModifier) +} + +var GRPCRouteRequestHeaderModifier = suite.ConformanceTest{ + ShortName: "GRPCRouteRequestHeaderModifier", + Description: "A GRPCRoute with RequestHeaderModifier filter should modify request headers", + Manifests: []string{"tests/grpcroute-request-header-modifier.yaml"}, + Features: []features.FeatureName{ + features.SupportGateway, + features.SupportGRPCRoute, + features.SupportGRPCRouteRequestHeaderModifier, + }, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "grpc-request-header-modifier", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, true, routeNN) + + testCases := []grpc.ExpectedResponse{ + { + TestCaseName: "Set headers -- X-Header-Set should have the original value", + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + Metadata: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + }, + }, + Response: grpc.Response{ + Headers: &metadata.MD{ + "Some-Other-Header": {"this-header-should-be-set"}, + "X-Header-Set": {"set-overwrites-values"}, + }, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + TestCaseName: "Set headers -- X-Header-Set should get overwritten with the original value", + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + Metadata: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Set": "this-value-should-be-overwritten", + }, + }, + Response: grpc.Response{ + Headers: &metadata.MD{ + "Some-Other-Header": {"this-header-should-be-set"}, + "X-Header-Set": {"set-overwrites-values"}, + }, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + TestCaseName: "Add headers -- X-Header-Add should have the original value", + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + Metadata: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + }, + }, + Response: grpc.Response{ + Headers: &metadata.MD{ + "Some-Other-Header": {"this-header-should-be-set"}, + "X-Header-Add": {"add-appends-values"}, + }, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, + { + TestCaseName: "Add headers -- X-Header-Add should append the new value to the original value", + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + Metadata: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Add": "this-value-should-be-appended", + }, + }, + Response: grpc.Response{ + Headers: &metadata.MD{ + "Some-Other-Header": {"this-header-should-be-set"}, + "X-Header-Add": {"this-value-should-be-appended", "add-appends-values"}, + }, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, { + TestCaseName: "Remove headers -- X-Header-Remove should be removed", + EchoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + Metadata: map[string]string{ + "X-Header-Remove": "this-should-be-removed", + }, + }, + Response: grpc.Response{ + Headers: &metadata.MD{}, + }, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + }, + { + TestCaseName: "Multiple operations - all header operations should be applied", + EchoTwoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + Metadata: map[string]string{ + "X-Header-Set-2": "set-header-2", + "X-Header-Add-2": "add-header-2", + "X-Header-Remove-2": "should-be-removed-2", + "Some-Other-Header": "another-header-val", + }, + }, + Response: grpc.Response{ + Headers: &metadata.MD{ + "X-Header-Set-1": {"header-set-1"}, + "X-Header-Set-2": {"header-set-2"}, + "X-Header-Add-1": {"header-add-1"}, + "X-Header-Add-2": {"header-add-1,add-header-2"}, + "Some-Other-Header": {"another-header-val"}, + }, + }, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, + { + TestCaseName: "Case sensitivity check for header names", + EchoTwoRequest: &pb.EchoRequest{}, + RequestMetadata: &grpc.RequestMetadata{ + Authority: "gateway_api_conformance.echo_basic.grpcecho.GrpcEcho", + // The filter uses canonicalized header names, + // the request uses lowercase names. + Metadata: map[string]string{ + "x-header-set-1": "original-set-1", + "x-header-add-1": "existing-add-1", + "x-header-remove-1": "should-be-removed-1", + }, + }, + Response: grpc.Response{ + Headers: &metadata.MD{ + "X-Header-Set-1": {"set-overwrites-values"}, + "X-Header-Set-2": {"header-set-2"}, + "X-Header-Add-1": {"existing-add-1", "add-appends-values"}, + "X-Header-Add-2": {"header-add-2"}, + }, + }, + Backend: "grpc-infra-backend-v2", + Namespace: ns, + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.TestCaseName, func(t *testing.T) { + t.Parallel() + grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.GRPCClient, suite.TimeoutConfig, gwAddr, tc) + }) + } + }, +} diff --git a/conformance/tests/grpcroute-request-header-modifier.yaml b/conformance/tests/grpcroute-request-header-modifier.yaml new file mode 100644 index 0000000000..992ae06b5f --- /dev/null +++ b/conformance/tests/grpcroute-request-header-modifier.yaml @@ -0,0 +1,70 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: GRPCRoute +metadata: + name: grpc-request-header-modifier + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Header-Set + value: set-overwrites-values + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + add: + - name: X-Header-Add + value: add-appends-values + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + remove: + - X-Header-Remove + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: EchoTwo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Header-Set-1 + value: header-set-1 + - name: X-Header-Set-2 + value: header-set-2 + add: + - name: X-Header-Add-1 + value: header-add-1 + - name: X-Header-Add-2 + value: header-add-2 + remove: + - X-Header-Remove-1 + - X-Header-Remove-2 + backendRefs: + - name: grpc-infra-backend-v2 + port: 8080 \ No newline at end of file diff --git a/conformance/tests/mesh/grpcroute-request-header-modifier.go b/conformance/tests/mesh/grpcroute-request-header-modifier.go new file mode 100644 index 0000000000..58a2eca1d6 --- /dev/null +++ b/conformance/tests/mesh/grpcroute-request-header-modifier.go @@ -0,0 +1,195 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package meshtests + +import ( + "testing" + + "sigs.k8s.io/gateway-api/conformance/utils/echo" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/pkg/features" +) + +func init() { + MeshConformanceTests = append(MeshConformanceTests, MeshGRPCRouteRequestHeaderModifier) +} + +var MeshGRPCRouteRequestHeaderModifier = suite.ConformanceTest{ + ShortName: "MeshGRPCRouteRequestHeaderModifier", + Description: "A GRPCRoute with RequestHeaderModifier filter should modify request headers in mesh mode", + Manifests: []string{"tests/mesh/grpcroute-request-header-modifier.yaml"}, + Features: []features.FeatureName{ + features.SupportMesh, + features.SupportGRPCRoute, + }, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + clientV1 := echo.ConnectToApp(t, s, echo.MeshAppEchoV1) + clientV2 := echo.ConnectToApp(t, s, echo.MeshAppEchoV2) + + testCases := []struct { + name string + client echo.MeshPod + headers map[string]string + targetHost string + absentHeaders []string + expectedHeaders map[string]string + }{ + { + name: "Set headers -- X-Header-Set should have the original value", + headers: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + }, + expectedHeaders: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Set": "set-overwrites-values", + }, + targetHost: "echo-v1", + client: clientV1, + }, + { + name: "Set headers -- X-Header-Set should get overwritten with the original value", + headers: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Set": "this-value-should-be-overwritten", + }, + expectedHeaders: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Set": "set-overwrites-values", + }, + targetHost: "echo-v1", + client: clientV1, + }, + { + name: "Add headers -- X-Header-Add should have the original value", + headers: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + }, + expectedHeaders: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Add": "add-appends-values", + }, + targetHost: "echo-v1", + client: clientV1, + }, + { + name: "X-Header-Add should append the new value to the original value", + headers: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Add": "this-value-should-be-appended", + }, + expectedHeaders: map[string]string{ + "Some-Other-Header": "this-header-should-be-set", + "X-Header-Add": "this-value-should-be-appended,add-appends-values", + }, + targetHost: "echo-v1", + client: clientV1, + }, + { + name: "Remove headers -- X-Header-Remove should be removed", + headers: map[string]string{ + "X-Header-Remove": "this-should-be-removed", + }, + absentHeaders: []string{"X-Header-Remove"}, + client: clientV1, + targetHost: "echo-v1", + }, + { + name: "Multiple operations - all header operations should be applied", + headers: map[string]string{ + "X-Header-Set-2": "set-header-2", + "X-Header-Add-2": "add-header-2", + "X-Header-Remove-2": "should-be-removed-2", + "Some-Other-Header": "another-header-val", + }, + absentHeaders: []string{ + "X-Header-Remove-1", + "X-Header-Remove-2", + }, + expectedHeaders: map[string]string{ + "X-Header-Set-1": "header-set-1", + "X-Header-Set-2": "header-set-2", + "X-Header-Add-1": "header-add-1", + "X-Header-Add-2": "add-header-2,header-add-2", + "X-Header-Add-3": "header-add-3", + "Some-Other-Header": "another-header-val", + }, + client: clientV2, + targetHost: "echo-v2", + }, + { + name: "Case sensitivity check for header names", + headers: map[string]string{ + "x-header-set-1": "original-set-1", + "x-header-add-1": "existing-add-1", + "x-header-remove-1": "should-be-removed-1", + }, + absentHeaders: []string{"X-Header-Remove-1"}, + expectedHeaders: map[string]string{ + "X-Header-Set-1": "header-set-1", + "X-Header-Set-2": "header-set-2", + "X-Header-Add-1": "existing-add-1,header-add-1", + "X-Header-Add-2": "header-add-2", + "X-Header-Add-3": "header-add-3", + }, + client: clientV2, + targetHost: "echo-v2", + }, + } + + for _, tc := range testCases { + tc := tc // capture for parallel execution + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + request := http.Request{ + Protocol: "grpc", + Host: "echo:7070", + Path: "", + Headers: tc.headers, + } + + expected := http.ExpectedResponse{ + Request: request, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: "gateway-conformance-mesh", + } + + // will remove based on input from Gateway API team + + // expected := http.ExpectedResponse{ + // Request: request, + // ExpectedRequest: &http.ExpectedRequest{ + // Request: http.Request{ + // Protocol: "grpc", + // Host: "echo:7070", + // Path: "", + // Headers: tc.expectedHeaders, + // }, + // AbsentHeaders: tc.absentHeaders, + // }, + // Namespace: "gateway-conformance-mesh", + // } + + // Make the request and validate headers are properly modified + tc.client.MakeRequestAndExpectEventuallyConsistentResponse(t, expected, s.TimeoutConfig) + }) + } + }, +} diff --git a/conformance/tests/mesh/grpcroute-request-header-modifier.yaml b/conformance/tests/mesh/grpcroute-request-header-modifier.yaml new file mode 100644 index 0000000000..c753390db0 --- /dev/null +++ b/conformance/tests/mesh/grpcroute-request-header-modifier.yaml @@ -0,0 +1,73 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: GRPCRoute +metadata: + name: grpc-request-header-modifier + namespace: gateway-conformance-mesh +spec: + parentRefs: + - group: "" + kind: Service + name: echo + port: 7070 + rules: + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Header-Set + value: set-overwrites-values + backendRefs: + - name: echo-v1 + port: 7070 + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + add: + - name: X-Header-Add + value: add-appends-values + backendRefs: + - name: echo-v1 + port: 7070 + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + remove: + - X-Header-Remove + backendRefs: + - name: echo-v1 + port: 7070 + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: EchoTwo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Header-Set-1 + value: header-set-1 + - name: X-Header-Set-2 + value: header-set-2 + add: + - name: X-Header-Add-1 + value: header-add-1 + - name: X-Header-Add-2 + value: header-add-2 + remove: + - X-Header-Remove-1 + - X-Header-Remove-2 + backendRefs: + - name: echo-v2 + port: 7070 \ No newline at end of file diff --git a/pkg/features/grpcroute.go b/pkg/features/grpcroute.go index 3ee21b41f5..200079abb8 100644 --- a/pkg/features/grpcroute.go +++ b/pkg/features/grpcroute.go @@ -46,6 +46,9 @@ var GRPCRouteCoreFeatures = sets.New( const ( // This option indicates support for the name field in the GRPCRouteRule (extended conformance) SupportGRPCRouteNamedRouteRule FeatureName = "GRPCRouteNamedRouteRule" + + // This option indicates support for RequestHeaderModifier filter in GRPCRoute (extended conformance) + SupportGRPCRouteRequestHeaderModifier FeatureName = "GRPCRouteRequestHeaderModifier" ) // GRPCRouteNamedRouteRule contains metadata for the SupportGRPCRouteNamedRouteRule feature. @@ -54,9 +57,16 @@ var GRPCRouteNamedRouteRule = Feature{ Channel: FeatureChannelStandard, } +// GRPCRouteRequestHeaderModifier contains metadata for the SupportGRPCRouteRequestHeaderModifier feature. +var GRPCRouteRequestHeaderModifier = Feature{ + Name: SupportGRPCRouteRequestHeaderModifier, + Channel: FeatureChannelStandard, +} + // GRPCRouteExtendedFeatures includes all extended features for GRPCRoute // conformance and can be used to opt-in to run all GRPCRoute extended features tests. // This does not include any Core Features. var GRPCRouteExtendedFeatures = sets.New( GRPCRouteNamedRouteRule, + GRPCRouteRequestHeaderModifier, )