init
This commit is contained in:
@@ -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"]
|
@@ -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)
|
@@ -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>
|
@@ -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):
|
||||
|
||||

|
||||
|
||||
## 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`.
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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() {}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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() {}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -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() {}
|
||||
}
|
@@ -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() {}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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}];
|
||||
}
|
@@ -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}];
|
||||
}
|
@@ -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}
|
||||
];
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
load("//bazel:api_build_system.bzl", "udpa_proto_package")
|
||||
|
||||
licenses(["notice"]) # Apache 2
|
||||
|
||||
udpa_proto_package()
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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)));
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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)));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user