Add Sentinel Envoy RLS server implementation (#1139)
* Add the sentinel-cluster-server-envoy-rls module, a Envoy RLS server implementation using Sentinel token server. Signed-off-by: Eric Zhao <sczyh16@gmail.com>
This commit is contained in:
parent
d38d5484ef
commit
b66680be87
|
|
@ -23,6 +23,7 @@
|
||||||
<module>sentinel-cluster-client-default</module>
|
<module>sentinel-cluster-client-default</module>
|
||||||
<module>sentinel-cluster-server-default</module>
|
<module>sentinel-cluster-server-default</module>
|
||||||
<module>sentinel-cluster-common-default</module>
|
<module>sentinel-cluster-common-default</module>
|
||||||
|
<module>sentinel-cluster-server-envoy-rls</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
<?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.7.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>sentinel-cluster-server-envoy-rls</artifactId>
|
||||||
|
<version>1.7.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<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.24.0</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>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.25</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,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,59 @@
|
||||||
|
/*
|
||||||
|
* 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 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 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,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,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,763 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
package validate;
|
||||||
|
|
||||||
|
option go_package = "github.com/lyft/protoc-gen-validate/validate";
|
||||||
|
option java_package = "com.lyft.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 = 919191;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation rules applied at the oneof level
|
||||||
|
extend google.protobuf.OneofOptions {
|
||||||
|
// Required ensures that exactly one the field options in a oneof is set;
|
||||||
|
// validation fails if no fields in the oneof are set.
|
||||||
|
optional bool required = 919191;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation rules applied at the field level
|
||||||
|
extend google.protobuf.FieldOptions {
|
||||||
|
// Rules specify the validations to be performed on this field. By default,
|
||||||
|
// no validation is performed against a field.
|
||||||
|
optional FieldRules rules = 919191;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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;
|
||||||
|
MessageRules message = 17;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue