This commit is contained in:
erabii
2022-09-21 16:45:44 +03:00
committed by GitHub
parent 0e27607898
commit 6e61cf20bf
63 changed files with 2538 additions and 81 deletions

View File

@@ -67,7 +67,12 @@ commands:
cd spring-cloud-kubernetes-integration-tests
while read integ_test; do
docker save -o /tmp/docker/images/${integ_test}.tar docker.io/springcloud/${integ_test}:$TAG
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q | grep -v 'spring-cloud-kubernetes-integration-tests')
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q \
| grep -v 'spring-cloud-kubernetes-integration-tests' \
| grep -v 'spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app' \
| grep -v 'spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app' )
cd ..
VIEW=$(ls -l /tmp/docker/images)
@@ -120,7 +125,12 @@ commands:
cd spring-cloud-kubernetes-integration-tests
while read integration_test_image; do
docker load -i /tmp/docker/images/${integration_test_image}.tar
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q | grep -v 'spring-cloud-kubernetes-integration-tests')
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q \
| grep -v 'spring-cloud-kubernetes-integration-tests' \
| grep -v 'spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app' \
| grep -v 'spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app' )
cd ..

View File

@@ -18,5 +18,10 @@ runs:
cd spring-cloud-kubernetes-integration-tests
while read integration_test_image; do
docker load -i /tmp/docker/images/${integration_test_image}.tar
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q | grep -v 'spring-cloud-kubernetes-integration-tests')
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q \
| grep -v 'spring-cloud-kubernetes-integration-tests' \
| grep -v 'spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app' \
| grep -v 'spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app' )
cd ..

View File

@@ -11,5 +11,10 @@ runs:
cd spring-cloud-kubernetes-integration-tests
while read integ_test; do
docker save -o /tmp/docker/images/${integ_test}.tar docker.io/springcloud/${integ_test}:$TAG
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q | grep -v 'spring-cloud-kubernetes-integration-tests')
done < <(mvn -Dexec.executable='echo' -Dexec.args='${project.artifactId}' exec:exec -q \
| grep -v 'spring-cloud-kubernetes-integration-tests' \
| grep -v 'spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app' \
| grep -v 'spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps' \
| grep -v 'spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app' )
cd ..

View File

@@ -112,11 +112,32 @@ Spring Cloud Kubernetes Configuration Watcher will react to changes in ConfigMap
or any Secret with a label of `spring.cloud.kubernetes.secret` with the value `true`. If the ConfigMap or Secret does not have either of those labels
or the values of those labels is not `true` then any changes will be ignored.
The labels Spring Cloud Kubernetes Configuration Watcher looks for on ConfigMaps and Secrets can be changed by setting
`spring.cloud.kubernetes.configuration.watcher.configLabel` and `spring.cloud.kubernetes.configuration.watcher.secretLabel` respectively.
If a change is made to a ConfigMap or Secret with valid labels then Spring Cloud Kubernetes Configuration Watcher will take the name of the ConfigMap or Secret
and send a notification to the application with that name.
and send a notification to the application with that name. This might not be enough for your use-case though, you could for example what to:
- bind a config-map to multiple applications, so that a change inside a single configmap triggers a refresh for many services
- have profile based sources trigger events for your application
For that reasons there is an addition annotation you could specify:
`spring.cloud.kubernetes.configmap.apps` or `spring.cloud.kubernetes.secret.apps`. It takes a String of apps separated by comma,
that specifies the names of applications that will receive a notification when changes happen in this secret/configmap.
For example:
====
[source,yaml]
----
kind: ConfigMap
apiVersion: v1
metadata:
name: example-configmap
labels:
spring.cloud.kubernetes.config: "true"
annotations:
spring.cloud.kubernetes.configmap.apps: "app-a, app-b"
----
====
### HTTP Implementation

View File

