Add support for the configuration watcher to shut down the application to refresh the application (#1799)
See #1772
This commit is contained in:
@@ -193,6 +193,9 @@ change to a ConfigMap or Secret occurs then the HTTP implementation will use the
|
||||
instances of the application which match the name of the ConfigMap or Secret and send an HTTP POST request to the application's actuator
|
||||
`/refresh` endpoint. By default, it will send the post request to `/actuator/refresh` using the port registered in the discovery client.
|
||||
|
||||
You can also configure the configuration watcher to call the instances `shutdown` actuator endpoint. To do this you can set
|
||||
`spring.cloud.kubernetes.configuration.watcher.refresh-strategy=shutdown`.
|
||||
|
||||
### Non-Default Management Port and Actuator Path
|
||||
|
||||
If the application is using a non-default actuator path and/or using a different port for the management endpoints, the Kubernetes service for the application
|
||||
@@ -224,7 +227,13 @@ Another way you can choose to configure the actuator path and/or management port
|
||||
## Messaging Implementation
|
||||
|
||||
The messaging implementation can be enabled by setting profile to either `bus-amqp` (RabbitMQ) or `bus-kafka` (Kafka) when the Spring Cloud Kubernetes Configuration Watcher
|
||||
application is deployed to Kubernetes.
|
||||
application is deployed to Kubernetes. By default, when using the messaging implementation the configuration watcher will send a `RefreshRemoteApplicationEvent` using
|
||||
Spring Cloud Bus to all application instances. This will cause the application instances to refresh the application's configuration properties without
|
||||
restarting the instance.
|
||||
|
||||
You can also configure the configuration to shut down the application instances in order to refresh the application's configuration properties.
|
||||
When the application shuts down, Kubernetes will restart the application instance and the new configuration properties will be loaded. To use
|
||||
this strategy set `spring.cloud.kubernetes.configuration.watcher.refresh-strategy=shutdown`.
|
||||
|
||||
## Configuring RabbitMQ
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import static org.springframework.cloud.client.discovery.DiscoveryClient.DEFAULT
|
||||
|
||||
/**
|
||||
* @param enabled if kubernetes discovery is enabled
|
||||
* @param allNamespaces if discover is enabled for all namespaces
|
||||
* @param allNamespaces if discovery is enabled for all namespaces
|
||||
* @param namespaces If set and allNamespaces is false, then only the services and
|
||||
* endpoints matching these namespaces will be fetched from the Kubernetes API server.
|
||||
* @param waitCacheReady wait for the discovery cache (service and endpoints) to be fully
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -126,6 +126,32 @@ class ActuatorRefreshIT {
|
||||
|
||||
}
|
||||
|
||||
void testActuatorShutdown() {
|
||||
TestUtil.patchForShutdownRefresh(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, DOCKER_IMAGE);
|
||||
WireMock.configureFor(WIREMOCK_HOST, WIREMOCK_PORT);
|
||||
await().timeout(Duration.ofSeconds(60))
|
||||
.ignoreException(SocketTimeoutException.class)
|
||||
.until(() -> WireMock
|
||||
.stubFor(WireMock.post(WireMock.urlEqualTo("/actuator/shutdown"))
|
||||
.willReturn(WireMock.aResponse().withBody("{}").withStatus(200)))
|
||||
.getResponse()
|
||||
.wasConfigured());
|
||||
|
||||
createConfigMap();
|
||||
|
||||
// Wait a bit before we verify
|
||||
await().atMost(Duration.ofSeconds(30))
|
||||
.until(() -> !WireMock.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/shutdown")))
|
||||
.isEmpty());
|
||||
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/shutdown")));
|
||||
|
||||
deleteConfigMap();
|
||||
|
||||
// the other test
|
||||
testActuatorRefreshReloadDisabled();
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* same test as above, but reload is disabled.
|
||||
*/
|
||||
|
||||
@@ -61,8 +61,41 @@ final class TestUtil {
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String USE_SHUTDOWN = """
|
||||
{
|
||||
"spec": {
|
||||
"template": {
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"name": "spring-cloud-kubernetes-configuration-watcher",
|
||||
"image": "image_name_here",
|
||||
"env": [
|
||||
{
|
||||
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_CONFIG_RELOAD",
|
||||
"value": "DEBUG"
|
||||
},
|
||||
{
|
||||
"name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CONFIGURATION_WATCHER",
|
||||
"value": "DEBUG"
|
||||
},
|
||||
{
|
||||
"name": "SPRING_CLOUD_KUBERNETES_CONFIGURATION_WATCHER_REFRESH_STRATEGY",
|
||||
"value": "shutdown"
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
static void patchForDisabledReload(String deploymentName, String namespace, String imageName) {
|
||||
patchWithReplace(imageName, deploymentName, namespace, BODY_ONE, POD_LABELS);
|
||||
}
|
||||
|
||||
static void patchForShutdownRefresh(String deploymentName, String namespace, String imageName) {
|
||||
patchWithReplace(imageName, deploymentName, namespace, USE_SHUTDOWN, POD_LABELS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user