diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java index 761a4a3bbd..220eb97f71 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java @@ -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 @@ 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 getRouteDefinitions() { - return Flux.fromIterable(discoveryClient.getServices()) - .map(serviceId -> { - RouteDefinition routeDefinition = new RouteDefinition(); - routeDefinition.setId(this.routeIdPrefix + serviceId); - routeDefinition.setUri(URI.create("lb://" + serviceId)); + SimpleEvaluationContext evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().build(); - // 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);*/ + SpelExpressionParser parser = new SpelExpressionParser(); + Expression includeExpr = parser.parseExpression(properties.getIncludeExpression()); + Expression urlExpr = parser.parseExpression(properties.getUrlExpression()); - // 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); + return Flux.fromIterable(discoveryClient.getServices()) + .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(); - //TODO: support for other default predicates + RouteDefinition routeDefinition = new RouteDefinition(); + routeDefinition.setId(this.routeIdPrefix + serviceId); + String uri = urlExpr.getValue(evalCtxt, instance, String.class); + routeDefinition.setUri(URI.create(uri)); - // add a filter that removes /serviceId by default - FilterDefinition filter = new FilterDefinition(); - filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class)); - String regex = "/" + serviceId + "/(?.*)"; - String replacement = "/${remaining}"; - filter.addArg(REGEXP_KEY, regex); - filter.addArg(REPLACEMENT_KEY, replacement); - routeDefinition.getFilters().add(filter); + for (PredicateDefinition original : this.properties.getPredicates()) { + PredicateDefinition predicate = new PredicateDefinition(); + predicate.setName(original.getName()); + for (Map.Entry entry : original.getArgs().entrySet()) { + String value = getValueFromExpr(evalCtxt, parser, instance, entry); + predicate.addArg(entry.getKey(), value); + } + routeDefinition.getPredicates().add(predicate); + } - //TODO: support for default filters + for (FilterDefinition original : this.properties.getFilters()) { + FilterDefinition filter = new FilterDefinition(); + filter.setName(original.getName()); + for (Map.Entry entry : original.getArgs().entrySet()) { + String value = getValueFromExpr(evalCtxt, parser, instance, entry); + filter.addArg(entry.getKey(), value); + } + routeDefinition.getFilters().add(filter); + } - return routeDefinition; + return routeDefinition; }); } + + String getValueFromExpr(SimpleEvaluationContext evalCtxt, SpelExpressionParser parser, ServiceInstance instance, Map.Entry entry) { + Expression valueExpr = parser.parseExpression(entry.getValue()); + return valueExpr.getValue(evalCtxt, instance, String.class); + } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryLocatorProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryLocatorProperties.java new file mode 100644 index 0000000000..8de0eacba3 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryLocatorProperties.java @@ -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 predicates = new ArrayList<>(); + + private List 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 getPredicates() { + return predicates; + } + + public void setPredicates(List predicates) { + this.predicates = predicates; + } + + public List getFilters() { + return filters; + } + + public void setFilters(List 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(); + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfiguration.java index c0d9d160a9..e7bb34e241 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/GatewayDiscoveryClientAutoConfiguration.java @@ -20,12 +20,26 @@ 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 @@ @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 initPredicates() { + ArrayList 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 initFilters() { + ArrayList definitions = new ArrayList<>(); + + // add a filter that removes /serviceId by default + FilterDefinition filter = new FilterDefinition(); + filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class)); + String regex = "'/' + serviceId + '/(?.*)'"; + String replacement = "'/${remaining}'"; + filter.addArg(REGEXP_KEY, regex); + filter.addArg(REPLACEMENT_KEY, replacement); + definitions.add(filter); + + return definitions; } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocatorTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocatorTests.java new file mode 100644 index 0000000000..18b33ad710 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocatorTests.java @@ -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 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/(?.*)") + .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 metadata) { + DefaultServiceInstance instance1 = new DefaultServiceInstance(serviceId, "localhost", 8001, + false, metadata); + when(discoveryClient.getInstances(serviceId)). + thenReturn(Collections.singletonList(instance1)); + } + } +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java index 95e6deebf4..bf6f6ce40a 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java @@ -23,6 +23,7 @@ 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); } }