diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java index fd31bf68..47214e00 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java @@ -16,24 +16,30 @@ package org.springframework.cloud.gateway.actuate; +import java.io.IOException; import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory; +import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionLocator; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; @@ -42,6 +48,9 @@ import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.Ordered; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; @@ -51,6 +60,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ResponseStatusException; @@ -76,16 +87,77 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis protected ApplicationEventPublisher publisher; + protected WebEndpointProperties webEndpointProperties; + + private final SimpleMetadataReaderFactory simpleMetadataReaderFactory = new SimpleMetadataReaderFactory(); + + @Deprecated public AbstractGatewayControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, List globalFilters, List gatewayFilters, List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { + this(routeDefinitionLocator, globalFilters, gatewayFilters, routePredicates, + routeDefinitionWriter, routeLocator, new WebEndpointProperties()); + } + + public AbstractGatewayControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, + List globalFilters, List gatewayFilters, + List routePredicates, RouteDefinitionWriter routeDefinitionWriter, + RouteLocator routeLocator, WebEndpointProperties webEndpointProperties) { this.routeDefinitionLocator = routeDefinitionLocator; this.globalFilters = globalFilters; this.GatewayFilters = gatewayFilters; this.routePredicates = routePredicates; this.routeDefinitionWriter = routeDefinitionWriter; this.routeLocator = routeLocator; + this.webEndpointProperties = webEndpointProperties; + } + + @GetMapping("/") + Mono> getEndpoints() { + List endpoints = mergeEndpoints( + getAvailableEndpointsForClass(AbstractGatewayControllerEndpoint.class.getName()), + getAvailableEndpointsForClass(GatewayControllerEndpoint.class.getName())); + + return Flux.fromIterable(endpoints).map(p -> p) + .flatMap(path -> this.routeLocator.getRoutes().map(r -> generateHref(r, path)).distinct().collectList() + .flatMapMany(Flux::fromIterable)) + .distinct() // Ensure overall uniqueness + .collectList(); + } + + private List mergeEndpoints(List listA, + List listB) { + Map> mergedMap = new HashMap<>(); + + Stream.concat(listA.stream(), listB.stream()).forEach(e -> mergedMap + .computeIfAbsent(e.getHref(), k -> new ArrayList<>()).addAll(Arrays.asList(e.getMethods()))); + + return mergedMap.entrySet().stream().map(entry -> new GatewayEndpointInfo(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private List getAvailableEndpointsForClass(String className) { + try { + MetadataReader metadataReader = simpleMetadataReaderFactory.getMetadataReader(className); + Set annotatedMethods = metadataReader.getAnnotationMetadata() + .getAnnotatedMethods(RequestMapping.class.getName()); + + String gatewayActuatorPath = webEndpointProperties.getBasePath() + "/gateway"; + return annotatedMethods.stream().map(method -> new GatewayEndpointInfo(gatewayActuatorPath + + ((String[]) method.getAnnotationAttributes(RequestMapping.class.getName()).get("path"))[0], + ((RequestMethod[]) method.getAnnotationAttributes(RequestMapping.class.getName()).get("method"))[0] + .name())) + .collect(Collectors.toList()); + } + catch (IOException exception) { + log.warn(exception.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, exception.getMessage()); + } + } + + private GatewayEndpointInfo generateHref(Route r, GatewayEndpointInfo path) { + return new GatewayEndpointInfo(path.getHref().replace("{id}", r.getId()), Arrays.asList(path.getMethods())); } @Override diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java index 109f2888..34553f86 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java @@ -24,6 +24,7 @@ import java.util.Map; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GlobalFilter; @@ -47,9 +48,10 @@ public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint public GatewayControllerEndpoint(List globalFilters, List gatewayFilters, List routePredicates, RouteDefinitionWriter routeDefinitionWriter, - RouteLocator routeLocator, RouteDefinitionLocator routeDefinitionLocator) { - super(routeDefinitionLocator, globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, - routeLocator); + RouteLocator routeLocator, RouteDefinitionLocator routeDefinitionLocator, + WebEndpointProperties webEndpointProperties) { + super(routeDefinitionLocator, globalFilters, gatewayFilters, routePredicates, + routeDefinitionWriter, routeLocator, webEndpointProperties); } @GetMapping("/routedefinitions") diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayEndpointInfo.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayEndpointInfo.java new file mode 100644 index 00000000..ffb1cf21 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayEndpointInfo.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.actuate; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * @author Marta Medio + */ +class GatewayEndpointInfo { + + private String href; + + private List methods; + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String[] getMethods() { + return methods.stream().toArray(String[]::new); + } + + GatewayEndpointInfo(String href, String method) { + this.href = href; + this.methods = Collections.singletonList(method); + } + + GatewayEndpointInfo(String href, List methods) { + this.href = href; + this.methods = methods; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GatewayEndpointInfo that = (GatewayEndpointInfo) o; + return Objects.equals(href, that.href) && Objects.equals(methods, that.methods); + } + + @Override + public int hashCode() { + return Objects.hash(href, methods); + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java index 5e2064cb..32f9b184 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java @@ -23,6 +23,7 @@ import java.util.Map; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GlobalFilter; @@ -47,9 +48,9 @@ public class GatewayLegacyControllerEndpoint extends AbstractGatewayControllerEn public GatewayLegacyControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, List globalFilters, List gatewayFilterFactories, List routePredicates, RouteDefinitionWriter routeDefinitionWriter, - RouteLocator routeLocator) { + RouteLocator routeLocator, WebEndpointProperties webEndpointProperties) { super(routeDefinitionLocator, globalFilters, gatewayFilterFactories, routePredicates, routeDefinitionWriter, - routeLocator); + routeLocator, webEndpointProperties); } @GetMapping("/routes") diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 534277df..2da24fe6 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -817,9 +818,9 @@ public class GatewayAutoConfiguration { public GatewayControllerEndpoint gatewayControllerEndpoint(List globalFilters, List gatewayFilters, List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator, - RouteDefinitionLocator routeDefinitionLocator) { + RouteDefinitionLocator routeDefinitionLocator, WebEndpointProperties webEndpointProperties) { return new GatewayControllerEndpoint(globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, - routeLocator, routeDefinitionLocator); + routeLocator, routeDefinitionLocator, webEndpointProperties); } @Bean @@ -828,9 +829,10 @@ public class GatewayAutoConfiguration { public GatewayLegacyControllerEndpoint gatewayLegacyControllerEndpoint( RouteDefinitionLocator routeDefinitionLocator, List globalFilters, List gatewayFilters, List routePredicates, - RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { + RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator, + WebEndpointProperties webEndpointProperties) { return new GatewayLegacyControllerEndpoint(routeDefinitionLocator, globalFilters, gatewayFilters, - routePredicates, routeDefinitionWriter, routeLocator); + routePredicates, routeDefinitionWriter, routeLocator, webEndpointProperties); } } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java index 193761a5..05878ff5 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java @@ -68,6 +68,34 @@ public class GatewayControllerEndpointTests { @LocalServerPort int port; + @Test + public void testEndpoints() { + testClient.get().uri("http://localhost:" + port + "/actuator/gateway").exchange() + .expectStatus().isOk().expectBodyList(Map.class).consumeWith(result -> { + List responseBody = result.getResponseBody(); + assertThat(responseBody).isNotEmpty(); + assertThat(responseBody).contains( + Map.of("href", "/actuator/gateway/", "methods", + List.of("GET")), + Map.of("href", "/actuator/gateway/globalfilters", "methods", + List.of("GET")), + Map.of("href", "/actuator/gateway/refresh", "methods", + List.of("POST")), + Map.of("href", "/actuator/gateway/routedefinitions", + "methods", List.of("GET")), + Map.of("href", "/actuator/gateway/routefilters", "methods", + List.of("GET")), + Map.of("href", "/actuator/gateway/routepredicates", "methods", + List.of("GET")), + Map.of("href", "/actuator/gateway/routes", "methods", + List.of("POST", "GET")), + Map.of("href", "/actuator/gateway/routes/test-service", + "methods", List.of("POST", "DELETE", "GET")), + Map.of("href", "/actuator/gateway/routes/route_with_metadata", + "methods", List.of("POST", "DELETE", "GET"))); + }); + } + @Test public void testRefresh() { testClient.post().uri("http://localhost:" + port + "/actuator/gateway/refresh").exchange().expectStatus()