@@ -70,21 +70,21 @@ public class KubernetesClientEventBasedConfigMapChangeDetector extends Configura
private final ResourceEventHandler<V1ConfigMap> handler = new ResourceEventHandler<>() {
@Override
public void onAdd(V1ConfigMap obj) {
LOG.debug(() -> "ConfigMap " + obj.getMetadata().getName() + " was added.");
onEvent(obj);
public void onAdd(V1ConfigMap configMap) {
LOG.debug(() -> "ConfigMap " + configMap.getMetadata().getName() + " was added.");
onEvent(configMap);
}
@Override
public void onUpdate(V1ConfigMap oldObj, V1ConfigMap newObj) {
LOG.debug(() -> "ConfigMap " + newObj.getMetadata().getName() + " was updated.");
onEvent(newObj);
public void onUpdate(V1ConfigMap oldConfigMap, V1ConfigMap newConfigMap) {
LOG.debug(() -> "ConfigMap " + newConfigMap.getMetadata().getName() + " was updated.");
onEvent(newConfigMap);
}
@Override
public void onDelete(V1ConfigMap obj, boolean deletedFinalStateUnknown) {
LOG.debug(() -> "ConfigMap " + obj.getMetadata() + " was deleted.");
onEvent(obj);
public void onDelete(V1ConfigMap configMap, boolean deletedFinalStateUnknown) {
LOG.debug(() -> "ConfigMap " + configMap.getMetadata().getName() + " was deleted.");
onEvent(configMap);
}
};

View File

@@ -70,21 +70,21 @@ public class KubernetesClientEventBasedSecretsChangeDetector extends Configurati
private final ResourceEventHandler<V1Secret> handler = new ResourceEventHandler<>() {
@Override
public void onAdd(V1Secret obj) {
LOG.debug(() -> "Secret " + obj.getMetadata().getName() + " was added.");
onEvent(obj);
public void onAdd(V1Secret secret) {
LOG.debug(() -> "Secret " + secret.getMetadata().getName() + " was added.");
onEvent(secret);
}
@Override
public void onUpdate(V1Secret oldObj, V1Secret newObj) {
LOG.debug(() -> "Secret " + newObj.getMetadata().getName() + " was updated.");
onEvent(newObj);
public void onUpdate(V1Secret oldSecret, V1Secret newSecret) {
LOG.debug(() -> "Secret " + newSecret.getMetadata().getName() + " was updated.");
onEvent(newSecret);
}
@Override
public void onDelete(V1Secret obj, boolean deletedFinalStateUnknown) {
LOG.debug(() -> "Secret " + obj.getMetadata() + " was deleted.");
onEvent(obj);
public void onDelete(V1Secret secret, boolean deletedFinalStateUnknown) {
LOG.debug(() -> "Secret " + secret.getMetadata().getName() + " was deleted.");
onEvent(secret);
}
};

View File

@@ -47,8 +47,8 @@ final class BusEventBasedConfigMapWatcherChangeDetector extends ConfigMapWatcher
}
@Override
public Mono<Void> triggerRefresh(KubernetesObject configMap) {
return busRefreshTrigger.triggerRefresh(configMap);
public Mono<Void> triggerRefresh(KubernetesObject configMap, String appName) {
return busRefreshTrigger.triggerRefresh(configMap, appName);
}
}

View File

@@ -47,8 +47,8 @@ final class BusEventBasedSecretsWatcherChangeDetector extends SecretsWatcherChan
}
@Override
public Mono<Void> triggerRefresh(KubernetesObject secret) {
return busRefreshTrigger.triggerRefresh(secret);
public Mono<Void> triggerRefresh(KubernetesObject secret, String appName) {
return busRefreshTrigger.triggerRefresh(secret, appName);
}
}

View File

@@ -40,9 +40,9 @@ final class BusRefreshTrigger implements RefreshTrigger {
}
@Override
public Mono<Void> triggerRefresh(KubernetesObject configMap) {
public Mono<Void> triggerRefresh(KubernetesObject configMap, String appName) {
applicationEventPublisher.publishEvent(new RefreshRemoteApplicationEvent(configMap, busId,
new PathDestinationFactory().getDestination(configMap.getMetadata().getName())));
new PathDestinationFactory().getDestination(appName)));
return Mono.empty();
}

View File

@@ -30,12 +30,16 @@ import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationU
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.CONFIG_MAP_APPS_ANNOTATION;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.CONFIG_MAP_LABEL;
/**
* @author Ryan Baxter
* @author Kris Iyer
*/
abstract class ConfigMapWatcherChangeDetector extends KubernetesClientEventBasedConfigMapChangeDetector
implements RefreshTrigger {
abstract sealed class ConfigMapWatcherChangeDetector extends KubernetesClientEventBasedConfigMapChangeDetector
implements
RefreshTrigger permits BusEventBasedConfigMapWatcherChangeDetector, HttpBasedConfigMapWatchChangeDetector {
private final ScheduledExecutorService executorService;
@@ -56,8 +60,8 @@ abstract class ConfigMapWatcherChangeDetector extends KubernetesClientEventBased
@Override
protected final void onEvent(KubernetesObject configMap) {
// this::refreshTrigger is coming from BusEventBasedConfigMapWatcherChangeDetector
WatcherUtil.onEvent(configMap, ConfigurationWatcherConfigurationProperties.CONFIG_LABEL, refreshDelay,
executorService, "config-map", this::triggerRefresh);
WatcherUtil.onEvent(configMap, CONFIG_MAP_LABEL, CONFIG_MAP_APPS_ANNOTATION, refreshDelay, executorService,
"config-map", this::triggerRefresh);
}
}

View File

@@ -31,13 +31,23 @@ public class ConfigurationWatcherConfigurationProperties {
/**
* label to enable refresh/restart when using configmaps.
*/
public static final String CONFIG_LABEL = "spring.cloud.kubernetes.config";
public static final String CONFIG_MAP_LABEL = "spring.cloud.kubernetes.config";
/**
* label to enable refresh/restart when using secrets.
*/
public static final String SECRET_LABEL = "spring.cloud.kubernetes.secret";
/**
* annotation name to enable refresh/restart for specific apps when using configmaps.
*/
public static final String CONFIG_MAP_APPS_ANNOTATION = "spring.cloud.kubernetes.configmap.apps";
/**
* annotation name to enable refresh/restart for specific apps when using secrets.
*/
public static final String SECRET_APPS_ANNOTATION = "spring.cloud.kubernetes.secret.apps";
/**
* Annotation key for actuator port and path.
*/

View File

@@ -48,8 +48,8 @@ final class HttpBasedConfigMapWatchChangeDetector extends ConfigMapWatcherChange
}
@Override
public Mono<Void> triggerRefresh(KubernetesObject configMap) {
return httpRefreshTrigger.triggerRefresh(configMap);
public Mono<Void> triggerRefresh(KubernetesObject configMap, String appName) {
return httpRefreshTrigger.triggerRefresh(configMap, appName);
}
}

View File

@@ -48,8 +48,8 @@ final class HttpBasedSecretsWatchChangeDetector extends SecretsWatcherChangeDete
}
@Override
public Mono<Void> triggerRefresh(KubernetesObject secret) {
return httpRefreshTrigger.triggerRefresh(secret);
public Mono<Void> triggerRefresh(KubernetesObject secret, String appName) {
return httpRefreshTrigger.triggerRefresh(secret, appName);
}
}

View File

@@ -54,16 +54,14 @@ final class HttpRefreshTrigger implements RefreshTrigger {
}
@Override
public Mono<Void> triggerRefresh(KubernetesObject kubernetesObject) {
public Mono<Void> triggerRefresh(KubernetesObject kubernetesObject, String appName) {
String name = kubernetesObject.getMetadata().getName();
return kubernetesReactiveDiscoveryClient.getInstances(name).flatMap(si -> {
return kubernetesReactiveDiscoveryClient.getInstances(appName).flatMap(si -> {
URI actuatorUri = getActuatorUri(si, k8SConfigurationProperties.getActuatorPath(),
k8SConfigurationProperties.getActuatorPort());
LOG.debug(() -> "Sending refresh request for " + name + " to URI " + actuatorUri);
LOG.debug(() -> "Sending refresh request for " + appName + " to URI " + actuatorUri);
return webClient.post().uri(actuatorUri).retrieve().toBodilessEntity()
.doOnSuccess(onSuccess(name, actuatorUri)).doOnError(onError(name));
.doOnSuccess(onSuccess(appName, actuatorUri)).doOnError(onError(appName));
}).then();
}
@@ -102,8 +100,7 @@ final class HttpRefreshTrigger implements RefreshTrigger {
// 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
// the
// 9090 in this case
// the 9090 in this case
if (annotationUri.getPort() < 0) {
if (annotationUri.getAuthority() != null) {
actuatorUriBuilder.port(annotationUri.getAuthority().replaceFirst(":", ""));

View File

@@ -24,11 +24,13 @@ import reactor.core.publisher.Mono;
*
* @author wind57
*/
interface RefreshTrigger {
sealed interface RefreshTrigger permits BusRefreshTrigger, ConfigMapWatcherChangeDetector, HttpRefreshTrigger, SecretsWatcherChangeDetector {
/**
* @param kubernetesObject either a config-map or secret at the moment.
* @param appName which is not necessarily equal to
* kubernetesObject.getMetadata().getName()
*/
Mono<Void> triggerRefresh(KubernetesObject kubernetesObject);
Mono<Void> triggerRefresh(KubernetesObject kubernetesObject, String appName);
}

View File

@@ -30,12 +30,15 @@ import org.springframework.cloud.kubernetes.commons.config.reload.ConfigurationU
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.SECRET_APPS_ANNOTATION;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.SECRET_LABEL;
/**
* @author Ryan Baxter
* @author Kris Iyer
*/
abstract class SecretsWatcherChangeDetector extends KubernetesClientEventBasedSecretsChangeDetector
implements RefreshTrigger {
abstract sealed class SecretsWatcherChangeDetector extends KubernetesClientEventBasedSecretsChangeDetector implements
RefreshTrigger permits BusEventBasedSecretsWatcherChangeDetector, HttpBasedSecretsWatchChangeDetector {
private final ScheduledExecutorService executorService;
@@ -56,8 +59,8 @@ abstract class SecretsWatcherChangeDetector extends KubernetesClientEventBasedSe
@Override
protected final void onEvent(KubernetesObject secret) {
// this::refreshTrigger is coming from BusEventBasedSecretsWatcherChangeDetector
WatcherUtil.onEvent(secret, ConfigurationWatcherConfigurationProperties.SECRET_LABEL, refreshDelay,
executorService, "secret", this::triggerRefresh);
WatcherUtil.onEvent(secret, SECRET_LABEL, SECRET_APPS_ANNOTATION, refreshDelay, executorService, "secret",
this::triggerRefresh);
}
}

View File

@@ -16,13 +16,18 @@
package org.springframework.cloud.kubernetes.configuration.watcher;
import java.util.Collections;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import io.kubernetes.client.common.KubernetesObject;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
@@ -40,37 +45,93 @@ final class WatcherUtil {
private WatcherUtil() {
}
static void onEvent(KubernetesObject kubernetesObject, String label, long refreshDelay,
static void onEvent(KubernetesObject kubernetesObject, String label, String annotationName, long refreshDelay,
ScheduledExecutorService executorService, String type,
Function<KubernetesObject, Mono<Void>> triggerRefresh) {
BiFunction<KubernetesObject, String, Mono<Void>> triggerRefresh) {
String name = kubernetesObject.getMetadata().getName();
boolean isSpringCloudKubernetes = isSpringCloudKubernetes(kubernetesObject, label);
if (isSpringCloudKubernetes) {
LOG.debug(() -> "Scheduling remote refresh event to be published for " + type + ": " + name
+ " to be published in " + refreshDelay + " milliseconds");
executorService.schedule(() -> {
try {
triggerRefresh.apply(kubernetesObject).subscribe();
}
catch (Throwable t) {
LOG.warn(t, "Error when refreshing ConfigMap " + name);
}
}, refreshDelay, TimeUnit.MILLISECONDS);
Set<String> apps = apps(kubernetesObject, annotationName);
if (apps.isEmpty()) {
apps.add(name);
}
LOG.info(() -> "will schedule remote refresh based on apps : " + apps);
apps.forEach(appName -> schedule(type, appName, refreshDelay, executorService, triggerRefresh,
kubernetesObject));
}
else {
LOG.debug(() -> "Not publishing event." + type + ": + name + does not contain the label " + label);
LOG.debug(() -> "Not publishing event." + type + ": " + name + " does not contain the label " + label);
}
}
private static boolean isSpringCloudKubernetes(KubernetesObject kubernetesObject, String label) {
static boolean isSpringCloudKubernetes(KubernetesObject kubernetesObject, String label) {
if (kubernetesObject.getMetadata() == null) {
return false;
}
return Boolean.parseBoolean(Optional.ofNullable(kubernetesObject.getMetadata().getLabels())
.orElse(Collections.emptyMap()).getOrDefault(label, "false"));
return Boolean.parseBoolean(labels(kubernetesObject).getOrDefault(label, "false"));
}
static Set<String> apps(KubernetesObject kubernetesObject, String annotationName) {
// mutable on purpose
Set<String> apps = new HashSet<>(1);
Map<String, String> annotations = annotations(kubernetesObject);
if (annotations.isEmpty()) {
LOG.debug(() -> annotationName + " not present (empty data)");
return apps;
}
String appsValue = annotations.get(annotationName);
if (appsValue == null) {
LOG.debug(() -> annotationName + " not present (missing in annotations)");
return apps;
}
if (appsValue.isBlank()) {
LOG.debug(() -> appsValue + " not present (blanks only)");
return apps;
}
return Arrays.stream(appsValue.split(",")).map(String::trim).collect(Collectors.toSet());
}
static Map<String, String> labels(KubernetesObject kubernetesObject) {
V1ObjectMeta metadata = kubernetesObject.getMetadata();
if (metadata == null) {
return Map.of();
}
return Optional.ofNullable(metadata.getLabels()).orElse(Map.of());
}
static Map<String, String> annotations(KubernetesObject kubernetesObject) {
V1ObjectMeta metadata = kubernetesObject.getMetadata();
if (metadata == null) {
return Map.of();
}
return Optional.ofNullable(metadata.getAnnotations()).orElse(Map.of());
}
private static void schedule(String type, String appName, long refreshDelay,
ScheduledExecutorService executorService, BiFunction<KubernetesObject, String, Mono<Void>> triggerRefresh,
KubernetesObject kubernetesObject) {
LOG.debug(() -> "Scheduling remote refresh event to be published for " + type + ": with appName : " + appName
+ " to be published in " + refreshDelay + " milliseconds");
executorService.schedule(() -> {
try {
triggerRefresh.apply(kubernetesObject, appName).subscribe();
}
catch (Throwable t) {
LOG.warn(t, "Error when refreshing appName " + appName);
}
}, refreshDelay, TimeUnit.MILLISECONDS);
}
}

View File

@@ -90,7 +90,7 @@ class BusEventBasedConfigMapWatcherChangeDetectorTests {
objectMeta.setName("foo");
V1ConfigMap configMap = new V1ConfigMap();
configMap.setMetadata(objectMeta);
changeDetector.triggerRefresh(configMap);
changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName());
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
.forClass(RefreshRemoteApplicationEvent.class);
verify(applicationEventPublisher).publishEvent(argumentCaptor.capture());

View File

@@ -90,7 +90,7 @@ class BusEventBasedSecretsWatcherChangeDetectorTests {
objectMeta.setName("foo");
V1Secret secret = new V1Secret();
secret.setMetadata(objectMeta);
changeDetector.triggerRefresh(secret);
changeDetector.triggerRefresh(secret, secret.getMetadata().getName());
ArgumentCaptor<RefreshRemoteApplicationEvent> argumentCaptor = ArgumentCaptor
.forClass(RefreshRemoteApplicationEvent.class);
verify(applicationEventPublisher).publishEvent(argumentCaptor.capture());

View File

@@ -128,7 +128,8 @@ class HttpBasedConfigMapWatchChangeDetectorTests {
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
StepVerifier.create(changeDetector.triggerRefresh(configMap)).verifyComplete();
StepVerifier.create(changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName()))
.verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh")));
}
@@ -143,7 +144,8 @@ class HttpBasedConfigMapWatchChangeDetectorTests {
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
StepVerifier.create(changeDetector.triggerRefresh(configMap)).verifyComplete();
StepVerifier.create(changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName()))
.verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
}
@@ -170,7 +172,8 @@ class HttpBasedConfigMapWatchChangeDetectorTests {
configMap.setMetadata(objectMeta);
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
StepVerifier.create(changeDetector.triggerRefresh(configMap)).verifyComplete();
StepVerifier.create(changeDetector.triggerRefresh(configMap, configMap.getMetadata().getName()))
.verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
}

View File

@@ -125,7 +125,7 @@ class HttpBasedSecretsWatchChangeDetectorTests {
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
StepVerifier.create(changeDetector.triggerRefresh(secret)).verifyComplete();
StepVerifier.create(changeDetector.triggerRefresh(secret, secret.getMetadata().getName())).verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/actuator/refresh")));
}
@@ -140,7 +140,7 @@ class HttpBasedSecretsWatchChangeDetectorTests {
WireMock.configureFor("localhost", WIRE_MOCK_SERVER.port());
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
StepVerifier.create(changeDetector.triggerRefresh(secret)).verifyComplete();
StepVerifier.create(changeDetector.triggerRefresh(secret, secret.getMetadata().getName())).verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
}
@@ -166,7 +166,7 @@ class HttpBasedSecretsWatchChangeDetectorTests {
secret.setMetadata(objectMeta);
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/my/custom/actuator/refresh"))
.willReturn(WireMock.aResponse().withStatus(200)));
StepVerifier.create(changeDetector.triggerRefresh(secret)).verifyComplete();
StepVerifier.create(changeDetector.triggerRefresh(secret, secret.getMetadata().getName())).verifyComplete();
WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/my/custom/actuator/refresh")));
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2013-2022 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.kubernetes.configuration.watcher;
import java.util.Map;
import java.util.Set;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1SecretBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.CONFIG_MAP_LABEL;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.SECRET_APPS_ANNOTATION;
import static org.springframework.cloud.kubernetes.configuration.watcher.ConfigurationWatcherConfigurationProperties.SECRET_LABEL;
class WatcherUtilTests {
@Test
void isSpringCloudKubernetesConfigFalse() {
V1ConfigMap configMap = new V1ConfigMapBuilder().withMetadata(new V1ObjectMeta().labels(Map.of())).build();
boolean present = WatcherUtil.isSpringCloudKubernetes(configMap, CONFIG_MAP_LABEL);
Assertions.assertFalse(present);
}
@Test
void isSpringCloudKubernetesConfigTrue() {
V1ConfigMap configMap = new V1ConfigMapBuilder()
.withMetadata(new V1ObjectMeta().labels(Map.of(CONFIG_MAP_LABEL, "true"))).build();
boolean present = WatcherUtil.isSpringCloudKubernetes(configMap, CONFIG_MAP_LABEL);
Assertions.assertTrue(present);
}
@Test
void isSpringCloudKubernetesSecretFalse() {
V1Secret secret = new V1SecretBuilder().withMetadata(new V1ObjectMeta().labels(Map.of())).build();
boolean present = WatcherUtil.isSpringCloudKubernetes(secret, SECRET_LABEL);
Assertions.assertFalse(present);
}
@Test
void isSpringCloudKubernetesSecretTrue() {
V1Secret secret = new V1SecretBuilder().withMetadata(new V1ObjectMeta().labels(Map.of(SECRET_LABEL, "true")))
.build();
boolean present = WatcherUtil.isSpringCloudKubernetes(secret, SECRET_LABEL);
Assertions.assertTrue(present);
}
@Test
void labelsMissing() {
V1Secret secret = new V1SecretBuilder().withMetadata(new V1ObjectMeta()).build();
Map<String, String> res = WatcherUtil.labels(secret);
Assertions.assertEquals(res.size(), 0);
}
@Test
void labelsPresent() {
V1Secret secret = new V1SecretBuilder().withMetadata(new V1ObjectMeta().labels(Map.of("a", "b"))).build();
Map<String, String> res = WatcherUtil.labels(secret);
Assertions.assertEquals(res.size(), 1);
}
@Test
void appsNoMetadata() {
V1Secret secret = new V1SecretBuilder().build();
Set<String> apps = WatcherUtil.apps(secret, SECRET_APPS_ANNOTATION);
Assertions.assertEquals(apps.size(), 0);
}
@Test
void appsNoAnnotations() {
V1Secret secret = new V1SecretBuilder().withMetadata(new V1ObjectMeta().annotations(Map.of())).build();
Set<String> apps = WatcherUtil.apps(secret, SECRET_APPS_ANNOTATION);
Assertions.assertEquals(apps.size(), 0);
}
@Test
void appsAnnotationNotFound() {
V1Secret secret = new V1SecretBuilder().withMetadata(new V1ObjectMeta().annotations(Map.of("a", "b"))).build();
Set<String> apps = WatcherUtil.apps(secret, SECRET_APPS_ANNOTATION);
Assertions.assertEquals(apps.size(), 0);
}
@Test
void appsSingleResult() {
V1Secret secret = new V1SecretBuilder()
.withMetadata(new V1ObjectMeta().annotations(Map.of(SECRET_APPS_ANNOTATION, "one-app"))).build();
Set<String> apps = WatcherUtil.apps(secret, SECRET_APPS_ANNOTATION);
Assertions.assertEquals(apps.size(), 1);
Assertions.assertEquals(apps.iterator().next(), "one-app");
}
@Test
void appsMultipleResults() {
V1Secret secret = new V1SecretBuilder()
.withMetadata(new V1ObjectMeta().annotations(Map.of(SECRET_APPS_ANNOTATION, "one, two, three ")))
.build();
Set<String> apps = WatcherUtil.apps(secret, SECRET_APPS_ANNOTATION);
Assertions.assertEquals(apps.size(), 3);
Assertions.assertTrue(apps.contains("one"));
Assertions.assertTrue(apps.contains("two"));
Assertions.assertTrue(apps.contains("three"));
}
}

View File

@@ -119,5 +119,7 @@
<module>spring-cloud-kubernetes-core-k8s-client-it</module>
<module>spring-cloud-kubernetes-client-secrets-event-reload</module>
<module>spring-cloud-kubernetes-client-configmap-event-reload</module>
<module>spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps</module>
<module>spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps</module>
</modules>
</project>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps</artifactId>
<packaging>pom</packaging>
<modules>
<module>spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a</module>
<module>spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b</module>
<module>spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app</module>
</modules>
</project>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
<imageBuilder>paketobuildpacks/builder</imageBuilder>
</configuration>
<executions>
<execution>
<id>build-image</id>
<configuration>
<skip>${skip.build.image}</skip>
</configuration>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
<execution>
<id>repackage</id>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2013-2022 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.kubernetes.configuration.watcher.appA;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wind57
*/
@SpringBootApplication
@RestController
public class AppATestApplication implements ApplicationListener<RefreshRemoteApplicationEvent> {
private final Log LOG = LogFactory.getLog(getClass());
private boolean value = false;
public static void main(String[] args) {
SpringApplication.run(AppATestApplication.class, args);
}
@GetMapping("/app-a")
public boolean index() {
LOG.info("Current value: " + value);
return value;
}
@Override
public void onApplicationEvent(RefreshRemoteApplicationEvent refreshRemoteApplicationEvent) {
LOG.info("Received remote refresh event from origin: " + refreshRemoteApplicationEvent.getOriginService()
+ " to destination : " + refreshRemoteApplicationEvent.getDestinationService());
this.value = true;
}
}

View File

@@ -0,0 +1,22 @@
#logging:
# level:
# org.springframework: DEBUG
#
spring:
application:
name: spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a
cloud:
bus:
refresh:
enabled: false
enabled: true
destination: multiple-apps
stream:
default-binder: kafka
management:
endpoint:
health:
probes:
enabled: true
server:
port: 8080

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
<relativePath>../../spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
<imageBuilder>paketobuildpacks/builder</imageBuilder>
</configuration>
<executions>
<execution>
<id>build-image</id>
<configuration>
<skip>${skip.build.image}</skip>
</configuration>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
<execution>
<id>repackage</id>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2013-2022 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.kubernetes.configuration.watcher.appB;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wind57
*/
@SpringBootApplication
@RestController
public class AppBTestApplication implements ApplicationListener<RefreshRemoteApplicationEvent> {
private final Log LOG = LogFactory.getLog(getClass());
private boolean value = false;
public static void main(String[] args) {
SpringApplication.run(AppBTestApplication.class, args);
}
@GetMapping("/app-b")
public boolean index() {
LOG.info("Current value: " + value);
return value;
}
@Override
public void onApplicationEvent(RefreshRemoteApplicationEvent refreshRemoteApplicationEvent) {
LOG.info("Received remote refresh event from origin: " + refreshRemoteApplicationEvent.getOriginService()
+ " to destination : " + refreshRemoteApplicationEvent.getDestinationService());
this.value = true;
}
}

View File

@@ -0,0 +1,22 @@
#logging:
# level:
# org.springframework: DEBUG
#
spring:
application:
name: spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b
cloud:
bus:
refresh:
enabled: false
enabled: true
destination: multiple-apps
stream:
default-binder: kafka
management:
endpoint:
health:
probes:
enabled: true
server:
port: 8081

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>spring-cloud-kubernetes-client-configuration-watcher-configmap-test-app</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-extended</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>k3s</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!-- ignore plain tests (in the 'test' phase), so that we could build the image first, see above -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>${testsToRun}</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,383 @@
/*
* Copyright 2013-2022 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.kubernetes.configuration.watcher.multiple.apps;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import io.kubernetes.client.openapi.apis.AppsV1Api;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.NetworkingV1Api;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1ConfigMapBuilder;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Ingress;
import io.kubernetes.client.openapi.models.V1Service;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.k3s.K3sContainer;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.createApiClient;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.getPomVersion;
/**
* @author wind57
*/
class ConfigurationWatcherMultipleAppsIT {
private static final String CONFIG_WATCHER_APP_A_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a";
private static final String CONFIG_WATCHER_APP_B_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b";
private static final String CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME = "app-a-deployment";
private static final String CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME = "app-b-deployment";
private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-configuration-watcher-deployment";
private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher";
private static final String CONFIG_MAP_NAME = "multiple-apps";
private static final String NAMESPACE = "default";
private static final String KAFKA_BROKER = "kafka-broker";
private static final String KAFKA_SERVICE = "kafka";
private static final String ZOOKEEPER_SERVICE = "zookeeper";
private static final String ZOOKEEPER_DEPLOYMENT = "zookeeper";
private static CoreV1Api api;
private static AppsV1Api appsApi;
private static NetworkingV1Api networkingApi;
private static K8SUtils k8SUtils;
private static final K3sContainer K3S = Commons.container();
@BeforeAll
static void beforeAll() throws Exception {
K3S.start();
Commons.validateImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S);
Commons.validateImage(CONFIG_WATCHER_APP_A_IMAGE, K3S);
Commons.loadSpringCloudKubernetesImage(CONFIG_WATCHER_APP_A_IMAGE, K3S);
Commons.validateImage(CONFIG_WATCHER_APP_B_IMAGE, K3S);
Commons.loadSpringCloudKubernetesImage(CONFIG_WATCHER_APP_B_IMAGE, K3S);
createApiClient(K3S.getKubeConfigYaml());
api = new CoreV1Api();
appsApi = new AppsV1Api();
k8SUtils = new K8SUtils(api, appsApi);
networkingApi = new NetworkingV1Api();
k8SUtils.setUp(NAMESPACE);
}
@AfterAll
static void afterAll() throws Exception {
Commons.cleanUp(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S);
Commons.cleanUp(CONFIG_WATCHER_APP_A_IMAGE, K3S);
Commons.cleanUp(CONFIG_WATCHER_APP_B_IMAGE, K3S);
}
@BeforeEach
void setup() throws Exception {
deployZookeeper();
deployKafka();
deployAppA();
deployAppB();
deployIngress();
deployConfigWatcher();
waitForDeployment(ZOOKEEPER_DEPLOYMENT);
waitForDeployment(KAFKA_BROKER);
waitForDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME);
waitForDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME);
waitForDeployment(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME);
}
@AfterEach
void after() throws Exception {
cleanUpKafka();
cleanUpZookeeper();
cleanUpServices();
cleanUpDeployments();
cleanUpIngress();
cleanUpConfigMaps();
k8SUtils.waitForDeploymentToBeDeleted(KAFKA_BROKER, NAMESPACE);
k8SUtils.waitForDeploymentToBeDeleted(ZOOKEEPER_DEPLOYMENT, NAMESPACE);
k8SUtils.waitForDeploymentToBeDeleted(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, NAMESPACE);
k8SUtils.waitForDeploymentToBeDeleted(CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME, NAMESPACE);
k8SUtils.waitForDeploymentToBeDeleted(CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME, NAMESPACE);
}
@Test
void testRefresh() throws Exception {
// configmap has one label, one that says that we should refresh
// and one annotation that says that we should refresh some specific services
V1ConfigMap configMap = new V1ConfigMapBuilder().editOrNewMetadata().withName(CONFIG_MAP_NAME)
.addToLabels("spring.cloud.kubernetes.config", "true")
.addToAnnotations("spring.cloud.kubernetes.configmap.apps",
"spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a, "
+ "spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b")
.endMetadata().addToData("foo", "hello world").build();
api.createNamespacedConfigMap(NAMESPACE, configMap, null, null, null);
WebClient.Builder builderA = builder();
WebClient serviceClientA = builderA.baseUrl("http://localhost:80/app-a").build();
WebClient.Builder builderB = builder();
WebClient serviceClientB = builderB.baseUrl("http://localhost:80/app-b").build();
Boolean[] valueA = new Boolean[1];
await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(240)).until(() -> {
valueA[0] = serviceClientA.method(HttpMethod.GET).retrieve().bodyToMono(Boolean.class)
.retryWhen(retrySpec()).block();
return valueA[0];
});
Assertions.assertThat(valueA[0]).isTrue();
Boolean[] valueB = new Boolean[1];
await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(240)).until(() -> {
valueB[0] = serviceClientB.method(HttpMethod.GET).retrieve().bodyToMono(Boolean.class)
.retryWhen(retrySpec()).block();
return valueB[0];
});
Assertions.assertThat(valueB[0]).isTrue();
}
/**
* <pre>
--------------------------------------------------- zookeeper ----------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployZookeeper() throws Exception {
api.createNamespacedService(NAMESPACE, getZookeeperService(), null, null, null);
V1Deployment deployment = getZookeeperDeployment();
String[] image = K8SUtils.getImageFromDeployment(deployment).split(":");
Commons.pullImage(image[0], image[1], K3S);
Commons.loadImage(image[0], image[1], "zookeeper", K3S);
appsApi.createNamespacedDeployment(NAMESPACE, deployment, null, null, null);
}
private V1Deployment getZookeeperDeployment() throws Exception {
return (V1Deployment) K8SUtils.readYamlFromClasspath("zookeeper/zookeeper-deployment.yaml");
}
private V1Service getZookeeperService() throws Exception {
return (V1Service) K8SUtils.readYamlFromClasspath("zookeeper/zookeeper-service.yaml");
}
/**
* <pre>
----------------------------------------------------- kafka ------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployKafka() throws Exception {
api.createNamespacedService(NAMESPACE, getKafkaService(), null, null, null);
V1Deployment deployment = getKafkaDeployment();
String[] image = K8SUtils.getImageFromDeployment(deployment).split(":");
Commons.pullImage(image[0], image[1], K3S);
Commons.loadImage(image[0], image[1], "kafka", K3S);
appsApi.createNamespacedDeployment(NAMESPACE, getKafkaDeployment(), null, null, null);
}
private V1Deployment getKafkaDeployment() throws Exception {
return (V1Deployment) K8SUtils.readYamlFromClasspath("kafka/kafka-deployment.yaml");
}
private V1Service getKafkaService() throws Exception {
return (V1Service) K8SUtils.readYamlFromClasspath("kafka/kafka-service.yaml");
}
/**
* <pre>
----------------------------------------------------- app-a ------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployAppA() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getAppADeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getAppAService(), null, null, null);
}
private V1Deployment getAppADeployment() throws Exception {
String urlString = "app-a/app-a-deployment.yaml";
V1Deployment deployment = (V1Deployment) K8SUtils.readYamlFromClasspath(urlString);
String image = K8SUtils.getImageFromDeployment(deployment) + ":" + getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Service getAppAService() throws Exception {
return (V1Service) K8SUtils.readYamlFromClasspath("app-a/app-a-service.yaml");
}
/**
* <pre>
--------------------------------------------------- app-b --------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployAppB() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getAppBDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getAppBService(), null, null, null);
}
private V1Deployment getAppBDeployment() throws Exception {
String urlString = "app-b/app-b-deployment.yaml";
V1Deployment deployment = (V1Deployment) K8SUtils.readYamlFromClasspath(urlString);
String image = K8SUtils.getImageFromDeployment(deployment) + ":" + getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Service getAppBService() throws Exception {
return (V1Service) K8SUtils.readYamlFromClasspath("app-b/app-b-service.yaml");
}
/**
* <pre>
------------------------------------------------ config-watcher --------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployConfigWatcher() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getConfigWatcherDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getConfigWatcherService(), null, null, null);
}
private V1Deployment getConfigWatcherDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) K8SUtils.readYamlFromClasspath(
"config-watcher/spring-cloud-kubernetes-configuration-watcher-bus-kafka-deployment.yaml");
String image = K8SUtils.getImageFromDeployment(deployment) + ":" + getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Service getConfigWatcherService() throws Exception {
return (V1Service) K8SUtils
.readYamlFromClasspath("config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml");
}
/**
* <pre>
------------------------------------------------ common ----------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployIngress() throws Exception {
V1Ingress ingress = (V1Ingress) K8SUtils.readYamlFromClasspath(
"ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml");
networkingApi.createNamespacedIngress(NAMESPACE, ingress, null, null, null);
k8SUtils.waitForIngress(ingress.getMetadata().getName(), NAMESPACE);
}
private void waitForDeployment(String deploymentName) {
await().pollInterval(Duration.ofSeconds(3)).atMost(600, TimeUnit.SECONDS)
.until(() -> k8SUtils.isDeploymentReady(deploymentName, NAMESPACE));
}
private void cleanUpKafka() throws Exception {
appsApi.deleteNamespacedDeployment(KAFKA_BROKER, NAMESPACE, null, null, null, null, null, null);
api.deleteNamespacedService(KAFKA_SERVICE, NAMESPACE, null, null, null, null, null, null);
}
private void cleanUpZookeeper() throws Exception {
appsApi.deleteNamespacedDeployment(ZOOKEEPER_DEPLOYMENT, NAMESPACE, null, null, null, null, null, null);
api.deleteNamespacedService(ZOOKEEPER_SERVICE, NAMESPACE, null, null, null, null, null, null);
}
private void cleanUpServices() throws Exception {
api.deleteNamespacedService("app-a", NAMESPACE, null, null, null, null, null, null);
api.deleteNamespacedService("app-b", NAMESPACE, null, null, null, null, null, null);
api.deleteNamespacedService(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, null, null, null, null, null,
null);
}
private void cleanUpDeployments() throws Exception {
appsApi.deleteNamespacedDeployment(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, NAMESPACE, null, null, null,
null, null, null);
appsApi.deleteNamespacedDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME, NAMESPACE, null, null, null, null,
null, null);
appsApi.deleteNamespacedDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME, NAMESPACE, null, null, null, null,
null, null);
}
private void cleanUpConfigMaps() throws Exception {
api.deleteNamespacedConfigMap(CONFIG_MAP_NAME, NAMESPACE, null, null, null, null, null, null);
}
private void cleanUpIngress() throws Exception {
networkingApi.deleteNamespacedIngress("it-ingress-multiple-apps", NAMESPACE, null, null, null, null, null,
null);
}
private WebClient.Builder builder() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
}
private RetryBackoffSpec retrySpec() {
return Retry.fixedDelay(240, Duration.ofSeconds(1)).filter(Objects::nonNull);
}
}

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-a-deployment
spec:
selector:
matchLabels:
app: app-a
template:
metadata:
labels:
app: app-a
spec:
containers:
- name: app-a
image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-a
imagePullPolicy: IfNotPresent
env:
- name: SPRING_PROFILES_ACTIVE
value: bus-kafka
- name: spring.kafka.bootstrap-servers
value: kafka:9092
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
ports:
- containerPort: 8080

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: app-a
name: app-a
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: app-a
type: ClusterIP

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-b-deployment
spec:
selector:
matchLabels:
app: app-b
template:
metadata:
labels:
app: app-b
spec:
containers:
- name: app-b
image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-configmap-app-b
imagePullPolicy: IfNotPresent
env:
- name: SPRING_PROFILES_ACTIVE
value: bus-kafka
- name: spring.kafka.bootstrap-servers
value: kafka:9092
readinessProbe:
httpGet:
port: 8081
path: /actuator/health/readiness
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
livenessProbe:
httpGet:
port: 8081
path: /actuator/health/liveness
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
ports:
- containerPort: 8081

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: app-b
name: app-b
spec:
ports:
- name: http
port: 8081
targetPort: 8081
selector:
app: app-b
type: ClusterIP

View File

@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-configuration-watcher-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-configuration-watcher
template:
metadata:
labels:
app: spring-cloud-kubernetes-configuration-watcher
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-configuration-watcher
image: docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher
imagePullPolicy: IfNotPresent
env:
- name: SPRING_PROFILES_ACTIVE
value: bus-kafka
- name: SPRING_CLOUD_BUS_DESTINATION
value: multiple-apps
- name: spring.kafka.bootstrap-servers
value: kafka:9092
- name: SPRING_CLOUD_KUBERNETES_CONFIGURATION_WATCHER_REFRESHDELAY
value: 1
readinessProbe:
httpGet:
port: 8888
path: /actuator/health/readiness
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
livenessProbe:
httpGet:
port: 8888
path: /actuator/health/liveness
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
ports:
- containerPort: 8888

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-configuration-watcher
name: spring-cloud-kubernetes-configuration-watcher
spec:
ports:
- name: http
port: 8888
targetPort: 8888
selector:
app: spring-cloud-kubernetes-configuration-watcher
type: ClusterIP

View File

@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: it-ingress-multiple-apps
namespace: default
spec:
rules:
- http:
paths:
- path: /app-a
pathType: Prefix
backend:
service:
name: app-a
port:
number: 8080
- http:
paths:
- path: /app-b
pathType: Prefix
backend:
service:
name: app-b
port:
number: 8081

View File

@@ -0,0 +1,54 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: kafka
component: kafka-broker
name: kafka-broker
spec:
replicas: 1
selector:
matchLabels:
app: kafka
component: kafka-broker
template:
metadata:
labels:
app: kafka
component: kafka-broker
spec:
# otherwise we will get an env var "KAFKA_PORT" (from service name: "kafka" and appended with "_PORT")
# and this will cause this problem: https://github.com/confluentinc/cp-docker-images/blob/master/debian/kafka/include/etc/confluent/docker/configure#L58-L62
# Another solution is to rename the service.
enableServiceLinks: false
containers:
- name: kafka
image: confluentinc/cp-kafka:7.2.1
ports:
- containerPort: 9092
env:
- name: KAFKA_LISTENERS
value: "INTERNAL://0.0.0.0:9092,OUTSIDE://0.0.0.0:9094"
- name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP
value: "INTERNAL:PLAINTEXT,OUTSIDE:PLAINTEXT"
- name: KAFKA_ADVERTISED_LISTENERS
value: "INTERNAL://kafka:9092,OUTSIDE://localhost:9094"
- name: KAFKA_INTER_BROKER_LISTENER_NAME
value: "INTERNAL"
- name: KAFKA_ADVERTISED_HOST_NAME
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: KAFKA_ZOOKEEPER_CONNECT
value: zookeeper:2181
# we have enabled auto creation of topics and when this happens there is a replication factor of 3
# that is set automatically. Since we don't have that many, producers will fail.
# This setting ensures that there is just one replication
- name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR
value: "1"

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: kafka
labels:
app: kafka
component: kafka-broker
spec:
ports:
- port: 9092
name: kafka-port
targetPort: 9092
protocol: TCP
selector:
app: kafka
component: kafka-broker

View File

@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.testcontainers" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
</configuration>

View File

@@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: kafka
component: zookeeper
name: zookeeper
spec:
replicas: 1
selector:
matchLabels:
app: kafka
component: zookeeper
template:
metadata:
labels:
app: kafka
component: zookeeper
spec:
containers:
- name: zookeeper
image: confluentinc/cp-zookeeper:7.2.1
ports:
- containerPort: 2181
env:
- name: ZOOKEEPER_ID
value: "1"
- name: ZOOKEEPER_SERVER_1
value: zookeeper
- name: ZOOKEEPER_CLIENT_PORT
value: 2181

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: zookeeper
labels:
app: kafka
component: zookeeper
spec:
ports:
- port: 2181
name: zookeeper-port
targetPort: 2181
protocol: TCP
selector:
app: kafka
component: zookeeper

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps</artifactId>
<packaging>pom</packaging>
<modules>
<module>spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a</module>
<module>spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b</module>
<module>spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app</module>
</modules>
</project>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
<imageBuilder>paketobuildpacks/builder</imageBuilder>
</configuration>
<executions>
<execution>
<id>build-image</id>
<configuration>
<skip>${skip.build.image}</skip>
</configuration>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
<execution>
<id>repackage</id>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2013-2022 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.kubernetes.configuration.watcher.appA;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wind57
*/
@SpringBootApplication
@RestController
public class AppATestApplication implements ApplicationListener<RefreshRemoteApplicationEvent> {
private final Log LOG = LogFactory.getLog(getClass());
private boolean value = false;
public static void main(String[] args) {
SpringApplication.run(AppATestApplication.class, args);
}
@GetMapping("/app-a")
public boolean index() {
LOG.info("Current value: " + value);
return value;
}
@Override
public void onApplicationEvent(RefreshRemoteApplicationEvent refreshRemoteApplicationEvent) {
LOG.info("Received remote refresh event from origin: " + refreshRemoteApplicationEvent.getOriginService()
+ " to destination : " + refreshRemoteApplicationEvent.getDestinationService());
this.value = true;
}
}

View File

@@ -0,0 +1,24 @@
#logging:
# level:
# org.springframework: DEBUG
#
spring:
application:
name: spring-cloud-kubernetes-client-configuration-watcher-secret-app-a
cloud:
bus:
refresh:
enabled: false
enabled: true
destination: multiple-apps
stream:
default-binder: rabbit
autoconfigure:
exclude: org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
management:
endpoint:
health:
probes:
enabled: true
server:
port: 8080

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
<imageBuilder>paketobuildpacks/builder</imageBuilder>
</configuration>
<executions>
<execution>
<id>build-image</id>
<configuration>
<skip>${skip.build.image}</skip>
</configuration>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
<execution>
<id>repackage</id>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2013-2022 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.kubernetes.configuration.watcher.appB;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wind57
*/
@SpringBootApplication
@RestController
public class AppBTestApplication implements ApplicationListener<RefreshRemoteApplicationEvent> {
private final Log LOG = LogFactory.getLog(getClass());
private boolean value = false;
public static void main(String[] args) {
SpringApplication.run(AppBTestApplication.class, args);
}
@GetMapping("/app-b")
public boolean index() {
LOG.info("Current value: " + value);
return value;
}
@Override
public void onApplicationEvent(RefreshRemoteApplicationEvent refreshRemoteApplicationEvent) {
LOG.info("Received remote refresh event from origin: " + refreshRemoteApplicationEvent.getOriginService()
+ " to destination : " + refreshRemoteApplicationEvent.getDestinationService());
this.value = true;
}
}

View File

@@ -0,0 +1,24 @@
#logging:
# level:
# org.springframework: DEBUG
#
spring:
application:
name: spring-cloud-kubernetes-client-configuration-watcher-secret-app-b
cloud:
bus:
refresh:
enabled: false
enabled: true
destination: multiple-apps
stream:
default-binder: rabbit
autoconfigure:
exclude: org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
management:
endpoint:
health:
probes:
enabled: true
server:
port: 8081

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>spring-cloud-kubernetes-client-configuration-watcher-secrets-test-app</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java-extended</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>k3s</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!-- ignore plain tests (in the 'test' phase), so that we could build the image first, see above -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>${testsToRun}</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,349 @@
/*
* Copyright 2013-2022 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.kubernetes.configuration.watcher.multiple.apps;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import io.kubernetes.client.openapi.apis.AppsV1Api;
import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.NetworkingV1Api;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Ingress;
import io.kubernetes.client.openapi.models.V1ReplicationController;
import io.kubernetes.client.openapi.models.V1Secret;
import io.kubernetes.client.openapi.models.V1SecretBuilder;
import io.kubernetes.client.openapi.models.V1Service;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.k3s.K3sContainer;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import static org.awaitility.Awaitility.await;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.createApiClient;
import static org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils.getPomVersion;
/**
* @author wind57
*/
class ConfigurationWatcherMultipleAppIT {
private static final String CONFIG_WATCHER_APP_A_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a";
private static final String CONFIG_WATCHER_APP_B_IMAGE = "spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b";
private static final String CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME = "app-a-deployment";
private static final String CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME = "app-b-deployment";
private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME = "spring-cloud-kubernetes-configuration-watcher-deployment";
private static final String SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME = "spring-cloud-kubernetes-configuration-watcher";
private static final String SECRET_NAME = "multiple-apps";
private static final String NAMESPACE = "default";
private static CoreV1Api api;
private static AppsV1Api appsApi;
private static NetworkingV1Api networkingApi;
private static K8SUtils k8SUtils;
private static final K3sContainer K3S = Commons.container();
@BeforeAll
static void beforeAll() throws Exception {
K3S.start();
Commons.validateImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S);
Commons.validateImage(CONFIG_WATCHER_APP_A_IMAGE, K3S);
Commons.loadSpringCloudKubernetesImage(CONFIG_WATCHER_APP_A_IMAGE, K3S);
Commons.validateImage(CONFIG_WATCHER_APP_B_IMAGE, K3S);
Commons.loadSpringCloudKubernetesImage(CONFIG_WATCHER_APP_B_IMAGE, K3S);
createApiClient(K3S.getKubeConfigYaml());
api = new CoreV1Api();
appsApi = new AppsV1Api();
k8SUtils = new K8SUtils(api, appsApi);
networkingApi = new NetworkingV1Api();
k8SUtils.setUp(NAMESPACE);
}
@AfterAll
static void afterAll() throws Exception {
Commons.cleanUp(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S);
Commons.cleanUp(CONFIG_WATCHER_APP_A_IMAGE, K3S);
Commons.cleanUp(CONFIG_WATCHER_APP_B_IMAGE, K3S);
}
@BeforeEach
void setup() throws Exception {
deployRabbitMq();
deployAppA();
deployAppB();
deployIngress();
deployConfigWatcher();
k8SUtils.waitForReplicationController("rabbitmq-controller", NAMESPACE);
waitForDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME);
waitForDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME);
waitForDeployment(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME);
}
@AfterEach
void after() throws Exception {
cleanRabbitMq();
cleanUpServices();
cleanUpDeployments();
cleanUpIngress();
cleanUpConfigMaps();
k8SUtils.waitForDeploymentToBeDeleted(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, NAMESPACE);
k8SUtils.waitForDeploymentToBeDeleted(CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME, NAMESPACE);
k8SUtils.waitForDeploymentToBeDeleted(CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME, NAMESPACE);
}
@Test
void testRefresh() throws Exception {
// secret has one label, one that says that we should refresh
// and one annotation that says that we should refresh some specific services
V1Secret secret = new V1SecretBuilder().editOrNewMetadata().withName(SECRET_NAME)
.addToLabels("spring.cloud.kubernetes.secret", "true")
.addToAnnotations("spring.cloud.kubernetes.secret.apps",
"spring-cloud-kubernetes-client-configuration-watcher-secret-app-a, "
+ "spring-cloud-kubernetes-client-configuration-watcher-secret-app-b")
.endMetadata().build();
api.createNamespacedSecret(NAMESPACE, secret, null, null, null);
WebClient.Builder builderA = builder();
WebClient serviceClientA = builderA.baseUrl("http://localhost:80/app-a").build();
WebClient.Builder builderB = builder();
WebClient serviceClientB = builderB.baseUrl("http://localhost:80/app-b").build();
Boolean[] valueA = new Boolean[1];
await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(240)).until(() -> {
valueA[0] = serviceClientA.method(HttpMethod.GET).retrieve().bodyToMono(Boolean.class)
.retryWhen(retrySpec()).block();
return valueA[0];
});
Assertions.assertThat(valueA[0]).isTrue();
Boolean[] valueB = new Boolean[1];
await().pollInterval(Duration.ofSeconds(3)).atMost(Duration.ofSeconds(240)).until(() -> {
valueB[0] = serviceClientB.method(HttpMethod.GET).retrieve().bodyToMono(Boolean.class)
.retryWhen(retrySpec()).block();
return valueB[0];
});
Assertions.assertThat(valueB[0]).isTrue();
}
/**
* <pre>
--------------------------------------------------- rabbitmq -----------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployRabbitMq() throws Exception {
api.createNamespacedService(NAMESPACE, getRabbitMqService(), null, null, null);
String[] image = getRabbitMQReplicationController().getSpec().getTemplate().getSpec().getContainers().get(0)
.getImage().split(":");
Commons.pullImage(image[0], image[1], K3S);
Commons.loadImage(image[0], image[1], "rabbitmq", K3S);
api.createNamespacedReplicationController(NAMESPACE, getRabbitMQReplicationController(), null, null, null);
}
private V1ReplicationController getRabbitMQReplicationController() throws Exception {
return (V1ReplicationController) K8SUtils.readYamlFromClasspath("rabbitmq/rabbitmq-controller.yaml");
}
private V1Service getRabbitMqService() throws Exception {
return (V1Service) K8SUtils.readYamlFromClasspath("rabbitmq/rabbitmq-service.yaml");
}
/**
* <pre>
----------------------------------------------------- app-a ------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployAppA() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getAppADeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getAppAService(), null, null, null);
}
private V1Deployment getAppADeployment() throws Exception {
String urlString = "app-a/app-a-deployment.yaml";
V1Deployment deployment = (V1Deployment) K8SUtils.readYamlFromClasspath(urlString);
String image = K8SUtils.getImageFromDeployment(deployment) + ":" + getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Service getAppAService() throws Exception {
return (V1Service) K8SUtils.readYamlFromClasspath("app-a/app-a-service.yaml");
}
/**
* <pre>
--------------------------------------------------- app-b --------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployAppB() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getAppBDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getAppBService(), null, null, null);
}
private V1Deployment getAppBDeployment() throws Exception {
String urlString = "app-b/app-b-deployment.yaml";
V1Deployment deployment = (V1Deployment) K8SUtils.readYamlFromClasspath(urlString);
String image = K8SUtils.getImageFromDeployment(deployment) + ":" + getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Service getAppBService() throws Exception {
return (V1Service) K8SUtils.readYamlFromClasspath("app-b/app-b-service.yaml");
}
/**
* <pre>
------------------------------------------------ config-watcher --------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployConfigWatcher() throws Exception {
appsApi.createNamespacedDeployment(NAMESPACE, getConfigWatcherDeployment(), null, null, null);
api.createNamespacedService(NAMESPACE, getConfigWatcherService(), null, null, null);
}
private V1Deployment getConfigWatcherDeployment() throws Exception {
V1Deployment deployment = (V1Deployment) K8SUtils.readYamlFromClasspath(
"config-watcher/spring-cloud-kubernetes-configuration-watcher-it-bus-amqp-deployment.yaml");
String image = K8SUtils.getImageFromDeployment(deployment) + ":" + getPomVersion();
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(image);
return deployment;
}
private V1Service getConfigWatcherService() throws Exception {
return (V1Service) K8SUtils
.readYamlFromClasspath("config-watcher/spring-cloud-kubernetes-configuration-watcher-service.yaml");
}
/**
* <pre>
------------------------------------------------ common ----------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
</pre>
*/
private void deployIngress() throws Exception {
V1Ingress ingress = (V1Ingress) K8SUtils.readYamlFromClasspath(
"ingress/spring-cloud-kubernetes-configuration-watcher-multiple-apps-ingress.yaml");
networkingApi.createNamespacedIngress(NAMESPACE, ingress, null, null, null);
k8SUtils.waitForIngress(ingress.getMetadata().getName(), NAMESPACE);
}
private void waitForDeployment(String deploymentName) {
await().pollInterval(Duration.ofSeconds(3)).atMost(600, TimeUnit.SECONDS)
.until(() -> k8SUtils.isDeploymentReady(deploymentName, NAMESPACE));
}
private void cleanRabbitMq() throws Exception {
api.deleteNamespacedService("rabbitmq-service", NAMESPACE, null, null, null, null, null, null);
try {
api.deleteNamespacedReplicationController("rabbitmq-controller", NAMESPACE, null, null, null, null, null,
null);
}
catch (Exception e) {
// swallowing this exception, delete does actually happen, it's a problem
// downstream from the k8s client; see:
// https://github.com/kubernetes-client/java/issues/86#issuecomment-411234259
}
}
private void cleanUpServices() throws Exception {
api.deleteNamespacedService("app-a", NAMESPACE, null, null, null, null, null, null);
api.deleteNamespacedService("app-b", NAMESPACE, null, null, null, null, null, null);
api.deleteNamespacedService(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, NAMESPACE, null, null, null, null, null,
null);
}
private void cleanUpDeployments() throws Exception {
appsApi.deleteNamespacedDeployment(SPRING_CLOUD_K8S_CONFIG_WATCHER_DEPLOYMENT_NAME, NAMESPACE, null, null, null,
null, null, null);
appsApi.deleteNamespacedDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_A_NAME, NAMESPACE, null, null, null, null,
null, null);
appsApi.deleteNamespacedDeployment(CONFIG_WATCHER_DEPLOYMENT_APP_B_NAME, NAMESPACE, null, null, null, null,
null, null);
}
private void cleanUpConfigMaps() throws Exception {
api.deleteNamespacedSecret(SECRET_NAME, NAMESPACE, null, null, null, null, null, null);
}
private void cleanUpIngress() throws Exception {
networkingApi.deleteNamespacedIngress("it-ingress-multiple-apps", NAMESPACE, null, null, null, null, null,
null);
}
private WebClient.Builder builder() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
}
private RetryBackoffSpec retrySpec() {
return Retry.fixedDelay(240, Duration.ofSeconds(1)).filter(Objects::nonNull);
}
}

View File

@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-a-deployment
spec:
selector:
matchLabels:
app: app-a
template:
metadata:
labels:
app: app-a
spec:
containers:
- name: app-a
image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-a
imagePullPolicy: IfNotPresent
env:
- name: SPRING_PROFILES_ACTIVE
value: bus-amqp
- name: SPRING_RABBITMQ_HOST
value: rabbitmq-service
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: app-a
name: app-a
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: app-a
type: ClusterIP

View File

@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-b-deployment
spec:
selector:
matchLabels:
app: app-b
template:
metadata:
labels:
app: app-b
spec:
containers:
- name: app-b
image: docker.io/springcloud/spring-cloud-kubernetes-client-configuration-watcher-secrets-app-b
imagePullPolicy: IfNotPresent
env:
- name: SPRING_PROFILES_ACTIVE
value: bus-amqp
- name: SPRING_RABBITMQ_HOST
value: rabbitmq-service
readinessProbe:
httpGet:
port: 8081
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8081
path: /actuator/health/liveness
ports:
- containerPort: 8081

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: app-b
name: app-b
spec:
ports:
- name: http
port: 8081
targetPort: 8081
selector:
app: app-b
type: ClusterIP

View File

@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-configuration-watcher-deployment
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-configuration-watcher
template:
metadata:
labels:
app: spring-cloud-kubernetes-configuration-watcher
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-configuration-watcher
image: docker.io/springcloud/spring-cloud-kubernetes-configuration-watcher
imagePullPolicy: IfNotPresent
env:
- name: SPRING_PROFILES_ACTIVE
value: bus-amqp
- name: SPRING_RABBITMQ_HOST
value: rabbitmq-service
- name: SPRING_CLOUD_BUS_DESTINATION
value: multiple-apps
- name: SPRING_CLOUD_KUBERNETES_CONFIGURATION_WATCHER_REFRESHDELAY
value: 1
readinessProbe:
httpGet:
port: 8888
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8888
path: /actuator/health/liveness
ports:
- containerPort: 8888

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-configuration-watcher
name: spring-cloud-kubernetes-configuration-watcher
spec:
ports:
- name: http
port: 8888
targetPort: 8888
selector:
app: spring-cloud-kubernetes-configuration-watcher
type: ClusterIP

View File

@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: it-ingress-multiple-apps
namespace: default
spec:
rules:
- http:
paths:
- path: /app-a
pathType: Prefix
backend:
service:
name: app-a
port:
number: 8080
- http:
paths:
- path: /app-b
pathType: Prefix
backend:
service:
name: app-b
port:
number: 8081

View File

@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.testcontainers" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
</configuration>

View File

@@ -0,0 +1,29 @@
apiVersion: v1
kind: ReplicationController
metadata:
labels:
component: rabbitmq
name: rabbitmq-controller
spec:
replicas: 1
template:
metadata:
labels:
app: taskQueue
component: rabbitmq
spec:
containers:
- image: rabbitmq:3-management
name: rabbitmq
ports:
- name: amqp
containerPort: 5672
- name: http-stats
containerPort: 15672
readinessProbe:
httpGet:
port: 15672
path: /api/healthchecks/node
httpHeaders:
- name: Authorization
value: Basic Z3Vlc3Q6Z3Vlc3Q=

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
labels:
component: rabbitmq
name: rabbitmq-service
spec:
ports:
- port: 5672
name: amqp
targetPort: 5672
- port: 15672
name: http-stats
targetPort: 15672
selector:
app: taskQueue
component: rabbitmq