Merge branch 'dennis-menge-master'

This commit is contained in:
spencergibb
2021-07-27 18:13:51 -04:00
6 changed files with 349 additions and 0 deletions

View File

@@ -2518,6 +2518,15 @@ The folloiwng table below summarizes the Spring Cloud Gateway actuator endpoints
|===
=== Sharing Routes between multiple Gateway instances
Spring Cloud Gateway offers two `RouteDefinitionRepository` implementations. The first one is the
`InMemoryRouteDefinitionRepository` which only lives within the memory of one Gateway instance.
This type of Repository is not suited to populate Routes across multiple Gateway instances.
In order to share Routes across a cluster of Spring Cloud Gateway instances, `RedisRouteDefinitionRepository` can be used.
To enable this kind of repository, the following property has to set to true: `spring.cloud.gateway.redis-route-definition-repository.enabled`
Likewise to the RedisRateLimiter Filter Factory it requires the use of the spring-boot-starter-data-redis-reactive Spring Boot starter.
[[troubleshooting]]
== Troubleshooting

View File

@@ -27,15 +27,21 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.route.RedisRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.web.reactive.DispatcherHandler;
@@ -65,4 +71,28 @@ class GatewayRedisAutoConfiguration {
return new RedisRateLimiter(redisTemplate, redisScript, configurationService);
}
@Bean
@ConditionalOnProperty(
value = "spring.cloud.gateway.redis-route-definition-repository.enabled",
havingValue = "true")
@ConditionalOnClass(ReactiveRedisTemplate.class)
public RedisRouteDefinitionRepository redisRouteDefinitionRepository(
ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate) {
return new RedisRouteDefinitionRepository(reactiveRedisTemplate);
}
@Bean
public ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisRouteDefinitionTemplate(
ReactiveRedisConnectionFactory factory) {
StringRedisSerializer keySerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<RouteDefinition> valueSerializer = new Jackson2JsonRedisSerializer<>(
RouteDefinition.class);
RedisSerializationContext.RedisSerializationContextBuilder<String, RouteDefinition> builder = RedisSerializationContext
.newSerializationContext(keySerializer);
RedisSerializationContext<String, RouteDefinition> context = builder
.value(valueSerializer).build();
return new ReactiveRedisTemplate<>(factory, context);
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2013-2017 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.route;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.ReactiveValueOperations;
import org.springframework.stereotype.Repository;
/**
* @author Dennis Menge
*/
@Repository
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private static final Logger log = LoggerFactory.getLogger(RedisRouteDefinitionRepository.class);
/**
* Key prefix for RouteDefinition queries to redis.
*/
private static final String ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY = "routedefinition_";
private ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate;
private ReactiveValueOperations<String, RouteDefinition> routeDefinitionReactiveValueOperations;
public RedisRouteDefinitionRepository(
ReactiveRedisTemplate<String, RouteDefinition> reactiveRedisTemplate) {
this.reactiveRedisTemplate = reactiveRedisTemplate;
this.routeDefinitionReactiveValueOperations = reactiveRedisTemplate.opsForValue();
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return reactiveRedisTemplate.keys(createKey("*"))
.flatMap(key -> reactiveRedisTemplate.opsForValue().get(key))
.onErrorContinue((throwable, routeDefinition) -> {
if (log.isErrorEnabled()) {
log.error("get routes from redis error cause : {}", throwable.toString(), throwable);
}
});
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> routeDefinitionReactiveValueOperations
.set(createKey(routeDefinition.getId()), routeDefinition)
.flatMap(success -> {
if (success) {
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new RuntimeException(
String.format("Could not add route to redis repository: %s",
routeDefinition))));
}));
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> routeDefinitionReactiveValueOperations
.delete(createKey(id)).flatMap(success -> {
if (success) {
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new NotFoundException(String
.format("Could not remove route from redis repository with id: %s",
routeId))));
}));
}
private String createKey(String routeId) {
return ROUTEDEFINITION_REDIS_KEY_PREFIX_QUERY + routeId;
}
}

View File

