diff --git a/test/e2e/testdata/ratelimit-header-invert-match.yaml b/test/e2e/testdata/ratelimit-header-invert-match.yaml new file mode 100644 index 00000000000..7261ef30e35 --- /dev/null +++ b/test/e2e/testdata/ratelimit-header-invert-match.yaml @@ -0,0 +1,42 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: ratelimit-anded-headers-with-invert + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: header-ratelimit + rateLimit: + type: Global + global: + rules: + - clientSelectors: + - headers: + - name: x-user-name + type: Distinct + - name: x-user-name + type: Exact + value: admin + invert: true + limit: + requests: 3 + unit: Hour +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: header-ratelimit + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /get + backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go index 80064e6d906..b87576b60aa 100644 --- a/test/e2e/tests/ratelimit.go +++ b/test/e2e/tests/ratelimit.go @@ -26,6 +26,7 @@ import ( func init() { ConformanceTests = append(ConformanceTests, RateLimitCIDRMatchTest) ConformanceTests = append(ConformanceTests, RateLimitHeaderMatchTest) + ConformanceTests = append(ConformanceTests, RateLimitHeaderInvertMatchTest) ConformanceTests = append(ConformanceTests, RateLimitHeadersDisabled) ConformanceTests = append(ConformanceTests, RateLimitBasedJwtClaimsTest) ConformanceTests = append(ConformanceTests, RateLimitMultipleListenersTest) @@ -170,6 +171,93 @@ var RateLimitHeaderMatchTest = suite.ConformanceTest{ }, } +var RateLimitHeaderInvertMatchTest = suite.ConformanceTest{ + ShortName: "RateLimitHeaderInvertMatch", + Description: "Limit all requests that match distinct headers except for which invert is set to true", + Manifests: []string{"testdata/ratelimit-header-invert-match.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "header-ratelimit", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + t.Run("all matched headers got limited", func(t *testing.T) { + requestHeaders := map[string]string{ + "x-user-name": "username", + } + + ratelimitHeader := make(map[string]string) + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/get", + Headers: requestHeaders, + }, + Response: http.Response{ + StatusCode: 200, + Headers: ratelimitHeader, + }, + Namespace: ns, + } + expectOkResp.Response.Headers["X-Ratelimit-Limit"] = "3, 3;w=3600" + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + expectLimitResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/get", + Headers: requestHeaders, + }, + Response: http.Response{ + StatusCode: 429, + }, + Namespace: ns, + } + expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http") + + // should just send exactly 4 requests, and expect 429 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest of the requests + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("failed to get expected response for the first three requests: %v", err) + } + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + t.Errorf("failed to get expected response for the last (fourth) request: %v", err) + } + }) + + t.Run("if header matched with invert will not get limited", func(t *testing.T) { + requestHeaders := map[string]string{ + "x-user-name": "admin", + } + + // it does not require any rate limit header, since this request never be rate limited. + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/get", + Headers: requestHeaders, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + // send exactly 4 requests, and still expect 200 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest of the requests + if err := GotExactExpectedResponse(t, 3, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("failed to get expected responses for the request: %v", err) + } + }) + }, +} + var RateLimitHeadersDisabled = suite.ConformanceTest{ ShortName: "RateLimitHeadersDisabled", Description: "Disable rate limit headers",