Skip to content

Commit 16eec95

Browse files
authored
Add support for the configuration watcher to shut down the application to refresh the application (#1799)
See #1772
1 parent 890af8f commit 16eec95

File tree

12 files changed

+343
-85
lines changed

12 files changed

+343
-85
lines changed

docs/modules/ROOT/pages/spring-cloud-kubernetes-configuration-watcher.adoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ change to a ConfigMap or Secret occurs then the HTTP implementation will use the
193193
instances of the application which match the name of the ConfigMap or Secret and send an HTTP POST request to the application's actuator
194194
`/refresh` endpoint. By default, it will send the post request to `/actuator/refresh` using the port registered in the discovery client.
195195

196+
You can also configure the configuration watcher to call the instances `shutdown` actuator endpoint. To do this you can set
197+
`spring.cloud.kubernetes.configuration.watcher.refresh-strategy=shutdown`.
198+
196199
### Non-Default Management Port and Actuator Path
197200

198201
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
224227
## Messaging Implementation
225228

226229
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
227-
application is deployed to Kubernetes.
230+
application is deployed to Kubernetes. By default, when using the messaging implementation the configuration watcher will send a `RefreshRemoteApplicationEvent` using
231+
Spring Cloud Bus to all application instances. This will cause the application instances to refresh the application's configuration properties without
232+
restarting the instance.
233+
234+
You can also configure the configuration to shut down the application instances in order to refresh the application's configuration properties.
235+
When the application shuts down, Kubernetes will restart the application instance and the new configuration properties will be loaded. To use
236+
this strategy set `spring.cloud.kubernetes.configuration.watcher.refresh-strategy=shutdown`.
228237

229238
## Configuring RabbitMQ
230239

spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
/**
2929
* @param enabled if kubernetes discovery is enabled
30-
* @param allNamespaces if discover is enabled for all namespaces
30+
* @param allNamespaces if discovery is enabled for all namespaces
3131
* @param namespaces If set and allNamespaces is false, then only the services and
3232
* endpoints matching these namespaces will be fetched from the Kubernetes API server.
3333
* @param waitCacheReady wait for the discovery cache (service and endpoints) to be fully

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/BusRefreshTrigger.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121

2222
import org.springframework.cloud.bus.event.PathDestinationFactory;
2323
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
24+
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
25+
import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
2426
import org.springframework.context.ApplicationEventPublisher;
2527

28+
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy.SHUTDOWN;
29+
2630
/**
2731
* An event publisher for an 'event bus' type of application.
2832
*
@@ -34,16 +38,28 @@ final class BusRefreshTrigger implements RefreshTrigger {
3438

3539
private final String busId;
3640

37-
BusRefreshTrigger(ApplicationEventPublisher applicationEventPublisher, String busId) {
41+
private final ConfigurationWatcherConfigurationProperties watcherConfigurationProperties;
42+
43+
BusRefreshTrigger(ApplicationEventPublisher applicationEventPublisher, String busId,
44+
ConfigurationWatcherConfigurationProperties watcherConfigurationProperties) {
3845
this.applicationEventPublisher = applicationEventPublisher;
3946
this.busId = busId;
47+
this.watcherConfigurationProperties = watcherConfigurationProperties;
4048
}
4149

4250
@Override
4351
public Mono<Void> triggerRefresh(KubernetesObject configMap, String appName) {
44-
applicationEventPublisher.publishEvent(new RefreshRemoteApplicationEvent(configMap, busId,
45-
new PathDestinationFactory().getDestination(appName)));
52+
applicationEventPublisher.publishEvent(createRefreshApplicationEvent(configMap, appName));
4653
return Mono.empty();
4754
}
4855

56+
private RemoteApplicationEvent createRefreshApplicationEvent(KubernetesObject configMap, String appName) {
57+
if (watcherConfigurationProperties.getRefreshStrategy() == SHUTDOWN) {
58+
return new ShutdownRemoteApplicationEvent(configMap, busId,
59+
new PathDestinationFactory().getDestination(appName));
60+
}
61+
return new RefreshRemoteApplicationEvent(configMap, busId,
62+
new PathDestinationFactory().getDestination(appName));
63+
}
64+
4965
}

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/ConfigurationWatcherConfigurationProperties.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public class ConfigurationWatcherConfigurationProperties {
7070
@DurationUnit(ChronoUnit.MILLIS)
7171
private Duration refreshDelay = Duration.ofMillis(120000);
7272

73+
private RefreshStrategy refreshStrategy = RefreshStrategy.REFRESH;
74+
7375
private int threadPoolSize = 1;
7476

7577
private String actuatorPath = "/actuator";
@@ -115,4 +117,28 @@ public void setThreadPoolSize(int threadPoolSize) {
115117
this.threadPoolSize = threadPoolSize;
116118
}
117119

120+
public RefreshStrategy getRefreshStrategy() {
121+
return refreshStrategy;
122+
}
123+
124+
public void setRefreshStrategy(RefreshStrategy refreshStrategy) {
125+
this.refreshStrategy = refreshStrategy;
126+
}
127+
128+
public enum RefreshStrategy {
129+
130+
/**
131+
* Call the Actuator refresh endpoint or send a refresh event over Spring Cloud
132+
* Bus.
133+
*/
134+
REFRESH,
135+
136+
/**
137+
* Call the Actuator shutdown endpoint or send a shutdown event over Spring Cloud
138+
* Bus.
139+
*/
140+
SHUTDOWN
141+
142+
}
143+
118144
}

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/HttpRefreshTrigger.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.springframework.web.reactive.function.client.WebClient;
3232
import org.springframework.web.util.UriComponentsBuilder;
3333