@@ -16,15 +16,23 @@
package org.springframework.cloud.gateway.config;
import java.io.IOException;
import javax.annotation.PreDestroy;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import redis.embedded.RedisServer;
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.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.route.RedisRouteDefinitionRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.test.context.junit4.SpringRunner;
@@ -75,4 +83,69 @@ public class GatewayRedisAutoConfigurationTests {
}
/**
* @author Dennis Menge
*/
@RunWith(SpringRunner.class)
@SpringBootTest(
classes = RedisRouteDefinitionRepositoryDisabledByProperty.TestConfig.class,
properties = "spring.cloud.gateway.redis-route-definition-repository.enabled=false")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public static class RedisRouteDefinitionRepositoryDisabledByProperty {
@Autowired(required = false)
private RedisRouteDefinitionRepository redisRouteDefinitionRepository;
@Test
public void redisRouteDefinitionRepository() {
assertThat(redisRouteDefinitionRepository).isNull();
}
@EnableAutoConfiguration
@SpringBootConfiguration
public static class TestConfig {
}
}
/**
* @author Dennis Menge
*/
@RunWith(SpringRunner.class)
@SpringBootTest(
classes = RedisRouteDefinitionRepositoryEnabledByProperty.TestConfig.class,
properties = "spring.cloud.gateway.redis-route-definition-repository.enabled=true")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public static class RedisRouteDefinitionRepositoryEnabledByProperty {
@Autowired(required = false)
private RedisRouteDefinitionRepository redisRouteDefinitionRepository;
@Test
public void redisRouteDefinitionRepository() {
assertThat(redisRouteDefinitionRepository).isNotNull();
}
@EnableAutoConfiguration
@SpringBootConfiguration
public static class TestConfig {
private RedisServer redisServer;
@Bean
public RedisServer redisServer() throws IOException {
redisServer = new RedisServer();
redisServer.start();
return redisServer;
}
@PreDestroy
public void destroy() {
redisServer.stop();
}
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2013-2017 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.route;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import javax.annotation.PreDestroy;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.core.publisher.Mono;
import redis.embedded.RedisServer;
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.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dennis Menge
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@ActiveProfiles("redis-route-repository")
public class RedisRouteDefinitionRepositoryTests {
@Autowired
private RedisRouteDefinitionRepository redisRouteDefinitionRepository;
@Test
public void testAddRouteToRedis() {
RouteDefinition testRouteDefinition = defaultTestRoute();
redisRouteDefinitionRepository.save(Mono.just(testRouteDefinition)).block();
List<RouteDefinition> routeDefinitions = redisRouteDefinitionRepository
.getRouteDefinitions().collectList().block();
assertThat(routeDefinitions.size()).isEqualTo(1);
assertThat(routeDefinitions.get(0)).isEqualTo(testRouteDefinition);
}
@Test
public void testRemoveRouteFromRedis() {
RouteDefinition testRouteDefinition = defaultTestRoute();
redisRouteDefinitionRepository.save(Mono.just(testRouteDefinition)).block();
List<RouteDefinition> routeDefinitions = redisRouteDefinitionRepository
.getRouteDefinitions().collectList().block();
String routeId = routeDefinitions.get(0).getId();
// Assert that route has been added.
assertThat(routeDefinitions.size()).isEqualTo(1);
// Delete route from repository
redisRouteDefinitionRepository.delete(Mono.just(routeId)).block();
// Assert that route has been removed.
assertThat(redisRouteDefinitionRepository.getRouteDefinitions().collectList()
.block().size()).isEqualTo(0);
}
@NotNull
private RouteDefinition defaultTestRoute() {
RouteDefinition testRouteDefinition = new RouteDefinition();
testRouteDefinition.setUri(URI.create("http://example.org"));
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));
return testRouteDefinition;
}
@EnableAutoConfiguration
@SpringBootConfiguration
public static class TestConfig {
RedisServer redisServer;
@Bean
public RedisServer redisServer() throws IOException {
redisServer = new RedisServer();
redisServer.start();
return redisServer;
}
@PreDestroy
public void destroy() {
redisServer.stop();
}
}
}

View File

@@ -0,0 +1,5 @@
spring:
cloud:
gateway:
redis-route-definition-repository:
enabled: true