Makes DiscoveryClient locator integration configurable.

fixes gh-185
This commit is contained in:
Spencer Gibb
2018-04-23 22:21:30 -04:00
parent 1a3de526dd
commit d894d31805
5 changed files with 336 additions and 43 deletions

View File

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

View File

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

View File

@@ -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;
}
}

View File

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

View File

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