34+
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy.SHUTDOWN;
35+
3436
/**
3537
* @author wind57
3638
*/
@@ -91,15 +93,15 @@ private URI getActuatorUri(ServiceInstance si, String actuatorPath, int actuator
9193
}
9294
else {
9395
int port = actuatorPort < 0 ? si.getPort() : actuatorPort;
94-
actuatorUriBuilder = actuatorUriBuilder.path(actuatorPath + "/refresh").port(port);
96+
actuatorUriBuilder = actuatorUriBuilder.path(actuatorPath + getRefreshStrategyEndpoint()).port(port);
9597
}
9698

9799
return actuatorUriBuilder.build().toUri();
98100
}
99101

100102
private void setActuatorUriFromAnnotation(UriComponentsBuilder actuatorUriBuilder, String metadataUri) {
101103
URI annotationUri = URI.create(metadataUri);
102-
actuatorUriBuilder.path(annotationUri.getPath() + "/refresh");
104+
actuatorUriBuilder.path(annotationUri.getPath() + getRefreshStrategyEndpoint());
103105

104106
// The URI may not contain a host so if that is the case the port in the URI will
105107
// be -1. The authority of the URI will be :<port> for example :9090, we just need
@@ -114,4 +116,11 @@ private void setActuatorUriFromAnnotation(UriComponentsBuilder actuatorUriBuilde
114116
}
115117
}
116118

