Makes DiscoveryClient locator integration configurable.
fixes gh-185
This commit is contained in:
@@ -18,22 +18,20 @@
|
||||
package org.springframework.cloud.gateway.discovery;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.gateway.filter.FilterDefinition;
|
||||
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinition;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
|
||||
|
||||
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REGEXP_KEY;
|
||||
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REPLACEMENT_KEY;
|
||||
import static org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory.PATTERN_KEY;
|
||||
import static org.springframework.cloud.gateway.support.NameUtils.normalizeFilterFactoryName;
|
||||
import static org.springframework.cloud.gateway.support.NameUtils.normalizeRoutePredicateName;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* TODO: developer configuration, in zuul, this was opt out, should be opt in
|
||||
@@ -43,47 +41,72 @@ import reactor.core.publisher.Flux;
|
||||
public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLocator {
|
||||
|
||||
private final DiscoveryClient discoveryClient;
|
||||
private final DiscoveryLocatorProperties properties;
|
||||
private final String routeIdPrefix;
|
||||
|
||||
public DiscoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient) {
|
||||
public DiscoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
|
||||
this.discoveryClient = discoveryClient;
|
||||
this.routeIdPrefix = this.discoveryClient.getClass().getSimpleName() + "_";
|
||||
this.properties = properties;
|
||||
if (StringUtils.hasText(properties.getRouteIdPrefix())) {
|
||||
this.routeIdPrefix = properties.getRouteIdPrefix();
|
||||
} else {
|
||||
this.routeIdPrefix = this.discoveryClient.getClass().getSimpleName() + "_";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<RouteDefinition> getRouteDefinitions() {
|
||||
SimpleEvaluationContext evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
Expression includeExpr = parser.parseExpression(properties.getIncludeExpression());
|
||||
Expression urlExpr = parser.parseExpression(properties.getUrlExpression());
|
||||
|
||||
return Flux.fromIterable(discoveryClient.getServices())
|
||||
.map(serviceId -> {
|
||||
RouteDefinition routeDefinition = new RouteDefinition();
|
||||
routeDefinition.setId(this.routeIdPrefix + serviceId);
|
||||
routeDefinition.setUri(URI.create("lb://" + serviceId));
|
||||
.map(discoveryClient::getInstances)
|
||||
.filter(instances -> !instances.isEmpty())
|
||||
.map(instances -> instances.get(0))
|
||||
.filter(instance -> {
|
||||
Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
|
||||
if (include == null) {
|
||||
return false;
|
||||
}
|
||||
return include;
|
||||
})
|
||||
.map(instance -> {
|
||||
String serviceId = instance.getServiceId();
|
||||
|
||||
// add a predicate that matches the url at /serviceId
|
||||
/*PredicateDefinition barePredicate = new PredicateDefinition();
|
||||
barePredicate.setName(normalizePredicateName(PathRoutePredicate.class));
|
||||
barePredicate.addArg(PATTERN_KEY, "/" + serviceId);
|
||||
routeDefinition.getPredicates().add(barePredicate);*/
|
||||
RouteDefinition routeDefinition = new RouteDefinition();
|
||||
routeDefinition.setId(this.routeIdPrefix + serviceId);
|
||||
String uri = urlExpr.getValue(evalCtxt, instance, String.class);
|
||||
routeDefinition.setUri(URI.create(uri));
|
||||
|
||||
// add a predicate that matches the url at /serviceId/**
|
||||
PredicateDefinition subPredicate = new PredicateDefinition();
|
||||
subPredicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class));
|
||||
subPredicate.addArg(PATTERN_KEY, "/" + serviceId + "/**");
|
||||
routeDefinition.getPredicates().add(subPredicate);
|
||||
for (PredicateDefinition original : this.properties.getPredicates()) {
|
||||
PredicateDefinition predicate = new PredicateDefinition();
|
||||
predicate.setName(original.getName());
|
||||
for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
|
||||
String value = getValueFromExpr(evalCtxt, parser, instance, entry);
|
||||
predicate.addArg(entry.getKey(), value);
|
||||
}
|
||||
routeDefinition.getPredicates().add(predicate);
|
||||
}
|
||||
|
||||
//TODO: support for other default predicates
|
||||
for (FilterDefinition original : this.properties.getFilters()) {
|
||||
FilterDefinition filter = new FilterDefinition();
|
||||
filter.setName(original.getName());
|
||||
for (Map.Entry<String, String> entry : original.getArgs().entrySet()) {
|
||||
String value = getValueFromExpr(evalCtxt, parser, instance, entry);
|
||||
filter.addArg(entry.getKey(), value);
|
||||
}
|
||||
routeDefinition.getFilters().add(filter);
|
||||
}
|
||||
|
||||
// add a filter that removes /serviceId by default
|
||||
FilterDefinition filter = new FilterDefinition();
|
||||
filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));
|
||||
String regex = "/" + serviceId + "/(?<remaining>.*)";
|
||||
String replacement = "/${remaining}";
|
||||
filter.addArg(REGEXP_KEY, regex);
|
||||
filter.addArg(REPLACEMENT_KEY, replacement);
|
||||
routeDefinition.getFilters().add(filter);
|
||||
|
||||
//TODO: support for default filters
|
||||
|
||||
return routeDefinition;
|
||||
return routeDefinition;
|
||||
});
|
||||
}
|
||||
|
||||
String getValueFromExpr(SimpleEvaluationContext evalCtxt, SpelExpressionParser parser, ServiceInstance instance, Map.Entry<String, String> entry) {
|
||||
Expression valueExpr = parser.parseExpression(entry.getValue());
|
||||
return valueExpr.getValue(evalCtxt, instance, String.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2013-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.discovery;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.gateway.filter.FilterDefinition;
|
||||
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
|
||||
@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
|
||||
public class DiscoveryLocatorProperties {
|
||||
|
||||
/** Flag that enables DiscoveryClient gateway integration */
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* The prefix for the routeId, defaults to discoveryClient.getClass().getSimpleName() + "_".
|
||||
* Service Id will be appended to create the routeId.
|
||||
*/
|
||||
private String routeIdPrefix;
|
||||
|
||||
/**
|
||||
* SpEL expression that will evaluate whether to include a service in gateway integration or not,
|
||||
* defaults to: true
|
||||
*/
|
||||
private String includeExpression = "true";
|
||||
|
||||
/** SpEL expression that create the uri for each route, defaults to: 'lb://'+serviceId */
|
||||
private String urlExpression = "'lb://'+serviceId";
|
||||
|
||||
private List<PredicateDefinition> predicates = new ArrayList<>();
|
||||
|
||||
private List<FilterDefinition> filters = new ArrayList<>();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getRouteIdPrefix() {
|
||||
return routeIdPrefix;
|
||||
}
|
||||
|
||||
public void setRouteIdPrefix(String routeIdPrefix) {
|
||||
this.routeIdPrefix = routeIdPrefix;
|
||||
}
|
||||
|
||||
public String getIncludeExpression() {
|
||||
return includeExpression;
|
||||
}
|
||||
|
||||
public void setIncludeExpression(String includeExpression) {
|
||||
this.includeExpression = includeExpression;
|
||||
}
|
||||
|
||||
public String getUrlExpression() {
|
||||
return urlExpression;
|
||||
}
|
||||
|
||||
public void setUrlExpression(String urlExpression) {
|
||||
this.urlExpression = urlExpression;
|
||||
}
|
||||
|
||||
public List<PredicateDefinition> getPredicates() {
|
||||
return predicates;
|
||||
}
|
||||
|
||||
public void setPredicates(List<PredicateDefinition> predicates) {
|
||||
this.predicates = predicates;
|
||||
}
|
||||
|
||||
public List<FilterDefinition> getFilters() {
|
||||
return filters;
|
||||
}
|
||||
|
||||
public void setFilters(List<FilterDefinition> filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("enabled", enabled)
|
||||
.append("routeIdPrefix", routeIdPrefix)
|
||||
.append("includeExpression", includeExpression)
|
||||
.append("urlExpression", urlExpression)
|
||||
.append("predicates", predicates)
|
||||
.append("filters", filters)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,26 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
|
||||
import org.springframework.cloud.gateway.filter.FilterDefinition;
|
||||
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REGEXP_KEY;
|
||||
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REPLACEMENT_KEY;
|
||||
import static org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory.PATTERN_KEY;
|
||||
import static org.springframework.cloud.gateway.support.NameUtils.normalizeFilterFactoryName;
|
||||
import static org.springframework.cloud.gateway.support.NameUtils.normalizeRoutePredicateName;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@@ -33,13 +47,50 @@ import org.springframework.web.reactive.DispatcherHandler;
|
||||
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
|
||||
@AutoConfigureBefore(GatewayAutoConfiguration.class)
|
||||
@ConditionalOnClass({DispatcherHandler.class, DiscoveryClient.class})
|
||||
@EnableConfigurationProperties
|
||||
public class GatewayDiscoveryClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(DiscoveryClient.class)
|
||||
@ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled")
|
||||
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient) {
|
||||
return new DiscoveryClientRouteDefinitionLocator(discoveryClient);
|
||||
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
|
||||
DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
|
||||
return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DiscoveryLocatorProperties discoveryLocatorProperties() {
|
||||
DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();
|
||||
properties.setPredicates(initPredicates());
|
||||
properties.setFilters(initFilters());
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static List<PredicateDefinition> initPredicates() {
|
||||
ArrayList<PredicateDefinition> definitions = new ArrayList<>();
|
||||
// TODO: add a predicate that matches the url at /serviceId?
|
||||
|
||||
// add a predicate that matches the url at /serviceId/**
|
||||
PredicateDefinition predicate = new PredicateDefinition();
|
||||
predicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class));
|
||||
predicate.addArg(PATTERN_KEY, "'/'+serviceId+'/**'");
|
||||
definitions.add(predicate);
|
||||
return definitions;
|
||||
}
|
||||
|
||||
public static List<FilterDefinition> initFilters() {
|
||||
ArrayList<FilterDefinition> definitions = new ArrayList<>();
|
||||
|
||||
// add a filter that removes /serviceId by default
|
||||
FilterDefinition filter = new FilterDefinition();
|
||||
filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));
|
||||
String regex = "'/' + serviceId + '/(?<remaining>.*)'";
|
||||
String replacement = "'/${remaining}'";
|
||||
filter.addArg(REGEXP_KEY, regex);
|
||||
filter.addArg(REPLACEMENT_KEY, replacement);
|
||||
definitions.add(filter);
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2013-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.discovery;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.gateway.filter.FilterDefinition;
|
||||
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REGEXP_KEY;
|
||||
import static org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory.REPLACEMENT_KEY;
|
||||
import static org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory.PATTERN_KEY;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = DiscoveryClientRouteDefinitionLocatorTests.Config.class,
|
||||
properties = {"spring.cloud.gateway.discovery.locator.enabled=true",
|
||||
"spring.cloud.gateway.discovery.locator.route-id-prefix=testedge_",
|
||||
"spring.cloud.gateway.discovery.locator.include-expression=metadata['edge'] == 'true'"})
|
||||
public class DiscoveryClientRouteDefinitionLocatorTests {
|
||||
|
||||
@Autowired(required = false)
|
||||
private DiscoveryClientRouteDefinitionLocator locator;
|
||||
|
||||
@Test
|
||||
public void includeExpressionWorks() {
|
||||
assertThat(locator)
|
||||
.as("DiscoveryClientRouteDefinitionLocator was null")
|
||||
.isNotNull();
|
||||
|
||||
List<RouteDefinition> definitions = locator.getRouteDefinitions().collectList().block();
|
||||
assertThat(definitions).hasSize(1);
|
||||
|
||||
RouteDefinition definition = definitions.get(0);
|
||||
assertThat(definition.getId()).isEqualTo("testedge_service1");
|
||||
assertThat(definition.getUri()).hasScheme("lb")
|
||||
.hasHost("service1");
|
||||
|
||||
assertThat(definition.getPredicates()).hasSize(1);
|
||||
PredicateDefinition predicate = definition.getPredicates().get(0);
|
||||
assertThat(predicate.getName()).isEqualTo("Path");
|
||||
assertThat(predicate.getArgs()).hasSize(1).containsEntry(PATTERN_KEY, "/service1/**");
|
||||
|
||||
assertThat(definition.getFilters()).hasSize(1);
|
||||
FilterDefinition filter = definition.getFilters().get(0);
|
||||
assertThat(filter.getName()).isEqualTo("RewritePath");
|
||||
assertThat(filter.getArgs()).hasSize(2)
|
||||
.containsEntry(REGEXP_KEY, "/service1/(?<remaining>.*)")
|
||||
.containsEntry(REPLACEMENT_KEY, "/${remaining}");
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
protected static class Config {
|
||||
|
||||
@Bean
|
||||
DiscoveryClient discoveryClient() {
|
||||
DiscoveryClient discoveryClient = mock(DiscoveryClient.class);
|
||||
when(discoveryClient.getServices()).thenReturn(Arrays.asList("service1", "service2"));
|
||||
whenInstance(discoveryClient, "service1", Collections.singletonMap("edge", "true"));
|
||||
whenInstance(discoveryClient, "service2", Collections.emptyMap());
|
||||
return discoveryClient;
|
||||
}
|
||||
|
||||
private void whenInstance(DiscoveryClient discoveryClient, String serviceId, Map<String, String> metadata) {
|
||||
DefaultServiceInstance instance1 = new DefaultServiceInstance(serviceId, "localhost", 8001,
|
||||
false, metadata);
|
||||
when(discoveryClient.getInstances(serviceId)).
|
||||
thenReturn(Collections.singletonList(instance1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.gateway.discovery.DiscoveryClientRouteDefinitionLocator;
|
||||
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
@@ -42,8 +43,9 @@ public class GatewayTestApplication {
|
||||
protected static class GatewayDiscoveryConfiguration {
|
||||
|
||||
@Bean
|
||||
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteLocator(DiscoveryClient discoveryClient) {
|
||||
return new DiscoveryClientRouteDefinitionLocator(discoveryClient);
|
||||
public DiscoveryClientRouteDefinitionLocator discoveryClientRouteLocator(DiscoveryClient discoveryClient,
|
||||
DiscoveryLocatorProperties properties) {
|
||||
return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user