diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 60e5cba7..d29f7a2a 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -2952,11 +2952,17 @@ The following table describes the structure of the response: |=== -=== Creating and Deleting a Particular Route +=== Creating and Deleting a Particular Route Definition -To create a route, make a `POST` request to `/gateway/routes/{id_route_to_create}` with a JSON body that specifies the fields of the route (see <>). +To create a route definition, make a `POST` request to `/gateway/routes/{id_route_to_create}` with a JSON body that specifies the fields of the route (see <>). -To delete a route, make a `DELETE` request to `/gateway/routes/{id_route_to_delete}`. +To delete a route definition, make a `DELETE` request to `/gateway/routes/{id_route_to_delete}`. + +=== Creating multiple Route Definitions + +To create multiple route definitions in a single request, make a `POST` request to `/gateway/routes` with a JSON body that specifies the fields of the route, including the route id (see <>). + +The route definitions will be discarded if any route raises an error during the creation of the routes. === Recap: The List of All endpoints 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 bcec1fad..194d984a 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 @@ -145,6 +145,28 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis .switchIfEmpty(Mono.defer(() -> Mono.just(ResponseEntity.badRequest().build()))); } + @PostMapping("/routes") + @SuppressWarnings("unchecked") + public Mono> save(@RequestBody List routes) { + routes.stream().forEach(routeDef -> { + validateRouteDefinition(routeDef); + validateRouteId(routeDef); + }); + + return Flux.fromIterable(routes) + .flatMap(routeDefinition -> this.routeDefinitionWriter.save(Mono.just(routeDefinition).map(r -> { + log.debug("Saving route: " + routeDefinition); + return r; + }))).then(Mono.defer(() -> Mono.just(ResponseEntity.ok().build()))) + .switchIfEmpty(Mono.defer(() -> Mono.just(ResponseEntity.badRequest().build()))); + } + + private void validateRouteId(RouteDefinition routeDefinition) { + if(routeDefinition.getId() == null) { + handleError("Saving multiple routes require specifying the ID for every route"); + } + } + private void validateRouteDefinition(RouteDefinition routeDefinition) { Set unavailableFilterDefinitions = routeDefinition.getFilters().stream().filter(rd -> !isAvailable(rd)) .map(FilterDefinition::getName).collect(Collectors.toSet()); 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 1ee29b84..918dfe1a 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 @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.Predicate; import org.assertj.core.util.Maps; @@ -141,7 +142,6 @@ public class GatewayControllerEndpointTests { @Test public void testPostValidRouteDefinition() { - RouteDefinition testRouteDefinition = new RouteDefinition(); testRouteDefinition.setUri(URI.create("http://example.org")); @@ -162,6 +162,91 @@ public class GatewayControllerEndpointTests { .expectStatus().isCreated(); } + @Test + public void testPostMultipleValidRouteDefinitions() { + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setUri(URI.create("http://example.org")); + String routeId1 = UUID.randomUUID().toString(); + testRouteDefinition.setId(routeId1); + + FilterDefinition prefixPathFilterDefinition = new FilterDefinition("PrefixPath=/test-path"); + FilterDefinition redirectToFilterDefinition = new FilterDefinition("RemoveResponseHeader=Sensitive-Header"); + FilterDefinition testFilterDefinition = new FilterDefinition("TestFilter"); + testRouteDefinition.setFilters( + Arrays.asList(prefixPathFilterDefinition, redirectToFilterDefinition, testFilterDefinition)); + + PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition("Host=myhost.org"); + PredicateDefinition methodRoutePredicateDefinition = new PredicateDefinition("Method=GET"); + PredicateDefinition testPredicateDefinition = new PredicateDefinition("Test=value"); + testRouteDefinition.setPredicates( + Arrays.asList(hostRoutePredicateDefinition, methodRoutePredicateDefinition, testPredicateDefinition)); + + RouteDefinition testRouteDefinition2 = new RouteDefinition(); + testRouteDefinition2.setUri(URI.create("http://example-2.org")); + String routeId2 = UUID.randomUUID().toString(); + testRouteDefinition2.setId(routeId2); + + FilterDefinition prefixPathFilterDefinition2 = new FilterDefinition("PrefixPath=/test-path-2"); + FilterDefinition redirectToFilterDefinition2 = new FilterDefinition("RemoveResponseHeader=Sensitive-Header-2"); + FilterDefinition testFilterDefinition2 = new FilterDefinition("TestFilter"); + testRouteDefinition2.setFilters( + Arrays.asList(prefixPathFilterDefinition2, redirectToFilterDefinition2, testFilterDefinition2)); + + PredicateDefinition hostRoutePredicateDefinition2 = new PredicateDefinition("Host=myhost-2.org"); + PredicateDefinition methodRoutePredicateDefinition2 = new PredicateDefinition("Method=GET"); + PredicateDefinition testPredicateDefinition2 = new PredicateDefinition("Test=value-2"); + testRouteDefinition2.setPredicates( + Arrays.asList(hostRoutePredicateDefinition2, methodRoutePredicateDefinition2, testPredicateDefinition2) + ); + + List multipleRouteDefs = List.of(testRouteDefinition, testRouteDefinition2); + + testClient.post().uri("http://localhost:" + port + "/actuator/gateway/routes") + .accept(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(multipleRouteDefs)).exchange() + .expectStatus().isOk(); + testClient.get().uri("http://localhost:" + port + "/actuator/gateway/routedefinitions") + .accept(MediaType.APPLICATION_JSON).exchange() + .expectBody() + .jsonPath("[?(@.id in ['%s','%s'])].id".formatted(routeId1, routeId2)).exists(); + } + + @Test + public void testPostMultipleRoutesWithOneWrong_doesntPersistRouteDefinitions() { + + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setUri(URI.create("http://example.org")); + String routeId1 = UUID.randomUUID().toString(); + testRouteDefinition.setId(routeId1); + + FilterDefinition prefixPathFilterDefinition = new FilterDefinition("PrefixPath=/test-path"); + FilterDefinition redirectToFilterDefinition = new FilterDefinition("RemoveResponseHeader=Sensitive-Header"); + FilterDefinition testFilterDefinition = new FilterDefinition("TestFilter"); + testRouteDefinition.setFilters( + Arrays.asList(prefixPathFilterDefinition, redirectToFilterDefinition, testFilterDefinition)); + + PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition("Host=myhost.org"); + PredicateDefinition methodRoutePredicateDefinition = new PredicateDefinition("Method=GET"); + PredicateDefinition testPredicateDefinition = new PredicateDefinition("Test=value"); + testRouteDefinition.setPredicates( + Arrays.asList(hostRoutePredicateDefinition, methodRoutePredicateDefinition, testPredicateDefinition)); + + RouteDefinition testRouteDefinition2 = new RouteDefinition(); + testRouteDefinition2.setUri(URI.create("this-is-wrong")); + String routeId2 = UUID.randomUUID().toString(); + testRouteDefinition2.setId(routeId2); + + List multipleRouteDefs = List.of(testRouteDefinition, testRouteDefinition2); + + testClient.post().uri("http://localhost:" + port + "/actuator/gateway/routes") + .accept(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(multipleRouteDefs)).exchange() + .expectStatus().is4xxClientError(); + + testClient.get().uri("http://localhost:" + port + "/actuator/gateway/routedefinitions") + .accept(MediaType.APPLICATION_JSON).exchange() + .expectBody() + .jsonPath("[?(@.id in ['%s','%s'])].id".formatted(routeId1, routeId2)).doesNotExist(); + } + @Test public void testPostValidShortcutRouteDefinition() { RouteDefinition testRouteDefinition = new RouteDefinition();