From 17d6b50bf5fa5decf65ebb16e907d03e46b6ded8 Mon Sep 17 00:00:00 2001 From: Taranpreet Natt Date: Wed, 13 Aug 2025 02:51:23 -0700 Subject: [PATCH 1/6] Added conformance test for GRPCRouteRequestMirror, incomplete testing logic, will add in later commit --- conformance/tests/grpcroute-request-mirror.go | 106 ++++++++++++++++++ .../tests/grpcroute-request-mirror.yaml | 51 +++++++++ conformance/utils/grpc/grpc.go | 13 +++ conformance/utils/grpc/mirror.go | 30 +++++ pkg/features/grpcroute.go | 21 +++- 5 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 conformance/tests/grpcroute-request-mirror.go create mode 100644 conformance/tests/grpcroute-request-mirror.yaml create mode 100644 conformance/utils/grpc/mirror.go diff --git a/conformance/tests/grpcroute-request-mirror.go b/conformance/tests/grpcroute-request-mirror.go new file mode 100644 index 0000000000..befb71362f --- /dev/null +++ b/conformance/tests/grpcroute-request-mirror.go @@ -0,0 +1,106 @@ +/* +Copyright 2023 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/codes" + "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, GRPCRouteRequestMirror) +} + +var GRPCRouteRequestMirror = suite.ConformanceTest{ + ShortName: "GRPCRouteRequestMirror", + Description: "A GRPCRoute with request mirror filter", + Manifests: []string{"tests/grpcroute-request-mirror.yaml"}, + Features: []features.FeatureName{ + features.SupportGRPCRoute, + features.SupportGateway, + features.SupportGRPCRouteRequestMirror, + }, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "grpc-request-mirror", 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{ + { + EchoRequest: &pb.EchoRequest{}, + Backend: "grpc-infra-backend-v1", + Namespace: ns, + MirroredTo: []grpc.MirroredBackend{ + { + BackendRef: grpc.BackendRef{ + Name: "grpc-infra-backend-v2", + Namespace: ns, + }, + }, + }, + Response: grpc.Response{ + Code: codes.OK, + }, + }, + { + EchoTwoRequest: &pb.EchoRequest{}, + Backend: "grpc-infra-backend-v1", + RequestMetadata: &grpc.RequestMetadata{ + Metadata: map[string]string{ + "X-Header-Remove": "remove-val", + "X-Header-Add-Append": "append-val-1", + }, + }, + Namespace: ns, + MirroredTo: []grpc.MirroredBackend{ + { + BackendRef: grpc.BackendRef{ + Name: "grpc-infra-backend-v2", + Namespace: ns, + }, + }, + }, + Response: grpc.Response{ + Code: codes.OK, + Headers: &metadata.MD{ + "x-header-set": []string{"set-overwrites-values"}, + "x-header-add": []string{"header-val-1"}, + "x-header-add-append": []string{"append-val-1", "header-val-2"}, + }, + }, + }, + } + for i := range testCases { + tc := testCases[i] + t.Run(tc.GetTestCaseName(i), func(t *testing.T) { + t.Parallel() + grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.GRPCClient, suite.TimeoutConfig, gwAddr, tc) + grpc.ExpectMirroredRequest(t, suite.Client, suite.Clientset, tc.MirroredTo, "Echo") + }) + } + }, +} diff --git a/conformance/tests/grpcroute-request-mirror.yaml b/conformance/tests/grpcroute-request-mirror.yaml new file mode 100644 index 0000000000..ac0cc79bc8 --- /dev/null +++ b/conformance/tests/grpcroute-request-mirror.yaml @@ -0,0 +1,51 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: GRPCRoute +metadata: + name: grpc-request-mirror + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: Echo + filters: + - type: RequestMirror + requestMirror: + backendRef: + name: grpc-infra-backend-v2 + namespace: gateway-conformance-infra + port: 8080 + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + namespace: gateway-conformance-infra + - matches: + - method: + service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho + method: EchoTwo + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Header-Set + value: set-overwrites-values + add: + - name: X-Header-Add + value: header-val-1 + - name: X-Header-Add-Append + value: header-val-2 + remove: + - X-Header-Remove + - type: RequestMirror + requestMirror: + backendRef: + name: grpc-infra-backend-v2 + namespace: gateway-conformance-infra + port: 8080 + backendRefs: + - name: grpc-infra-backend-v1 + port: 8080 + namespace: gateway-conformance-infra diff --git a/conformance/utils/grpc/grpc.go b/conformance/utils/grpc/grpc.go index 70adfe2eac..b53cbed541 100644 --- a/conformance/utils/grpc/grpc.go +++ b/conformance/utils/grpc/grpc.go @@ -70,6 +70,16 @@ type RequestMetadata struct { Metadata map[string]string } +type BackendRef struct { + Name string + Namespace string +} + +type MirroredBackend struct { + BackendRef + Percent *int32 +} + // ExpectedResponse defines the response expected for a given request. type ExpectedResponse struct { // Defines the request to make. Only one of EchoRequest and EchoTwoRequest @@ -88,6 +98,9 @@ type ExpectedResponse struct { Backend string Namespace string + // MirroredTo is the destination BackendRefs of the mirrored request + MirroredTo []MirroredBackend + // User Given TestCase name TestCaseName string } diff --git a/conformance/utils/grpc/mirror.go b/conformance/utils/grpc/mirror.go new file mode 100644 index 0000000000..4e5760e92c --- /dev/null +++ b/conformance/utils/grpc/mirror.go @@ -0,0 +1,30 @@ +/* +Copyright 2023 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 grpc + +import ( + "testing" + + "sigs.k8s.io/gateway-api/conformance/utils/tlog" + + clientset "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func ExpectMirroredRequest(t *testing.T, client client.Client, clientset clientset.Interface, mirrorPods []MirroredBackend, methodName string) { + tlog.Log(t, "TODO: Check for mirroring in grpc") +} diff --git a/pkg/features/grpcroute.go b/pkg/features/grpcroute.go index 3ee21b41f5..73fae3ad4f 100644 --- a/pkg/features/grpcroute.go +++ b/pkg/features/grpcroute.go @@ -46,17 +46,28 @@ 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 GRPCRoute request mirror (extended conformance) + SupportGRPCRouteRequestMirror FeatureName = "GRPCRouteRequestMirror" ) -// GRPCRouteNamedRouteRule contains metadata for the SupportGRPCRouteNamedRouteRule feature. -var GRPCRouteNamedRouteRule = Feature{ - Name: SupportGRPCRouteNamedRouteRule, - Channel: FeatureChannelStandard, -} +var ( + // GRPCRouteNamedRouteRule contains metadata for the SupportGRPCRouteNamedRouteRule feature. + GRPCRouteNamedRouteRule = Feature{ + Name: SupportGRPCRouteNamedRouteRule, + Channel: FeatureChannelStandard, + } + // GRPCRouteRequestMirrorFeature contains metadata for the GRPCRouteRequestMirror feature. + GRPCRouteRequestMirrorFeature = Feature{ + Name: SupportGRPCRouteRequestMirror, + 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, + GRPCRouteRequestMirrorFeature, ) From 3f1a7ec0e08f0c86bf171a9b4f30f0d426b0d874 Mon Sep 17 00:00:00 2001 From: Taranpreet Natt Date: Fri, 22 Aug 2025 18:22:48 -0700 Subject: [PATCH 2/6] Added logic to check if the headers were modified correctly by the GRPCRoute --- conformance/tests/grpcroute-request-mirror.go | 15 +++-- .../tests/grpcroute-request-mirror.yaml | 24 ++++---- conformance/utils/grpc/grpc.go | 55 +++++++++++++++++-- 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/conformance/tests/grpcroute-request-mirror.go b/conformance/tests/grpcroute-request-mirror.go index befb71362f..79c7af34c3 100644 --- a/conformance/tests/grpcroute-request-mirror.go +++ b/conformance/tests/grpcroute-request-mirror.go @@ -86,11 +86,16 @@ var GRPCRouteRequestMirror = suite.ConformanceTest{ }, Response: grpc.Response{ Code: codes.OK, - Headers: &metadata.MD{ - "x-header-set": []string{"set-overwrites-values"}, - "x-header-add": []string{"header-val-1"}, - "x-header-add-append": []string{"append-val-1", "header-val-2"}, - }, + Headers: func() *metadata.MD { + md := metadata.Pairs( + "x-header-set", "set-overwrites-values", + "x-header-add", "header-val-1", + "x-header-add-append", "append-val-1", + "x-header-add-append", "header-val-2", + ) + return &md + }(), + AbsentHeaders: []string{"X-Header-Remove"}, }, }, } diff --git a/conformance/tests/grpcroute-request-mirror.yaml b/conformance/tests/grpcroute-request-mirror.yaml index ac0cc79bc8..da33e02a42 100644 --- a/conformance/tests/grpcroute-request-mirror.yaml +++ b/conformance/tests/grpcroute-request-mirror.yaml @@ -12,12 +12,12 @@ spec: service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho method: Echo filters: - - type: RequestMirror - requestMirror: - backendRef: - name: grpc-infra-backend-v2 - namespace: gateway-conformance-infra - port: 8080 + # - type: RequestMirror + # requestMirror: + # backendRef: + # name: grpc-infra-backend-v2 + # namespace: gateway-conformance-infra + # port: 8080 backendRefs: - name: grpc-infra-backend-v1 port: 8080 @@ -39,12 +39,12 @@ spec: value: header-val-2 remove: - X-Header-Remove - - type: RequestMirror - requestMirror: - backendRef: - name: grpc-infra-backend-v2 - namespace: gateway-conformance-infra - port: 8080 + # - type: RequestMirror + # requestMirror: + # backendRef: + # name: grpc-infra-backend-v2 + # namespace: gateway-conformance-infra + # port: 8080 backendRefs: - name: grpc-infra-backend-v1 port: 8080 diff --git a/conformance/utils/grpc/grpc.go b/conformance/utils/grpc/grpc.go index b53cbed541..ef9031805e 100644 --- a/conformance/utils/grpc/grpc.go +++ b/conformance/utils/grpc/grpc.go @@ -19,6 +19,7 @@ package grpc import ( "context" "fmt" + "slices" "sort" "strings" "testing" @@ -56,10 +57,11 @@ type DefaultClient struct { } type Response struct { - Code codes.Code - Headers *metadata.MD - Trailers *metadata.MD - Response *pb.EchoResponse + Code codes.Code + Headers *metadata.MD + Trailers *metadata.MD + Response *pb.EchoResponse + AbsentHeaders []string } type RequestMetadata struct { @@ -262,6 +264,51 @@ func compareResponse(expected *ExpectedResponse, response *Response) error { if !strings.HasPrefix(response.Response.GetAssertions().GetContext().GetPod(), expected.Backend) { return fmt.Errorf("expected pod name to start with %s, got %s", expected.Backend, response.Response.GetAssertions().GetContext().GetPod()) } + + // Check if the correct headers were received by the backend + receivedMap := make(map[string][]string) + receivedHeaders := response.Response.GetAssertions().GetHeaders() + for _, receivedHeader := range receivedHeaders { + receivedKey := strings.ToLower(receivedHeader.GetKey()) + receivedValue := receivedHeader.GetValue() + receivedMap[receivedKey] = append(receivedMap[receivedKey], receivedValue) + } + + expectedHeaders := expected.Response.Headers + if expectedHeaders != nil { + + if receivedHeaders == nil { + return fmt.Errorf("No headers captured: expected %v headers", len(*expectedHeaders)) + } + + for expectedHeader, expectedValues := range *expectedHeaders { + expectedHeader = strings.ToLower(expectedHeader) + receivedValues, ok := receivedMap[expectedHeader] + if !ok { + return fmt.Errorf("expected header %s not found", expectedHeader) + } + sortedExpectedValues := slices.Clone(expectedValues) + sortedReceivedValues := slices.Clone(receivedValues) + + slices.Sort(sortedExpectedValues) + slices.Sort(sortedReceivedValues) + + if !slices.Equal(sortedExpectedValues, sortedReceivedValues) { + return fmt.Errorf("Header: %s, Expected values %v not equal to received values %v", expectedHeader, sortedExpectedValues, sortedReceivedValues) + } + } + } + + // Check if the headers that were supposed to be removed by the Gateway are removed + if len(expected.Response.AbsentHeaders) > 0 { + for _, absentHeader := range expected.Response.AbsentHeaders { + val, ok := receivedMap[strings.ToLower(absentHeader)] + if ok { + return fmt.Errorf("Header: %s, should not be present, got %s", absentHeader, val) + } + } + } + } return nil } From 2fd8d8efe056b90af81635796848882affa6a011 Mon Sep 17 00:00:00 2001 From: Taranpreet Natt Date: Fri, 22 Aug 2025 21:45:15 -0700 Subject: [PATCH 3/6] Finished GRPCRouteRequestMirror conformance test --- conformance/tests/grpcroute-request-mirror.go | 2 +- .../tests/grpcroute-request-mirror.yaml | 24 +++++----- conformance/utils/grpc/mirror.go | 47 ++++++++++++++++++- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/conformance/tests/grpcroute-request-mirror.go b/conformance/tests/grpcroute-request-mirror.go index 79c7af34c3..34de2dfbf8 100644 --- a/conformance/tests/grpcroute-request-mirror.go +++ b/conformance/tests/grpcroute-request-mirror.go @@ -104,7 +104,7 @@ var GRPCRouteRequestMirror = suite.ConformanceTest{ t.Run(tc.GetTestCaseName(i), func(t *testing.T) { t.Parallel() grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.GRPCClient, suite.TimeoutConfig, gwAddr, tc) - grpc.ExpectMirroredRequest(t, suite.Client, suite.Clientset, tc.MirroredTo, "Echo") + grpc.ExpectMirroredRequest(t, suite.Client, suite.Clientset, tc.MirroredTo) }) } }, diff --git a/conformance/tests/grpcroute-request-mirror.yaml b/conformance/tests/grpcroute-request-mirror.yaml index da33e02a42..ac0cc79bc8 100644 --- a/conformance/tests/grpcroute-request-mirror.yaml +++ b/conformance/tests/grpcroute-request-mirror.yaml @@ -12,12 +12,12 @@ spec: service: gateway_api_conformance.echo_basic.grpcecho.GrpcEcho method: Echo filters: - # - type: RequestMirror - # requestMirror: - # backendRef: - # name: grpc-infra-backend-v2 - # namespace: gateway-conformance-infra - # port: 8080 + - type: RequestMirror + requestMirror: + backendRef: + name: grpc-infra-backend-v2 + namespace: gateway-conformance-infra + port: 8080 backendRefs: - name: grpc-infra-backend-v1 port: 8080 @@ -39,12 +39,12 @@ spec: value: header-val-2 remove: - X-Header-Remove - # - type: RequestMirror - # requestMirror: - # backendRef: - # name: grpc-infra-backend-v2 - # namespace: gateway-conformance-infra - # port: 8080 + - type: RequestMirror + requestMirror: + backendRef: + name: grpc-infra-backend-v2 + namespace: gateway-conformance-infra + port: 8080 backendRefs: - name: grpc-infra-backend-v1 port: 8080 diff --git a/conformance/utils/grpc/mirror.go b/conformance/utils/grpc/mirror.go index 4e5760e92c..8dcbc8d4a1 100644 --- a/conformance/utils/grpc/mirror.go +++ b/conformance/utils/grpc/mirror.go @@ -17,14 +17,57 @@ limitations under the License. package grpc import ( + "regexp" + "sync" "testing" + "time" + "github.com/stretchr/testify/require" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/tlog" clientset "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" ) -func ExpectMirroredRequest(t *testing.T, client client.Client, clientset clientset.Interface, mirrorPods []MirroredBackend, methodName string) { - tlog.Log(t, "TODO: Check for mirroring in grpc") +func ExpectMirroredRequest(t *testing.T, client client.Client, clientset clientset.Interface, mirrorPods []MirroredBackend) { + for i, mirrorPod := range mirrorPods { + if mirrorPod.Name == "" { + tlog.Fatalf(t, "Mirrored BackendRef[%d].Name wasn't provided in the testcase, this test should only check grpc request mirror.", i) + } + } + + var wg sync.WaitGroup + wg.Add(len(mirrorPods)) + + assertionStart := time.Now() + + for _, mirrorPod := range mirrorPods { + go func(mirrorPod MirroredBackend) { + defer wg.Done() + + require.Eventually(t, func() bool { + mirrorLogRegexp := regexp.MustCompile("Received over plaintext:") + + tlog.Log(t, "Searching for the mirrored request log") + tlog.Logf(t, `Reading "%s/%s" logs`, mirrorPod.Namespace, mirrorPod.Name) + logs, err := kubernetes.DumpEchoLogs(mirrorPod.Namespace, mirrorPod.Name, client, clientset, assertionStart) + if err != nil { + tlog.Logf(t, `Couldn't read "%s/%s" logs: %v`, mirrorPod.Namespace, mirrorPod.Name, err) + return false + } + + for _, log := range logs { + if mirrorLogRegexp.MatchString(log) { + return true + } + } + return false + }, 60*time.Second, time.Millisecond*100, `Couldn't find mirrored request in "%s/%s" logs`, mirrorPod.Namespace, mirrorPod.Name) + }(mirrorPod) + } + + wg.Wait() + + tlog.Log(t, "Found mirrored request log in all desired backends") } From f78a5ae3ee89148609a9c68ff602fa009fdb3ffb Mon Sep 17 00:00:00 2001 From: Taranpreet Natt Date: Fri, 22 Aug 2025 22:16:40 -0700 Subject: [PATCH 4/6] Improved variable naming: receivedMap -> receivedHeadersMap for clarity --- conformance/utils/grpc/grpc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conformance/utils/grpc/grpc.go b/conformance/utils/grpc/grpc.go index ef9031805e..2545fbd9f2 100644 --- a/conformance/utils/grpc/grpc.go +++ b/conformance/utils/grpc/grpc.go @@ -266,12 +266,12 @@ func compareResponse(expected *ExpectedResponse, response *Response) error { } // Check if the correct headers were received by the backend - receivedMap := make(map[string][]string) + receivedHeadersMap := make(map[string][]string) receivedHeaders := response.Response.GetAssertions().GetHeaders() for _, receivedHeader := range receivedHeaders { receivedKey := strings.ToLower(receivedHeader.GetKey()) receivedValue := receivedHeader.GetValue() - receivedMap[receivedKey] = append(receivedMap[receivedKey], receivedValue) + receivedHeadersMap[receivedKey] = append(receivedHeadersMap[receivedKey], receivedValue) } expectedHeaders := expected.Response.Headers @@ -283,7 +283,7 @@ func compareResponse(expected *ExpectedResponse, response *Response) error { for expectedHeader, expectedValues := range *expectedHeaders { expectedHeader = strings.ToLower(expectedHeader) - receivedValues, ok := receivedMap[expectedHeader] + receivedValues, ok := receivedHeadersMap[expectedHeader] if !ok { return fmt.Errorf("expected header %s not found", expectedHeader) } @@ -302,7 +302,7 @@ func compareResponse(expected *ExpectedResponse, response *Response) error { // Check if the headers that were supposed to be removed by the Gateway are removed if len(expected.Response.AbsentHeaders) > 0 { for _, absentHeader := range expected.Response.AbsentHeaders { - val, ok := receivedMap[strings.ToLower(absentHeader)] + val, ok := receivedHeadersMap[strings.ToLower(absentHeader)] if ok { return fmt.Errorf("Header: %s, should not be present, got %s", absentHeader, val) } From c758ff1be995559443684175377f166bdaacedc2 Mon Sep 17 00:00:00 2001 From: Taranpreet Natt Date: Tue, 2 Sep 2025 18:45:50 -0700 Subject: [PATCH 5/6] Refactored ExprectMirroredRequest to one function and be shared between HTTP and GRPC Makes the HTTP mirror validation function configurable with a logPattern parameter so it can be reused by gRPC tests, eliminating code duplication. - Parameterize logPattern in HTTP ExpectMirroredRequest - Update gRPC to use shared function with gRPC-specific pattern - Remove duplicate grpc/mirror.go implementation - Make error messages protocol-agnostic Addresses reviewer feedback in PR #4015. --- conformance/tests/grpcroute-request-mirror.go | 13 ++-- conformance/tests/httproute-request-mirror.go | 3 +- conformance/utils/grpc/grpc.go | 12 +-- conformance/utils/grpc/mirror.go | 73 ------------------- conformance/utils/http/mirror.go | 7 +- 5 files changed, 13 insertions(+), 95 deletions(-) delete mode 100644 conformance/utils/grpc/mirror.go diff --git a/conformance/tests/grpcroute-request-mirror.go b/conformance/tests/grpcroute-request-mirror.go index 34de2dfbf8..616f3d0745 100644 --- a/conformance/tests/grpcroute-request-mirror.go +++ b/conformance/tests/grpcroute-request-mirror.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Kubernetes Authors. +Copyright 2025 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. @@ -25,6 +25,7 @@ import ( 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/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" "sigs.k8s.io/gateway-api/pkg/features" @@ -54,9 +55,9 @@ var GRPCRouteRequestMirror = suite.ConformanceTest{ EchoRequest: &pb.EchoRequest{}, Backend: "grpc-infra-backend-v1", Namespace: ns, - MirroredTo: []grpc.MirroredBackend{ + MirroredTo: []http.MirroredBackend{ { - BackendRef: grpc.BackendRef{ + BackendRef: http.BackendRef{ Name: "grpc-infra-backend-v2", Namespace: ns, }, @@ -76,9 +77,9 @@ var GRPCRouteRequestMirror = suite.ConformanceTest{ }, }, Namespace: ns, - MirroredTo: []grpc.MirroredBackend{ + MirroredTo: []http.MirroredBackend{ { - BackendRef: grpc.BackendRef{ + BackendRef: http.BackendRef{ Name: "grpc-infra-backend-v2", Namespace: ns, }, @@ -104,7 +105,7 @@ var GRPCRouteRequestMirror = suite.ConformanceTest{ t.Run(tc.GetTestCaseName(i), func(t *testing.T) { t.Parallel() grpc.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.GRPCClient, suite.TimeoutConfig, gwAddr, tc) - grpc.ExpectMirroredRequest(t, suite.Client, suite.Clientset, tc.MirroredTo) + http.ExpectMirroredRequest(t, suite.Client, suite.Clientset, tc.MirroredTo, "Received over plaintext:") }) } }, diff --git a/conformance/tests/httproute-request-mirror.go b/conformance/tests/httproute-request-mirror.go index 30edb7d12b..1044cd7042 100644 --- a/conformance/tests/httproute-request-mirror.go +++ b/conformance/tests/httproute-request-mirror.go @@ -17,6 +17,7 @@ limitations under the License. package tests import ( + "fmt" "testing" "k8s.io/apimachinery/pkg/types" @@ -105,7 +106,7 @@ var HTTPRouteRequestMirror = suite.ConformanceTest{ t.Run(tc.GetTestCaseName(i), func(t *testing.T) { t.Parallel() http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, tc) - http.ExpectMirroredRequest(t, suite.Client, suite.Clientset, tc.MirroredTo, tc.Request.Path) + http.ExpectMirroredRequest(t, suite.Client, suite.Clientset, tc.MirroredTo, fmt.Sprintf("Echoing back request made to \\%s to client", tc.Request.Path)) }) } }, diff --git a/conformance/utils/grpc/grpc.go b/conformance/utils/grpc/grpc.go index 2545fbd9f2..9cadf416e4 100644 --- a/conformance/utils/grpc/grpc.go +++ b/conformance/utils/grpc/grpc.go @@ -72,16 +72,6 @@ type RequestMetadata struct { Metadata map[string]string } -type BackendRef struct { - Name string - Namespace string -} - -type MirroredBackend struct { - BackendRef - Percent *int32 -} - // ExpectedResponse defines the response expected for a given request. type ExpectedResponse struct { // Defines the request to make. Only one of EchoRequest and EchoTwoRequest @@ -101,7 +91,7 @@ type ExpectedResponse struct { Namespace string // MirroredTo is the destination BackendRefs of the mirrored request - MirroredTo []MirroredBackend + MirroredTo []http.MirroredBackend // User Given TestCase name TestCaseName string diff --git a/conformance/utils/grpc/mirror.go b/conformance/utils/grpc/mirror.go deleted file mode 100644 index 8dcbc8d4a1..0000000000 --- a/conformance/utils/grpc/mirror.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2023 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 grpc - -import ( - "regexp" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" - "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" - "sigs.k8s.io/gateway-api/conformance/utils/tlog" - - clientset "k8s.io/client-go/kubernetes" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func ExpectMirroredRequest(t *testing.T, client client.Client, clientset clientset.Interface, mirrorPods []MirroredBackend) { - for i, mirrorPod := range mirrorPods { - if mirrorPod.Name == "" { - tlog.Fatalf(t, "Mirrored BackendRef[%d].Name wasn't provided in the testcase, this test should only check grpc request mirror.", i) - } - } - - var wg sync.WaitGroup - wg.Add(len(mirrorPods)) - - assertionStart := time.Now() - - for _, mirrorPod := range mirrorPods { - go func(mirrorPod MirroredBackend) { - defer wg.Done() - - require.Eventually(t, func() bool { - mirrorLogRegexp := regexp.MustCompile("Received over plaintext:") - - tlog.Log(t, "Searching for the mirrored request log") - tlog.Logf(t, `Reading "%s/%s" logs`, mirrorPod.Namespace, mirrorPod.Name) - logs, err := kubernetes.DumpEchoLogs(mirrorPod.Namespace, mirrorPod.Name, client, clientset, assertionStart) - if err != nil { - tlog.Logf(t, `Couldn't read "%s/%s" logs: %v`, mirrorPod.Namespace, mirrorPod.Name, err) - return false - } - - for _, log := range logs { - if mirrorLogRegexp.MatchString(log) { - return true - } - } - return false - }, 60*time.Second, time.Millisecond*100, `Couldn't find mirrored request in "%s/%s" logs`, mirrorPod.Namespace, mirrorPod.Name) - }(mirrorPod) - } - - wg.Wait() - - tlog.Log(t, "Found mirrored request log in all desired backends") -} diff --git a/conformance/utils/http/mirror.go b/conformance/utils/http/mirror.go index 0ac85ec5ae..e759cc15f2 100644 --- a/conformance/utils/http/mirror.go +++ b/conformance/utils/http/mirror.go @@ -17,7 +17,6 @@ limitations under the License. package http import ( - "fmt" "regexp" "sync" "testing" @@ -31,10 +30,10 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/tlog" ) -func ExpectMirroredRequest(t *testing.T, client client.Client, clientset clientset.Interface, mirrorPods []MirroredBackend, path string) { +func ExpectMirroredRequest(t *testing.T, client client.Client, clientset clientset.Interface, mirrorPods []MirroredBackend, logPattern string) { for i, mirrorPod := range mirrorPods { if mirrorPod.Name == "" { - tlog.Fatalf(t, "Mirrored BackendRef[%d].Name wasn't provided in the testcase, this test should only check http request mirror.", i) + tlog.Fatalf(t, "Mirrored BackendRef[%d].Name wasn't provided in the testcase, this test should only validate request mirroring.", i) } } @@ -48,7 +47,7 @@ func ExpectMirroredRequest(t *testing.T, client client.Client, clientset clients defer wg.Done() require.Eventually(t, func() bool { - mirrorLogRegexp := regexp.MustCompile(fmt.Sprintf("Echoing back request made to \\%s to client", path)) + mirrorLogRegexp := regexp.MustCompile(logPattern) tlog.Log(t, "Searching for the mirrored request log") tlog.Logf(t, `Reading "%s/%s" logs`, mirrorPod.Namespace, mirrorPod.Name) From f7852a0c4c76c8de426c27a20f144be7876d831a Mon Sep 17 00:00:00 2001 From: Taranpreet Natt Date: Tue, 2 Sep 2025 19:17:33 -0700 Subject: [PATCH 6/6] Fixed linting errors --- conformance/tests/grpcroute-request-mirror.go | 1 + conformance/utils/grpc/grpc.go | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/conformance/tests/grpcroute-request-mirror.go b/conformance/tests/grpcroute-request-mirror.go index 616f3d0745..85dc233815 100644 --- a/conformance/tests/grpcroute-request-mirror.go +++ b/conformance/tests/grpcroute-request-mirror.go @@ -22,6 +22,7 @@ import ( "google.golang.org/grpc/codes" "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" diff --git a/conformance/utils/grpc/grpc.go b/conformance/utils/grpc/grpc.go index 9cadf416e4..ca1144567a 100644 --- a/conformance/utils/grpc/grpc.go +++ b/conformance/utils/grpc/grpc.go @@ -266,7 +266,6 @@ func compareResponse(expected *ExpectedResponse, response *Response) error { expectedHeaders := expected.Response.Headers if expectedHeaders != nil { - if receivedHeaders == nil { return fmt.Errorf("No headers captured: expected %v headers", len(*expectedHeaders)) } @@ -298,7 +297,6 @@ func compareResponse(expected *ExpectedResponse, response *Response) error { } } } - } return nil }