Add support for the configuration watcher to shut down the application to refresh the application (#1799)

See #1772
This commit is contained in:
Ryan Baxter
2024-11-21 18:55:58 -05:00
committed by GitHub
parent 890af8f202
commit 16eec95fd0
12 changed files with 344 additions and 86 deletions

View File

@@ -21,8 +21,12 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.bus.event.PathDestinationFactory;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy.SHUTDOWN;
/**
* An event publisher for an 'event bus' type of application.
*
@@ -34,16 +38,28 @@ final class BusRefreshTrigger implements RefreshTrigger {
private final String busId;
BusRefreshTrigger(ApplicationEventPublisher applicationEventPublisher, String busId) {
private final ConfigurationWatcherConfigurationProperties watcherConfigurationProperties;
BusRefreshTrigger(ApplicationEventPublisher applicationEventPublisher, String busId,
ConfigurationWatcherConfigurationProperties watcherConfigurationProperties) {
this.applicationEventPublisher = applicationEventPublisher;
this.busId = busId;
this.watcherConfigurationProperties = watcherConfigurationProperties;
}
@Override
public Mono<Void> triggerRefresh(KubernetesObject configMap, String appName) {
applicationEventPublisher.publishEvent(new RefreshRemoteApplicationEvent(configMap, busId,
new PathDestinationFactory().getDestination(appName)));
applicationEventPublisher.publishEvent(createRefreshApplicationEvent(configMap, appName));
return Mono.empty();
}
private RemoteApplicationEvent createRefreshApplicationEvent(KubernetesObject configMap, String appName) {
if (watcherConfigurationProperties.getRefreshStrategy() == SHUTDOWN) {
return new ShutdownRemoteApplicationEvent(configMap, busId,
new PathDestinationFactory().getDestination(appName));
}
return new RefreshRemoteApplicationEvent(configMap, busId,
new PathDestinationFactory().getDestination(appName));
}
}

View File

@@ -70,6 +70,8 @@ public class ConfigurationWatcherConfigurationProperties {
@DurationUnit(ChronoUnit.MILLIS)
private Duration refreshDelay = Duration.ofMillis(120000);
private RefreshStrategy refreshStrategy = RefreshStrategy.REFRESH;
private int threadPoolSize = 1;
private String actuatorPath = "/actuator";
@@ -115,4 +117,28 @@ public class ConfigurationWatcherConfigurationProperties {
this.threadPoolSize = threadPoolSize;
}
public RefreshStrategy getRefreshStrategy() {
return refreshStrategy;
}
public void setRefreshStrategy(RefreshStrategy refreshStrategy) {
this.refreshStrategy = refreshStrategy;
}
public enum RefreshStrategy {
/**
* Call the Actuator refresh endpoint or send a refresh event over Spring Cloud
* Bus.
*/
REFRESH,
/**
* Call the Actuator shutdown endpoint or send a shutdown event over Spring Cloud
* Bus.
*/
SHUTDOWN
}
}

View File

@@ -31,6 +31,8 @@ import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy.SHUTDOWN;
/**
* @author wind57
*/
@@ -91,7 +93,7 @@ final class HttpRefreshTrigger implements RefreshTrigger {
}
else {
int port = actuatorPort < 0 ? si.getPort() : actuatorPort;
actuatorUriBuilder = actuatorUriBuilder.path(actuatorPath + "/refresh").port(port);
actuatorUriBuilder = actuatorUriBuilder.path(actuatorPath + getRefreshStrategyEndpoint()).port(port);
}
return actuatorUriBuilder.build().toUri();
@@ -99,7 +101,7 @@ final class HttpRefreshTrigger implements RefreshTrigger {
private void setActuatorUriFromAnnotation(UriComponentsBuilder actuatorUriBuilder, String metadataUri) {
URI annotationUri = URI.create(metadataUri);
actuatorUriBuilder.path(annotationUri.getPath() + "/refresh");
actuatorUriBuilder.path(annotationUri.getPath() + getRefreshStrategyEndpoint());
// The URI may not contain a host so if that is the case the port in the URI will
// be -1. The authority of the URI will be :<port> for example :9090, we just need
@@ -114,4 +116,11 @@ final class HttpRefreshTrigger implements RefreshTrigger {
}
}
private String getRefreshStrategyEndpoint() {
if (k8SConfigurationProperties.getRefreshStrategy() == SHUTDOWN) {
return "/shutdown";
}
return "/refresh";
}
}

