feat: support gateway context, feign eager-load support default value. ()

Co-authored-by: Haotian Zhang <928016560@qq.com>
pull/1498/head
shedfreewu 2 months ago committed by GitHub
parent 4aeca717cd
commit b12c31619c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4,3 +4,4 @@
- [feat: implement circuit breaker in enhance plugin, support listen config group, support refresh single config in refresh_context mode.](https://github.com/Tencent/spring-cloud-tencent/pull/1490)
- [feat:support polaris event.](https://github.com/Tencent/spring-cloud-tencent/pull/1494)
- [feat:support circuit breaker metrics reporting.](https://github.com/Tencent/spring-cloud-tencent/pull/1495)
- [feat: support gateway context, feign eager-load support default value.](https://github.com/Tencent/spring-cloud-tencent/pull/1496)

@ -55,6 +55,11 @@
<artifactId>spring-cloud-starter-tencent-trace-plugin</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-gateway-plugin</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-auth</artifactId>

@ -22,6 +22,7 @@ import java.util.Map;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.constant.OrderConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.common.util.JacksonUtils;
import com.tencent.cloud.common.util.UrlUtils;
@ -95,6 +96,11 @@ public class DecodeTransferMetadataReactiveFilter implements WebFilter, Ordered
MetadataConstant.HeaderName.METADATA_CONTEXT,
MetadataContextHolder.get());
String targetNamespace = serverWebExchange.getRequest().getHeaders().getFirst(MetadataConstant.HeaderName.NAMESPACE);
if (StringUtils.isNotBlank(targetNamespace)) {
MetadataContextHolder.get().putFragmentContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, targetNamespace);
}
TransHeadersTransfer.transfer(serverHttpRequest);
return webFilterChain.filter(serverWebExchange)
.doFinally((type) -> MetadataContextHolder.remove());

@ -28,6 +28,7 @@ import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedRequestContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedResponseContext;
import com.tencent.cloud.rpc.enhancement.plugin.reporter.SuccessPolarisReporter;
import com.tencent.polaris.api.utils.StringUtils;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import com.tencent.polaris.metadata.core.MetadataType;
import org.slf4j.Logger;
@ -69,10 +70,10 @@ public class CircuitBreakerPlugin implements EnhancedPlugin {
EnhancedRequestContext request = context.getRequest();
EnhancedResponseContext response = context.getResponse();
String governanceNamespace = MetadataContext.LOCAL_NAMESPACE;
String host = request.getServiceUrl() != null ? request.getServiceUrl().getHost() : request.getUrl().getHost();
String path = request.getServiceUrl() != null ? request.getServiceUrl().getPath() : request.getUrl().getPath();
String governanceNamespace = StringUtils.isNotEmpty(request.getGovernanceNamespace()) ? request.getGovernanceNamespace() : MetadataContext.LOCAL_NAMESPACE;
String httpMethod = request.getHttpMethod().name();
CircuitBreaker circuitBreaker = circuitBreakerFactory.create(governanceNamespace + "#" + host + "#" + path + "#http#" + httpMethod);

@ -28,6 +28,7 @@ import com.tencent.cloud.polaris.config.config.ConfigFileGroup;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.config.enums.ConfigFileFormat;
import com.tencent.cloud.polaris.context.config.PolarisContextProperties;
import com.tencent.polaris.api.utils.ClassUtils;
import com.tencent.polaris.configuration.api.core.ConfigFileMetadata;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.api.core.ConfigKVFile;
@ -213,13 +214,17 @@ public class PolarisConfigFileLocator implements PropertySourceLocator {
String tsfNamespaceName = environment.getProperty("tsf_namespace_name");
String tsfGroupName = environment.getProperty("tsf_group_name");
if (StringUtils.isEmpty(tsfId) || StringUtils.isEmpty(tsfNamespaceName) || StringUtils.isEmpty(tsfGroupName)) {
if (StringUtils.isEmpty(tsfNamespaceName) || StringUtils.isEmpty(tsfGroupName)) {
return;
}
String namespace = polarisContextProperties.getNamespace();
List<String> tsfConfigGroups = Arrays.asList(
tsfId + "." + tsfGroupName + ".application_config_group",
tsfId + "." + tsfNamespaceName + ".global_config_group");
List<String> tsfConfigGroups = new ArrayList<>();
tsfConfigGroups.add((StringUtils.hasText(tsfId) ? tsfId + "." : "") + tsfGroupName + ".application_config_group");
tsfConfigGroups.add((StringUtils.hasText(tsfId) ? tsfId + "." : "") + tsfNamespaceName + ".global_config_group");
if (ClassUtils.isClassPresent("org.springframework.cloud.gateway.filter.GlobalFilter")) {
tsfConfigGroups.add((StringUtils.hasText(tsfId) ? tsfId + "." : "") + tsfGroupName + ".gateway_config_group");
}
for (String tsfConfigGroup : tsfConfigGroups) {
PolarisPropertySource polarisPropertySource = loadGroupPolarisPropertySource(configFileService, namespace, tsfConfigGroup);
if (polarisPropertySource == null) {

@ -58,6 +58,18 @@
</dependency>
<!-- Polaris dependencies end -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

@ -17,6 +17,9 @@
package com.tencent.cloud.polaris.discovery;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.polaris.PolarisDiscoveryProperties;
import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
import com.tencent.polaris.api.core.ConsumerAPI;
@ -49,7 +52,9 @@ public class PolarisDiscoveryHandler {
* @return list of healthy instances
*/
public InstancesResponse getHealthyInstances(String service) {
String namespace = polarisDiscoveryProperties.getNamespace();
String namespace = MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, polarisDiscoveryProperties.getNamespace());
GetHealthyInstancesRequest getHealthyInstancesRequest = new GetHealthyInstancesRequest();
getHealthyInstancesRequest.setNamespace(namespace);
getHealthyInstancesRequest.setService(service);

@ -29,7 +29,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.enabled", havingValue = "true")
@ConditionalOnProperty(name = "spring.cloud.polaris.discovery.eager-load.enabled", havingValue = "true", matchIfMissing = true)
public class PolarisEagerLoadAutoConfiguration {
@Bean

@ -17,13 +17,17 @@
package com.tencent.cloud.polaris.eager.instrument.feign;
import com.tencent.cloud.common.util.FeignUtil;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
import com.tencent.polaris.api.utils.StringUtils;
import feign.Target;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle;
@ -47,36 +51,59 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle {
@Override
public void start() {
LOG.info("feign eager-load start");
for (String name : applicationContext.getBeanDefinitionNames()) {
for (Object bean : applicationContext.getBeansWithAnnotation(FeignClient.class).values()) {
try {
if (name.contains(FeignUtil.FEIGN_CLIENT_SPECIF) && !name.startsWith(FeignUtil.FEIGN_CLIENT_DEFAULT)) {
String feignName = FeignUtil.analysisFeignName(name, applicationContext);
if (StringUtils.isNotBlank(feignName)) {
LOG.info("[{}] eager-load start", feignName);
if (polarisDiscoveryClient != null) {
polarisDiscoveryClient.getInstances(feignName);
}
else if (polarisReactiveDiscoveryClient != null) {
polarisReactiveDiscoveryClient.getInstances(feignName).subscribe();
}
else {
LOG.warn("[{}] no discovery client found.", feignName);
if (Proxy.isProxyClass(bean.getClass())) {
Target.HardCodedTarget<?> hardCodedTarget = getHardCodedTarget(bean);
if (hardCodedTarget != null) {
FeignClient feignClient = hardCodedTarget.type().getAnnotation(FeignClient.class);
// if feignClient contains url, it doesn't need to eager load.
if (StringUtils.isEmpty(feignClient.url())) {
// support variables and default values.
String feignName = hardCodedTarget.name();
LOG.info("[{}] eager-load start", feignName);
if (polarisDiscoveryClient != null) {
polarisDiscoveryClient.getInstances(feignName);
}
else if (polarisReactiveDiscoveryClient != null) {
polarisReactiveDiscoveryClient.getInstances(feignName).subscribe();
}
else {
LOG.warn("[{}] no discovery client found.", feignName);
}
LOG.info("[{}] eager-load end", feignName);
}
LOG.info("[{}] eager-load end", feignName);
}
else {
LOG.warn("feign name is blank.");
}
}
}
catch (Exception e) {
LOG.error("[{}] eager-load failed.", name, e);
LOG.debug("[{}] eager-load failed.", bean, e);
}
}
LOG.info("feign eager-load end");
}
public static Target.HardCodedTarget<?> getHardCodedTarget(Object proxy) {
try {
Object invocationHandler = Proxy.getInvocationHandler(proxy);
for (Field field : invocationHandler.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object fieldValue = field.get(invocationHandler);
if (fieldValue instanceof Target.HardCodedTarget) {
return (Target.HardCodedTarget<?>) fieldValue;
}
}
}
catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug("proxy:{}, getTarget failed.", proxy, e);
}
}
return null;
}
@Override
public void stop() {

@ -29,7 +29,6 @@ import com.tencent.cloud.polaris.registry.PolarisRegistration;
import com.tencent.cloud.polaris.registry.PolarisRegistrationCustomizer;
import com.tencent.polaris.plugins.connector.common.constant.ConsulConstant;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.util.StringUtils;

@ -25,7 +25,9 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.rpc.enhancement.transformer.InstanceTransformer;
import com.tencent.polaris.api.pojo.DefaultServiceInstances;
import com.tencent.polaris.api.pojo.Instance;
@ -72,7 +74,10 @@ public final class RouterUtils {
serviceMetadata = instanceList.get(0).getServiceMetadata();
}
ServiceKey serviceKey = new ServiceKey(MetadataContext.LOCAL_NAMESPACE, serviceName);
String namespace = MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, MetadataContext.LOCAL_NAMESPACE);
ServiceKey serviceKey = new ServiceKey(namespace, serviceName);
return new DefaultServiceInstances(serviceKey, instanceList, serviceMetadata);
}

@ -74,6 +74,8 @@ public class RouterUtilsTest {
.thenReturn(testNamespaceAndService);
MetadataContext metadataContext = Mockito.mock(MetadataContext.class);
mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext);
Mockito.when(metadataContext.getContext(anyString(), anyString(), anyString()))
.thenReturn(testNamespaceAndService);
int instanceSize = 100;
int weight = 50;

@ -41,6 +41,10 @@ public final class MetadataConstant {
* polaris transitive header prefix length.
*/
public static final int POLARIS_TRANSITIVE_HEADER_PREFIX_LENGTH = POLARIS_TRANSITIVE_HEADER_PREFIX.length();
/**
* Name of polaris target namespace.
*/
public static final String POLARIS_TARGET_NAMESPACE = "POLARIS_TARGET_NAMESPACE";
private MetadataConstant() {
@ -70,6 +74,10 @@ public final class MetadataConstant {
* Metadata context.
*/
public static final String METADATA_CONTEXT = "SCT-METADATA-CONTEXT";
/**
* Namespace context.
*/
public static final String NAMESPACE = "SCT-NAMESPACE";
}
public static class DefaultMetadata {

@ -59,6 +59,10 @@ public class MetadataContext extends com.tencent.polaris.metadata.core.manager.M
* disposable Context.
*/
public static final String FRAGMENT_APPLICATION = "application";
/**
* none Context.
*/
public static final String FRAGMENT_APPLICATION_NONE = "application-none";
/**
* upstream disposable Context.
@ -125,6 +129,10 @@ public class MetadataContext extends com.tencent.polaris.metadata.core.manager.M
super(MetadataConstant.POLARIS_TRANSITIVE_HEADER_PREFIX);
}
public static void setLocalService(String service) {
LOCAL_SERVICE = service;
}
private Map<String, String> getMetadataAsMap(MetadataType metadataType, TransitiveType transitiveType, boolean caller) {
MetadataContainer metadataContainer = getMetadataContainer(metadataType, caller);
Map<String, String> values = new HashMap<>();
@ -266,6 +274,8 @@ public class MetadataContext extends com.tencent.polaris.metadata.core.manager.M
return getMetadataAsMap(MetadataType.CUSTOM, TransitiveType.DISPOSABLE, true);
case FRAGMENT_APPLICATION:
return getMetadataAsMap(MetadataType.APPLICATION, TransitiveType.DISPOSABLE, false);
case FRAGMENT_APPLICATION_NONE:
return getMetadataAsMap(MetadataType.APPLICATION, TransitiveType.NONE, false);
case FRAGMENT_UPSTREAM_APPLICATION:
return getMetadataAsMap(MetadataType.APPLICATION, TransitiveType.DISPOSABLE, true);
case FRAGMENT_RAW_TRANSHEADERS:
@ -277,6 +287,10 @@ public class MetadataContext extends com.tencent.polaris.metadata.core.manager.M
}
}
public String getContext(String fragment, String key, String defaultValue) {
return getFragmentContext(fragment).getOrDefault(key, defaultValue);
}
public String getContext(String fragment, String key) {
Map<String, String> fragmentContext = getFragmentContext(fragment);
if (fragmentContext == null) {
@ -305,6 +319,9 @@ public class MetadataContext extends com.tencent.polaris.metadata.core.manager.M
case FRAGMENT_APPLICATION:
putMetadataAsMap(MetadataType.APPLICATION, TransitiveType.DISPOSABLE, false, context);
break;
case FRAGMENT_APPLICATION_NONE:
putMetadataAsMap(MetadataType.APPLICATION, TransitiveType.NONE, false, context);
break;
case FRAGMENT_UPSTREAM_APPLICATION:
putMetadataAsMap(MetadataType.APPLICATION, TransitiveType.DISPOSABLE, true, context);
break;
@ -320,7 +337,9 @@ public class MetadataContext extends com.tencent.polaris.metadata.core.manager.M
}
}
public static void setLocalService(String service) {
LOCAL_SERVICE = service;
public void putFragmentContext(String fragment, String key, String value) {
Map<String, String> context = new HashMap<>(1);
context.put(key, value);
putFragmentContext(fragment, context);
}
}

@ -1,83 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.common.util;
import org.springframework.context.ApplicationContext;
import org.springframework.util.StringUtils;
/**
* @author heihuliliu
*/
public final class FeignUtil {
/**
* Feign client spec.
*/
public static final String FEIGN_CLIENT_SPECIF = ".FeignClientSpecification";
/**
* Default Feign client spec.
*/
public static final String FEIGN_CLIENT_DEFAULT = "default.";
/**
* regular expression that parses ${xxx} .
*/
public static final String REGEX = "^[$][{](.*)[}]$";
/**
* replacement of ${xxx}.
*/
public static final String REPLACEMENT = "$1";
private FeignUtil() {
}
/**
* TODO If @FeignClient specifies contextId, the service name will not be obtained correctly, but the contextId will be obtained.
*
* @param name feign name.
* @param context application context.
* @return service name.
*/
public static String analysisFeignName(String name, ApplicationContext context) {
String feignName = "";
String feignPath = name.substring(0, name.indexOf(FEIGN_CLIENT_SPECIF));
// Handle the case where the service name is a variable
if (feignPath.matches(REGEX)) {
feignPath = context.getEnvironment().getProperty(feignPath.replaceAll(REGEX, REPLACEMENT));
}
if (StringUtils.hasText(feignPath)) {
// The case of multi-level paths
String[] feignNames = feignPath.split("/");
if (feignNames.length > 1) {
for (int i = 0; i < feignNames.length; i++) {
if (StringUtils.hasText(feignNames[i])) {
feignName = feignNames[i];
break;
}
}
}
else {
feignName = feignNames[0];
}
}
return feignName;
}
}

@ -86,7 +86,7 @@
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-gateway-plugin</artifactId>
<artifactId>spring-cloud-starter-tencent-gateway-plugin</artifactId>
</dependency>
<dependency>

@ -194,7 +194,7 @@
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-gateway-plugin</artifactId>
<artifactId>spring-cloud-starter-tencent-gateway-plugin</artifactId>
<version>${revision}</version>
</dependency>

@ -20,7 +20,7 @@
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-gateway-plugin</artifactId>
<artifactId>spring-cloud-starter-tencent-gateway-plugin</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>

@ -21,7 +21,7 @@
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-tencent-gateway-plugin</artifactId>
<artifactId>spring-cloud-starter-tencent-gateway-plugin</artifactId>
</dependency>
<dependency>

@ -43,13 +43,6 @@ spring:
args:
regexp: '''/'' + serviceId + ''/(?<remaining>.*)'''
replacement: '''/$\{remaining}'''
'filters[1]':
name: CircuitBreaker
args:
# statusCodes 缺省时会自动识别 "5**" 为错误
# statusCodes: '''404,5**'''
# fallbackUri 缺省时会在熔断触发后拉取 plaris server 配置的降级作为 response
fallbackUri: '''forward:/polaris-fallback'''
routes:
- id: QuickstartCallerService
uri: lb://QuickstartCallerService

@ -16,7 +16,7 @@
<modules>
<module>spring-cloud-tencent-featureenv-plugin</module>
<module>spring-cloud-tencent-gateway-plugin</module>
<module>spring-cloud-starter-tencent-gateway-plugin</module>
<module>spring-cloud-starter-tencent-discovery-adapter-plugin</module>
<module>spring-cloud-tencent-lossless-plugin</module>
<module>spring-cloud-starter-tencent-threadlocal-plugin</module>

@ -10,8 +10,8 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-tencent-gateway-plugin</artifactId>
<name>Spring Cloud Tencent Gateway Plugin</name>
<artifactId>spring-cloud-starter-tencent-gateway-plugin</artifactId>
<name>Spring Cloud Starter Tencent Gateway Plugin</name>
<dependencies>
<dependency>
@ -19,6 +19,12 @@
<artifactId>spring-cloud-starter-tencent-polaris-config</artifactId>
</dependency>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-discovery</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server</artifactId>

@ -0,0 +1,109 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway;
import com.tencent.cloud.plugin.gateway.context.ContextGatewayFilterFactory;
import com.tencent.cloud.plugin.gateway.context.ContextGatewayProperties;
import com.tencent.cloud.plugin.gateway.context.ContextGatewayPropertiesManager;
import com.tencent.cloud.plugin.gateway.context.ContextPropertiesRouteDefinitionLocator;
import com.tencent.cloud.plugin.gateway.context.ContextRoutePredicateFactory;
import com.tencent.cloud.plugin.gateway.context.GatewayConfigChangeListener;
import com.tencent.cloud.polaris.config.ConditionalOnPolarisConfigEnabled;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
/**
* Auto configuration for spring cloud gateway plugins.
* @author lepdou 2022-07-06
*/
@Configuration
@ConditionalOnPolarisEnabled
@ConditionalOnProperty(value = "spring.cloud.tencent.plugin.scg.enabled", matchIfMissing = true)
public class GatewayPluginAutoConfiguration {
@Configuration
@ConditionalOnProperty(value = "spring.cloud.tencent.plugin.scg.context.enabled", matchIfMissing = true)
@ConditionalOnPolarisConfigEnabled
@ConditionalOnClass(GlobalFilter.class)
@Import(ContextGatewayProperties.class)
public static class ContextPluginConfiguration {
@Value("${spring.cloud.polaris.discovery.eager-load.enabled:#{'true'}}")
private boolean commonEagerLoadEnabled;
@Value("${spring.cloud.polaris.discovery.eager-load.gateway.enabled:#{'true'}}")
private boolean gatewayEagerLoadEnabled;
@Bean
public ContextGatewayFilterFactory contextGatewayFilterFactory(ContextGatewayPropertiesManager contextGatewayPropertiesManager) {
return new ContextGatewayFilterFactory(contextGatewayPropertiesManager);
}
@Bean
public ContextPropertiesRouteDefinitionLocator contextPropertiesRouteDefinitionLocator(ContextGatewayProperties properties) {
return new ContextPropertiesRouteDefinitionLocator(properties);
}
@Bean
public ContextRoutePredicateFactory contextServiceRoutePredicateFactory() {
return new ContextRoutePredicateFactory();
}
@Bean
public ContextGatewayPropertiesManager contextGatewayPropertiesManager(ContextGatewayProperties properties,
@Autowired(required = false) PolarisDiscoveryClient polarisDiscoveryClient,
@Autowired(required = false) PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
ContextGatewayPropertiesManager contextGatewayPropertiesManager = new ContextGatewayPropertiesManager();
contextGatewayPropertiesManager.setGroupRouteMap(properties.getGroups());
if (commonEagerLoadEnabled && gatewayEagerLoadEnabled) {
contextGatewayPropertiesManager.eagerLoad(polarisDiscoveryClient, polarisReactiveDiscoveryClient);
}
return contextGatewayPropertiesManager;
}
@Bean
public GatewayRegistrationCustomizer gatewayRegistrationCustomizer() {
return new GatewayRegistrationCustomizer();
}
@Bean
public GatewayConfigChangeListener gatewayConfigChangeListener(ContextGatewayPropertiesManager manager,
ApplicationEventPublisher publisher, Environment environment) {
return new GatewayConfigChangeListener(manager, publisher, environment);
}
@Bean
public PolarisReactiveLoadBalancerClientFilterBeanPostProcessor polarisReactiveLoadBalancerClientFilterBeanPostProcessor(
ApplicationContext applicationContext) {
return new PolarisReactiveLoadBalancerClientFilterBeanPostProcessor(applicationContext);
}
}
}

@ -0,0 +1,28 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway;
import com.tencent.cloud.polaris.registry.PolarisRegistration;
import com.tencent.cloud.polaris.registry.PolarisRegistrationCustomizer;
public class GatewayRegistrationCustomizer implements PolarisRegistrationCustomizer {
@Override
public void customize(PolarisRegistration registration) {
registration.getMetadata().put("internal-service-type", "spring-cloud-gateway");
}
}

@ -0,0 +1,56 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.web.server.ServerWebExchange;
public class PolarisReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter {
private final ReactiveLoadBalancerClientFilter clientFilter;
public PolarisReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,
GatewayLoadBalancerProperties properties, ReactiveLoadBalancerClientFilter clientFilter) {
super(clientFactory, properties);
this.clientFilter = clientFilter;
}
@Override
public int getOrder() {
return clientFilter.getOrder();
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// restore context from exchange
MetadataContext metadataContext = (MetadataContext) exchange.getAttributes().get(
MetadataConstant.HeaderName.METADATA_CONTEXT);
if (metadataContext != null) {
MetadataContextHolder.set(metadataContext);
}
return clientFilter.filter(exchange, chain);
}
}

@ -0,0 +1,51 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
public class PolarisReactiveLoadBalancerClientFilterBeanPostProcessor implements BeanPostProcessor, Ordered {
private ApplicationContext applicationContext;
public PolarisReactiveLoadBalancerClientFilterBeanPostProcessor(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ReactiveLoadBalancerClientFilter && !(bean instanceof PolarisReactiveLoadBalancerClientFilter)) {
LoadBalancerClientFactory clientFactory = applicationContext.getBean(LoadBalancerClientFactory.class);
GatewayLoadBalancerProperties properties = applicationContext.getBean(GatewayLoadBalancerProperties.class);
return new PolarisReactiveLoadBalancerClientFilter(clientFactory, properties, (ReactiveLoadBalancerClientFilter) bean);
}
return bean;
}
@Override
public int getOrder() {
return 0;
}
}

@ -15,23 +15,15 @@
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.plugin.gateway.staining;
import java.util.Map;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
/**
* Staining according to request parameters. for example, when the request parameter uid=0, staining env=blue.
* @author lepdou 2022-07-06
*/
public interface TrafficStainer extends Ordered {
package com.tencent.cloud.plugin.gateway.context;
public enum ApiType {
/**
* Ms api type.
*/
MS,
/**
* get stained labels from request.
* @param exchange the request.
* @return stained labels.
* External api type.
*/
Map<String, String> apply(ServerWebExchange exchange);
EXTERNAL,
}

@ -0,0 +1,199 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.util.HashMap;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.polaris.api.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
public class ContextGatewayFilter implements GatewayFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(ContextGatewayFilter.class);
private ContextGatewayPropertiesManager manager;
private ContextGatewayFilterFactory.Config config;
public ContextGatewayFilter(ContextGatewayPropertiesManager manager, ContextGatewayFilterFactory.Config config) {
this.manager = manager;
this.config = config;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
GroupContext groupContext = manager.getGroups().get(config.getGroup());
if (ApiType.MS.equals(groupContext.getPredicate().getApiType())) {
return msFilter(exchange, chain, groupContext);
}
else {
return externalFilter(exchange, chain, groupContext);
}
}
private Mono<Void> externalFilter(ServerWebExchange exchange, GatewayFilterChain chain, GroupContext groupContext) {
ServerHttpRequest request = exchange.getRequest();
String[] apis = rebuildExternalApi(request, request.getPath().value());
GroupContext.ContextRoute contextRoute = manager.getGroupPathRoute(config.getGroup(), apis[0]);
if (contextRoute == null) {
throw new RuntimeException(String.format("Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], request.getPath()));
}
updateRouteMetadata(exchange, contextRoute);
URI requestUri = URI.create(contextRoute.getHost() + apis[1]);
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUri);
// 调整为正确路径
ServerHttpRequest newRequest = request.mutate().path(apis[1]).build();
return chain.filter(exchange.mutate().request(newRequest).build());
}
private Mono<Void> msFilter(ServerWebExchange exchange, GatewayFilterChain chain, GroupContext groupContext) {
ServerHttpRequest request = exchange.getRequest();
String[] apis = rebuildMsApi(request, groupContext, request.getPath().value());
// 判断 api 是否匹配
GroupContext.ContextRoute contextRoute = manager.getGroupPathRoute(config.getGroup(), apis[0]);
if (contextRoute == null) {
throw new RuntimeException(String.format("Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], request.getPath()));
}
updateRouteMetadata(exchange, contextRoute);
MetadataContext metadataContext = (MetadataContext) exchange.getAttributes().get(
MetadataConstant.HeaderName.METADATA_CONTEXT);
metadataContext.putFragmentContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, contextRoute.getNamespace());
URI requestUri = URI.create("lb://" + contextRoute.getService() + apis[1]);
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUri);
// 调整为正确路径
ServerHttpRequest newRequest = request.mutate().path(apis[1]).build();
return chain.filter(exchange.mutate().request(newRequest).build());
}
/**
* e.g. "/context/api/test" [ "GET|/api/test", "/api/test"]
*/
private String[] rebuildExternalApi(ServerHttpRequest request, String path) {
String[] pathSegments = path.split("/");
StringBuilder matchPath = new StringBuilder();
StringBuilder realPath = new StringBuilder();
int index = 2;
matchPath.append(request.getMethodValue()).append("|");
for (int i = index; i < pathSegments.length; i++) {
matchPath.append("/").append(pathSegments[i]);
realPath.append("/").append(pathSegments[i]);
}
if (path.endsWith("/")) {
matchPath.append("/");
realPath.append("/");
}
return new String[] {matchPath.toString(), realPath.toString()};
}
/**
* returns an array of two strings, the first is the match path, the second is the real path.
* e.g. "/context/namespace/svc/api/test" [ "GET|/namespace/svc/api/test", "/api/test"]
*/
private String[] rebuildMsApi(ServerHttpRequest request, GroupContext groupContext, String path) {
String[] pathSegments = path.split("/");
StringBuilder matchPath = new StringBuilder();
int index = 2;
matchPath.append(request.getMethodValue()).append("|");
Position namespacePosition = groupContext.getPredicate().getNamespace().getPosition();
switch (namespacePosition) {
case QUERY:
matchPath.append("/").append(request.getQueryParams().getFirst(groupContext.getPredicate().getNamespace().getKey()));
break;
case HEADER:
matchPath.append("/").append(request.getHeaders().getFirst(groupContext.getPredicate().getNamespace().getKey()));
break;
case PATH:
default:
matchPath.append("/").append(pathSegments[index++]);
break;
}
Position servicePosition = groupContext.getPredicate().getService().getPosition();
switch (servicePosition) {
case QUERY:
matchPath.append("/").append(request.getQueryParams().getFirst(groupContext.getPredicate().getService().getKey()));
break;
case HEADER:
matchPath.append("/").append(request.getHeaders().getFirst(groupContext.getPredicate().getService().getKey()));
break;
case PATH:
default:
matchPath.append("/").append(pathSegments[index++]);
}
StringBuilder realPath = new StringBuilder();
for (int i = index; i < pathSegments.length; i++) {
matchPath.append("/").append(pathSegments[i]);
realPath.append("/").append(pathSegments[i]);
}
if (path.endsWith("/")) {
matchPath.append("/");
realPath.append("/");
}
return new String[] {matchPath.toString(), realPath.toString()};
}
private void updateRouteMetadata(ServerWebExchange exchange, GroupContext.ContextRoute contextRoute) {
if (CollectionUtils.isEmpty(contextRoute.getMetadata())) {
return;
}
Route route = (Route) exchange.getAttributes().get(GATEWAY_ROUTE_ATTR);
Constructor constructor = Route.class.getDeclaredConstructors()[1];
constructor.setAccessible(true);
try {
HashMap<String, Object> metadata = new HashMap<>(route.getMetadata());
metadata.putAll(contextRoute.getMetadata());
Route newRoute = (Route) constructor.newInstance(route.getId(), route.getUri(),
route.getOrder(), route.getPredicate(), route.getFilters(), metadata);
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);
}
catch (Exception e) {
logger.debug("[updateRouteMetadata] update route metadata failed", e);
}
}
@Override
public int getOrder() {
// after RouteToRequestUrlFilter, DecodeTransferMetadataReactiveFilter
return RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER + 12;
}
}

@ -0,0 +1,58 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import java.util.Arrays;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
public class ContextGatewayFilterFactory extends AbstractGatewayFilterFactory<ContextGatewayFilterFactory.Config> {
private ContextGatewayPropertiesManager manager;
public ContextGatewayFilterFactory(ContextGatewayPropertiesManager manager) {
super(Config.class);
this.manager = manager;
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("group");
}
@Override
public GatewayFilter apply(Config config) {
return new ContextGatewayFilter(manager, config);
}
public static class Config {
private String group;
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
}
}

@ -0,0 +1,70 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.core.style.ToStringCreator;
@ConfigurationProperties(ContextGatewayProperties.PREFIX)
public class ContextGatewayProperties {
/**
* Properties prefix.
*/
public static final String PREFIX = "spring.cloud.tencent.gateway";
private final Log logger = LogFactory.getLog(getClass());
private Map<String, RouteDefinition> routes = new HashMap<>();
private Map<String, GroupContext> groups = new HashMap<>();
public Map<String, RouteDefinition> getRoutes() {
return routes;
}
public void setRoutes(Map<String, RouteDefinition> routes) {
this.routes = routes;
if (routes != null && routes.size() > 0 && logger.isDebugEnabled()) {
logger.debug("Routes supplied from Gateway Properties: " + routes);
}
}
public Map<String, GroupContext> getGroups() {
return groups;
}
public void setGroups(Map<String, GroupContext> groups) {
this.groups = groups;
}
@Override
public String toString() {
return new ToStringCreator(this).append("routes", routes)
.append("groups", groups).toString();
}
}

@ -0,0 +1,150 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
import com.tencent.polaris.api.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.AntPathMatcher;
public class ContextGatewayPropertiesManager {
private static final Logger logger = LoggerFactory.getLogger(ContextGatewayPropertiesManager.class);
/**
* context -> {path key -> route}.
*/
private volatile ConcurrentHashMap<String, Map<String, GroupContext.ContextRoute>> groupPathRouteMap = new ConcurrentHashMap<>();
/**
* context -> {wildcard path key -> route}.
*/
private volatile ConcurrentHashMap<String, Map<String, GroupContext.ContextRoute>> groupWildcardPathRouteMap = new ConcurrentHashMap<>();
private Map<String, GroupContext> groups = new HashMap<>();
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public Map<String, Map<String, GroupContext.ContextRoute>> getGroupPathRouteMap() {
return groupPathRouteMap;
}
public void setGroupRouteMap(Map<String, GroupContext> groups) {
ConcurrentHashMap<String, Map<String, GroupContext.ContextRoute>> newGroupPathRouteMap = new ConcurrentHashMap<>();
ConcurrentHashMap<String, Map<String, GroupContext.ContextRoute>> newGroupWildcardPathRouteMap = new ConcurrentHashMap<>();
if (groups != null) {
for (Map.Entry<String, GroupContext> entry : groups.entrySet()) {
Map<String, GroupContext.ContextRoute> newGroupPathRoute = new HashMap<>();
Map<String, GroupContext.ContextRoute> newGroupWildcardPathRoute = new HashMap<>();
for (GroupContext.ContextRoute route : entry.getValue().getRoutes()) {
String path = route.getPath();
// convert path parameter to group wildcard path
if (path.contains("{") && path.contains("}") || path.contains("*")) {
newGroupWildcardPathRoute.put(buildPathKey(entry.getValue(), route), route);
}
else {
newGroupPathRoute.put(buildPathKey(entry.getValue(), route), route);
}
}
newGroupWildcardPathRouteMap.put(entry.getKey(), newGroupWildcardPathRoute);
newGroupPathRouteMap.put(entry.getKey(), newGroupPathRoute);
}
}
this.groupPathRouteMap = newGroupPathRouteMap;
this.groupWildcardPathRouteMap = newGroupWildcardPathRouteMap;
this.groups = groups;
}
public Map<String, GroupContext> getGroups() {
return groups;
}
public GroupContext.ContextRoute getGroupPathRoute(String group, String path) {
Map<String, GroupContext.ContextRoute> groupPathRouteMap = this.groupPathRouteMap.get(group);
if (groupPathRouteMap != null && groupPathRouteMap.containsKey(path)) {
return groupPathRouteMap.get(path);
}
Map<String, GroupContext.ContextRoute> groupWildcardPathRouteMap = this.groupWildcardPathRouteMap.get(group);
if (groupWildcardPathRouteMap != null) {
for (Map.Entry<String, GroupContext.ContextRoute> entry : groupWildcardPathRouteMap.entrySet()) {
boolean matched = antPathMatcher.match(entry.getKey(), path);
if (matched) {
return entry.getValue();
}
}
}
return null;
}
public void eagerLoad(PolarisDiscoveryClient polarisDiscoveryClient,
PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
for (Map<String, GroupContext.ContextRoute> contextRouteMap : groupPathRouteMap.values()) {
for (GroupContext.ContextRoute contextRoute : contextRouteMap.values()) {
eagerLoadFromRoute(contextRoute, polarisDiscoveryClient, polarisReactiveDiscoveryClient);
}
}
for (Map<String, GroupContext.ContextRoute> contextRouteMap : groupWildcardPathRouteMap.values()) {
for (GroupContext.ContextRoute contextRoute : contextRouteMap.values()) {
eagerLoadFromRoute(contextRoute, polarisDiscoveryClient, polarisReactiveDiscoveryClient);
}
}
}
private void eagerLoadFromRoute(GroupContext.ContextRoute contextRoute, PolarisDiscoveryClient polarisDiscoveryClient,
PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient) {
String namespace = contextRoute.getNamespace();
String service = contextRoute.getService();
if (StringUtils.isNotEmpty(namespace) && StringUtils.isNotEmpty(service)) {
logger.info("[{},{}] eager-load start", namespace, service);
MetadataContextHolder.get().putFragmentContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, namespace);
if (polarisDiscoveryClient != null) {
polarisDiscoveryClient.getInstances(service);
}
else if (polarisReactiveDiscoveryClient != null) {
polarisReactiveDiscoveryClient.getInstances(service).subscribe();
}
else {
logger.warn("[{}] no discovery client found.", service);
}
logger.info("[{},{}] eager-load end", namespace, service);
}
}
private String buildPathKey(GroupContext groupContext, GroupContext.ContextRoute route) {
switch (groupContext.getPredicate().getApiType()) {
case MS:
return String.format("%s|/%s/%s%s", route.getMethod(), route.getNamespace(), route.getService(), route.getPath());
case EXTERNAL:
default:
return String.format("%s|%s", route.getMethod(), route.getPath());
}
}
}

@ -0,0 +1,37 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import reactor.core.publisher.Flux;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
public class ContextPropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
private final ContextGatewayProperties properties;
public ContextPropertiesRouteDefinitionLocator(ContextGatewayProperties properties) {
this.properties = properties;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(this.properties.getRoutes().values());
}
}

@ -0,0 +1,78 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.web.server.ServerWebExchange;
public class ContextRoutePredicateFactory extends AbstractRoutePredicateFactory<ContextRoutePredicateFactory.Config> {
public ContextRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
// TODO: do path-rewriting , put to GATEWAY_PREDICATE_PATH_CONTAINER_ATTR
return true;
}
@Override
public Object getConfig() {
return config;
}
@Override
public String toString() {
return String.format("Config: %s", config);
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("group");
}
public static class Config {
private String group;
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
@Override
public String toString() {
return "Config{" +
"group='" + group + '\'' +
'}';
}
}
}

@ -0,0 +1,57 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import com.tencent.cloud.polaris.config.annotation.PolarisConfigKVFileChangeListener;
import com.tencent.cloud.polaris.config.listener.ConfigChangeEvent;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.env.Environment;
public class GatewayConfigChangeListener {
private ApplicationEventPublisher publisher;
private ContextGatewayPropertiesManager manager;
private Environment environment;
public GatewayConfigChangeListener(ContextGatewayPropertiesManager manager,
ApplicationEventPublisher publisher, Environment environment) {
this.manager = manager;
this.publisher = publisher;
this.environment = environment;
}
@PolarisConfigKVFileChangeListener(interestedKeyPrefixes = ContextGatewayProperties.PREFIX)
public void onChangeTencentGatewayProperties(ConfigChangeEvent event) {
Binder binder = Binder.get(environment);
BindResult<ContextGatewayProperties> result = binder.bind(ContextGatewayProperties.PREFIX, ContextGatewayProperties.class);
manager.setGroupRouteMap(result.get().getGroups());
this.publisher.publishEvent(new RefreshRoutesEvent(event));
}
@PolarisConfigKVFileChangeListener(interestedKeyPrefixes = GatewayProperties.PREFIX)
public void onChangeGatewayConfigChangeListener(ConfigChangeEvent event) {
this.publisher.publishEvent(new RefreshRoutesEvent(event));
}
}

@ -0,0 +1,223 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
import java.util.List;
import java.util.Map;
public class GroupContext {
private String comment;
private ApiType apiType;
private ContextPredicate predicate;
private List<ContextRoute> routes;
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public ApiType getApiType() {
return apiType;
}
public void setApiType(ApiType apiType) {
this.apiType = apiType;
}
public ContextPredicate getPredicate() {
return predicate;
}
public void setPredicate(ContextPredicate predicate) {
this.predicate = predicate;
}
public List<ContextRoute> getRoutes() {
return routes;
}
public void setRoutes(List<ContextRoute> routes) {
this.routes = routes;
}
public static class ContextPredicate {
private ApiType apiType;
private String context;
private ContextNamespace namespace;
private ContextService service;
public ApiType getApiType() {
return apiType;
}
public void setApiType(ApiType apiType) {
this.apiType = apiType;
}
public ContextNamespace getNamespace() {
return namespace;
}
public void setNamespace(ContextNamespace namespace) {
this.namespace = namespace;
}
public ContextService getService() {
return service;
}
public void setService(ContextService service) {
this.service = service;
}
public String getContext() {
return context;
}
public void setContext(String context) {
this.context = context;
}
}
public static class ContextNamespace {
private Position position;
private String key;
public Position getPosition() {
return position;
}
public void setPosition(Position position) {
this.position = position;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
public static class ContextService {
private Position position;
private String key;
public Position getPosition() {
return position;
}
public void setPosition(Position position) {
this.position = position;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
public static class ContextRoute {
private String path;
private String pathMapping;
private String method;
private String service;
private String host;
private String namespace;
private Map<String, String> metadata;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getPathMapping() {
return pathMapping;
}
public void setPathMapping(String pathMapping) {
this.pathMapping = pathMapping;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public Map<String, String> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, String> metadata) {
this.metadata = metadata;
}
}
}

@ -0,0 +1,33 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.context;
public enum Position {
/**
* Path position.
*/
PATH,
/**
* Query position.
*/
QUERY,
/**
* Header position.
*/
HEADER,
}

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tencent.cloud.plugin.gateway.GatewayPluginAutoConfiguration

@ -1,75 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway;
import java.util.List;
import com.tencent.cloud.plugin.gateway.staining.TrafficStainer;
import com.tencent.cloud.plugin.gateway.staining.TrafficStainingGatewayFilter;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleStainingExecutor;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleStainingProperties;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleTrafficStainer;
import com.tencent.cloud.plugin.gateway.staining.rule.StainingRuleManager;
import com.tencent.cloud.polaris.config.ConditionalOnPolarisConfigEnabled;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Auto configuration for spring cloud gateway plugins.
* @author lepdou 2022-07-06
*/
@Configuration
@ConditionalOnPolarisEnabled
@ConditionalOnProperty(value = "spring.cloud.tencent.plugin.scg.enabled", matchIfMissing = true)
public class SCGPluginsAutoConfiguration {
@Configuration
@ConditionalOnProperty("spring.cloud.tencent.plugin.scg.staining.rule-staining.enabled")
@ConditionalOnPolarisConfigEnabled
public static class RuleStainingPluginConfiguration {
@Bean
public RuleStainingProperties ruleStainingProperties() {
return new RuleStainingProperties();
}
@Bean
public StainingRuleManager stainingRuleManager(RuleStainingProperties stainingProperties, ConfigFileService configFileService) {
return new StainingRuleManager(stainingProperties, configFileService);
}
@Bean
public TrafficStainingGatewayFilter trafficStainingGatewayFilter(List<TrafficStainer> trafficStainer) {
return new TrafficStainingGatewayFilter(trafficStainer);
}
@Bean
public RuleStainingExecutor ruleStainingExecutor() {
return new RuleStainingExecutor();
}
@Bean
public RuleTrafficStainer ruleTrafficStainer(StainingRuleManager stainingRuleManager, RuleStainingExecutor ruleStainingExecutor) {
return new RuleTrafficStainer(stainingRuleManager, ruleStainingExecutor);
}
}
}

@ -1,118 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.common.util.JacksonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter.ROUTE_TO_URL_FILTER_ORDER;
/**
* Staining the request, and the stained labels will be passed to the link through transitive metadata.
* @author lepdou 2022-07-06
*/
public class TrafficStainingGatewayFilter implements GlobalFilter, Ordered {
private static final Logger LOGGER = LoggerFactory.getLogger(TrafficStainingGatewayFilter.class);
private final List<TrafficStainer> trafficStainers;
public TrafficStainingGatewayFilter(List<TrafficStainer> trafficStainers) {
if (!CollectionUtils.isEmpty(trafficStainers)) {
trafficStainers.sort(Comparator.comparingInt(Ordered::getOrder));
}
this.trafficStainers = trafficStainers;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (CollectionUtils.isEmpty(trafficStainers)) {
return chain.filter(exchange);
}
// 1. get stained labels from request
Map<String, String> stainedLabels = getStainedLabels(exchange);
if (CollectionUtils.isEmpty(stainedLabels)) {
return chain.filter(exchange);
}
// 2. put stained labels to metadata context
ServerHttpRequest request = exchange.getRequest().mutate().headers((httpHeaders) -> {
MetadataContext metadataContext = exchange.getAttribute(MetadataConstant.HeaderName.METADATA_CONTEXT);
if (metadataContext == null) {
metadataContext = MetadataContextHolder.get();
}
Map<String, String> oldTransitiveMetadata = metadataContext.getTransitiveMetadata();
// append new transitive metadata
Map<String, String> newTransitiveMetadata = new HashMap<>(oldTransitiveMetadata);
newTransitiveMetadata.putAll(stainedLabels);
metadataContext.setTransitiveMetadata(newTransitiveMetadata);
}).build();
return chain.filter(exchange.mutate().request(request).build());
}
Map<String, String> getStainedLabels(ServerWebExchange exchange) {
Map<String, String> stainedLabels = new HashMap<>();
int size = trafficStainers.size();
TrafficStainer stainer = null;
for (int i = size - 1; i >= 0; i--) {
try {
stainer = trafficStainers.get(i);
Map<String, String> labels = stainer.apply(exchange);
if (!CollectionUtils.isEmpty(labels)) {
stainedLabels.putAll(labels);
}
}
catch (Exception e) {
if (stainer != null) {
LOGGER.error("[SCT] traffic stained error. stainer = {}", stainer.getClass().getName(), e);
}
}
}
LOGGER.debug("[SCT] traffic stained labels. {}", JacksonUtils.serialize2Json(stainedLabels));
return stainedLabels;
}
@Override
public int getOrder() {
return ROUTE_TO_URL_FILTER_ORDER + 1;
}
}

@ -1,69 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.tencent.cloud.common.rule.Condition;
import com.tencent.cloud.common.rule.ConditionUtils;
import com.tencent.cloud.common.rule.KVPairUtils;
import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* Resolve labels from request by staining rule.
* @author lepdou 2022-07-11
*/
public class RuleStainingExecutor {
Map<String, String> execute(ServerWebExchange exchange, StainingRule stainingRule) {
if (stainingRule == null) {
return Collections.emptyMap();
}
List<StainingRule.Rule> rules = stainingRule.getRules();
if (CollectionUtils.isEmpty(rules)) {
return Collections.emptyMap();
}
Map<String, String> parsedLabels = new HashMap<>();
for (StainingRule.Rule rule : rules) {
List<Condition> conditions = rule.getConditions();
Set<String> keys = new HashSet<>();
conditions.forEach(condition -> keys.add(condition.getKey()));
Map<String, String> actualValues = SpringWebExpressionLabelUtils.resolve(exchange, keys);
if (!ConditionUtils.match(actualValues, conditions)) {
continue;
}
parsedLabels.putAll(KVPairUtils.toMap(rule.getLabels()));
}
return parsedLabels;
}
}

@ -1,72 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* The properties for rule staining.
* @author lepdou 2022-07-11
*/
@ConfigurationProperties("spring.cloud.tencent.plugin.scg.staining.rule-staining")
public class RuleStainingProperties {
@Value("${spring.cloud.tencent.plugin.scg.staining.rule-staining.namespace:${spring.cloud.tencent.namespace:default}}")
private String namespace;
@Value("${spring.cloud.tencent.plugin.scg.staining.rule-staining.group:${spring.application.name:spring-cloud-gateway}}")
private String group;
@Value("${spring.cloud.tencent.plugin.scg.staining.rule-staining.fileName:rule/staining.json}")
private String fileName;
private boolean enabled = true;
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

@ -1,56 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import java.util.Collections;
import java.util.Map;
import com.tencent.cloud.plugin.gateway.staining.TrafficStainer;
import org.springframework.web.server.ServerWebExchange;
/**
* Staining the request by staining rules.
* @author lepdou 2022-07-06
*/
public class RuleTrafficStainer implements TrafficStainer {
private final StainingRuleManager stainingRuleManager;
private final RuleStainingExecutor ruleStainingExecutor;
public RuleTrafficStainer(StainingRuleManager stainingRuleManager, RuleStainingExecutor ruleStainingExecutor) {
this.stainingRuleManager = stainingRuleManager;
this.ruleStainingExecutor = ruleStainingExecutor;
}
@Override
public Map<String, String> apply(ServerWebExchange exchange) {
StainingRule stainingRule = stainingRuleManager.getStainingRule();
if (stainingRule == null) {
return Collections.emptyMap();
}
return ruleStainingExecutor.execute(exchange, stainingRule);
}
@Override
public int getOrder() {
return 0;
}
}

@ -1,77 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import java.util.List;
import com.tencent.cloud.common.rule.Condition;
import com.tencent.cloud.common.rule.KVPair;
/**
* The rules for staining.
* @author lepdou 2022-07-07
*/
public class StainingRule {
private List<Rule> rules;
public List<Rule> getRules() {
return rules;
}
public void setRules(List<Rule> rules) {
this.rules = rules;
}
@Override
public String toString() {
return "StainingRule{" +
"rules=" + rules +
'}';
}
public static class Rule {
private List<Condition> conditions;
private List<KVPair> labels;
public List<Condition> getConditions() {
return conditions;
}
public void setConditions(List<Condition> conditions) {
this.conditions = conditions;
}
public List<KVPair> getLabels() {
return labels;
}
public void setLabels(List<KVPair> labels) {
this.labels = labels;
}
@Override
public String toString() {
return "Rule{" +
"conditions=" + conditions +
", labels=" + labels +
'}';
}
}
}

@ -1,79 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import com.tencent.cloud.common.util.JacksonUtils;
import com.tencent.polaris.configuration.api.core.ConfigFile;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Fetch staining rule from polaris, and deserialize to {@link StainingRule}.
* @author lepdou 2022-07-07
*/
public class StainingRuleManager {
private static final Logger LOGGER = LoggerFactory.getLogger(StainingRuleManager.class);
private final RuleStainingProperties stainingProperties;
private final ConfigFileService configFileService;
private StainingRule stainingRule;
public StainingRuleManager(RuleStainingProperties stainingProperties, ConfigFileService configFileService) {
this.stainingProperties = stainingProperties;
this.configFileService = configFileService;
initStainingRule();
}
private void initStainingRule() {
ConfigFile rulesFile = configFileService.getConfigFile(stainingProperties.getNamespace(), stainingProperties.getGroup(),
stainingProperties.getFileName());
rulesFile.addChangeListener(event -> {
LOGGER.info("[SCT] update scg staining rules. {}", event);
deserialize(event.getNewValue());
});
String ruleJson = rulesFile.getContent();
LOGGER.info("[SCT] init scg staining rules. {}", ruleJson);
deserialize(ruleJson);
}
private void deserialize(String ruleJsonStr) {
if (StringUtils.isBlank(ruleJsonStr)) {
stainingRule = null;
return;
}
try {
stainingRule = JacksonUtils.deserialize(ruleJsonStr, StainingRule.class);
}
catch (Exception e) {
LOGGER.error("[SCT] deserialize staining rule error.", e);
throw e;
}
}
public StainingRule getStainingRule() {
return stainingRule;
}
}

@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tencent.cloud.plugin.gateway.SCGPluginsAutoConfiguration

@ -1,71 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway;
import com.tencent.cloud.plugin.gateway.staining.TrafficStainingGatewayFilter;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleStainingExecutor;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleStainingProperties;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleTrafficStainer;
import com.tencent.cloud.plugin.gateway.staining.rule.StainingRuleManager;
import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.factory.ConfigFileServiceFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
/**
* Test for {@link SCGPluginsAutoConfiguration}.
* @author derek.yi 2022-11-03
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = DEFINED_PORT, classes = SCGPluginsAutoConfigurationTest.TestApplication.class,
properties = {"server.port=48081", "spring.config.location = classpath:application-test.yml",
"spring.cloud.tencent.plugin.scg.staining.rule-staining.enabled = true"})
public class SCGPluginsAutoConfigurationTest {
@Autowired
private ApplicationContext applicationContext;
@Test
public void testAutoConfiguration() {
assertThat(applicationContext.getBeansOfType(RuleStainingProperties.class).size()).isEqualTo(1);
assertThat(applicationContext.getBeansOfType(StainingRuleManager.class).size()).isEqualTo(1);
assertThat(applicationContext.getBeansOfType(TrafficStainingGatewayFilter.class).size()).isEqualTo(1);
assertThat(applicationContext.getBeansOfType(RuleStainingExecutor.class).size()).isEqualTo(1);
assertThat(applicationContext.getBeansOfType(RuleTrafficStainer.class).size()).isEqualTo(1);
}
@SpringBootApplication
public static class TestApplication {
@Bean
public ConfigFileService configFileService(PolarisSDKContextManager polarisSDKContextManager) {
return ConfigFileServiceFactory.createConfigFileService(polarisSDKContextManager.getSDKContext());
}
}
}

@ -1,173 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleStainingExecutor;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleStainingProperties;
import com.tencent.cloud.plugin.gateway.staining.rule.RuleTrafficStainer;
import com.tencent.cloud.plugin.gateway.staining.rule.StainingRuleManager;
import com.tencent.polaris.configuration.api.core.ConfigFile;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test for {@link TrafficStainingGatewayFilter}.
* @author lepdou 2022-07-12
*/
@ExtendWith(MockitoExtension.class)
public class TrafficStainingGatewayFilterTest {
private final String testNamespace = "testNamespace";
private final String testGroup = "testGroup";
private final String testFileName = "rule.json";
@Mock
private GatewayFilterChain chain;
@Mock
private ServerWebExchange exchange;
@Mock
private ConfigFileService configFileService;
@BeforeAll
static void beforeAll() {
Mockito.mockStatic(ApplicationContextAwareUtils.class);
when(ApplicationContextAwareUtils
.getProperties(any())).thenReturn("fooBar");
}
@Test
public void testNoneTrafficStainingImplement() {
TrafficStainingGatewayFilter filter = new TrafficStainingGatewayFilter(null);
when(chain.filter(exchange)).thenReturn(Mono.empty());
filter.filter(exchange, chain);
verify(chain).filter(exchange);
}
@Test
public void testMultiStaining() {
TrafficStainer trafficStainer1 = Mockito.mock(TrafficStainer.class);
TrafficStainer trafficStainer2 = Mockito.mock(TrafficStainer.class);
when(trafficStainer1.getOrder()).thenReturn(1);
when(trafficStainer2.getOrder()).thenReturn(2);
Map<String, String> labels1 = new HashMap<>();
labels1.put("k1", "v1");
labels1.put("k2", "v2");
when(trafficStainer1.apply(exchange)).thenReturn(labels1);
Map<String, String> labels2 = new HashMap<>();
labels2.put("k1", "v11");
labels2.put("k3", "v3");
when(trafficStainer2.apply(exchange)).thenReturn(labels2);
TrafficStainingGatewayFilter filter = new TrafficStainingGatewayFilter(Arrays.asList(trafficStainer1, trafficStainer2));
Map<String, String> result = filter.getStainedLabels(exchange);
assertThat(result).isNotEmpty();
assertThat(result.get("k1")).isEqualTo("v1");
assertThat(result.get("k2")).isEqualTo("v2");
assertThat(result.get("k3")).isEqualTo("v3");
}
@Test
public void testNoTrafficStainers() {
MetadataContext metadataContext = new MetadataContext();
MetadataContextHolder.set(metadataContext);
TrafficStainingGatewayFilter filter = new TrafficStainingGatewayFilter(null);
filter.filter(exchange, chain);
Map<String, String> map = metadataContext.getTransitiveMetadata();
assertThat(map).isEmpty();
}
@Test
public void testWithTrafficStainers() {
MetadataContext metadataContext = new MetadataContext();
MetadataContextHolder.set(metadataContext);
RuleStainingProperties ruleStainingProperties = new RuleStainingProperties();
ruleStainingProperties.setNamespace(testNamespace);
ruleStainingProperties.setGroup(testGroup);
ruleStainingProperties.setFileName(testFileName);
ConfigFile configFile = Mockito.mock(ConfigFile.class);
when(configFile.getContent()).thenReturn("{\n"
+ " \"rules\":[\n"
+ " {\n"
+ " \"conditions\":[\n"
+ " {\n"
+ " \"key\":\"${http.query.uid}\",\n"
+ " \"values\":[\"1000\"],\n"
+ " \"operation\":\"EQUALS\"\n"
+ " }\n"
+ " ],\n"
+ " \"labels\":[\n"
+ " {\n"
+ " \"key\":\"env\",\n"
+ " \"value\":\"blue\"\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}");
when(configFileService.getConfigFile(testNamespace, testGroup, testFileName)).thenReturn(configFile);
StainingRuleManager stainingRuleManager = new StainingRuleManager(ruleStainingProperties, configFileService);
RuleStainingExecutor ruleStainingExecutor = new RuleStainingExecutor();
RuleTrafficStainer ruleTrafficStainer = new RuleTrafficStainer(stainingRuleManager, ruleStainingExecutor);
TrafficStainingGatewayFilter filter = new TrafficStainingGatewayFilter(Collections.singletonList(ruleTrafficStainer));
MockServerHttpRequest request = MockServerHttpRequest.get("/users")
.queryParam("uid", "1000").build();
MockServerWebExchange exchange = new MockServerWebExchange.Builder(request).build();
filter.filter(exchange, chain);
Map<String, String> map = metadataContext.getTransitiveMetadata();
assertThat(map).isNotNull();
assertThat(map.size()).isEqualTo(1);
assertThat(map.get("env")).isEqualTo("blue");
}
}

@ -1,188 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import com.tencent.cloud.common.rule.Condition;
import com.tencent.cloud.common.rule.KVPair;
import com.tencent.cloud.common.rule.Operation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link RuleStainingExecutor}.
* @author lepdou 2022-07-12
*/
@ExtendWith(MockitoExtension.class)
public class RuleStainingExecutorTest {
@Test
public void testMatchCondition() {
Condition condition1 = new Condition();
condition1.setKey("${http.header.uid}");
condition1.setOperation(Operation.EQUALS.toString());
condition1.setValues(Collections.singletonList("1000"));
Condition condition2 = new Condition();
condition2.setKey("${http.query.source}");
condition2.setOperation(Operation.IN.toString());
condition2.setValues(Collections.singletonList("wx"));
StainingRule.Rule rule = new StainingRule.Rule();
rule.setConditions(Arrays.asList(condition1, condition2));
KVPair kvPair = new KVPair();
kvPair.setKey("env");
kvPair.setValue("blue");
rule.setLabels(Collections.singletonList(kvPair));
StainingRule stainingRule = new StainingRule();
stainingRule.setRules(Collections.singletonList(rule));
MockServerHttpRequest request = MockServerHttpRequest.get("/users")
.queryParam("source", "wx")
.header("uid", "1000").build();
MockServerWebExchange exchange = new MockServerWebExchange.Builder(request).build();
RuleStainingExecutor executor = new RuleStainingExecutor();
Map<String, String> stainedLabels = executor.execute(exchange, stainingRule);
assertThat(stainedLabels).isNotNull();
assertThat(stainedLabels.size()).isEqualTo(1);
assertThat(stainedLabels.get("env")).isEqualTo("blue");
}
@Test
public void testNotMatchCondition() {
Condition condition1 = new Condition();
condition1.setKey("${http.header.uid}");
condition1.setOperation(Operation.EQUALS.toString());
condition1.setValues(Collections.singletonList("1000"));
Condition condition2 = new Condition();
condition2.setKey("${http.query.source}");
condition2.setOperation(Operation.IN.toString());
condition2.setValues(Collections.singletonList("wx"));
StainingRule.Rule rule = new StainingRule.Rule();
rule.setConditions(Arrays.asList(condition1, condition2));
KVPair kvPair = new KVPair();
kvPair.setKey("env");
kvPair.setValue("blue");
rule.setLabels(Collections.singletonList(kvPair));
StainingRule stainingRule = new StainingRule();
stainingRule.setRules(Collections.singletonList(rule));
MockServerHttpRequest request = MockServerHttpRequest.get("/users")
.queryParam("source", "wx")
.header("uid", "10001").build();
MockServerWebExchange exchange = new MockServerWebExchange.Builder(request).build();
RuleStainingExecutor executor = new RuleStainingExecutor();
Map<String, String> stainedLabels = executor.execute(exchange, stainingRule);
assertThat(stainedLabels).isNotNull();
assertThat(stainedLabels.size()).isEqualTo(0);
}
@Test
public void testMatchTwoRulesAndNotMatchOneRule() {
Condition condition1 = new Condition();
condition1.setKey("${http.header.uid}");
condition1.setOperation(Operation.EQUALS.toString());
condition1.setValues(Collections.singletonList("1000"));
Condition condition2 = new Condition();
condition2.setKey("${http.query.source}");
condition2.setOperation(Operation.IN.toString());
condition2.setValues(Collections.singletonList("wx"));
// rule1 matched
StainingRule.Rule rule1 = new StainingRule.Rule();
rule1.setConditions(Arrays.asList(condition1, condition2));
KVPair kvPair = new KVPair();
kvPair.setKey("env");
kvPair.setValue("blue");
rule1.setLabels(Collections.singletonList(kvPair));
// rule2 matched
StainingRule.Rule rule2 = new StainingRule.Rule();
rule2.setConditions(Collections.singletonList(condition1));
KVPair kvPair2 = new KVPair();
kvPair2.setKey("label1");
kvPair2.setValue("value1");
KVPair kvPair3 = new KVPair();
kvPair3.setKey("label2");
kvPair3.setValue("value2");
rule2.setLabels(Arrays.asList(kvPair2, kvPair3));
// rule3 not matched
Condition condition3 = new Condition();
condition3.setKey("${http.query.type}");
condition3.setOperation(Operation.IN.toString());
condition3.setValues(Collections.singletonList("wx"));
StainingRule.Rule rule3 = new StainingRule.Rule();
rule3.setConditions(Collections.singletonList(condition3));
KVPair kvPair4 = new KVPair();
kvPair4.setKey("label3");
kvPair4.setValue("value3");
rule3.setLabels(Collections.singletonList(kvPair4));
StainingRule stainingRule = new StainingRule();
stainingRule.setRules(Arrays.asList(rule1, rule2, rule3));
MockServerHttpRequest request = MockServerHttpRequest.get("/users")
.queryParam("source", "wx")
.header("uid", "1000").build();
MockServerWebExchange exchange = new MockServerWebExchange.Builder(request).build();
RuleStainingExecutor executor = new RuleStainingExecutor();
Map<String, String> stainedLabels = executor.execute(exchange, stainingRule);
assertThat(stainedLabels).isNotNull();
assertThat(stainedLabels.size()).isEqualTo(3);
assertThat(stainedLabels.get("env")).isEqualTo("blue");
assertThat(stainedLabels.get("label1")).isEqualTo("value1");
assertThat(stainedLabels.get("label2")).isEqualTo("value2");
}
@Test
public void testNoStainingRule() {
RuleStainingExecutor executor = new RuleStainingExecutor();
assertThat(executor.execute(null, null)).isEmpty();
assertThat(executor.execute(null, new StainingRule())).isEmpty();
}
}

@ -1,109 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import java.util.Map;
import com.tencent.polaris.configuration.api.core.ConfigFile;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
/**
* Test for {@link RuleTrafficStainer}.
* @author derek.yi 2022-11-03
*/
@ExtendWith(MockitoExtension.class)
public class RuleTrafficStainerTest {
private final String testNamespace = "testNamespace";
private final String testGroup = "testGroup";
private final String testFileName = "rule.json";
@Mock
private ConfigFileService configFileService;
@Test
public void testNoStainingRule() {
RuleStainingProperties ruleStainingProperties = new RuleStainingProperties();
ruleStainingProperties.setNamespace(testNamespace);
ruleStainingProperties.setGroup(testGroup);
ruleStainingProperties.setFileName(testFileName);
ConfigFile configFile = Mockito.mock(ConfigFile.class);
when(configFile.getContent()).thenReturn("");
when(configFileService.getConfigFile(testNamespace, testGroup, testFileName)).thenReturn(configFile);
StainingRuleManager stainingRuleManager = new StainingRuleManager(ruleStainingProperties, configFileService);
RuleStainingExecutor ruleStainingExecutor = new RuleStainingExecutor();
RuleTrafficStainer ruleTrafficStainer = new RuleTrafficStainer(stainingRuleManager, ruleStainingExecutor);
Map<String, String> map = ruleTrafficStainer.apply(null);
assertThat(map).isEmpty();
}
@Test
public void testWithStainingRule() {
RuleStainingProperties ruleStainingProperties = new RuleStainingProperties();
ruleStainingProperties.setNamespace(testNamespace);
ruleStainingProperties.setGroup(testGroup);
ruleStainingProperties.setFileName(testFileName);
ConfigFile configFile = Mockito.mock(ConfigFile.class);
when(configFile.getContent()).thenReturn("{\n"
+ " \"rules\":[\n"
+ " {\n"
+ " \"conditions\":[\n"
+ " {\n"
+ " \"key\":\"${http.query.uid}\",\n"
+ " \"values\":[\"1000\"],\n"
+ " \"operation\":\"EQUALS\"\n"
+ " }\n"
+ " ],\n"
+ " \"labels\":[\n"
+ " {\n"
+ " \"key\":\"env\",\n"
+ " \"value\":\"blue\"\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}");
when(configFileService.getConfigFile(testNamespace, testGroup, testFileName)).thenReturn(configFile);
StainingRuleManager stainingRuleManager = new StainingRuleManager(ruleStainingProperties, configFileService);
RuleStainingExecutor ruleStainingExecutor = new RuleStainingExecutor();
RuleTrafficStainer ruleTrafficStainer = new RuleTrafficStainer(stainingRuleManager, ruleStainingExecutor);
MockServerHttpRequest request = MockServerHttpRequest.get("/users")
.queryParam("uid", "1000").build();
MockServerWebExchange exchange = new MockServerWebExchange.Builder(request).build();
Map<String, String> map = ruleTrafficStainer.apply(exchange);
assertThat(map).isNotNull();
assertThat(map.size()).isEqualTo(1);
assertThat(map.get("env")).isEqualTo("blue");
}
}

@ -1,133 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.tencent.cloud.plugin.gateway.staining.rule;
import com.tencent.polaris.configuration.api.core.ConfigFile;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.Mockito.when;
/**
* Test for {@link StainingRuleManager}.
* @author lepdou 2022-07-12
*/
@ExtendWith(MockitoExtension.class)
public class StainingRuleManagerTest {
private final String testNamespace = "testNamespace";
private final String testGroup = "testGroup";
private final String testFileName = "rule.json";
@Mock
private ConfigFileService configFileService;
@Test
public void testNormalRule() {
RuleStainingProperties ruleStainingProperties = new RuleStainingProperties();
ruleStainingProperties.setNamespace(testNamespace);
ruleStainingProperties.setGroup(testGroup);
ruleStainingProperties.setFileName(testFileName);
ConfigFile configFile = Mockito.mock(ConfigFile.class);
when(configFile.getContent()).thenReturn("{\n"
+ " \"rules\":[\n"
+ " {\n"
+ " \"conditions\":[\n"
+ " {\n"
+ " \"key\":\"${http.query.uid}\",\n"
+ " \"values\":[\"1000\"],\n"
+ " \"operation\":\"EQUALS\"\n"
+ " }\n"
+ " ],\n"
+ " \"labels\":[\n"
+ " {\n"
+ " \"key\":\"env\",\n"
+ " \"value\":\"blue\"\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}");
when(configFileService.getConfigFile(testNamespace, testGroup, testFileName)).thenReturn(configFile);
StainingRuleManager stainingRuleManager = new StainingRuleManager(ruleStainingProperties, configFileService);
StainingRule stainingRule = stainingRuleManager.getStainingRule();
assertThat(stainingRule).isNotNull();
assertThat(stainingRule.getRules().size()).isEqualTo(1);
StainingRule.Rule rule = stainingRule.getRules().get(0);
assertThat(rule.getConditions().size()).isEqualTo(1);
assertThat(rule.getLabels().size()).isEqualTo(1);
}
@Test
public void testWrongRule() {
assertThatCode(() -> {
RuleStainingProperties ruleStainingProperties = new RuleStainingProperties();
ruleStainingProperties.setNamespace(testNamespace);
ruleStainingProperties.setGroup(testGroup);
ruleStainingProperties.setFileName(testFileName);
ConfigFile configFile = Mockito.mock(ConfigFile.class);
when(configFile.getContent()).thenReturn("{\n"
+ " \"rules\":[\n"
+ " {\n"
+ " \"conditionsxxxx\":[\n"
+ " {\n"
+ " \"key\":\"${http.query.uid}\",\n"
+ " \"values\":[\"1000\"],\n"
+ " \"operation\":\"EQUALS\"\n"
+ " }\n"
+ " ],\n"
+ " \"labels\":[\n"
+ " {\n"
+ " \"key\":\"env\",\n"
+ " \"value\":\"blue\"\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}");
when(configFileService.getConfigFile(testNamespace, testGroup, testFileName)).thenReturn(configFile);
new StainingRuleManager(ruleStainingProperties, configFileService);
}).isInstanceOf(RuntimeException.class);
}
@Test
public void testEmptyRule() {
RuleStainingProperties ruleStainingProperties = new RuleStainingProperties();
ruleStainingProperties.setNamespace(testNamespace);
ruleStainingProperties.setGroup(testGroup);
ruleStainingProperties.setFileName(testFileName);
ConfigFile configFile = Mockito.mock(ConfigFile.class);
when(configFile.getContent()).thenReturn(null);
when(configFileService.getConfigFile(testNamespace, testGroup, testFileName)).thenReturn(configFile);
StainingRuleManager stainingRuleManager = new StainingRuleManager(ruleStainingProperties, configFileService);
assertThat(stainingRuleManager.getStainingRule()).isNull();
}
}

@ -97,6 +97,7 @@ public class EnhancedFeignClient implements Client {
try {
// Run pre enhanced plugins.
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext);
startMillis = System.currentTimeMillis();
Response response = delegate.execute(request, options);
enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis);

@ -79,30 +79,12 @@ public class EnhancedRestTemplateWrapInterceptor {
enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance());
// Run pre enhanced plugins.
long startMillis = System.currentTimeMillis();
try {
// Run pre enhanced plugins.
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext);
}
catch (CallAbortedException callAbortedException) {
MetadataObjectValue<Object> fallbackResponseValue = MetadataContextHolder.get().
getMetadataContainer(MetadataType.APPLICATION, true).
getMetadataValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE);
boolean existFallback = Optional.ofNullable(fallbackResponseValue).
map(MetadataObjectValue::getObjectValue).map(Optional::isPresent).orElse(false);
if (existFallback) {
Object fallbackResponse = fallbackResponseValue.getObjectValue().orElse(null);
if (fallbackResponse instanceof ClientHttpResponse) {
return (ClientHttpResponse) fallbackResponse;
}
}
throw callAbortedException;
}
startMillis = System.currentTimeMillis();
long startMillis = System.currentTimeMillis();
try {
ClientHttpResponse response = delegate.execute(serviceId, loadBalancerRequest);
// get target instance after execute
enhancedPluginContext.setTargetServiceInstance((ServiceInstance) MetadataContextHolder.get()
@ -121,6 +103,22 @@ public class EnhancedRestTemplateWrapInterceptor {
return response;
}
catch (CallAbortedException callAbortedException) {
MetadataObjectValue<Object> fallbackResponseValue = MetadataContextHolder.get().
getMetadataContainer(MetadataType.APPLICATION, true).
getMetadataValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE);
boolean existFallback = Optional.ofNullable(fallbackResponseValue).
map(MetadataObjectValue::getObjectValue).map(Optional::isPresent).orElse(false);
if (existFallback) {
Object fallbackResponse = fallbackResponseValue.getObjectValue().orElse(null);
if (fallbackResponse instanceof ClientHttpResponse) {
return (ClientHttpResponse) fallbackResponse;
}
}
throw callAbortedException;
}
catch (IOException e) {
enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis);
enhancedPluginContext.setThrowable(e);

@ -21,7 +21,10 @@ import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.constant.OrderConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
@ -67,6 +70,19 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered {
String serviceId = Optional.ofNullable(serviceInstanceResponse).map(Response::getServer).
map(ServiceInstance::getServiceId).orElse(null);
MetadataContext metadataContext = (MetadataContext) originExchange.getAttributes().get(
MetadataConstant.HeaderName.METADATA_CONTEXT);
if (metadataContext != null) {
MetadataContextHolder.set(metadataContext);
}
else {
metadataContext = MetadataContextHolder.get();
}
String governanceNamespace = metadataContext.getContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, MetadataContext.LOCAL_NAMESPACE);
EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext();
EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder()
@ -74,6 +90,7 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered {
.httpMethod(originExchange.getRequest().getMethod())
.url(originExchange.getRequest().getURI())
.serviceUrl(getServiceUri(originExchange, serviceId))
.governanceNamespace(governanceNamespace)
.build();
enhancedPluginContext.setRequest(enhancedRequestContext);
enhancedPluginContext.setOriginRequest(originExchange);
@ -83,6 +100,8 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered {
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext);
}
catch (CallAbortedException e) {
// Run finally enhanced plugins.
pluginRunner.run(EnhancedPluginType.Client.FINALLY, enhancedPluginContext);
if (e.getFallbackInfo() == null) {
throw e;
}
@ -106,7 +125,8 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered {
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
enhancedPluginContext.getRequest().setUrl(uri);
if (uri != null) {
if (route != null && route.getUri().getScheme().contains("lb") && StringUtils.isNotEmpty(serviceId)) {
if (route != null && route.getUri().getScheme()
.contains("lb") && StringUtils.isNotEmpty(serviceId)) {
DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
serviceInstance.setServiceId(serviceId);
serviceInstance.setHost(uri.getHost());

@ -74,6 +74,9 @@ public class EnhancedWebClientExchangeFilterFunction implements ExchangeFilterFu
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext);
}
catch (CallAbortedException e) {
// Run finally enhanced plugins.
pluginRunner.run(EnhancedPluginType.Client.FINALLY, enhancedPluginContext);
if (e.getFallbackInfo() == null) {
throw e;
}

@ -37,6 +37,8 @@ public class EnhancedRequestContext {
private URI serviceUrl;
private String governanceNamespace;
public HttpMethod getHttpMethod() {
return httpMethod;
}
@ -69,6 +71,14 @@ public class EnhancedRequestContext {
this.serviceUrl = serviceUrl;
}
public String getGovernanceNamespace() {
return governanceNamespace;
}
public void setGovernanceNamespace(String governanceNamespace) {
this.governanceNamespace = governanceNamespace;
}
public static EnhancedContextRequestBuilder builder() {
return new EnhancedContextRequestBuilder();
}
@ -87,8 +97,8 @@ public class EnhancedRequestContext {
private HttpMethod httpMethod;
private HttpHeaders httpHeaders;
private URI url;
private URI serviceUrl;
private String governanceNamespace;
private EnhancedContextRequestBuilder() {
}
@ -113,12 +123,18 @@ public class EnhancedRequestContext {
return this;
}
public EnhancedContextRequestBuilder governanceNamespace(String governanceNamespace) {
this.governanceNamespace = governanceNamespace;
return this;
}
public EnhancedRequestContext build() {
EnhancedRequestContext enhancedRequestContext = new EnhancedRequestContext();
enhancedRequestContext.httpMethod = this.httpMethod;
enhancedRequestContext.url = this.url;
enhancedRequestContext.httpHeaders = this.httpHeaders;
enhancedRequestContext.serviceUrl = this.serviceUrl;
enhancedRequestContext.governanceNamespace = this.governanceNamespace;
return enhancedRequestContext;
}
}

@ -33,8 +33,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tencent.cloud.common.constant.HeaderConstant;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.constant.RouterConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.common.util.ApplicationContextAwareUtils;
import com.tencent.cloud.common.util.RequestLabelUtils;
import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties;
@ -133,7 +135,11 @@ public final class PolarisEnhancedPluginUtils {
public static ResourceStat createInstanceResourceStat(
@Nullable String calleeServiceName, @Nullable String calleeHost, @Nullable Integer calleePort,
URI uri, @Nullable Integer statusCode, long delay, @Nullable Throwable exception) {
ServiceKey calleeServiceKey = new ServiceKey(MetadataContext.LOCAL_NAMESPACE, StringUtils.isBlank(calleeServiceName) ? uri.getHost() : calleeServiceName);
String governanceNamespace = MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, MetadataContext.LOCAL_NAMESPACE);
ServiceKey calleeServiceKey = new ServiceKey(governanceNamespace, StringUtils.isBlank(calleeServiceName) ? uri.getHost() : calleeServiceName);
ServiceKey callerServiceKey = new ServiceKey(MetadataContext.LOCAL_NAMESPACE, MetadataContext.LOCAL_SERVICE);
Resource resource = new InstanceResource(
calleeServiceKey,

@ -17,6 +17,9 @@
package com.tencent.cloud.rpc.enhancement.transformer;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.common.metadata.MetadataContext;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.common.pojo.PolarisServiceInstance;
import com.tencent.polaris.api.pojo.DefaultInstance;
import com.tencent.polaris.api.utils.CollectionUtils;
@ -42,6 +45,11 @@ public class PolarisInstanceTransformer implements InstanceTransformer {
if (CollectionUtils.isNotEmpty(polarisServiceInstance.getServiceMetadata())) {
instance.setServiceMetadata(polarisServiceInstance.getServiceMetadata());
}
String namespace = MetadataContextHolder.get().getContext(MetadataContext.FRAGMENT_APPLICATION_NONE,
MetadataConstant.POLARIS_TARGET_NAMESPACE, instance.getNamespace());
instance.setNamespace(namespace);
}
}

@ -173,7 +173,7 @@ class EnhancedRestTemplateWrapInterceptorTest {
when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance);
doThrow(abortedException)
.when(pluginRunner)
.run(any(), any());
.run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class));
Object fallbackResponse = new MockClientHttpResponse();
MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true).

Loading…
Cancel
Save