This commit is contained in:
shuhongfan
2023-09-04 16:40:17 +08:00
commit cf5ac25c14
8267 changed files with 1305066 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
FROM java:8-jre
ENV SENTINEL_HOME /sentinel
COPY target/sentinel-envoy-rls-token-server.jar $SENTINEL_HOME/
WORKDIR $SENTINEL_HOME
ENTRYPOINT ["sh", "-c"]
CMD ["java -Dcsp.sentinel.log.dir=/sentinel/logs/ ${JAVA_OPTS} -jar sentinel-envoy-rls-token-server.jar"]

View File

@@ -0,0 +1,59 @@
# Sentinel Token Server (Envoy RLS implementation)
This module provides the [Envoy rate limiting gRPC service](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/global_rate_limiting#arch-overview-rate-limit) implementation
with Sentinel token server.
> Note: the gRPC stub classes for Envoy RLS service is generated via `protobuf-maven-plugin` during the `compile` goal.
> The generated classes is located in the directory: `target/generated-sources/protobuf`.
## Build
Build the executable jar:
```bash
mvn clean package -P prod
```
## Rule configuration
Sentinel RLS token server supports dynamic rule configuration via the yaml file.
The file may provide rules for one *domain* (defined in Envoy's conf file).
In Envoy, one rate limit request might carry multiple *rate limit descriptors*
(which will be generated from [Envoy rate limit actions](https://www.envoyproxy.io/docs/envoy/v1.12.1/api-v2/api/v2/route/route.proto#envoy-api-msg-route-ratelimit)).
One rate limit descriptor may have multiple entries (key-value pair).
We may set different threshold for each rate limit descriptors.
A sample rule configuration file:
```yaml
domain: foo
descriptors:
- resources:
- key: "destination_cluster"
value: "service_httpbin"
count: 1
```
This rule only takes effect for domain `foo`. It will limit the max QPS to 1 for
all requests targeted to the `service_httpbin` cluster.
We need to provide the path to yaml file via the `SENTINEL_RLS_RULE_FILE_PATH` env
(or `-Dcsp.sentinel.rls.rule.file` opts). Then as soon as the content in the rule file has been changed,
Sentinel will reload the new rules from the file to the `EnvoyRlsRuleManager`.
We may check the logs in `~/logs/csp/sentinel-record.log.xxx` to see whether the rules has been loaded.
We may also retrieve the converted `FlowRule` via the command API `localhost:8719/cluster/server/flowRules`.
## Configuration items
The configuration list:
| Item (env) | Item (JVM property) | Description | Default Value | Required |
|--------|--------|--------|--------|--------|
| `SENTINEL_RLS_GRPC_PORT` | `csp.sentinel.grpc.server.port` | The RLS gRPC server port | **10240** | false |
| `SENTINEL_RLS_RULE_FILE_PATH` | `csp.sentinel.rls.rule.file` | The path of the RLS rule yaml file | - | **true** |
| `SENTINEL_RLS_ACCESS_LOG` | - | Whether to enable the access log (`on` for enable) | off | false |
## Samples
- [Kubernetes sample](./sample/k8s)

View File

@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sentinel-cluster</artifactId>
<groupId>com.alibaba.csp</groupId>
<version>1.8.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sentinel-cluster-server-envoy-rls</artifactId>
<properties>
<java.source.version>1.8</java.source.version>
<java.target.version>1.8</java.target.version>
<protobuf.version>3.10.0</protobuf.version>
<grpc.version>1.30.2</grpc.version>
<maven.shade.version>3.2.1</maven.shade.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-server-default</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax.annotation-api.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>${maven.pmd.version}</version>
<configuration>
<excludeRoots>
<excludeRoot>target/generated-sources</excludeRoot>
</excludeRoots>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>prod</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven.shade.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>sentinel-envoy-rls-token-server</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>
com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsServer
</mainClass>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,111 @@
# Sentinel Envoy RLS - Kubernetes sample
This sample will illustrate how to use Sentinel RLS token server with Envoy in Kubernetes clusters.
## Build the Docker image
We could use the pre-built Docker image: `registry.cn-hangzhou.aliyuncs.com/sentinel-docker-repo/sentinel-envoy-rls-server:latest`
We can also manually build the Docker image in the `sentinel-cluster-server-envoy-rls` directory:
```bash
docker build -t "sentinel/sentinel-envoy-rls-server:latest" -f ./Dockerfile .
```
## Deploy Sentinel RLS token server
Next we could deploy the Sentinel RLS token server in the K8S cluster.
We've provided a deployment template for Sentinel RLS token server in `sentinel-rls.yml`.
It includes:
- A `ConfigMap` that contains the cluster flow control rule for Envoy global rate limiting.
This will be mounted as a file in the target `Deployment`, so that the Sentinel RLS token server
could load the rules dynamically as soon as the rule in the `ConfigMap` has been updated.
- A `Deployment` for Sentinel RLS token server. It will mount the `ConfigMap` as a volume
for dynamic rule configuration.
- A `Service` that exports the Sentinel command port (8719) and the RLS gRPC port (by default 10245)
on a cluster-internal IP so that the Envoy pods could communicate with the RLS server.
The sample rate limiting rule in the `sentinel-rule-cm`:
```yaml
domain: foo
descriptors:
# For requests to the "service_httpbin" cluster, limit the max QPS to 1
- resources:
- key: "destination_cluster"
value: "service_httpbin"
count: 1
```
You may enable the access log in the Sentinel RLS token server (output to console)
via the `SENTINEL_RLS_ACCESS_LOG` env:
```yaml
env:
- name: SENTINEL_RLS_ACCESS_LOG
value: "on"
```
You may also append JVM opts via the `JAVA_OPTS` env.
After preparing the yaml template, you may deploy the Sentinel RLS token server:
```bash
kubectl apply -f sample/k8s/sentinel-rls.yml
```
## Deploy Envoy
Next we could deploy the Envoy instances in the K8S cluster. If you've already had Envoy instances running,
you could configure the address (`sentinel-rls-service`) and the port (`10245`)
of the rate limit cluster in your Envoy configuration.
We've provided a deployment template for Envoy in `envoy.yml`.
It includes:
- A `ConfigMap` that contains the configuration for Envoy.
This will be mounted as a file in the target `Deployment`, which will be loaded as the configuration
file by Envoy.
- A `Deployment` for Envoy. It will mount the `ConfigMap` as a volume
for configuration.
- A `Service` that exports the Envoy endpoint port (by default 10000) on a cluster-internal IP
so that it could be accessible from other pods. If you need external access, you could choose the
`LoadBalancer` type or add a frontend ingress.
In the sample, we have two [Envoy clusters](https://www.envoyproxy.io/docs/envoy/latest/api-v2/clusters/clusters):
- `service_httpbin`: HTTP proxy to `httpbin.org`
- `rate_limit_cluster`: the cluster of the Sentinel RLS token server
This route configuration tells Envoy to route incoming requests to `httpbin.org`. In the `http_filters` conf item,
we added the `envoy.rate_limit` filter to the filter chain so that the global rate limiting is enabled.
We set the rate limit domain as `foo`, which matches the item in the Envoy RLS rule.
In the `route_config`, we provide the rate limit action: `{destination_cluster: {}}`, which will
generate the rate limit descriptor containing the actual target cluster name (e.g. `service_httpbin`).
Then we could set rate limit rules for each target clusters.
After preparing the yaml template, you may deploy the Envoy instance:
```bash
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.
Since we set the max QPS to 1, we could emit concurrent requests to the URL, and we
could see the first request passes, while the latter requests are blocked (status 429):
![image](https://user-images.githubusercontent.com/9434884/68571798-d0a46500-049e-11ea-8488-5e90f56f23a5.png)
## Update the rules dynamically
You could update the rules in the `sentinel-rule-cm` ConfigMap. Once the content is updated,
Sentinel will perceive the changes and load the new rules to `EnvoyRlsRuleManager`.

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

@@ -0,0 +1,132 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-cm
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.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
host_rewrite: httpbin.org
cluster: service_httpbin
rate_limits:
- stage: 0
actions:
- {destination_cluster: {}}
http_filters:
- name: envoy.rate_limit
config:
domain: foo
stage: 0
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: rate_limit_cluster
timeout: 0.25s
- name: envoy.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: LOGICAL_DNS
connect_timeout: 0.25s
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/v1beta2
kind: Deployment
metadata:
name: envoy-deployment-basic
labels:
app: envoy
spec:
replicas: 1
selector:
matchLabels:
app: envoy
template:
metadata:
labels:
app: envoy
spec:
containers:
- name: envoy
image: envoyproxy/envoy:v1.12.0
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
items:
- key: envoy-yml
path: envoy.yaml
---
apiVersion: v1
kind: Service
metadata:
name: envoy-service
labels:
name: envoy-service
spec:
type: ClusterIP
ports:
- port: 10000
targetPort: 10000
protocol: TCP
selector:
app: envoy

View File

@@ -0,0 +1,68 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: sentinel-rule-cm
data:
rule-yaml: |-
domain: foo
descriptors:
- resources:
- key: "destination_cluster"
value: "service_httpbin"
count: 1
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: sentinel-rls-server
labels:
app: sentinel
spec:
replicas: 1
selector:
matchLabels:
app: sentinel
template:
metadata:
labels:
app: sentinel
spec:
containers:
- name: sentinelserver
# You could replace the image with your own image here
image: "registry.cn-hangzhou.aliyuncs.com/sentinel-docker-repo/sentinel-envoy-rls-server:latest"
imagePullPolicy: Always
ports:
- containerPort: 10245
- containerPort: 8719
volumeMounts:
- name: sentinel-rule-config
mountPath: /tmp/sentinel
env:
- name: SENTINEL_RLS_RULE_FILE_PATH
value: "/tmp/sentinel/rule.yaml"
volumes:
- name: sentinel-rule-config
configMap:
name: sentinel-rule-cm
items:
- key: rule-yaml
path: rule.yaml
---
apiVersion: v1
kind: Service
metadata:
name: sentinel-rls-service
labels:
name: sentinel-rls-service
spec:
type: ClusterIP
ports:
- port: 8719
targetPort: 8719
name: sentinel-command
- port: 10245
targetPort: 10245
name: sentinel-grpc
selector:
app: sentinel

View File

@@ -0,0 +1,34 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls;
/**
* @author Eric Zhao
*/
public final class SentinelEnvoyRlsConstants {
public static final int DEFAULT_GRPC_PORT = 10245;
public static final String SERVER_APP_NAME = "sentinel-rls-token-server";
public static final String GRPC_PORT_ENV_KEY = "SENTINEL_RLS_GRPC_PORT";
public static final String GRPC_PORT_PROPERTY_KEY = "csp.sentinel.grpc.server.port";
public static final String RULE_FILE_PATH_ENV_KEY = "SENTINEL_RLS_RULE_FILE_PATH";
public static final String RULE_FILE_PATH_PROPERTY_KEY = "csp.sentinel.rls.rule.file";
public static final String ENABLE_ACCESS_LOG_ENV_KEY = "SENTINEL_RLS_ACCESS_LOG";
private SentinelEnvoyRlsConstants() {}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls;
import java.util.Optional;
import com.alibaba.csp.sentinel.cluster.server.envoy.rls.datasource.EnvoyRlsRuleDataSourceService;
import com.alibaba.csp.sentinel.config.SentinelConfig;
import com.alibaba.csp.sentinel.init.InitExecutor;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.util.StringUtil;
/**
* @author Eric Zhao
*/
public class SentinelEnvoyRlsServer {
public static void main(String[] args) throws Exception {
System.setProperty("project.name", SentinelEnvoyRlsConstants.SERVER_APP_NAME);
EnvoyRlsRuleDataSourceService dataSourceService = new EnvoyRlsRuleDataSourceService();
dataSourceService.init();
int port = resolvePort();
SentinelRlsGrpcServer server = new SentinelRlsGrpcServer(port);
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("[SentinelEnvoyRlsServer] Shutting down gRPC RLS server since JVM is shutting down");
server.shutdown();
dataSourceService.onShutdown();
System.err.println("[SentinelEnvoyRlsServer] Server has been shut down");
}));
InitExecutor.doInit();
server.blockUntilShutdown();
}
private static int resolvePort() {
final int defaultPort = SentinelEnvoyRlsConstants.DEFAULT_GRPC_PORT;
// Order: system env > property
String portStr = Optional.ofNullable(System.getenv(SentinelEnvoyRlsConstants.GRPC_PORT_ENV_KEY))
.orElse(SentinelConfig.getConfig(SentinelEnvoyRlsConstants.GRPC_PORT_PROPERTY_KEY));
if (StringUtil.isBlank(portStr)) {
return defaultPort;
}
try {
int port = Integer.parseInt(portStr);
if (port <= 0 || port > 65535) {
RecordLog.warn("[SentinelEnvoyRlsServer] Invalid port <" + portStr + ">, using default" + defaultPort);
return defaultPort;
}
return port;
} catch (Exception ex) {
RecordLog.warn("[SentinelEnvoyRlsServer] Failed to resolve port, using default " + defaultPort);
System.err.println("[SentinelEnvoyRlsServer] Failed to resolve port, using default " + defaultPort);
return defaultPort;
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls;
import java.util.ArrayList;
import java.util.List;
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.api.v2.ratelimit.RateLimitDescriptor;
import io.envoyproxy.envoy.api.v2.ratelimit.RateLimitDescriptor.Entry;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitRequest;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.Code;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.DescriptorStatus;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.RateLimit;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.RateLimit.Unit;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitServiceGrpc;
import io.grpc.stub.StreamObserver;
import static com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter.SEPARATOR;
/**
* @author Eric Zhao
* @since 1.7.0
*/
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(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 (Entry resource : descriptor.getEntriesList()) {
sb.append(SEPARATOR).append(resource.getKey()).append(SEPARATOR).append(resource.getValue());
}
return sb.toString();
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls;
import java.io.IOException;
import com.alibaba.csp.sentinel.log.RecordLog;
import io.grpc.Server;
import io.grpc.ServerBuilder;
/**
* @author Eric Zhao
*/
public class SentinelRlsGrpcServer {
private final Server server;
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();
}
public void start() throws IOException {
// The gRPC server has already checked the start status, so we don't check here.
server.start();
String message = "[SentinelRlsGrpcServer] RLS server is running at port " + server.getPort();
RecordLog.info(message);
System.out.println(message);
}
public void shutdown() {
server.shutdownNow();
}
public boolean isShutdown() {
return server.isShutdown();
}
public void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls.datasource;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsConstants;
import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule;
import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRuleManager;
import com.alibaba.csp.sentinel.config.SentinelConfig;
import com.alibaba.csp.sentinel.datasource.FileRefreshableDataSource;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.util.StringUtil;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.representer.Representer;
/**
* @author Eric Zhao
* @since 1.7.0
*/
public class EnvoyRlsRuleDataSourceService {
private final Yaml yaml;
private ReadableDataSource<String, List<EnvoyRlsRule>> ds;
public EnvoyRlsRuleDataSourceService() {
this.yaml = createYamlParser();
}
private Yaml createYamlParser() {
Representer representer = new Representer();
representer.getPropertyUtils().setSkipMissingProperties(true);
return new Yaml(representer);
}
public synchronized void init() throws Exception {
if (ds != null) {
return;
}
String configPath = getRuleConfigPath();
if (StringUtil.isBlank(configPath)) {
throw new IllegalStateException("Empty rule config path, please set the file path in the env: "
+ SentinelEnvoyRlsConstants.RULE_FILE_PATH_ENV_KEY);
}
this.ds = new FileRefreshableDataSource<>(configPath, s -> Arrays.asList(yaml.loadAs(s, EnvoyRlsRule.class)));
EnvoyRlsRuleManager.register2Property(ds.getProperty());
}
public synchronized void onShutdown() {
if (ds != null) {
try {
ds.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private String getRuleConfigPath() {
return Optional.ofNullable(System.getenv(SentinelEnvoyRlsConstants.RULE_FILE_PATH_ENV_KEY))
.orElse(SentinelConfig.getConfig(SentinelEnvoyRlsConstants.RULE_FILE_PATH_PROPERTY_KEY));
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls.flow;
import com.alibaba.csp.sentinel.cluster.TokenResult;
import com.alibaba.csp.sentinel.cluster.TokenResultStatus;
import com.alibaba.csp.sentinel.cluster.flow.statistic.ClusterMetricStatistics;
import com.alibaba.csp.sentinel.cluster.flow.statistic.data.ClusterFlowEvent;
import com.alibaba.csp.sentinel.cluster.flow.statistic.metric.ClusterMetric;
import com.alibaba.csp.sentinel.cluster.server.config.ClusterServerConfigManager;
import com.alibaba.csp.sentinel.cluster.server.log.ClusterServerStatLogUtil;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
/**
* @author Eric Zhao
* @since 1.7.0
*/
public final class SimpleClusterFlowChecker {
public static TokenResult acquireClusterToken(/*@Valid*/ FlowRule rule, int acquireCount) {
Long id = rule.getClusterConfig().getFlowId();
ClusterMetric metric = ClusterMetricStatistics.getMetric(id);
if (metric == null) {
return new TokenResult(TokenResultStatus.FAIL);
}
double latestQps = metric.getAvg(ClusterFlowEvent.PASS);
double globalThreshold = rule.getCount() * ClusterServerConfigManager.getExceedCount();
double nextRemaining = globalThreshold - latestQps - acquireCount;
if (nextRemaining >= 0) {
metric.add(ClusterFlowEvent.PASS, acquireCount);
metric.add(ClusterFlowEvent.PASS_REQUEST, 1);
ClusterServerStatLogUtil.log("flow|pass|" + id, acquireCount);
ClusterServerStatLogUtil.log("flow|pass_request|" + id, 1);
// Remaining count is cut down to a smaller integer.
return new TokenResult(TokenResultStatus.OK)
.setRemaining((int) nextRemaining)
.setWaitInMs(0);
} else {
// Blocked.
metric.add(ClusterFlowEvent.BLOCK, acquireCount);
metric.add(ClusterFlowEvent.BLOCK_REQUEST, 1);
ClusterServerStatLogUtil.log("flow|block|" + id, acquireCount);
ClusterServerStatLogUtil.log("flow|block_request|" + id, 1);
return blockedResult();
}
}
private static TokenResult blockedResult() {
return new TokenResult(TokenResultStatus.BLOCKED)
.setRemaining(0)
.setWaitInMs(0);
}
private SimpleClusterFlowChecker() {}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls.log;
import com.alibaba.csp.sentinel.cluster.server.envoy.rls.SentinelEnvoyRlsConstants;
import com.alibaba.csp.sentinel.util.StringUtil;
/**
* @author Eric Zhao
*/
public final class RlsAccessLogger {
private static boolean enabled = false;
static {
try {
enabled = "on".equalsIgnoreCase(System.getenv(SentinelEnvoyRlsConstants.ENABLE_ACCESS_LOG_ENV_KEY));
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static boolean isEnabled() {
return enabled;
}
public static void log(String info) {
if (enabled && StringUtil.isNotEmpty(info)) {
System.out.println(info);
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.alibaba.csp.sentinel.util.AssertUtil;
/**
* @author Eric Zhao
* @since 1.7.0
*/
public class EnvoyRlsRule {
private String domain;
private List<ResourceDescriptor> descriptors;
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public List<ResourceDescriptor> getDescriptors() {
return descriptors;
}
public void setDescriptors(List<ResourceDescriptor> descriptors) {
this.descriptors = descriptors;
}
@Override
public String toString() {
return "EnvoyRlsRule{" +
"domain='" + domain + '\'' +
", descriptors=" + descriptors +
'}';
}
public static class ResourceDescriptor {
private Set<KeyValueResource> resources;
private Double count;
public ResourceDescriptor() {}
public ResourceDescriptor(Set<KeyValueResource> resources, Double count) {
this.resources = resources;
this.count = count;
}
public Set<KeyValueResource> getResources() {
return resources;
}
public void setResources(Set<KeyValueResource> resources) {
this.resources = resources;
}
public Double getCount() {
return count;
}
public void setCount(Double count) {
this.count = count;
}
@Override
public String toString() {
return "ResourceDescriptor{" +
"resources=" + resources +
", count=" + count +
'}';
}
}
public static class KeyValueResource {
private String key;
private String value;
public KeyValueResource() {}
public KeyValueResource(String key, String value) {
AssertUtil.assertNotBlank(key, "key cannot be blank");
AssertUtil.assertNotBlank(value, "value cannot be blank");
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
KeyValueResource that = (KeyValueResource)o;
return Objects.equals(key, that.key) &&
Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(key, value);
}
@Override
public String toString() {
return "KeyValueResource{" +
"key='" + key + '\'' +
", value='" + value + '\'' +
'}';
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
import com.alibaba.csp.sentinel.cluster.flow.rule.ClusterFlowRuleManager;
import com.alibaba.csp.sentinel.cluster.server.ServerConstants;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.property.DynamicSentinelProperty;
import com.alibaba.csp.sentinel.property.PropertyListener;
import com.alibaba.csp.sentinel.property.SentinelProperty;
import com.alibaba.csp.sentinel.property.SimplePropertyListener;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
/**
* @author Eric Zhao
* @since 1.7.0
*/
public final class EnvoyRlsRuleManager {
private static final ConcurrentMap<String, EnvoyRlsRule> RULE_MAP = new ConcurrentHashMap<>();
private static final PropertyListener<List<EnvoyRlsRule>> PROPERTY_LISTENER = new EnvoyRlsRulePropertyListener();
private static SentinelProperty<List<EnvoyRlsRule>> currentProperty = new DynamicSentinelProperty<>();
static {
currentProperty.addListener(PROPERTY_LISTENER);
}
/**
* Listen to the {@link SentinelProperty} for Envoy RLS rules. The property is the source of {@link EnvoyRlsRule}.
*
* @param property the property to listen
*/
public static void register2Property(SentinelProperty<List<EnvoyRlsRule>> property) {
AssertUtil.notNull(property, "property cannot be null");
synchronized (PROPERTY_LISTENER) {
RecordLog.info("[EnvoyRlsRuleManager] Registering new property to Envoy rate limit service rule manager");
currentProperty.removeListener(PROPERTY_LISTENER);
property.addListener(PROPERTY_LISTENER);
currentProperty = property;
}
}
/**
* Load Envoy RLS rules, while former rules will be replaced.
*
* @param rules new rules to load
* @return true if there are actual changes, otherwise false
*/
public static boolean loadRules(List<EnvoyRlsRule> rules) {
return currentProperty.updateValue(rules);
}
public static List<EnvoyRlsRule> getRules() {
return new ArrayList<>(RULE_MAP.values());
}
static final class EnvoyRlsRulePropertyListener extends SimplePropertyListener<List<EnvoyRlsRule>> {
@Override
public synchronized void configUpdate(List<EnvoyRlsRule> conf) {
Map<String, EnvoyRlsRule> ruleMap = generateRuleMap(conf);
List<FlowRule> flowRules = ruleMap.values().stream()
.flatMap(e -> EnvoySentinelRuleConverter.toSentinelFlowRules(e).stream())
.collect(Collectors.toList());
RULE_MAP.clear();
RULE_MAP.putAll(ruleMap);
RecordLog.info("[EnvoyRlsRuleManager] Envoy RLS rules loaded: {}", flowRules);
// Use the "default" namespace.
ClusterFlowRuleManager.loadRules(ServerConstants.DEFAULT_NAMESPACE, flowRules);
}
Map<String, EnvoyRlsRule> generateRuleMap(List<EnvoyRlsRule> conf) {
if (conf == null || conf.isEmpty()) {
return new HashMap<>(2);
}
Map<String, EnvoyRlsRule> map = new HashMap<>(conf.size());
for (EnvoyRlsRule rule : conf) {
if (!isValidRule(rule)) {
RecordLog.warn("[EnvoyRlsRuleManager] Ignoring invalid rule when loading new RLS rules: " + rule);
continue;
}
if (map.containsKey(rule.getDomain())) {
RecordLog.warn("[EnvoyRlsRuleManager] Ignoring duplicate RLS rule for specific domain: " + rule);
continue;
}
map.put(rule.getDomain(), rule);
}
return map;
}
}
/**
* Check whether the given Envoy RLS rule is valid.
*
* @param rule the rule to check
* @return true if the rule is valid, otherwise false
*/
public static boolean isValidRule(EnvoyRlsRule rule) {
if (rule == null || StringUtil.isBlank(rule.getDomain())) {
return false;
}
List<EnvoyRlsRule.ResourceDescriptor> descriptors = rule.getDescriptors();
if (descriptors == null || descriptors.isEmpty()) {
return false;
}
for (EnvoyRlsRule.ResourceDescriptor descriptor : descriptors) {
if (descriptor == null || descriptor.getCount() == null || descriptor.getCount() < 0) {
return false;
}
Set<EnvoyRlsRule.KeyValueResource> resources = descriptor.getResources();
if (resources == null || resources.isEmpty()) {
return false;
}
for (EnvoyRlsRule.KeyValueResource resource : resources) {
if (resource == null ||
StringUtil.isBlank(resource.getKey()) || StringUtil.isBlank(resource.getValue())) {
return false;
}
}
}
return true;
}
private EnvoyRlsRuleManager() {}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule;
import java.util.List;
import java.util.stream.Collectors;
import com.alibaba.csp.sentinel.slots.block.ClusterRuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.ClusterFlowConfig;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
/**
* @author Eric Zhao
* @since 1.7.0
*/
public final class EnvoySentinelRuleConverter {
/**
* Currently we use "|" to separate each key/value entries.
*/
public static final String SEPARATOR = "|";
/**
* Convert the {@link EnvoyRlsRule} to a list of Sentinel flow rules.
*
* @param rule a valid Envoy RLS rule
* @return converted rules
*/
public static List<FlowRule> toSentinelFlowRules(EnvoyRlsRule rule) {
if (!EnvoyRlsRuleManager.isValidRule(rule)) {
throw new IllegalArgumentException("Not a valid RLS rule");
}
return rule.getDescriptors().stream()
.map(e -> toSentinelFlowRule(rule.getDomain(), e))
.collect(Collectors.toList());
}
public static FlowRule toSentinelFlowRule(String domain, EnvoyRlsRule.ResourceDescriptor descriptor) {
// One descriptor could have only one rule.
String identifier = generateKey(domain, descriptor);
long flowId = generateFlowId(identifier);
return new FlowRule(identifier)
.setCount(descriptor.getCount())
.setClusterMode(true)
.setClusterConfig(new ClusterFlowConfig()
.setFlowId(flowId)
.setThresholdType(ClusterRuleConstant.FLOW_THRESHOLD_GLOBAL)
.setSampleCount(1)
.setFallbackToLocalWhenFail(false));
}
public static long generateFlowId(String key) {
if (StringUtil.isBlank(key)) {
return -1L;
}
// Add offset to avoid negative ID.
return (long) Integer.MAX_VALUE + key.hashCode();
}
public static String generateKey(String domain, EnvoyRlsRule.ResourceDescriptor descriptor) {
AssertUtil.assertNotBlank(domain, "domain cannot be blank");
AssertUtil.notNull(descriptor, "EnvoyRlsRule.ResourceDescriptor cannot be null");
AssertUtil.assertNotEmpty(descriptor.getResources(), "resources in descriptor cannot be null");
StringBuilder sb = new StringBuilder(domain);
for (EnvoyRlsRule.KeyValueResource resource : descriptor.getResources()) {
sb.append(SEPARATOR).append(resource.getKey()).append(SEPARATOR).append(resource.getValue());
}
return sb.toString();
}
private EnvoySentinelRuleConverter() {}
}

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,26 @@
syntax = "proto3";
package envoy.api.v2.core;
option java_outer_classname = "BaseProto";
option java_multiple_files = true;
option java_package = "io.envoyproxy.envoy.api.v2.core";
import "google/protobuf/any.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/wrappers.proto";
import "validate/validate.proto";
// Header name/value pair.
message HeaderValue {
// Header name.
string key = 1 [(validate.rules).string = {min_bytes: 1 max_bytes: 16384}];
// 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}];
}

View File

@@ -0,0 +1,65 @@
syntax = "proto3";
package envoy.api.v2.ratelimit;
option java_outer_classname = "RatelimitProto";
option java_multiple_files = true;
option java_package = "io.envoyproxy.envoy.api.v2.ratelimit";
import "validate/validate.proto";
// [#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.
message RateLimitDescriptor {
message Entry {
// Descriptor key.
string key = 1 [(validate.rules).string = {min_bytes: 1}];
// Descriptor value.
string value = 2 [(validate.rules).string = {min_bytes: 1}];
}
// Descriptor entries.
repeated Entry entries = 1 [(validate.rules).repeated = {min_items: 1}];
}

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,109 @@
syntax = "proto3";
package envoy.service.ratelimit.v2;
option java_outer_classname = "RlsProto";
option java_multiple_files = true;
option java_package = "io.envoyproxy.envoy.service.ratelimit.v2";
option java_generic_services = true;
import "envoy/api/v2/core/base.proto";
import "envoy/api/v2/ratelimit/ratelimit.proto";
import "validate/validate.proto";
// [#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 {
// 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 api.v2.ratelimit.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.
message 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 {
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;
}
// The number of requests per unit of time.
uint32 requests_per_unit = 1;
// The unit of time.
Unit unit = 2;
}
message 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;
}
// 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;
// [#next-major-version: rename to response_headers_to_add]
repeated api.v2.core.HeaderValue headers = 3;
// A list of headers to add to the request when forwarded
repeated api.v2.core.HeaderValue request_headers_to_add = 4;
}

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

@@ -0,0 +1,863 @@
syntax = "proto2";
package 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";
import "google/protobuf/timestamp.proto";
// Validation rules applied at the message level
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 = 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 = 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 = 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;
DoubleRules double = 2;
Int32Rules int32 = 3;
Int64Rules int64 = 4;
UInt32Rules uint32 = 5;
UInt64Rules uint64 = 6;
SInt32Rules sint32 = 7;
SInt64Rules sint64 = 8;
Fixed32Rules fixed32 = 9;
Fixed64Rules fixed64 = 10;
SFixed32Rules sfixed32 = 11;
SFixed64Rules sfixed64 = 12;
BoolRules bool = 13;
StringRules string = 14;
BytesRules bytes = 15;
// Complex Field Types
EnumRules enum = 16;
RepeatedRules repeated = 18;
MapRules map = 19;
// Well-Known Field Types
AnyRules any = 20;
DurationRules duration = 21;
TimestampRules timestamp = 22;
}
}
// FloatRules describes the constraints applied to `float` values
message FloatRules {
// Const specifies that this field must be exactly the specified value
optional float const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional float lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional float lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional float gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional float gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated float in = 6;
// 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
message DoubleRules {
// Const specifies that this field must be exactly the specified value
optional double const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional double lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional double lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional double gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional double gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated double in = 6;
// 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
message Int32Rules {
// Const specifies that this field must be exactly the specified value
optional int32 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional int32 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional int32 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional int32 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional int32 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated int32 in = 6;
// 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
message Int64Rules {
// Const specifies that this field must be exactly the specified value
optional int64 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional int64 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional int64 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional int64 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional int64 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated int64 in = 6;
// 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
message UInt32Rules {
// Const specifies that this field must be exactly the specified value
optional uint32 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional uint32 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional uint32 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional uint32 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional uint32 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated uint32 in = 6;
// 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
message UInt64Rules {
// Const specifies that this field must be exactly the specified value
optional uint64 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional uint64 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional uint64 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional uint64 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional uint64 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated uint64 in = 6;
// 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
message SInt32Rules {
// Const specifies that this field must be exactly the specified value
optional sint32 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional sint32 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional sint32 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional sint32 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional sint32 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated sint32 in = 6;
// 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
message SInt64Rules {
// Const specifies that this field must be exactly the specified value
optional sint64 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional sint64 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional sint64 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional sint64 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional sint64 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated sint64 in = 6;
// 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
message Fixed32Rules {
// Const specifies that this field must be exactly the specified value
optional fixed32 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional fixed32 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional fixed32 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional fixed32 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional fixed32 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated fixed32 in = 6;
// 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
message Fixed64Rules {
// Const specifies that this field must be exactly the specified value
optional fixed64 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional fixed64 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional fixed64 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional fixed64 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional fixed64 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated fixed64 in = 6;
// 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
message SFixed32Rules {
// Const specifies that this field must be exactly the specified value
optional sfixed32 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional sfixed32 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional sfixed32 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional sfixed32 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional sfixed32 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated sfixed32 in = 6;
// 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
message SFixed64Rules {
// Const specifies that this field must be exactly the specified value
optional sfixed64 const = 1;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional sfixed64 lt = 2;
// Lte specifies that this field must be less than or equal to the
// specified value, inclusive
optional sfixed64 lte = 3;
// Gt specifies that this field must be greater than the specified value,
// exclusive. If the value of Gt is larger than a specified Lt or Lte, the
// range is reversed.
optional sfixed64 gt = 4;
// Gte specifies that this field must be greater than or equal to the
// specified value, inclusive. If the value of Gte is larger than a
// specified Lt or Lte, the range is reversed.
optional sfixed64 gte = 5;
// In specifies that this field must be equal to one of the specified
// values
repeated sfixed64 in = 6;
// 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
message BoolRules {
// Const specifies that this field must be exactly the specified value
optional bool const = 1;
}
// StringRules describe the constraints applied to `string` values
message StringRules {
// Const specifies that this field must be exactly the specified value
optional string const = 1;
// Len specifies that this field must be the specified number of
// characters (Unicode code points). Note that the number of
// characters may differ from the number of bytes in the string.
optional uint64 len = 19;
// MinLen specifies that this field must be the specified number of
// characters (Unicode code points) at a minimum. Note that the number of
// characters may differ from the number of bytes in the string.
optional uint64 min_len = 2;
// MaxLen specifies that this field must be the specified number of
// characters (Unicode code points) at a maximum. Note that the number of
// characters may differ from the number of bytes in the string.
optional uint64 max_len = 3;
// LenBytes specifies that this field must be the specified number of bytes
// at a minimum
optional uint64 len_bytes = 20;
// MinBytes specifies that this field must be the specified number of bytes
// at a minimum
optional uint64 min_bytes = 4;
// MaxBytes specifies that this field must be the specified number of bytes
// at a maximum
optional uint64 max_bytes = 5;
// Pattern specifes that this field must match against the specified
// regular expression (RE2 syntax). The included expression should elide
// any delimiters.
optional string pattern = 6;
// Prefix specifies that this field must have the specified substring at
// the beginning of the string.
optional string prefix = 7;
// Suffix specifies that this field must have the specified substring at
// the end of the string.
optional string suffix = 8;
// Contains specifies that this field must have the specified substring
// 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;
// NotIn specifies that this field cannot be equal to one of the specified
// values
repeated string not_in = 11;
// WellKnown rules provide advanced constraints against common string
// patterns
oneof well_known {
// Email specifies that the field must be a valid email address as
// defined by RFC 5322
bool email = 12;
// Hostname specifies that the field must be a valid hostname as
// defined by RFC 1034. This constraint does not support
// internationalized domain names (IDNs).
bool hostname = 13;
// Ip specifies that the field must be a valid IP (v4 or v6) address.
// Valid IPv6 addresses should not include surrounding square brackets.
bool ip = 14;
// Ipv4 specifies that the field must be a valid IPv4 address.
bool ipv4 = 15;
// Ipv6 specifies that the field must be a valid IPv6 address. Valid
// IPv6 addresses should not include surrounding square brackets.
bool ipv6 = 16;
// Uri specifies that the field must be a valid, absolute URI as defined
// by RFC 3986
bool uri = 17;
// 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
message BytesRules {
// Const specifies that this field must be exactly the specified value
optional bytes const = 1;
// Len specifies that this field must be the specified number of bytes
optional uint64 len = 13;
// MinLen specifies that this field must be the specified number of bytes
// at a minimum
optional uint64 min_len = 2;
// MaxLen specifies that this field must be the specified number of bytes
// at a maximum
optional uint64 max_len = 3;
// Pattern specifes that this field must match against the specified
// regular expression (RE2 syntax). The included expression should elide
// any delimiters.
optional string pattern = 4;
// Prefix specifies that this field must have the specified bytes at the
// beginning of the string.
optional bytes prefix = 5;
// Suffix specifies that this field must have the specified bytes at the
// end of the string.
optional bytes suffix = 6;
// Contains specifies that this field must have the specified bytes
// anywhere in the string.
optional bytes contains = 7;
// In specifies that this field must be equal to one of the specified
// values
repeated bytes in = 8;
// NotIn specifies that this field cannot be equal to one of the specified
// values
repeated bytes not_in = 9;
// WellKnown rules provide advanced constraints against common byte
// patterns
oneof well_known {
// Ip specifies that the field must be a valid IP (v4 or v6) address in
// byte format
bool ip = 10;
// Ipv4 specifies that the field must be a valid IPv4 address in byte
// format
bool ipv4 = 11;
// Ipv6 specifies that the field must be a valid IPv6 address in byte
// 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
message EnumRules {
// Const specifies that this field must be exactly the specified value
optional int32 const = 1;
// DefinedOnly specifies that this field must be only one of the defined
// values for this enum, failing on any undefined value.
optional bool defined_only = 2;
// In specifies that this field must be equal to one of the specified
// values
repeated int32 in = 3;
// NotIn specifies that this field cannot be equal to one of the specified
// values
repeated int32 not_in = 4;
}
// MessageRules describe the constraints applied to embedded message values.
// For message-type fields, validation is performed recursively.
message MessageRules {
// Skip specifies that the validation rules of this field should not be
// evaluated
optional bool skip = 1;
// Required specifies that this field must be set
optional bool required = 2;
}
// RepeatedRules describe the constraints applied to `repeated` values
message RepeatedRules {
// MinItems specifies that this field must have the specified number of
// items at a minimum
optional uint64 min_items = 1;
// MaxItems specifies that this field must have the specified number of
// items at a maximum
optional uint64 max_items = 2;
// Unique specifies that all elements in this field must be unique. This
// contraint is only applicable to scalar and enum types (messages are not
// supported).
optional bool unique = 3;
// Items specifies the contraints to be applied to each item in the field.
// 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
message MapRules {
// MinPairs specifies that this field must have the specified number of
// KVs at a minimum
optional uint64 min_pairs = 1;
// MaxPairs specifies that this field must have the specified number of
// KVs at a maximum
optional uint64 max_pairs = 2;
// NoSparse specifies values in this field cannot be unset. This only
// applies to map's with message value types.
optional bool no_sparse = 3;
// Keys specifies the constraints to be applied to each key in the field.
optional FieldRules keys = 4;
// Values specifies the constraints to be applied to the value of each key
// 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
// `google.protobuf.Any` well-known type
message AnyRules {
// Required specifies that this field must be set
optional bool required = 1;
// In specifies that this field's `type_url` must be equal to one of the
// specified values.
repeated string in = 2;
// NotIn specifies that this field's `type_url` must not be equal to any of
// the specified values.
repeated string not_in = 3;
}
// DurationRules describe the constraints applied exclusively to the
// `google.protobuf.Duration` well-known type
message DurationRules {
// Required specifies that this field must be set
optional bool required = 1;
// Const specifies that this field must be exactly the specified value
optional google.protobuf.Duration const = 2;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional google.protobuf.Duration lt = 3;
// Lt specifies that this field must be less than the specified value,
// inclusive
optional google.protobuf.Duration lte = 4;
// Gt specifies that this field must be greater than the specified value,
// exclusive
optional google.protobuf.Duration gt = 5;
// Gte specifies that this field must be greater than the specified value,
// inclusive
optional google.protobuf.Duration gte = 6;
// In specifies that this field must be equal to one of the specified
// values
repeated google.protobuf.Duration in = 7;
// NotIn specifies that this field cannot be equal to one of the specified
// values
repeated google.protobuf.Duration not_in = 8;
}
// TimestampRules describe the constraints applied exclusively to the
// `google.protobuf.Timestamp` well-known type
message TimestampRules {
// Required specifies that this field must be set
optional bool required = 1;
// Const specifies that this field must be exactly the specified value
optional google.protobuf.Timestamp const = 2;
// Lt specifies that this field must be less than the specified value,
// exclusive
optional google.protobuf.Timestamp lt = 3;
// Lte specifies that this field must be less than the specified value,
// inclusive
optional google.protobuf.Timestamp lte = 4;
// Gt specifies that this field must be greater than the specified value,
// exclusive
optional google.protobuf.Timestamp gt = 5;
// Gte specifies that this field must be greater than the specified value,
// inclusive
optional google.protobuf.Timestamp gte = 6;
// LtNow specifies that this must be less than the current time. LtNow
// can only be used with the Within rule.
optional bool lt_now = 7;
// GtNow specifies that this must be greater than the current time. GtNow
// can only be used with the Within rule.
optional bool gt_now = 8;
// Within specifies that this field must be within this duration of the
// current time. This constraint can be used alone or with the LtNow and
// GtNow rules.
optional google.protobuf.Duration within = 9;
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls;
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.api.v2.ratelimit.RateLimitDescriptor;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitRequest;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse;
import io.envoyproxy.envoy.service.ratelimit.v2.RateLimitResponse.Code;
import io.grpc.stub.StreamObserver;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* @author Eric Zhao
*/
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("a1").setValue("b1").build())
.build();
RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder()
.addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a2").setValue("b2").build())
.addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a3").setValue("b3").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(Code.OK, response.getOverallCode());
response.getStatusesList()
.forEach(e -> assertEquals(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("a1").setValue("b1").build())
.build();
RateLimitDescriptor descriptor2 = RateLimitDescriptor.newBuilder()
.addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a2").setValue("b2").build())
.addEntries(RateLimitDescriptor.Entry.newBuilder().setKey("a3").setValue("b3").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(Code.OVER_LIMIT, response.getOverallCode());
assertEquals(2, response.getStatusesCount());
assertTrue(response.getStatusesList().stream()
.anyMatch(e -> e.getCode().equals(Code.OVER_LIMIT)));
assertFalse(response.getStatusesList().stream()
.allMatch(e -> e.getCode().equals(Code.OVER_LIMIT)));
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 1999-2019 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule.KeyValueResource;
import com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoyRlsRule.ResourceDescriptor;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import org.junit.Test;
import static com.alibaba.csp.sentinel.cluster.server.envoy.rls.rule.EnvoySentinelRuleConverter.SEPARATOR;
import static org.junit.Assert.*;
/**
* @author Eric Zhao
*/
public class EnvoySentinelRuleConverterTest {
@Test
public void testConvertToSentinelFlowRules() {
String domain = "testConvertToSentinelFlowRules";
EnvoyRlsRule rlsRule = new EnvoyRlsRule();
rlsRule.setDomain(domain);
List<ResourceDescriptor> descriptors = new ArrayList<>();
ResourceDescriptor d1 = new ResourceDescriptor();
d1.setCount(10d);
d1.setResources(Collections.singleton(new KeyValueResource("k1", "v1")));
descriptors.add(d1);
ResourceDescriptor d2 = new ResourceDescriptor();
d2.setCount(20d);
d2.setResources(new HashSet<>(Arrays.asList(
new KeyValueResource("k2", "v2"),
new KeyValueResource("k3", "v3")
)));
descriptors.add(d2);
rlsRule.setDescriptors(descriptors);
List<FlowRule> rules = EnvoySentinelRuleConverter.toSentinelFlowRules(rlsRule);
final String expectedK1 = domain + SEPARATOR + "k1" + SEPARATOR + "v1";
FlowRule r1 = rules.stream()
.filter(e -> e.getResource().equals(expectedK1))
.findAny()
.orElseThrow(() -> new AssertionError("the converted rule does not exist, expected key: " + expectedK1));
assertEquals(10d, r1.getCount(), 0.01);
final String expectedK2 = domain + SEPARATOR + "k2" + SEPARATOR + "v2" + SEPARATOR + "k3" + SEPARATOR + "v3";
FlowRule r2 = rules.stream()
.filter(e -> e.getResource().equals(expectedK2))
.findAny()
.orElseThrow(() -> new AssertionError("the converted rule does not exist, expected key: " + expectedK2));
assertEquals(20d, r2.getCount(), 0.01);
}
}

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