119+
private String getRefreshStrategyEndpoint() {
120+
if (k8SConfigurationProperties.getRefreshStrategy() == SHUTDOWN) {
121+
return "/shutdown";
122+
}
123+
return "/refresh";
124+
}
125+
117126
}

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/main/java/org/springframework/cloud/kubernetes/configuration/watcher/RefreshTriggerAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ class RefreshTriggerAutoConfiguration {
4141
@ConditionalOnMissingBean
4242
@Profile({ AMQP, KAFKA })
4343
BusRefreshTrigger busRefreshTrigger(ApplicationEventPublisher applicationEventPublisher,
44-
BusProperties busProperties) {
45-
return new BusRefreshTrigger(applicationEventPublisher, busProperties.getId());
44+
BusProperties busProperties, ConfigurationWatcherConfigurationProperties properties) {
45+
return new BusRefreshTrigger(applicationEventPublisher, busProperties.getId(), properties);
4646
}
4747

4848
@Bean

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedConfigMapWatcherChangeDetectorTests.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
import org.springframework.cloud.bus.BusProperties;
3030
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
31+
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
32+
import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent;
3133
import org.springframework.cloud.kubernetes.client.config.KubernetesClientConfigMapPropertySourceLocator;
3234
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
3335
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
@@ -39,6 +41,7 @@
3941
import static org.assertj.core.api.Assertions.assertThat;
4042
import static org.mockito.Mockito.verify;
4143
import static org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider.NAMESPACE_PROPERTY;
44+
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy;
4245

4346
/**
4447
* @author Ryan Baxter
@@ -68,31 +71,52 @@ class BusEventBasedConfigMapWatcherChangeDetectorTests {
6871

6972
private BusProperties busProperties;
7073

74+
private MockEnvironment mockEnvironment;
75+
7176
@BeforeEach
7277
void setup() {
73-
MockEnvironment mockEnvironment = new MockEnvironment();
78+
mockEnvironment = new MockEnvironment();
7479
mockEnvironment.setProperty(NAMESPACE_PROPERTY, "default");
75-
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
7680
busProperties = new BusProperties();
77-
changeDetector = new BusEventBasedConfigMapWatcherChangeDetector(coreV1Api, mockEnvironment,
78-
ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY, configMapPropertySourceLocator,
79-
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
80-
threadPoolTaskExecutor, new BusRefreshTrigger(applicationEventPublisher, busProperties.getId()));
8181
}
8282

8383
@Test
8484
void triggerRefreshWithConfigMap() {
85-
V1ObjectMeta objectMeta = new V1ObjectMeta();
86-
objectMeta.setName("foo");
87-
V1ConfigMap configMap = new V1ConfigMap();
88-
configMap.setMetadata(objectMeta);
89-
changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName());
9085
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
9186
.forClass(RefreshRemoteApplicationEvent.class);
87+
triggerRefreshWithConfigMap(RefreshStrategy.REFRESH, argumentCaptor);
88+
}
89+
90+
@Test
91+
void triggerRefreshWithConfigMapUsingShutdown() {
92+
ArgumentCaptor<ShutdownRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
93+
.forClass(ShutdownRemoteApplicationEvent.class);
94+
triggerRefreshWithConfigMap(RefreshStrategy.SHUTDOWN, argumentCaptor);
95+
}
96+
97+
void triggerRefreshWithConfigMap(RefreshStrategy strategy,
98+
ArgumentCaptor<? extends RemoteApplicationEvent> argumentCaptor) {
99+
V1ObjectMeta objectMeta = new V1ObjectMeta();
100+
objectMeta.setName("foo");
101+
V1ConfigMap configMap = getV1ConfigMap(objectMeta, strategy);
92102
verify(applicationEventPublisher).publishEvent(argumentCaptor.capture());
93103
assertThat(argumentCaptor.getValue().getSource()).isEqualTo(configMap);
94104
assertThat(argumentCaptor.getValue().getOriginService()).isEqualTo(busProperties.getId());
95105
assertThat(argumentCaptor.getValue().getDestinationService()).isEqualTo("foo:**");
96106
}
97107

108+
private V1ConfigMap getV1ConfigMap(V1ObjectMeta objectMeta, RefreshStrategy refreshStrategy) {
109+
V1ConfigMap configMap = new V1ConfigMap();
110+
configMap.setMetadata(objectMeta);
111+
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
112+
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
113+
BusEventBasedConfigMapWatcherChangeDetector changeDetector = new BusEventBasedConfigMapWatcherChangeDetector(
114+
coreV1Api, mockEnvironment, ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY,
115+
configMapPropertySourceLocator, new KubernetesNamespaceProvider(mockEnvironment),
116+
configurationWatcherConfigurationProperties, threadPoolTaskExecutor, new BusRefreshTrigger(
117+
applicationEventPublisher, busProperties.getId(), configurationWatcherConfigurationProperties));
118+
changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName());
119+
return configMap;
120+
}
121+
98122
}

spring-cloud-kubernetes-controllers/spring-cloud-kubernetes-configuration-watcher/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/BusEventBasedSecretsWatcherChangeDetectorTests.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.cloud.bus.BusProperties;
3030
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
31+
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
3132
import org.springframework.cloud.kubernetes.client.config.KubernetesClientSecretsPropertySourceLocator;
3233
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
3334
import org.springframework.cloud.kubernetes.commons.config.reload.ConfigReloadProperties;
@@ -39,6 +40,7 @@
3940
import static org.assertj.core.api.Assertions.assertThat;
4041
import static org.mockito.Mockito.verify;
4142
import static org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider.NAMESPACE_PROPERTY;
43+
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.RefreshStrategy;
4244

4345
/**
4446
* @author Ryan Baxter
@@ -64,35 +66,55 @@ class BusEventBasedSecretsWatcherChangeDetectorTests {
6466
@Mock
6567
private ApplicationEventPublisher applicationEventPublisher;
6668

67-
private BusEventBasedSecretsWatcherChangeDetector changeDetector;
68-
6969
private BusProperties busProperties;
7070

71+
private MockEnvironment mockEnvironment;
72+
7173
@BeforeEach
7274
void setup() {
73-
MockEnvironment mockEnvironment = new MockEnvironment();
75+
mockEnvironment = new MockEnvironment();
7476
mockEnvironment.setProperty(NAMESPACE_PROPERTY, "default");
75-
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
7677
busProperties = new BusProperties();
77-
changeDetector = new BusEventBasedSecretsWatcherChangeDetector(coreV1Api, mockEnvironment,
78-
ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY, secretsPropertySourceLocator,
79-
new KubernetesNamespaceProvider(mockEnvironment), configurationWatcherConfigurationProperties,
80-
threadPoolTaskExecutor, new BusRefreshTrigger(applicationEventPublisher, busProperties.getId()));
8178
}
8279

8380
@Test
8481
void triggerRefreshWithSecret() {
85-
V1ObjectMeta objectMeta = new V1ObjectMeta();
86-
objectMeta.setName("foo");
87-
V1Secret secret = new V1Secret();
88-
secret.setMetadata(objectMeta);
89-
changeDetector.triggerRefresh(secret, secret.getMetadata().getName());
9082
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
9183
.forClass(RefreshRemoteApplicationEvent.class);
84+
triggerRefreshWithSecret(ConfigurationWatcherConfigurationProperties.RefreshStrategy.REFRESH, argumentCaptor);
85+
}
86+
87+
@Test
88+
void triggerRefreshWithSecretWithShutdown() {
89+
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
90+
.forClass(RefreshRemoteApplicationEvent.class);
91+
triggerRefreshWithSecret(ConfigurationWatcherConfigurationProperties.RefreshStrategy.REFRESH, argumentCaptor);
92+
}
93+
94+
void triggerRefreshWithSecret(RefreshStrategy strategy,
95+
ArgumentCaptor<? extends RemoteApplicationEvent> argumentCaptor) {
96+
V1ObjectMeta objectMeta = new V1ObjectMeta();
97+
objectMeta.setName("foo");
98+
V1Secret secret = getV1Secret(objectMeta, strategy);
9299
verify(applicationEventPublisher).publishEvent(argumentCaptor.capture());
93100
assertThat(argumentCaptor.getValue().getSource()).isEqualTo(secret);
94101
assertThat(argumentCaptor.getValue().getOriginService()).isEqualTo(busProperties.getId());
95102
assertThat(argumentCaptor.getValue().getDestinationService()).isEqualTo("foo:**");
96103
}
97104

105+
private V1Secret getV1Secret(V1ObjectMeta objectMeta,
106+
ConfigurationWatcherConfigurationProperties.RefreshStrategy refreshStrategy) {
107+
V1Secret secret = new V1Secret();
108+
secret.setMetadata(objectMeta);
109+
ConfigurationWatcherConfigurationProperties configurationWatcherConfigurationProperties = new ConfigurationWatcherConfigurationProperties();
110+
configurationWatcherConfigurationProperties.setRefreshStrategy(refreshStrategy);
111+
BusEventBasedSecretsWatcherChangeDetector changeDetector = new BusEventBasedSecretsWatcherChangeDetector(
112+
coreV1Api, mockEnvironment, ConfigReloadProperties.DEFAULT, UPDATE_STRATEGY,
113+
secretsPropertySourceLocator, new KubernetesNamespaceProvider(mockEnvironment),
114+
configurationWatcherConfigurationProperties, threadPoolTaskExecutor, new BusRefreshTrigger(
115+
applicationEventPublisher, busProperties.getId(), configurationWatcherConfigurationProperties));
116+
changeDetector.triggerRefresh(secret, secret.getMetadata().getName());
117+
return secret;
118+
}
119+
98120
}

0 commit comments

Comments
 (0)