From 0a52efa8ba499d3e3410160cbb274b8ad4791703 Mon Sep 17 00:00:00 2001 From: winjay Date: Fri, 13 Aug 2021 17:46:51 +0800 Subject: [PATCH 1/2] feat(envoy-rls): Update sentinel-cluster-server-envoy-rls to support envoy v2 and v3 api. Add envoy v3 api implementation. Make it to be compatible with envoy v2 api. Add k8s envoy v3 api demo. --- .../sample/k8s/README.md | 8 + .../sample/k8s/envoy-v3-api.yaml | 141 +++++++++++++ .../envoy/rls/SentinelRlsGrpcServer.java | 2 + .../v3/SentinelEnvoyRlsServiceImpl.java | 118 +++++++++++ .../proto/envoy/config/core/v3/base.proto | 33 +++ .../common/ratelimit/v3/ratelimit.proto | 94 +++++++++ .../envoy/service/ratelimit/v3/rls.proto | 196 ++++++++++++++++++ .../proto/envoy/type/v3/ratelimit_unit.proto | 30 +++ .../src/main/proto/udpa/annotations/BUILD | 5 + .../main/proto/udpa/annotations/migrate.proto | 49 +++++ .../proto/udpa/annotations/security.proto | 31 +++ .../proto/udpa/annotations/sensitive.proto | 14 ++ .../main/proto/udpa/annotations/status.proto | 34 +++ .../proto/udpa/annotations/versioning.proto | 17 ++ .../src/main/proto/validate/validate.proto | 112 +++++++++- .../v3/SentinelEnvoyRlsServiceImplTest.java | 109 ++++++++++ 16 files changed, 987 insertions(+), 6 deletions(-) create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/envoy-v3-api.yaml create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImpl.java create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/config/core/v3/base.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/extensions/common/ratelimit/v3/ratelimit.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v3/rls.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/type/v3/ratelimit_unit.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/BUILD create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/migrate.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/security.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/sensitive.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/status.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/versioning.proto create mode 100644 sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImplTest.java diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md index 3fd58ba887..ea5bfdb29c 100644 --- a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md @@ -91,6 +91,14 @@ After preparing the yaml template, you may deploy the Envoy instance: kubectl apply -f sample/k8s/envoy.yml ``` +for v3 api: + +```bash +kubectl apply -f sample/k8s/envoy-v3-api.yml +``` + + + ## Test the rate limiting Now it's show time! We could visit the URL `envoy-service:10000/json` in K8S pods. diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/envoy-v3-api.yaml b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/envoy-v3-api.yaml new file mode 100644 index 0000000000..124c986481 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/envoy-v3-api.yaml @@ -0,0 +1,141 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: envoy-cm-17 +data: + envoy-yml: |- + admin: + access_log_path: /tmp/admin_access.log + address: + socket_address: + protocol: TCP + address: 127.0.0.1 + port_value: 9901 + static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: + prefix: "/" + route: + cluster: service_httpbin + typed_per_filter_config: + envoy.filters.http.dynamic_forward_proxy: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.PerRouteConfig + host_rewrite_literal: httpbin.org + rate_limits: + - stage: 0 + actions: + - {destination_cluster: {}} + http_filters: + - name: envoy.filters.http.ratelimit + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: foo + request_type: external + failure_mode_deny: false + stage: 0 + rate_limit_service: + grpc_service: + envoy_grpc: + cluster_name: rate_limit_cluster + timeout: 2s + transport_api_version: V3 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - name: service_httpbin + connect_timeout: 0.5s + type: LOGICAL_DNS + # Comment out the following line to test on v6 networks + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service_httpbin + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: httpbin.org + port_value: 80 + - name: rate_limit_cluster + type: STRICT_DNS + connect_timeout: 10s + lb_policy: ROUND_ROBIN + http2_protocol_options: {} + load_assignment: + cluster_name: rate_limit_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: sentinel-rls-service + port_value: 10245 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: envoy-deployment-basic-17 + labels: + app: envoy-17 +spec: + replicas: 1 + selector: + matchLabels: + app: envoy-17 + template: + metadata: + labels: + app: envoy-17 + spec: + containers: + - name: envoy + image: envoyproxy/envoy:v1.17.3 + ports: + - containerPort: 10000 + command: ["envoy"] + args: ["-c", "/tmp/envoy/envoy.yaml"] + volumeMounts: + - name: envoy-config + mountPath: /tmp/envoy + volumes: + - name: envoy-config + configMap: + name: envoy-cm-17 + items: + - key: envoy-yml + path: envoy.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: envoy-service-17 + labels: + name: envoy-service-17 +spec: + type: NodePort + ports: + - port: 10000 + targetPort: 10000 + protocol: TCP + selector: + app: envoy-17 diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java index 459540ad69..4f49edb0ee 100644 --- a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/SentinelRlsGrpcServer.java @@ -31,7 +31,9 @@ public class SentinelRlsGrpcServer { public SentinelRlsGrpcServer(int port) { ServerBuilder builder = ServerBuilder.forPort(port) + .addService(new com.alibaba.csp.sentinel.cluster.server.envoy.rls.service.v3.SentinelEnvoyRlsServiceImpl()) .addService(new SentinelEnvoyRlsServiceImpl()); + server = builder.build(); } diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImpl.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImpl.java new file mode 100644 index 0000000000..f776c204a4 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImpl.java @@ -0,0 +1,118 @@ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls.service.v3; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterFlowRuleManager; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.flow.SimpleClusterFlowChecker; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.log.RlsAccessLogger; +import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.function.Tuple2; +import com.google.protobuf.TextFormat; +import io.envoyproxy.envoy.extensions.common.ratelimit.v3.RateLimitDescriptor; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitRequest; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitResponse; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitResponse.Code; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitResponse.RateLimit; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitResponse.DescriptorStatus; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitServiceGrpc; +import io.grpc.stub.StreamObserver; + +import java.util.ArrayList; +import java.util.List; + +import static com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter.SEPARATOR; + +/** + * gRPC限流入口,实现envoy rls v3 api + * + * @author Winjay chan + * @date 2021/8/4 + */ +public class SentinelEnvoyRlsServiceImpl extends RateLimitServiceGrpc.RateLimitServiceImplBase { + @Override + public void shouldRateLimit(RateLimitRequest request, StreamObserver responseObserver) { + int acquireCount = request.getHitsAddend(); + if (acquireCount < 0) { + responseObserver.onError(new IllegalArgumentException( + "acquireCount should be positive, but actual: " + acquireCount)); + return; + } + if (acquireCount == 0) { + // Not present, use the default "1" by default. + acquireCount = 1; + } + + String domain = request.getDomain(); + boolean blocked = false; + List statusList = new ArrayList<>(request.getDescriptorsCount()); + for (RateLimitDescriptor descriptor : request.getDescriptorsList()) { + Tuple2 t = checkToken(domain, descriptor, acquireCount); + TokenResult r = t.r2; + + printAccessLogIfNecessary(domain, descriptor, r); + + if (r.getStatus() == TokenResultStatus.NO_RULE_EXISTS) { + // If the rule of the descriptor is absent, the request will pass directly. + r.setStatus(TokenResultStatus.OK); + } + + if (!blocked && r.getStatus() != TokenResultStatus.OK) { + blocked = true; + } + + Code statusCode = r.getStatus() == TokenResultStatus.OK ? Code.OK : Code.OVER_LIMIT; + DescriptorStatus.Builder descriptorStatusBuilder = DescriptorStatus.newBuilder() + .setCode(statusCode); + if (t.r1 != null) { + descriptorStatusBuilder + .setCurrentLimit(RateLimit.newBuilder().setUnit(RateLimit.Unit.SECOND) + .setRequestsPerUnit((int)t.r1.getCount()) + .build()) + .setLimitRemaining(r.getRemaining()); + } + statusList.add(descriptorStatusBuilder.build()); + } + + Code overallStatus = blocked ? Code.OVER_LIMIT :Code.OK; + RateLimitResponse response = RateLimitResponse.newBuilder() + .setOverallCode(overallStatus) + .addAllStatuses(statusList) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + private void printAccessLogIfNecessary(String domain, RateLimitDescriptor descriptor, TokenResult result) { + if (!RlsAccessLogger.isEnabled()) { + return; + } + String message = new StringBuilder("[RlsAccessLog] domain=").append(domain) + .append(", descriptor=").append(TextFormat.shortDebugString(descriptor)) + .append(", checkStatus=").append(result.getStatus()) + .append(", remaining=").append(result.getRemaining()) + .toString(); + RlsAccessLogger.log(message); + } + + protected Tuple2 checkToken(String domain, RateLimitDescriptor descriptor, int acquireCount) { + long ruleId = EnvoySentinelRuleConverter.generateFlowId(generateKey(domain, descriptor)); + + FlowRule rule = ClusterFlowRuleManager.getFlowRuleById(ruleId); + if (rule == null) { + // Pass if the target rule is absent. + return Tuple2.of(null, new TokenResult(TokenResultStatus.NO_RULE_EXISTS)); + } + // If the rule is present, it should be valid. + return Tuple2.of(rule, SimpleClusterFlowChecker.acquireClusterToken(rule, acquireCount)); + } + + private String generateKey(String domain, RateLimitDescriptor descriptor) { + StringBuilder sb = new StringBuilder(domain); + for (RateLimitDescriptor.Entry resource : descriptor.getEntriesList()) { + sb.append(SEPARATOR).append(resource.getKey()).append(SEPARATOR).append(resource.getValue()); + } + return sb.toString(); + } +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/config/core/v3/base.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/config/core/v3/base.proto new file mode 100644 index 0000000000..48cd25fa5f --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/config/core/v3/base.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package envoy.config.core.v3; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.core.v3"; +option java_outer_classname = "BaseProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + + + +// Header name/value pair. +message HeaderValue { + option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.HeaderValue"; + + // Header name. + string key = 1 + [(validate.rules).string = + {min_len: 1 max_bytes: 16384 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // Header value. + // + // The same :ref:`format specifier ` as used for + // :ref:`HTTP access logging ` applies here, however + // unknown header values are replaced with the empty string instead of `-`. + string value = 2 [ + (validate.rules).string = {max_bytes: 16384 well_known_regex: HTTP_HEADER_VALUE strict: false} + ]; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/extensions/common/ratelimit/v3/ratelimit.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/extensions/common/ratelimit/v3/ratelimit.proto new file mode 100644 index 0000000000..1047582db3 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/extensions/common/ratelimit/v3/ratelimit.proto @@ -0,0 +1,94 @@ +syntax = "proto3"; + +package envoy.extensions.common.ratelimit.v3; + +import "envoy/type/v3/ratelimit_unit.proto"; +// +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.common.ratelimit.v3"; +option java_outer_classname = "RatelimitProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Common rate limit components] + +// A RateLimitDescriptor is a list of hierarchical entries that are used by the service to +// determine the final rate limit key and overall allowed limit. Here are some examples of how +// they might be used for the domain "envoy". +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["remote_address": "10.0.0.1"] +// +// What it does: Limits all unauthenticated traffic for the IP address 10.0.0.1. The +// configuration supplies a default limit for the *remote_address* key. If there is a desire to +// raise the limit for 10.0.0.1 or block it entirely it can be specified directly in the +// configuration. +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["path": "/foo/bar"] +// +// What it does: Limits all unauthenticated traffic globally for a specific path (or prefix if +// configured that way in the service). +// +// .. code-block:: cpp +// +// ["authenticated": "false"], ["path": "/foo/bar"], ["remote_address": "10.0.0.1"] +// +// What it does: Limits unauthenticated traffic to a specific path for a specific IP address. +// Like (1) we can raise/block specific IP addresses if we want with an override configuration. +// +// .. code-block:: cpp +// +// ["authenticated": "true"], ["client_id": "foo"] +// +// What it does: Limits all traffic for an authenticated client "foo" +// +// .. code-block:: cpp +// +// ["authenticated": "true"], ["client_id": "foo"], ["path": "/foo/bar"] +// +// What it does: Limits traffic to a specific path for an authenticated client "foo" +// +// The idea behind the API is that (1)/(2)/(3) and (4)/(5) can be sent in 1 request if desired. +// This enables building complex application scenarios with a generic backend. +// +// Optionally the descriptor can contain a limit override under a "limit" key, that specifies +// the number of requests per unit to use instead of the number configured in the +// rate limiting service. +message RateLimitDescriptor { + option (udpa.annotations.versioning).previous_message_type = + "envoy.api.v2.ratelimit.RateLimitDescriptor"; + + message Entry { + option (udpa.annotations.versioning).previous_message_type = + "envoy.api.v2.ratelimit.RateLimitDescriptor.Entry"; + + // Descriptor key. + string key = 1 [(validate.rules).string = {min_len: 1}]; + + // Descriptor value. + string value = 2 [(validate.rules).string = {min_len: 1}]; + } + + // Override rate limit to apply to this descriptor instead of the limit + // configured in the rate limit service. See :ref:`rate limit override + // ` for more information. + message RateLimitOverride { + // The number of requests per unit of time. + uint32 requests_per_unit = 1; + + // The unit of time. + type.v3.RateLimitUnit unit = 2 [(validate.rules).enum = {defined_only: true}]; + } + + // Descriptor entries. + repeated Entry entries = 1 [(validate.rules).repeated = {min_items: 1}]; + + // Optional rate limit override to supply to the ratelimit service. + RateLimitOverride limit = 2; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v3/rls.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v3/rls.proto new file mode 100644 index 0000000000..c2823cf7f9 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/service/ratelimit/v3/rls.proto @@ -0,0 +1,196 @@ +syntax = "proto3"; + +package envoy.service.ratelimit.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/extensions/common/ratelimit/v3/ratelimit.proto"; + +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +// +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.service.ratelimit.v3"; +option java_outer_classname = "RlsProto"; +option java_multiple_files = true; +option java_generic_services = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Rate Limit Service (RLS)] + +service RateLimitService { + // Determine whether rate limiting should take place. + rpc ShouldRateLimit(RateLimitRequest) returns (RateLimitResponse) { + } +} + +// Main message for a rate limit request. The rate limit service is designed to be fully generic +// in the sense that it can operate on arbitrary hierarchical key/value pairs. The loaded +// configuration will parse the request and find the most specific limit to apply. In addition, +// a RateLimitRequest can contain multiple "descriptors" to limit on. When multiple descriptors +// are provided, the server will limit on *ALL* of them and return an OVER_LIMIT response if any +// of them are over limit. This enables more complex application level rate limiting scenarios +// if desired. +message RateLimitRequest { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.ratelimit.v2.RateLimitRequest"; + + // All rate limit requests must specify a domain. This enables the configuration to be per + // application without fear of overlap. E.g., "envoy". + string domain = 1; + + // All rate limit requests must specify at least one RateLimitDescriptor. Each descriptor is + // processed by the service (see below). If any of the descriptors are over limit, the entire + // request is considered to be over limit. + repeated envoy.extensions.common.ratelimit.v3.RateLimitDescriptor descriptors = 2; + + // Rate limit requests can optionally specify the number of hits a request adds to the matched + // limit. If the value is not set in the message, a request increases the matched limit by 1. + uint32 hits_addend = 3; +} + +// A response from a ShouldRateLimit call. +// [#next-free-field: 7] +message RateLimitResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.ratelimit.v2.RateLimitResponse"; + + enum Code { + // The response code is not known. + UNKNOWN = 0; + + // The response code to notify that the number of requests are under limit. + OK = 1; + + // The response code to notify that the number of requests are over limit. + OVER_LIMIT = 2; + } + + // Defines an actual rate limit in terms of requests per unit of time and the unit itself. + message RateLimit { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.ratelimit.v2.RateLimitResponse.RateLimit"; + + // Identifies the unit of of time for rate limit. + // [#comment: replace by envoy/type/v3/ratelimit_unit.proto in v4] + enum Unit { + // The time unit is not known. + UNKNOWN = 0; + + // The time unit representing a second. + SECOND = 1; + + // The time unit representing a minute. + MINUTE = 2; + + // The time unit representing an hour. + HOUR = 3; + + // The time unit representing a day. + DAY = 4; + } + + // A name or description of this limit. + string name = 3; + + // The number of requests per unit of time. + uint32 requests_per_unit = 1; + + // The unit of time. + Unit unit = 2; + } + + // Cacheable quota for responses, see documentation for the :ref:`quota + // ` field. + // [#not-implemented-hide:] + message Quota { + // Number of matching requests granted in quota. Must be 1 or more. + uint32 requests = 1 [(validate.rules).uint32 = {gt: 0}]; + + oneof expiration_specifier { + // Point in time at which the quota expires. + google.protobuf.Timestamp valid_until = 2; + } + } + + // [#next-free-field: 6] + message DescriptorStatus { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.ratelimit.v2.RateLimitResponse.DescriptorStatus"; + + // The response code for an individual descriptor. + Code code = 1; + + // The current limit as configured by the server. Useful for debugging, etc. + RateLimit current_limit = 2; + + // The limit remaining in the current time unit. + uint32 limit_remaining = 3; + + // Duration until reset of the current limit window. + google.protobuf.Duration duration_until_reset = 4; + + // Quota granted for the descriptor. This is a certain number of requests over a period of time. + // The client may cache this result and apply the effective RateLimitResponse to future matching + // requests containing a matching descriptor without querying rate limit service. + // + // Quota is available for a request if its descriptor set has cached quota available for all + // descriptors. + // + // If quota is available, a RLS request will not be made and the quota will be reduced by 1 for + // all matching descriptors. + // + // If there is not sufficient quota, there are three cases: + // 1. A cached entry exists for a RLS descriptor that is out-of-quota, but not expired. + // In this case, the request will be treated as OVER_LIMIT. + // 2. Some RLS descriptors have a cached entry that has valid quota but some RLS descriptors + // have no cached entry. This will trigger a new RLS request. + // When the result is returned, a single unit will be consumed from the quota for all + // matching descriptors. + // If the server did not provide a quota, such as the quota message is empty for some of + // the descriptors, then the request admission is determined by the + // :ref:`overall_code `. + // 3. All RLS descriptors lack a cached entry, this will trigger a new RLS request, + // When the result is returned, a single unit will be consumed from the quota for all + // matching descriptors. + // If the server did not provide a quota, such as the quota message is empty for some of + // the descriptors, then the request admission is determined by the + // :ref:`overall_code `. + // + // When quota expires due to timeout, a new RLS request will also be made. + // The implementation may choose to preemptively query the rate limit server for more quota on or + // before expiration or before the available quota runs out. + // [#not-implemented-hide:] + Quota quota = 5; + } + + // The overall response code which takes into account all of the descriptors that were passed + // in the RateLimitRequest message. + Code overall_code = 1; + + // A list of DescriptorStatus messages which matches the length of the descriptor list passed + // in the RateLimitRequest. This can be used by the caller to determine which individual + // descriptors failed and/or what the currently configured limits are for all of them. + repeated DescriptorStatus statuses = 2; + + // A list of headers to add to the response + repeated config.core.v3.HeaderValue response_headers_to_add = 3; + + // A list of headers to add to the request when forwarded + repeated config.core.v3.HeaderValue request_headers_to_add = 4; + + // A response body to send to the downstream client when the response code is not OK. + bytes raw_body = 5; + + // Optional response metadata that will be emitted as dynamic metadata to be consumed by the next + // filter. This metadata lives in a namespace specified by the canonical name of extension filter + // that requires it: + // + // - :ref:`envoy.filters.http.ratelimit ` for HTTP filter. + // - :ref:`envoy.filters.network.ratelimit ` for network filter. + // - :ref:`envoy.filters.thrift.rate_limit ` for Thrift filter. + google.protobuf.Struct dynamic_metadata = 6; +} \ No newline at end of file diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/type/v3/ratelimit_unit.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/type/v3/ratelimit_unit.proto new file mode 100644 index 0000000000..a3fb27ff47 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/envoy/type/v3/ratelimit_unit.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package envoy.type.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.type.v3"; +option java_outer_classname = "RatelimitUnitProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Ratelimit Time Unit] + +// Identifies the unit of of time for rate limit. +enum RateLimitUnit { + // The time unit is not known. + UNKNOWN = 0; + + // The time unit representing a second. + SECOND = 1; + + // The time unit representing a minute. + MINUTE = 2; + + // The time unit representing an hour. + HOUR = 3; + + // The time unit representing a day. + DAY = 4; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/BUILD b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/BUILD new file mode 100644 index 0000000000..b8846b031e --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/BUILD @@ -0,0 +1,5 @@ +load("//bazel:api_build_system.bzl", "udpa_proto_package") + +licenses(["notice"]) # Apache 2 + +udpa_proto_package() diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/migrate.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/migrate.proto new file mode 100644 index 0000000000..1c42a6404d --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/migrate.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +package udpa.annotations; + +import "google/protobuf/descriptor.proto"; + +// Magic number in this file derived from top 28bit of SHA256 digest of +// "udpa.annotation.migrate". + +extend google.protobuf.MessageOptions { + MigrateAnnotation message_migrate = 171962766; +} + +extend google.protobuf.FieldOptions { + FieldMigrateAnnotation field_migrate = 171962766; +} + +extend google.protobuf.EnumOptions { + MigrateAnnotation enum_migrate = 171962766; +} + +extend google.protobuf.EnumValueOptions { + MigrateAnnotation enum_value_migrate = 171962766; +} + +extend google.protobuf.FileOptions { + FileMigrateAnnotation file_migrate = 171962766; +} + +message MigrateAnnotation { + // Rename the message/enum/enum value in next version. + string rename = 1; +} + +message FieldMigrateAnnotation { + // Rename the field in next version. + string rename = 1; + + // Add the field to a named oneof in next version. If this already exists, the + // field will join its siblings under the oneof, otherwise a new oneof will be + // created with the given name. + string oneof_promotion = 2; +} + +message FileMigrateAnnotation { + // Move all types in the file to another package, this implies changing proto + // file path. + string move_to_package = 2; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/security.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/security.proto new file mode 100644 index 0000000000..7191fe30c8 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/security.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package udpa.annotations; + +import "udpa/annotations/status.proto"; + +import "google/protobuf/any.proto"; +import "google/protobuf/descriptor.proto"; + +import "validate/validate.proto"; + +// All annotations in this file are experimental and subject to change. Their +// only consumer today is the Envoy APIs and SecuritAnnotationValidator protoc +// plugin in this repository. +option (udpa.annotations.file_status).work_in_progress = true; + +extend google.protobuf.FieldOptions { + // Magic number is the 28 most significant bits in the sha256sum of + // "udpa.annotations.security". + FieldSecurityAnnotation security = 11122993; +} + +// These annotations indicate metadata for the purpose of understanding the +// security significance of fields. +message FieldSecurityAnnotation { + // Field should be set in the presence of untrusted downstreams. + bool configure_for_untrusted_downstream = 1; + + // Field should be set in the presence of untrusted upstreams. + bool configure_for_untrusted_upstream = 2; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/sensitive.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/sensitive.proto new file mode 100644 index 0000000000..8dc921f24b --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/sensitive.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package udpa.annotations; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.FieldOptions { + // Magic number is the 28 most significant bits in the sha256sum of "udpa.annotations.sensitive". + // When set to true, `sensitive` indicates that this field contains sensitive data, such as + // personally identifiable information, passwords, or private keys, and should be redacted for + // display by tools aware of this annotation. Note that that this has no effect on standard + // Protobuf functions such as `TextFormat::PrintToString`. + bool sensitive = 76569463; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/status.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/status.proto new file mode 100644 index 0000000000..9832ffd3a2 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/status.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package udpa.annotations; + +import "google/protobuf/descriptor.proto"; + +// Magic number in this file derived from top 28bit of SHA256 digest of +// "udpa.annotation.status". +extend google.protobuf.FileOptions { + StatusAnnotation file_status = 222707719; +} + +enum PackageVersionStatus { + // Unknown package version status. + UNKNOWN = 0; + + // This version of the package is frozen. + FROZEN = 1; + + // This version of the package is the active development version. + ACTIVE = 2; + + // This version of the package is the candidate for the next major version. It + // is typically machine generated from the active development version. + NEXT_MAJOR_VERSION_CANDIDATE = 3; +} + +message StatusAnnotation { + // The entity is work-in-progress and subject to breaking changes. + bool work_in_progress = 1; + + // The entity belongs to a package with the given version status. + PackageVersionStatus package_version_status = 2; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/versioning.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/versioning.proto new file mode 100644 index 0000000000..16f6dc30ca --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/udpa/annotations/versioning.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package udpa.annotations; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.MessageOptions { + // Magic number derived from 0x78 ('x') 0x44 ('D') 0x53 ('S') + VersioningAnnotation versioning = 7881811; +} + +message VersioningAnnotation { + // Track the previous message type. E.g. this message might be + // udpa.foo.v3alpha.Foo and it was previously udpa.bar.v2.Bar. This + // information is consumed by UDPA via proto descriptors. + string previous_message_type = 1; +} diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto index b662551352..167ebdfb26 100644 --- a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/main/proto/validate/validate.proto @@ -1,8 +1,8 @@ syntax = "proto2"; package validate; -option go_package = "github.com/lyft/protoc-gen-validate/validate"; -option java_package = "com.lyft.pgv.validate"; +option go_package = "github.com/envoyproxy/protoc-gen-validate/validate"; +option java_package = "io.envoyproxy.pgv.validate"; import "google/protobuf/descriptor.proto"; import "google/protobuf/duration.proto"; @@ -12,26 +12,29 @@ import "google/protobuf/timestamp.proto"; extend google.protobuf.MessageOptions { // Disabled nullifies any validation rules for this message, including any // message fields associated with it that do support validation. - optional bool disabled = 919191; + optional bool disabled = 1071; + // Ignore skips generation of validation methods for this message. + optional bool ignored = 1072; } // Validation rules applied at the oneof level extend google.protobuf.OneofOptions { // Required ensures that exactly one the field options in a oneof is set; // validation fails if no fields in the oneof are set. - optional bool required = 919191; + optional bool required = 1071; } // Validation rules applied at the field level extend google.protobuf.FieldOptions { // Rules specify the validations to be performed on this field. By default, // no validation is performed against a field. - optional FieldRules rules = 919191; + optional FieldRules rules = 1071; } // FieldRules encapsulates the rules for each type of field. Depending on the // field, the correct set should be used to ensure proper validations. message FieldRules { + optional MessageRules message = 17; oneof type { // Scalar Field Types FloatRules float = 1; @@ -52,7 +55,6 @@ message FieldRules { // Complex Field Types EnumRules enum = 16; - MessageRules message = 17; RepeatedRules repeated = 18; MapRules map = 19; @@ -93,6 +95,10 @@ message FloatRules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated float not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // DoubleRules describes the constraints applied to `double` values @@ -125,6 +131,10 @@ message DoubleRules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated double not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // Int32Rules describes the constraints applied to `int32` values @@ -157,6 +167,10 @@ message Int32Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated int32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // Int64Rules describes the constraints applied to `int64` values @@ -189,6 +203,10 @@ message Int64Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated int64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // UInt32Rules describes the constraints applied to `uint32` values @@ -221,6 +239,10 @@ message UInt32Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated uint32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // UInt64Rules describes the constraints applied to `uint64` values @@ -253,6 +275,10 @@ message UInt64Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated uint64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // SInt32Rules describes the constraints applied to `sint32` values @@ -285,6 +311,10 @@ message SInt32Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated sint32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // SInt64Rules describes the constraints applied to `sint64` values @@ -317,6 +347,10 @@ message SInt64Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated sint64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // Fixed32Rules describes the constraints applied to `fixed32` values @@ -349,6 +383,10 @@ message Fixed32Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated fixed32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // Fixed64Rules describes the constraints applied to `fixed64` values @@ -381,6 +419,10 @@ message Fixed64Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated fixed64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // SFixed32Rules describes the constraints applied to `sfixed32` values @@ -413,6 +455,10 @@ message SFixed32Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated sfixed32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // SFixed64Rules describes the constraints applied to `sfixed64` values @@ -445,6 +491,10 @@ message SFixed64Rules { // NotIn specifies that this field cannot be equal to one of the specified // values repeated sfixed64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; } // BoolRules describes the constraints applied to `bool` values @@ -502,6 +552,10 @@ message StringRules { // anywhere in the string. optional string contains = 9; + // NotContains specifies that this field cannot have the specified substring + // anywhere in the string. + optional string not_contains = 23; + // In specifies that this field must be equal to one of the specified // values repeated string in = 10; @@ -540,7 +594,41 @@ message StringRules { // UriRef specifies that the field must be a valid URI as defined by RFC // 3986 and may be relative or absolute. bool uri_ref = 18; + + // Address specifies that the field must be either a valid hostname as + // defined by RFC 1034 (which does not support internationalized domain + // names or IDNs), or it can be a valid IP (v4 or v6). + bool address = 21; + + // Uuid specifies that the field must be a valid UUID as defined by + // RFC 4122 + bool uuid = 22; + + // WellKnownRegex specifies a common well known pattern defined as a regex. + KnownRegex well_known_regex = 24; } + + // This applies to regexes HTTP_HEADER_NAME and HTTP_HEADER_VALUE to enable + // strict header validation. + // By default, this is true, and HTTP header validations are RFC-compliant. + // Setting to false will enable a looser validations that only disallows + // \r\n\0 characters, which can be used to bypass header matching rules. + optional bool strict = 25 [default = true]; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 26; +} + +// WellKnownRegex contain some well-known patterns. +enum KnownRegex { + UNKNOWN = 0; + + // HTTP header name as defined by RFC 7230. + HTTP_HEADER_NAME = 1; + + // HTTP header value as defined by RFC 7230. + HTTP_HEADER_VALUE = 2; } // BytesRules describe the constraints applied to `bytes` values @@ -599,6 +687,10 @@ message BytesRules { // format bool ipv6 = 12; } + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 14; } // EnumRules describe the constraints applied to enum values @@ -649,6 +741,10 @@ message RepeatedRules { // Repeated message fields will still execute validation against each item // unless skip is specified here. optional FieldRules items = 4; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 5; } // MapRules describe the constraints applied to `map` values @@ -672,6 +768,10 @@ message MapRules { // in the field. Message values will still have their validations evaluated // unless skip is specified here. optional FieldRules values = 5; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 6; } // AnyRules describe constraints applied exclusively to the diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImplTest.java b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImplTest.java new file mode 100644 index 0000000000..3ca43e4313 --- /dev/null +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/src/test/java/com/alibaba/csp/sentinel/cluster/server/envoy/rls/service/v3/SentinelEnvoyRlsServiceImplTest.java @@ -0,0 +1,109 @@ +package com.alibaba.csp.sentinel.cluster.server.envoy.rls.service.v3; + +import com.alibaba.csp.sentinel.cluster.TokenResult; +import com.alibaba.csp.sentinel.cluster.TokenResultStatus; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.util.function.Tuple2; + +import io.envoyproxy.envoy.extensions.common.ratelimit.v3.RateLimitDescriptor; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitRequest; +import io.envoyproxy.envoy.service.ratelimit.v3.RateLimitResponse; +import io.grpc.stub.StreamObserver; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; + +/** + * Created by Winjay + * + * @author Winjay chan + * @date 2021/8/13 16:31 + */ +public class SentinelEnvoyRlsServiceImplTest { + @Test + public void testShouldRateLimitPass() { + SentinelEnvoyRlsServiceImpl rlsService = mock(SentinelEnvoyRlsServiceImpl.class); + StreamObserver streamObserver = mock(StreamObserver.class); + String domain = "testShouldRateLimitPass"; + int acquireCount = 1; + + RateLimitDescriptor descriptor1 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("rk1").setValue("rv1").build()) + .build(); + RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("rk2").setValue("rv2").build()) + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("rk3").setValue("rv3").build()) + .build(); + + ArgumentCaptor responseCapture = ArgumentCaptor.forClass(RateLimitResponse.class); + doNothing().when(streamObserver) + .onNext(responseCapture.capture()); + + doCallRealMethod().when(rlsService).shouldRateLimit(any(), any()); + when(rlsService.checkToken(eq(domain), same(descriptor1), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + when(rlsService.checkToken(eq(domain), same(descriptor2), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + + RateLimitRequest rateLimitRequest = RateLimitRequest.newBuilder() + .addDescriptors(descriptor1) + .addDescriptors(descriptor2) + .setDomain(domain) + .setHitsAddend(acquireCount) + .build(); + rlsService.shouldRateLimit(rateLimitRequest, streamObserver); + + RateLimitResponse response = responseCapture.getValue(); + assertEquals(RateLimitResponse.Code.OK, response.getOverallCode()); + response.getStatusesList() + .forEach(e -> assertEquals(RateLimitResponse.Code.OK, e.getCode())); + } + + @Test + public void testShouldRatePartialBlock() { + SentinelEnvoyRlsServiceImpl rlsService = mock(SentinelEnvoyRlsServiceImpl.class); + StreamObserver streamObserver = mock(StreamObserver.class); + String domain = "testShouldRatePartialBlock"; + int acquireCount = 1; + + RateLimitDescriptor descriptor1 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("rk1").setValue("rv1").build()) + .build(); + RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder() + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("rk2").setValue("rv2").build()) + .addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("rk3").setValue("rv3").build()) + .build(); + + ArgumentCaptor responseCapture = ArgumentCaptor.forClass(RateLimitResponse.class); + doNothing().when(streamObserver) + .onNext(responseCapture.capture()); + + doCallRealMethod().when(rlsService).shouldRateLimit(any(), any()); + when(rlsService.checkToken(eq(domain), same(descriptor1), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.BLOCKED))); + when(rlsService.checkToken(eq(domain), same(descriptor2), eq(acquireCount))) + .thenReturn(Tuple2.of(new FlowRule(), new TokenResult(TokenResultStatus.OK))); + + RateLimitRequest rateLimitRequest = RateLimitRequest.newBuilder() + .addDescriptors(descriptor1) + .addDescriptors(descriptor2) + .setDomain(domain) + .setHitsAddend(acquireCount) + .build(); + rlsService.shouldRateLimit(rateLimitRequest, streamObserver); + + RateLimitResponse response = responseCapture.getValue(); + assertEquals(RateLimitResponse.Code.OVER_LIMIT, response.getOverallCode()); + assertEquals(2, response.getStatusesCount()); + assertTrue(response.getStatusesList().stream() + .anyMatch(e -> e.getCode().equals(RateLimitResponse.Code.OVER_LIMIT))); + assertFalse(response.getStatusesList().stream() + .allMatch(e -> e.getCode().equals(RateLimitResponse.Code.OVER_LIMIT))); + } +} From 236a1ab8197a3ad19adb6edc1487c87caccbee00 Mon Sep 17 00:00:00 2001 From: winjay Date: Fri, 13 Aug 2021 18:33:58 +0800 Subject: [PATCH 2/2] docs(envoy-rls): update the README.md update the README.md for document-lint check --- .../sentinel-cluster-server-envoy-rls/sample/k8s/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md index ea5bfdb29c..245e43a847 100644 --- a/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md +++ b/sentinel-cluster/sentinel-cluster-server-envoy-rls/sample/k8s/README.md @@ -97,8 +97,6 @@ for v3 api: kubectl apply -f sample/k8s/envoy-v3-api.yml ``` - - ## Test the rate limiting Now it's show time! We could visit the URL `envoy-service:10000/json` in K8S pods.