From c363f43a99b5624f3d0b168d72338a294757433f Mon Sep 17 00:00:00 2001 From: huabing zhao Date: Fri, 1 Dec 2023 17:01:05 +0800 Subject: [PATCH] e2e test Signed-off-by: huabing zhao --- api/v1alpha1/ratelimit_types.go | 4 +- ....envoyproxy.io_backendtrafficpolicies.yaml | 3 +- site/content/en/latest/api/extension_types.md | 2 +- test/e2e/testdata/local-ratelimit.yaml | 92 +++++++++ test/e2e/tests/local-ratelimit.go | 191 ++++++++++++++++++ test/e2e/tests/ratelimit.go | 14 +- 6 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 test/e2e/testdata/local-ratelimit.yaml create mode 100644 test/e2e/tests/local-ratelimit.go diff --git a/api/v1alpha1/ratelimit_types.go b/api/v1alpha1/ratelimit_types.go index bf8488f46bda..580f6c24c2da 100644 --- a/api/v1alpha1/ratelimit_types.go +++ b/api/v1alpha1/ratelimit_types.go @@ -9,7 +9,7 @@ package v1alpha1 // +union type RateLimitSpec struct { // Type decides the scope for the RateLimits. - // Valid RateLimitType values are "Global". + // Valid RateLimitType values are "Global" or "Local". // // +unionDiscriminator Type RateLimitType `json:"type"` @@ -25,7 +25,7 @@ type RateLimitSpec struct { } // RateLimitType specifies the types of RateLimiting. -// +kubebuilder:validation:Enum=Global +// +kubebuilder:validation:Enum=Global;Local type RateLimitType string const ( diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index d585a9aa69ca..145af99d5447 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -370,9 +370,10 @@ spec: type: object type: description: Type decides the scope for the RateLimits. Valid - RateLimitType values are "Global". + RateLimitType values are "Global" or "Local". enum: - Global + - Local type: string required: - type diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 3226e3c1a68f..44c277be9705 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1527,7 +1527,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `type` _[RateLimitType](#ratelimittype)_ | Type decides the scope for the RateLimits. Valid RateLimitType values are "Global". | +| `type` _[RateLimitType](#ratelimittype)_ | Type decides the scope for the RateLimits. Valid RateLimitType values are "Global" or "Local". | | `global` _[GlobalRateLimit](#globalratelimit)_ | Global defines global rate limit configuration. | | `local` _[LocalRateLimit](#localratelimit)_ | Local defines local rate limit configuration. | diff --git a/test/e2e/testdata/local-ratelimit.yaml b/test/e2e/testdata/local-ratelimit.yaml new file mode 100644 index 000000000000..e2139359fa20 --- /dev/null +++ b/test/e2e/testdata/local-ratelimit.yaml @@ -0,0 +1,92 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: ratelimit-specific-user + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-ratelimit-specific-user + namespace: gateway-conformance-infra + rateLimit: + type: Local + local: + rules: + - clientSelectors: + - headers: + - name: x-user-id + value: john + limit: + requests: 3 + unit: Hour +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: ratelimit-all-traffic + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-ratelimit-all-traffic + namespace: gateway-conformance-infra + rateLimit: + type: Local + local: + rules: + - limit: + requests: 3 + unit: Hour +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-ratelimit-specific-user + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 + matches: + - path: + type: Exact + value: /ratelimit-specific-user +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-ratelimit-all-traffic + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 + matches: + - path: + type: Exact + value: /ratelimit-all-traffic +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-no-ratelimit + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 + matches: + - path: + type: Exact + value: /no-ratelimit diff --git a/test/e2e/tests/local-ratelimit.go b/test/e2e/tests/local-ratelimit.go new file mode 100644 index 000000000000..baaf45f50520 --- /dev/null +++ b/test/e2e/tests/local-ratelimit.go @@ -0,0 +1,191 @@ +// 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. + +//go:build e2e +// +build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, LocalRateLimitSpecificUserTest) + ConformanceTests = append(ConformanceTests, LocalRateLimitAllTrafficTest) + ConformanceTests = append(ConformanceTests, LocalRateLimitNoLimitRouteTest) +} + +var LocalRateLimitSpecificUserTest = suite.ConformanceTest{ + ShortName: "LocalRateLimitSpecificUser", + Description: "Limit a specific user", + Manifests: []string{"testdata/local-ratelimit.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("limit a specific user", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-ratelimit-specific-user", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-specific-user", Namespace: ns}) + + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-specific-user", + Headers: map[string]string{ + "x-user-id": "john", + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + expectLimitResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-specific-user", + Headers: map[string]string{ + "x-user-id": "john", + }, + }, + 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 request + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at first three request: %v", err) + } + + // this request should be limited because the user is john and the limit is 3 + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + t.Errorf("fail to get expected response at last fourth request: %v", err) + } + + // test another user + expectOkResp = http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-specific-user", + Headers: map[string]string{ + "x-user-id": "mike", + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + expectOkReq = http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + // the requests should not be limited because the user is mike + if err := GotExactExpectedResponse(t, 4, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at first three request: %v", err) + } + }) + }, +} + +var LocalRateLimitAllTrafficTest = suite.ConformanceTest{ + ShortName: "LocalRateLimitAllTraffic", + Description: "Limit all traffic on a route", + Manifests: []string{"testdata/local-ratelimit.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("limit all traffic on a route", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-ratelimit-all-traffic", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "ratelimit-all-traffic", Namespace: ns}) + + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-all-traffic", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + expectLimitResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/ratelimit-all-traffic", + }, + 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 request + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at first three request: %v", err) + } + + // this request should be limited because the user is john and the limit is 3 + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + t.Errorf("fail to get expected response at last fourth request: %v", err) + } + }) + }, +} + +var LocalRateLimitNoLimitRouteTest = suite.ConformanceTest{ + ShortName: "LocalRateLimitNoLimitRoute", + Description: "No rate limit on this route", + Manifests: []string{"testdata/local-ratelimit.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("no rate limit on this route", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: \"ratelimit-specific-user\", Namespace: ns})", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/no-ratelimit", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + expectOkReq := http.MakeRequest(t, &expectOkResp, 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) + + // the requests should not be limited because there is no rate limit on this route + if err := GotExactExpectedResponse(t, 3, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("fail to get expected response at last fourth request: %v", err) + } + }) + }, +} diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go index 62937315512b..eb48f955f3c5 100644 --- a/test/e2e/tests/ratelimit.go +++ b/test/e2e/tests/ratelimit.go @@ -64,10 +64,10 @@ var RateLimitTest = suite.ConformanceTest{ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) // fire the rest request - if err := GotExactNExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { t.Errorf("fail to get expected response at first three request: %v", err) } - if err := GotExactNExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { t.Errorf("fail to get expected response at last fourth request: %v", err) } }) @@ -165,15 +165,15 @@ var RateLimitBasedJwtClaimsTest = suite.ConformanceTest{ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, JwtOkResp) // fire the rest request - if err := GotExactNExpectedResponse(t, 2, suite.RoundTripper, JwtReq, JwtOkResp); err != nil { + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, JwtReq, JwtOkResp); err != nil { t.Errorf("failed to get expected response at third request: %v", err) } - if err := GotExactNExpectedResponse(t, 1, suite.RoundTripper, JwtReq, expectLimitResp); err != nil { + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, JwtReq, expectLimitResp); err != nil { t.Errorf("failed to get expected response at the fourth request: %v", err) } // Carrying different jwt claims will not be limited - if err := GotExactNExpectedResponse(t, 4, suite.RoundTripper, difJwtReq, expectOkResp); err != nil { + if err := GotExactExpectedResponse(t, 4, suite.RoundTripper, difJwtReq, expectOkResp); err != nil { t.Errorf("failed to get expected response for the request with a different jwt: %v", err) } @@ -188,7 +188,7 @@ var RateLimitBasedJwtClaimsTest = suite.ConformanceTest{ Namespace: ns, } noTokenReq := http.MakeRequest(t, &noTokenResp, gwAddr, "HTTP", "http") - if err := GotExactNExpectedResponse(t, 1, suite.RoundTripper, noTokenReq, noTokenResp); err != nil { + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, noTokenReq, noTokenResp); err != nil { t.Errorf("failed to get expected response: %v", err) } @@ -196,7 +196,7 @@ var RateLimitBasedJwtClaimsTest = suite.ConformanceTest{ }, } -func GotExactNExpectedResponse(t *testing.T, n int, r roundtripper.RoundTripper, req roundtripper.Request, resp http.ExpectedResponse) error { +func GotExactExpectedResponse(t *testing.T, n int, r roundtripper.RoundTripper, req roundtripper.Request, resp http.ExpectedResponse) error { for i := 0; i < n; i++ { cReq, cRes, err := r.CaptureRoundTrip(req) if err != nil {