From 466e7300c832939e431e78ad3ae19c4736bc73df Mon Sep 17 00:00:00 2001 From: David Black Date: Wed, 13 May 2020 09:56:07 -0600 Subject: [PATCH 1/2] implement an http1+json endpoint to enable use of ratelimit service from istio < 1.5 Using the ratelimit service from istio requires EnvoyFilters from istio >= 1.5. We're running istio 1.1, so we'll need to implement rate limiting from lua. Lua doesn't support gRPC. So we need an http1+json endpoint Signed-off-by: David Black --- README.md | 30 +++++++++++++++++ docker-compose.yml | 9 +++++ src/server/server.go | 2 ++ src/server/server_impl.go | 33 +++++++++++++++++++ src/service_cmd/runner/runner.go | 2 ++ test/integration/integration_test.go | 23 +++++++++++++ .../current/ratelimit/config/basic.yaml | 5 +++ 7 files changed, 104 insertions(+) diff --git a/README.md b/README.md index fdc749199..b139a4cde 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ - [Loading Configuration](#loading-configuration) - [Request Fields](#request-fields) - [Statistics](#statistics) +- [HTTP Port](#http-port) + - [/json endpoint](#json-endpoint) - [Debug Port](#debug-port) - [Local Cache](#local-cache) - [Redis](#redis) @@ -360,6 +362,34 @@ ratelimit.service.rate_limit.messaging.message_type_marketing.to_number.over_lim ratelimit.service.rate_limit.messaging.message_type_marketing.to_number.total_hits: 0 ``` +# HTTP Port + +The ratelimit service listens to http 1.1 (by default on port 8080) with two endpoints: +1. /healthcheck → return a 200 if this service is healthy +1. /json → http 1.1 endpoint for interacting with ratelimit service + +## /json endpoint + +Takes an HTTP POST with a JSON body of the form e.g. +```json +{ + "domain": "dummy", + "descriptors": [ + {"entries": [ + {"key": "one_per_day", + "value": "something"} + ]} + ] +} +``` +The service will return an http 200 if this request is allowed (if no ratelimits exceeded) or 409 if one or more +ratelimits were exceeded. Endpoint does not currently return detailed information on which limits were exceeded. + +See the test case in `test/integration/integration_test.go` paired with the config file +`test/integration/runtime/current/ratelimit/config/basic.yaml` (which configures the `one_per_minute` descriptor in the + `basic` domain) +for an example + # Debug Port The debug port can be used to interact with the running process. diff --git a/docker-compose.yml b/docker-compose.yml index c4ab4443f..51360d361 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,14 @@ services: - .:/go/src/github.com/envoyproxy/ratelimit - binary:/usr/local/bin/ + ratelimit-client-build: + image: golang:1.14-alpine + working_dir: /go/src/github.com/envoyproxy/ratelimit + command: go build -o /usr/local/bin/ratelimit_client ./src/client_cmd/main.go + volumes: + - .:/go/src/github.com/envoyproxy/ratelimit + - binary:/usr/local/bin/ + ratelimit: image: alpine:3.6 command: /usr/local/bin/ratelimit @@ -29,6 +37,7 @@ services: depends_on: - redis - ratelimit-build + - ratelimit-client-build networks: - ratelimit-network volumes: diff --git a/src/server/server.go b/src/server/server.go index 820085744..38520092a 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -1,6 +1,7 @@ package server import ( + pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" "net/http" "github.com/lyft/goruntime/loader" @@ -25,6 +26,7 @@ type Server interface { * Add an HTTP endpoint to the local debug port. */ AddDebugHttpEndpoint(path string, help string, handler http.HandlerFunc) + AddJsonHandler(pb.RateLimitServiceServer) /** * Returns the embedded gRPC server to be used for registering gRPC endpoints. diff --git a/src/server/server_impl.go b/src/server/server_impl.go index 2b27f06cd..b0349594e 100644 --- a/src/server/server_impl.go +++ b/src/server/server_impl.go @@ -1,6 +1,7 @@ package server import ( + "encoding/json" "expvar" "fmt" "io" @@ -17,6 +18,7 @@ import ( "net" "github.com/coocood/freecache" + pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" "github.com/envoyproxy/ratelimit/src/settings" "github.com/gorilla/mux" reuseport "github.com/kavu/go_reuseport" @@ -52,6 +54,37 @@ func (server *server) AddDebugHttpEndpoint(path string, help string, handler htt server.debugListener.endpoints[path] = help } +// add an http/1 handler at the /json endpoint which allows this ratelimit service to work with +// clients that cannot use the gRPC interface (e.g. lua) +// example usage from cURL with domain "dummy" and descriptor "perday": +// echo '{"domain": "dummy", "descriptors": [{"entries": [{"key": "perday"}]}]}' | curl -vvvXPOST --data @/dev/stdin localhost:8080/json +func (server *server) AddJsonHandler(svc pb.RateLimitServiceServer) { + handler := func(writer http.ResponseWriter, request *http.Request) { + var req pb.RateLimitRequest + + if err := json.NewDecoder(request.Body).Decode(&req); err != nil { + logger.Warnf("error: %s", err.Error()) + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + + resp, err := svc.ShouldRateLimit(nil, &req) + if err != nil { + logger.Warnf("error: %s", err.Error()) + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + logger.Debugf("resp:%s", resp) + if resp.OverallCode == pb.RateLimitResponse_OVER_LIMIT { + http.Error(writer, "over limit", http.StatusTooManyRequests) + } else if resp.OverallCode == pb.RateLimitResponse_UNKNOWN { + http.Error(writer, "unknown", http.StatusInternalServerError) + } + + } + server.router.HandleFunc("/json", handler) +} + func (server *server) GrpcServer() *grpc.Server { return server.grpcServer } diff --git a/src/service_cmd/runner/runner.go b/src/service_cmd/runner/runner.go index ee9da4b39..a6a5d0d8f 100644 --- a/src/service_cmd/runner/runner.go +++ b/src/service_cmd/runner/runner.go @@ -75,6 +75,8 @@ func (runner *Runner) Run() { io.WriteString(writer, service.GetCurrentConfig().Dump()) }) + srv.AddJsonHandler(service) + // Ratelimit is compatible with two proto definitions // 1. data-plane-api rls.proto: https://github.com/envoyproxy/data-plane-api/blob/master/envoy/service/ratelimit/v2/rls.proto pb.RegisterRateLimitServiceServer(srv.GrpcServer(), service) diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index b33683c8e..6eee4ebf3 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -3,8 +3,10 @@ package integration_test import ( + "bytes" "fmt" "math/rand" + "net/http" "os" "strconv" "testing" @@ -344,6 +346,7 @@ func TestBasicConfigLegacy(t *testing.T) { assert := assert.New(t) conn, err := grpc.Dial("localhost:8083", grpc.WithInsecure()) + assert.NoError(err) defer conn.Close() c := pb_legacy.NewRateLimitServiceClient(conn) @@ -358,6 +361,26 @@ func TestBasicConfigLegacy(t *testing.T) { response) assert.NoError(err) + json_body := []byte(`{ + "domain": "basic", + "descriptors": [ + { + "entries": [ + { + "key": "one_per_minute" + } + ] + } + ] + }`) + http_resp, _ := http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(json_body)) + assert.Equal(http_resp.StatusCode, 200) + http_resp.Body.Close() + + http_resp, _ = http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(json_body)) + assert.Equal(http_resp.StatusCode, 429) + http_resp.Body.Close() + response, err = c.ShouldRateLimit( context.Background(), common.NewRateLimitRequestLegacy("basic_legacy", [][][2]string{{{"key1", "foo"}}}, 1)) diff --git a/test/integration/runtime/current/ratelimit/config/basic.yaml b/test/integration/runtime/current/ratelimit/config/basic.yaml index 41cd5a31a..843b98873 100644 --- a/test/integration/runtime/current/ratelimit/config/basic.yaml +++ b/test/integration/runtime/current/ratelimit/config/basic.yaml @@ -9,3 +9,8 @@ descriptors: rate_limit: unit: second requests_per_unit: 50 + + - key: one_per_minute + rate_limit: + unit: minute + requests_per_unit: 1 \ No newline at end of file From 6ccf8c7bdcde221b6239a0069cfc6065c099fce3 Mon Sep 17 00:00:00 2001 From: David Black Date: Tue, 26 May 2020 08:11:48 -0600 Subject: [PATCH 2/2] address PR review feedback * s/http/HTTP/g * add test case for invalid json returning 400 resp. code * remove reference to config file from README.md Signed-off-by: David Black --- README.md | 11 +++-------- test/integration/integration_test.go | 4 ++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b139a4cde..59744a159 100644 --- a/README.md +++ b/README.md @@ -364,9 +364,9 @@ ratelimit.service.rate_limit.messaging.message_type_marketing.to_number.total_hi # HTTP Port -The ratelimit service listens to http 1.1 (by default on port 8080) with two endpoints: +The ratelimit service listens to HTTP 1.1 (by default on port 8080) with two endpoints: 1. /healthcheck → return a 200 if this service is healthy -1. /json → http 1.1 endpoint for interacting with ratelimit service +1. /json → HTTP 1.1 endpoint for interacting with ratelimit service ## /json endpoint @@ -382,14 +382,9 @@ Takes an HTTP POST with a JSON body of the form e.g. ] } ``` -The service will return an http 200 if this request is allowed (if no ratelimits exceeded) or 409 if one or more +The service will return an http 200 if this request is allowed (if no ratelimits exceeded) or 429 if one or more ratelimits were exceeded. Endpoint does not currently return detailed information on which limits were exceeded. -See the test case in `test/integration/integration_test.go` paired with the config file -`test/integration/runtime/current/ratelimit/config/basic.yaml` (which configures the `one_per_minute` descriptor in the - `basic` domain) -for an example - # Debug Port The debug port can be used to interact with the running process. diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index 6eee4ebf3..067a9a312 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -381,6 +381,10 @@ func TestBasicConfigLegacy(t *testing.T) { assert.Equal(http_resp.StatusCode, 429) http_resp.Body.Close() + invalid_json := []byte(`{"unclosed quote: []}`) + http_resp, _ = http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(invalid_json)) + assert.Equal(http_resp.StatusCode, 400) + response, err = c.ShouldRateLimit( context.Background(), common.NewRateLimitRequestLegacy("basic_legacy", [][][2]string{{{"key1", "foo"}}}, 1))