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