Add support for Envoy v3 API in sentinel-cluster-server-envoy-rls token server module (#2336)

* 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.
* docs(envoy-rls): update the README.md
This commit is contained in:
winjaychan 2021-08-20 19:28:53 +08:00 committed by GitHub
parent 20a0e4d7f0
commit 27514f0262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 985 additions and 6 deletions

View File

@ -91,6 +91,12 @@ 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.

View File

@ -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

View File

@ -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();
}

View File

@ -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<RateLimitResponse> 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<DescriptorStatus> statusList = new ArrayList<>(request.getDescriptorsCount());
for (RateLimitDescriptor descriptor : request.getDescriptorsList()) {
Tuple2<FlowRule, TokenResult> 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<FlowRule, TokenResult> 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();
}
}

View File

@ -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 <config_access_log_format>` as used for
// :ref:`HTTP access logging <config_access_log>` 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}
];
}

View File

@ -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
// <config_http_filters_rate_limit_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;
}

View File

@ -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
// <envoy_v3_api_field_service.ratelimit.v3.RateLimitResponse.DescriptorStatus.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 <envoy_v3_api_field_service.ratelimit.v3.RateLimitResponse.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 <envoy_v3_api_field_service.ratelimit.v3.RateLimitResponse.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 <config_http_filters_ratelimit_dynamic_metadata>` for HTTP filter.
// - :ref:`envoy.filters.network.ratelimit <config_network_filters_ratelimit_dynamic_metadata>` for network filter.
// - :ref:`envoy.filters.thrift.rate_limit <config_thrift_filters_rate_limit_dynamic_metadata>` for Thrift filter.
google.protobuf.Struct dynamic_metadata = 6;
}

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
load("//bazel:api_build_system.bzl", "udpa_proto_package")
licenses(["notice"]) # Apache 2
udpa_proto_package()

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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<RateLimitResponse> 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<RateLimitResponse> 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<RateLimitResponse> 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<RateLimitResponse> 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)));
}
}