View File

@@ -41,8 +41,8 @@ class RefreshTriggerAutoConfiguration {
@ConditionalOnMissingBean
@Profile({ AMQP, KAFKA })
BusRefreshTrigger busRefreshTrigger(ApplicationEventPublisher applicationEventPublisher,
BusProperties busProperties) {
return new BusRefreshTrigger(applicationEventPublisher, busProperties.getId());
BusProperties busProperties, ConfigurationWatcherConfigurationProperties properties) {
return new BusRefreshTrigger(applicationEventPublisher, busProperties.getId(), properties);
}
@Bean

View File

@@ -28,6 +28,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
@@ -39,6 +41,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider.NAMESPACE_PROPERTY;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy;
/**
* @author Ryan Baxter
@@ -68,31 +71,52 @@ class BusEventBasedConfigMapWatcherChangeDetectorTests {
private BusProperties busProperties;
private MockEnvironment mockEnvironment;
@BeforeEach
void setup() {
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment = new MockEnvironment();
mockEnvironment.setProperty(NAMESPACE_PROPERTY, "default");
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
busProperties = new BusProperties();
changeDetector = new BusEventBasedConfigMapWatcherChangeDetector(coreV1Api, mockEnvironment,
ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY, configMapPropertySourceLocator,
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
threadPoolTaskExecutor, new BusRefreshTrigger(applicationEventPublisher, busProperties.getId()));
}
@Test
void triggerRefreshWithConfigMap() {
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
V1ConfigMap configMap = new V1ConfigMap();
configMap.setMetadata(objectMeta);
changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName());
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
.forClass(RefreshRemoteApplicationEvent.class);
triggerRefreshWithConfigMap(RefreshStrategy.REFRESH, argumentCaptor);
}
@Test
void triggerRefreshWithConfigMapUsingShutdown() {
ArgumentCaptor<ShutdownRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
.forClass(ShutdownRemoteApplicationEvent.class);
triggerRefreshWithConfigMap(RefreshStrategy.SHUTDOWN, argumentCaptor);
}
void triggerRefreshWithConfigMap(RefreshStrategy strategy,
ArgumentCaptor<? extends RemoteApplicationEvent> argumentCaptor) {
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
V1ConfigMap configMap = getV1ConfigMap(objectMeta, strategy);
verify(applicationEventPublisher).publishEvent(argumentCaptor.capture());
assertThat(argumentCaptor.getValue().getSource()).isEqualTo(configMap);
assertThat(argumentCaptor.getValue().getOriginService()).isEqualTo(busProperties.getId());
assertThat(argumentCaptor.getValue().getDestinationService()).isEqualTo("foo:**");
}
private V1ConfigMap getV1ConfigMap(V1ObjectMeta objectMeta, RefreshStrategy refreshStrategy) {
V1ConfigMap configMap = new V1ConfigMap();
configMap.setMetadata(objectMeta);
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
BusEventBasedConfigMapWatcherChangeDetector changeDetector = new BusEventBasedConfigMapWatcherChangeDetector(
coreV1Api, mockEnvironment, ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY,
configMapPropertySourceLocator, new KubernetesNamespaceProvider(mockEnvironment),
configurationWatcherConfigurationProperties, threadPoolTaskExecutor, new BusRefreshTrigger(
applicationEventPublisher, busProperties.getId(), configurationWatcherConfigurationProperties));
changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName());
return configMap;
}
}

View File

@@ -28,6 +28,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
@@ -39,6 +40,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider.NAMESPACE_PROPERTY;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy;
/**
* @author Ryan Baxter
@@ -64,35 +66,55 @@ class BusEventBasedSecretsWatcherChangeDetectorTests {
@Mock
private ApplicationEventPublisher applicationEventPublisher;
private BusEventBasedSecretsWatcherChangeDetector changeDetector;
private BusProperties busProperties;
private MockEnvironment mockEnvironment;
@BeforeEach
void setup() {
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment = new MockEnvironment();
mockEnvironment.setProperty(NAMESPACE_PROPERTY, "default");
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
busProperties = new BusProperties();
changeDetector = new BusEventBasedSecretsWatcherChangeDetector(coreV1Api, mockEnvironment,
ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY, secretsPropertySourceLocator,
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
threadPoolTaskExecutor, new BusRefreshTrigger(applicationEventPublisher, busProperties.getId()));
}
@Test
void triggerRefreshWithSecret() {
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
V1Secret secret = new V1Secret();
secret.setMetadata(objectMeta);
changeDetector.triggerRefresh(secret, secret.getMetadata().getName());
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
.forClass(RefreshRemoteApplicationEvent.class);
triggerRefreshWithSecret(ConfigurationWatcherConfigurationProperties.RefreshStrategy.REFRESH, argumentCaptor);
}
@Test
void triggerRefreshWithSecretWithShutdown() {
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
.forClass(RefreshRemoteApplicationEvent.class);
triggerRefreshWithSecret(ConfigurationWatcherConfigurationProperties.RefreshStrategy.REFRESH, argumentCaptor);
}
void triggerRefreshWithSecret(RefreshStrategy strategy,
ArgumentCaptor<? extends RemoteApplicationEvent> argumentCaptor) {
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
V1Secret secret = getV1Secret(objectMeta, strategy);
verify(applicationEventPublisher).publishEvent(argumentCaptor.capture());
assertThat(argumentCaptor.getValue().getSource()).isEqualTo(secret);
assertThat(argumentCaptor.getValue().getOriginService()).isEqualTo(busProperties.getId());
assertThat(argumentCaptor.getValue().getDestinationService()).isEqualTo("foo:**");
}
private V1Secret getV1Secret(V1ObjectMeta objectMeta,
ConfigurationWatcherConfigurationProperties.RefreshStrategy refreshStrategy) {
V1Secret secret = new V1Secret();
secret.setMetadata(objectMeta);
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
BusEventBasedSecretsWatcherChangeDetector changeDetector = new BusEventBasedSecretsWatcherChangeDetector(
coreV1Api, mockEnvironment, ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY,
secretsPropertySourceLocator, new KubernetesNamespaceProvider(mockEnvironment),
configurationWatcherConfigurationProperties, threadPoolTaskExecutor, new BusRefreshTrigger(
applicationEventPublisher, busProperties.getId(), configurationWatcherConfigurationProperties));
changeDetector.triggerRefresh(secret, secret.getMetadata().getName());
return secret;
}
}

View File

@@ -30,6 +30,7 @@ import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1EndpointAddress;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.ClientBuilder;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@@ -58,6 +59,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider.NAMESPACE_PROPERTY;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy;
/**
* @author Ryan Baxter
@@ -83,9 +85,11 @@ class HttpBasedConfigMapWatchChangeDetectorTests {
@Mock
private KubernetesInformerReactiveDiscoveryClient reactiveDiscoveryClient;
private HttpBasedConfigMapWatchChangeDetector changeDetector;
private MockEnvironment mockEnvironment;
private ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties;
private WebClient webClient;
private ConfigurationUpdateStrategy strategy;
@BeforeAll
static void beforeAll() {
@@ -105,58 +109,112 @@ class HttpBasedConfigMapWatchChangeDetectorTests {
@BeforeEach
void setup() {
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment = new MockEnvironment();
mockEnvironment.setProperty(NAMESPACE_PROPERTY, "default");
configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
WebClient webClient = WebClient.builder().build();
ConfigurationUpdateStrategy strategy = new ConfigurationUpdateStrategy("refresh", () -> {
webClient = WebClient.builder().build();
strategy = new ConfigurationUpdateStrategy("refresh", () -> {
});
changeDetector = new HttpBasedConfigMapWatchChangeDetector(coreV1Api, mockEnvironment,
ConfigReloadProperties.DEFAULT, strategy, configMapPropertySourceLocator,
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
threadPoolTaskExecutor, new HttpRefreshTrigger(reactiveDiscoveryClient,
configurationWatcherConfigurationProperties, webClient));
}
@Test
void triggerConfigMapRefresh() {
void triggerConfigMapRefreshUsingRefresh() {
triggerConfigMapRefresh("/actuator/refresh", RefreshStrategy.REFRESH);
}
@Test
void triggerConfigMapRefreshUsingShutdown() {
triggerConfigMapRefresh("/actuator/shutdown", RefreshStrategy.SHUTDOWN);
}
void triggerConfigMapRefresh(String actuatorPath, RefreshStrategy refreshStrategy) {
stubReactiveCall();
V1ConfigMap configMap = new V1ConfigMap();
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
configMap.setMetadata(objectMeta);
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
WireMock
.stubFor(WireMock.post(WireMock.urlEqualTo(actuatorPath)).willReturn(WireMock.aResponse().withStatus(200)));
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
HttpBasedConfigMapWatchChangeDetector changeDetector = new HttpBasedConfigMapWatchChangeDetector(coreV1Api,
mockEnvironment, ConfigReloadProperties.DEFAULT, strategy, configMapPropertySourceLocator,
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
threadPoolTaskExecutor, new HttpRefreshTrigger(reactiveDiscoveryClient,
configurationWatcherConfigurationProperties, webClient));
StepVerifier.create(changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName()))
.verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh")));
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo(actuatorPath)));
}
@Test
void triggerConfigMapRefreshWithPropertiesBasedActuatorPath() {
void triggerConfigMapRefreshWithPropertiesBasedActuatorPathUsingRefresh() {
triggerConfigMapRefreshWithPropertiesBasedActuatorPath("/refresh", RefreshStrategy.REFRESH);
}
@Test
void triggerConfigMapRefreshWithPropertiesBasedActuatorPathUsingShutdown() {
triggerConfigMapRefreshWithPropertiesBasedActuatorPath("/shutdown", RefreshStrategy.SHUTDOWN);
}
void triggerConfigMapRefreshWithPropertiesBasedActuatorPath(String endpoint, RefreshStrategy refreshStrategy) {
stubReactiveCall();
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
configurationWatcherConfigurationProperties.setActuatorPath("/my/custom/actuator");
V1ConfigMap configMap = new V1ConfigMap();
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
configMap.setMetadata(objectMeta);
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator" + endpoint))
.willReturn(WireMock.aResponse().withStatus(200)));
HttpBasedConfigMapWatchChangeDetector changeDetector = new HttpBasedConfigMapWatchChangeDetector(coreV1Api,
mockEnvironment, ConfigReloadProperties.DEFAULT, strategy, configMapPropertySourceLocator,
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
threadPoolTaskExecutor, new HttpRefreshTrigger(reactiveDiscoveryClient,
configurationWatcherConfigurationProperties, webClient));
StepVerifier.create(changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName()))
.verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator" + endpoint)));
}
@Test
void triggerConfigMapRefreshWithAnnotationActuatorPath() {
void triggerConfigMapRefreshWithAnnotationActuatorPathUsingRefresh() {
triggerConfigMapRefreshWithAnnotationActuatorPath("/refresh", RefreshStrategy.REFRESH);
}
@Test
void triggerConfigMapRefreshWithAnnotationActuatorPathUsingShutdown() {
triggerConfigMapRefreshWithAnnotationActuatorPath("/shutdown", RefreshStrategy.SHUTDOWN);
}
void triggerConfigMapRefreshWithAnnotationActuatorPath(String endpoint, RefreshStrategy refreshStrategy) {
int port = WIRE_MOCK_SERVER.port();
WireMock.configureFor("localhost", port);
List<ServiceInstance> instances = getServiceInstances(port);
when(reactiveDiscoveryClient.getInstances(eq("foo"))).thenReturn(Flux.fromIterable(instances));
V1ConfigMap configMap = new V1ConfigMap();
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
configMap.setMetadata(objectMeta);
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator" + endpoint))
.willReturn(WireMock.aResponse().withStatus(200)));
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
HttpBasedConfigMapWatchChangeDetector changeDetector = new HttpBasedConfigMapWatchChangeDetector(coreV1Api,
mockEnvironment, ConfigReloadProperties.DEFAULT, strategy, configMapPropertySourceLocator,
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
threadPoolTaskExecutor, new HttpRefreshTrigger(reactiveDiscoveryClient,
configurationWatcherConfigurationProperties, webClient));
StepVerifier.create(changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName()))
.verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator" + endpoint)));
}
private static @NotNull List<ServiceInstance> getServiceInstances(int port) {
Map<String, String> metadata = new HashMap<>();
metadata.put(ConfigurationWatcherConfigurationProperties.ANNOTATION_KEY,
"http://:" + port + "/my/custom/actuator");
@@ -169,20 +227,10 @@ class HttpBasedConfigMapWatchChangeDetectorTests {
DefaultKubernetesServiceInstance fooServiceInstance = new DefaultKubernetesServiceInstance("foo", "foo",
fooEndpointAddress.getIp(), fooEndpointPort.getPort(), metadata, false);
instances.add(fooServiceInstance);
when(reactiveDiscoveryClient.getInstances(eq("foo"))).thenReturn(Flux.fromIterable(instances));
V1ConfigMap configMap = new V1ConfigMap();
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
configMap.setMetadata(objectMeta);
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
StepVerifier.create(changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName()))
.verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
return instances;
}
private void stubReactiveCall() {
V1EndpointAddress fooEndpointAddress = new V1EndpointAddress();
fooEndpointAddress.setIp("127.0.0.1");
fooEndpointAddress.setHostname("localhost");

View File

@@ -52,12 +52,14 @@ import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationU
import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider.NAMESPACE_PROPERTY;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy;
/**
* @author Ryan Baxter
@@ -86,21 +88,15 @@ class HttpBasedSecretsWatchChangeDetectorTests {
@Mock
private KubernetesInformerReactiveDiscoveryClient reactiveDiscoveryClient;
private HttpBasedSecretsWatchChangeDetector changeDetector;
private MockEnvironment mockEnvironment;
private ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties;
private WebClient webClient;
@BeforeEach
void setup() {
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment = new MockEnvironment();
mockEnvironment.setProperty(NAMESPACE_PROPERTY, "default");
configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
WebClient webClient = WebClient.builder().build();
changeDetector = new HttpBasedSecretsWatchChangeDetector(coreV1Api, mockEnvironment,
ConfigReloadProperties.DEFAULT, updateStrategy, secretsPropertySourceLocator,
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
threadPoolTaskExecutor, new HttpRefreshTrigger(reactiveDiscoveryClient,
configurationWatcherConfigurationProperties, webClient));
webClient = WebClient.builder().build();
}
@BeforeAll
@@ -120,36 +116,83 @@ class HttpBasedSecretsWatchChangeDetectorTests {
}
@Test
void triggerSecretRefresh() {
void triggerSecretRefreshUsingRefresh() {
triggerSecretRefresh("/refresh", RefreshStrategy.REFRESH);
}
@Test
void triggerSecretRefreshUsingShutdown() {
triggerSecretRefresh("/shutdown", RefreshStrategy.SHUTDOWN);
}
void triggerSecretRefresh(String endpoint, RefreshStrategy refreshStrategy) {
stubReactiveCall();
V1Secret secret = new V1Secret();
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
secret.setMetadata(objectMeta);
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/actuator/refresh"))
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/actuator" + endpoint))
.willReturn(WireMock.aResponse().withStatus(200)));
HttpBasedSecretsWatchChangeDetector changeDetector = getHttpBasedSecretsWatchChangeDetector(refreshStrategy);
StepVerifier.create(changeDetector.triggerRefresh(secret, secret.getMetadata().getName())).verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh")));
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator" + endpoint)));
}
private HttpBasedSecretsWatchChangeDetector getHttpBasedSecretsWatchChangeDetector(
RefreshStrategy refreshStrategy) {
return getHttpBasedSecretsWatchChangeDetector(null, refreshStrategy);
}
private HttpBasedSecretsWatchChangeDetector getHttpBasedSecretsWatchChangeDetector(String actuatorPath,
RefreshStrategy refreshStrategy) {
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
if (StringUtils.hasText(actuatorPath)) {
configurationWatcherConfigurationProperties.setActuatorPath(actuatorPath);
}
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
return new HttpBasedSecretsWatchChangeDetector(coreV1Api, mockEnvironment, ConfigReloadProperties.DEFAULT,
updateStrategy, secretsPropertySourceLocator, new KubernetesNamespaceProvider(mockEnvironment),
configurationWatcherConfigurationProperties, threadPoolTaskExecutor, new HttpRefreshTrigger(
reactiveDiscoveryClient, configurationWatcherConfigurationProperties, webClient));
}
@Test
void triggerSecretRefreshWithPropertiesBasedActuatorPath() {
void triggerSecretRefreshWithPropertiesBasedActuatorPathUsingRefresh() {
triggerSecretRefreshWithPropertiesBasedActuatorPath("/refresh", RefreshStrategy.REFRESH);
}
@Test
void triggerSecretRefreshWithPropertiesBasedActuatorPathUsingShutdown() {
triggerSecretRefreshWithPropertiesBasedActuatorPath("/shutdown", RefreshStrategy.SHUTDOWN);
}
void triggerSecretRefreshWithPropertiesBasedActuatorPath(String endpoint, RefreshStrategy refreshStrategy) {
stubReactiveCall();
configurationWatcherConfigurationProperties.setActuatorPath("/my/custom/actuator");
V1Secret secret = new V1Secret();
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
secret.setMetadata(objectMeta);
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator" + endpoint))
.willReturn(WireMock.aResponse().withStatus(200)));
HttpBasedSecretsWatchChangeDetector changeDetector = getHttpBasedSecretsWatchChangeDetector(
"/my/custom/actuator", refreshStrategy);
StepVerifier.create(changeDetector.triggerRefresh(secret, secret.getMetadata().getName())).verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator" + endpoint)));
}
@Test
void triggerSecretRefreshWithAnnotationActuatorPath() {
void triggerSecretRefreshWithAnnotationActuatorPathUsingRefresh() {
triggerSecretRefreshWithAnnotationActuatorPath("/refresh", RefreshStrategy.REFRESH);
}
@Test
void triggerSecretRefreshWithAnnotationActuatorPathUsingShutdown() {
triggerSecretRefreshWithAnnotationActuatorPath("/shutdown", RefreshStrategy.SHUTDOWN);
}
void triggerSecretRefreshWithAnnotationActuatorPath(String endpoint, RefreshStrategy refreshStrategy) {
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
Map<String, String> metadata = new HashMap<>();
metadata.put(ConfigurationWatcherConfigurationProperties.ANNOTATION_KEY,
@@ -168,10 +211,12 @@ class HttpBasedSecretsWatchChangeDetectorTests {
V1ObjectMeta objectMeta = new V1ObjectMeta();
objectMeta.setName("foo");
secret.setMetadata(objectMeta);
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator" + endpoint))
.willReturn(WireMock.aResponse().withStatus(200)));
HttpBasedSecretsWatchChangeDetector changeDetector = getHttpBasedSecretsWatchChangeDetector(
"/my/custom/actuator", refreshStrategy);
StepVerifier.create(changeDetector.triggerRefresh(secret, secret.getMetadata().getName())).verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator" + endpoint)));
}
private void stubReactiveCall() {