diff --git a/docs/src/main/asciidoc/discovery-client.adoc b/docs/src/main/asciidoc/discovery-client.adoc
index cb49eac9..f5132639 100644
--- a/docs/src/main/asciidoc/discovery-client.adoc
+++ b/docs/src/main/asciidoc/discovery-client.adoc
@@ -117,5 +117,15 @@ this to work, you need to align the Kubernetes service name with the `spring.app
NOTE: `spring.application.name` has no effect as far as the name registered for the application within Kubernetes
Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the
-`DiscoveryClient` implementation accordingly. In order to enable this functionality you need to add
+`DiscoveryClient` implementation accordingly. By "watch" we mean that we will publish a heartbeat event every `spring.cloud.kubernetes.discovery.catalog-services-watch-delay`
+milliseconds (by default it is `30000`). The heartbeat event will contain the target references (and their namespaces of the addresses of all endpoints
+(for the exact details of what will get returned you can take a look inside `KubernetesCatalogWatch`). This is an implementation detail, and listeners of the heartbeat event
+should not rely on the details. Instead, they should see if there are differences between two subsequent heartbeats via `equals` method. We will take care to return a correct implementation that adheres to the equals contract.
+The endpoints will be queried in either all namespaces (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`), or
+we will use: xref:property-source-config.adoc#namespace-resolution[Namespace Resolution].
+
+
+
+
+In order to enable this functionality you need to add
`@EnableScheduling` on a configuration class in your application.
diff --git a/istio-cli/istio-1.13.3/bin/istioctl b/istio-cli/istio-1.16.0/bin/istioctl
similarity index 81%
rename from istio-cli/istio-1.13.3/bin/istioctl
rename to istio-cli/istio-1.16.0/bin/istioctl
index 0319896a..65494bd8 100755
Binary files a/istio-cli/istio-1.13.3/bin/istioctl and b/istio-cli/istio-1.16.0/bin/istioctl differ
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java
index 20ead0d8..90b8d556 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java
@@ -85,13 +85,15 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
.list().getItems();
}
- /*
- *
- An "Endpoints" holds a List of EndpointSubset. - A single
- * EndpointSubset holds a List of EndpointAddress
+ /**
+ *
+ * - An "Endpoints" holds a List of EndpointSubset.
+ * - A single EndpointSubset holds a List of EndpointAddress
*
- * - (The union of all EndpointSubsets is the Set of all Endpoints) - Set of
- * Endpoints is the cartesian product of : EndpointSubset::getAddresses and
- * EndpointSubset::getPorts (each is a List)
+ * - (The union of all EndpointSubsets is the Set of all Endpoints)
+ * - Set of Endpoints is the cartesian product of :
+ * EndpointSubset::getAddresses and EndpointSubset::getPorts (each is a List)
+ *
*/
List currentState = endpoints.stream().map(Endpoints::getSubsets)
.filter(Objects::nonNull).flatMap(List::stream).map(EndpointSubset::getAddresses)
diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml
index c3597f1b..5c86ae63 100644
--- a/spring-cloud-kubernetes-integration-tests/pom.xml
+++ b/spring-cloud-kubernetes-integration-tests/pom.xml
@@ -90,5 +90,6 @@
spring-cloud-kubernetes-client-configmap-event-reload
spring-cloud-kubernetes-client-configmap-event-reload-multiple-apps
spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps
+ spring-cloud-kubernetes-fabric8-client-catalog-watcher
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml
new file mode 100644
index 00000000..d7f82013
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/pom.xml
@@ -0,0 +1,115 @@
+
+
+
+ spring-cloud-kubernetes-integration-tests
+ org.springframework.cloud
+ 3.0.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-cloud-kubernetes-fabric8-client-catalog-watcher
+
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-fabric8-discovery
+
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-test-support
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ com.github.docker-java
+ docker-java-core
+ test
+
+
+ com.github.docker-java
+ docker-java-transport-httpclient5
+ test
+
+
+
+
+
+
+ ../src/main/resources
+ true
+
+
+ src/main/resources
+ true
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ docker.io/springcloud/${project.artifactId}:${project.version}
+ paketobuildpacks/builder
+
+
+
+ build-image
+
+ ${skip.build.image}
+
+ package
+
+ build-image
+
+
+
+ repackage
+ package
+
+ repackage
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+
+
+
+
+
+ ${testsToRun}
+
+
+
+
+
+
+
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Application.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Application.java
new file mode 100644
index 00000000..a3062c09
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/Application.java
@@ -0,0 +1,34 @@
+/*
+ * 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.fabric8.catalog.watch;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * @author wind57
+ */
+@SpringBootApplication
+@EnableScheduling
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/EndpointNameAndNamespaceService.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/EndpointNameAndNamespaceService.java
new file mode 100644
index 00000000..c4a3e86d
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/EndpointNameAndNamespaceService.java
@@ -0,0 +1,43 @@
+/*
+ * 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.fabric8.catalog.watch;
+
+import java.util.List;
+
+import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
+import org.springframework.stereotype.Service;
+
+/**
+ * holds an EndpointNameAndNamespace object, needed so that the controller can poll to see
+ * if anything has changed. And we call the controller from our tests.
+ *
+ * @author wind57
+ */
+@Service
+public class EndpointNameAndNamespaceService {
+
+ private List result;
+
+ public List result() {
+ return result;
+ }
+
+ public void setResult(List result) {
+ this.result = result;
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartBeatListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartBeatListener.java
new file mode 100644
index 00000000..926c7b7f
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartBeatListener.java
@@ -0,0 +1,51 @@
+/*
+ * 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.fabric8.catalog.watch;
+
+import java.util.List;
+
+import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
+import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * Listener that will catch events from
+ * {@link org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesCatalogWatch}
+ *
+ * @author wind57
+ */
+@Component
+public class HeartBeatListener implements ApplicationListener {
+
+ private final EndpointNameAndNamespaceService service;
+
+ public HeartBeatListener(EndpointNameAndNamespaceService service) {
+ this.service = service;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onApplicationEvent(ApplicationEvent event) {
+ if (event instanceof HeartbeatEvent heartbeatEvent) {
+ List result = (List) heartbeatEvent.getValue();
+ service.setResult(result);
+ }
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartbeatController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartbeatController.java
new file mode 100644
index 00000000..f2889faa
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/main/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/HeartbeatController.java
@@ -0,0 +1,39 @@
+/*
+ * 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.fabric8.catalog.watch;
+
+import java.util.List;
+
+import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class HeartbeatController {
+
+ private final EndpointNameAndNamespaceService service;
+
+ public HeartbeatController(EndpointNameAndNamespaceService service) {
+ this.service = service;
+ }
+
+ @GetMapping("/result")
+ public List result() {
+ return service.result();
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/CatalogWatchIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/CatalogWatchIT.java
new file mode 100644
index 00000000..18305b3b
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/CatalogWatchIT.java
@@ -0,0 +1,236 @@
+/*
+ * 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.fabric8.catalog.watch;
+
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.DefaultKubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+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.commons.discovery.EndpointNameAndNamespace;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Fabric8Utils;
+import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.ResolvableType;
+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;
+
+/**
+ * @author wind57
+ */
+class CatalogWatchIT {
+
+ private static final String APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher";
+
+ private static final String NAMESPACE = "default";
+
+ private static final K3sContainer K3S = Commons.container();
+
+ private static KubernetesClient client;
+
+ private String busyboxServiceName;
+
+ private String busyboxDeploymentName;
+
+ private String appDeploymentName;
+
+ private String appServiceName;
+
+ private String appIngressName;
+
+ @BeforeAll
+ static void beforeAll() {
+ K3S.start();
+ Config config = Config.fromKubeconfig(K3S.getKubeConfigYaml());
+ client = new DefaultKubernetesClient(config);
+ }
+
+ /**
+ *
+ * - we deploy a busybox service with 2 replica pods
+ * - we receive an event from KubernetesCatalogWatcher, assert what is inside it
+ * - delete the busybox service
+ * - assert that we receive only spring-cloud-kubernetes-fabric8-client-catalog-watcher pod
+ *
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ void testCatalogWatch() throws Exception {
+
+ Commons.validateImage(APP_NAME, K3S);
+ Commons.loadSpringCloudKubernetesImage(APP_NAME, K3S);
+
+ Fabric8Utils.setUp(client, "default");
+
+ deployBusyboxManifests();
+ deployApp();
+
+ WebClient client = builder().baseUrl("localhost/result").build();
+ EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2];
+ ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class);
+
+ await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
+ List result = (List) client.method(HttpMethod.GET)
+ .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
+ .retryWhen(retrySpec()).block();
+
+ // we get 3 pods as input, but because they are sorted by name in the catalog watcher implementation
+ // we will get the first busybox instances here.
+ if (result != null) {
+ holder[0] = result.get(0);
+ holder[1] = result.get(1);
+ return true;
+ }
+
+ return false;
+ });
+
+ EndpointNameAndNamespace resultOne = holder[0];
+ EndpointNameAndNamespace resultTwo = holder[1];
+
+ Assertions.assertNotNull(resultOne);
+ Assertions.assertNotNull(resultTwo);
+
+ Assertions.assertTrue(resultOne.endpointName().contains("busybox"));
+ Assertions.assertTrue(resultTwo.endpointName().contains("busybox"));
+ Assertions.assertEquals("default", resultOne.namespace());
+ Assertions.assertEquals("default", resultTwo.namespace());
+
+ deleteBusyboxApp();
+
+ // what we get after delete
+ EndpointNameAndNamespace[] afterDelete = new EndpointNameAndNamespace[1];
+
+ await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> {
+ List result = (List) client.method(HttpMethod.GET)
+ .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType()))
+ .retryWhen(retrySpec()).block();
+
+ // we will only receive one pod here, our own
+ if (result != null) {
+ Assertions.assertEquals(1, result.size());
+ afterDelete[0] = result.get(0);
+ return true;
+ }
+
+ return false;
+ });
+
+ Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME));
+ Assertions.assertEquals("default", afterDelete[0].namespace());
+
+ deleteApp();
+
+ }
+
+ private void deployBusyboxManifests() {
+
+ Deployment deployment = client.apps().deployments().load(getBusyboxDeployment()).get();
+ client.apps().deployments().inNamespace(NAMESPACE).create(deployment);
+ busyboxDeploymentName = deployment.getMetadata().getName();
+
+ Service busyboxService = client.services().load(getBusyboxService()).get();
+ busyboxServiceName = busyboxService.getMetadata().getName();
+ client.services().inNamespace(NAMESPACE).create(busyboxService);
+
+ Fabric8Utils.waitForDeployment(client, busyboxDeploymentName, NAMESPACE, 2, 600);
+
+ }
+
+ private void deployApp() {
+
+ Deployment appDeployment = client.apps().deployments().load(getAppDeployment()).get();
+
+ String version = K8SUtils.getPomVersion();
+ String currentImage = appDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage();
+ appDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(currentImage + ":" + version);
+
+ client.apps().deployments().inNamespace(NAMESPACE).create(appDeployment);
+ appDeploymentName = appDeployment.getMetadata().getName();
+
+ Service appService = client.services().load(getAppService()).get();
+ appServiceName = appService.getMetadata().getName();
+ client.services().inNamespace(NAMESPACE).create(appService);
+
+ Fabric8Utils.waitForDeployment(client, appDeploymentName, NAMESPACE, 2, 600);
+
+ Ingress appIngress = client.network().v1().ingresses().load(getAppIngress()).get();
+ appIngressName = appIngress.getMetadata().getName();
+ client.network().v1().ingresses().inNamespace(NAMESPACE).create(appIngress);
+
+ Fabric8Utils.waitForIngress(client, appIngressName, NAMESPACE);
+
+ }
+
+ private void deleteBusyboxApp() {
+ Fabric8Utils.deleteDeployment(client, NAMESPACE, busyboxDeploymentName);
+ Fabric8Utils.deleteService(client, NAMESPACE, busyboxServiceName);
+ }
+
+ private void deleteApp() {
+ Fabric8Utils.deleteDeployment(client, NAMESPACE, appDeploymentName);
+ Fabric8Utils.deleteService(client, NAMESPACE, appServiceName);
+ client.network().v1().ingresses().withName(appIngressName).delete();
+ }
+
+ private static InputStream getBusyboxService() {
+ return Fabric8Utils.inputStream("busybox/service.yaml");
+ }
+
+ private static InputStream getBusyboxDeployment() {
+ return Fabric8Utils.inputStream("busybox/deployment.yaml");
+ }
+
+ private static InputStream getAppDeployment() {
+ return Fabric8Utils.inputStream("app/watcher-deployment.yaml");
+ }
+
+ private static InputStream getAppIngress() {
+ return Fabric8Utils.inputStream("app/watcher-ingress.yaml");
+ }
+
+ private static InputStream getAppService() {
+ return Fabric8Utils.inputStream("app/watcher-service.yaml");
+ }
+
+ private WebClient.Builder builder() {
+ return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
+ }
+
+ private RetryBackoffSpec retrySpec() {
+ return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml
new file mode 100644
index 00000000..a8ae8920
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-deployment.yaml
@@ -0,0 +1,31 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: spring-cloud-kubernetes-fabric8-client-catalog-watcher
+spec:
+ selector:
+ matchLabels:
+ app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
+ template:
+ metadata:
+ labels:
+ app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
+ spec:
+ serviceAccountName: spring-cloud-kubernetes-serviceaccount
+ containers:
+ - name: spring-cloud-kubernetes-fabric8-client-catalog-watcher
+ image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-catalog-watcher
+ imagePullPolicy: IfNotPresent
+ readinessProbe:
+ httpGet:
+ port: 8080
+ path: /actuator/health/readiness
+ livenessProbe:
+ httpGet:
+ port: 8080
+ path: /actuator/health/liveness
+ ports:
+ - containerPort: 8080
+ env:
+ - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY
+ value: DEBUG
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml
new file mode 100644
index 00000000..7b0343ed
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-ingress.yaml
@@ -0,0 +1,16 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: spring-cloud-kubernetes-fabric8-client-catalog-watcher-ingress
+ namespace: default
+spec:
+ rules:
+ - http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: spring-cloud-kubernetes-fabric8-client-catalog-watcher-service
+ port:
+ number: 8080
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-service.yaml
new file mode 100644
index 00000000..6b9374dc
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/app/watcher-service.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: spring-cloud-kubernetes-fabric8-client-catalog-watcher-service
+ name: spring-cloud-kubernetes-fabric8-client-catalog-watcher-service
+spec:
+ ports:
+ - name: http
+ port: 8080
+ targetPort: 8080
+ selector:
+ app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
+ type: ClusterIP
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/busybox/deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/busybox/deployment.yaml
new file mode 100644
index 00000000..75dd2268
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/busybox/deployment.yaml
@@ -0,0 +1,22 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: busybox
+spec:
+ selector:
+ matchLabels:
+ app: busybox
+ version: v1
+ replicas: 2
+ template:
+ metadata:
+ labels:
+ app: busybox
+ version: v1
+ spec:
+ containers:
+ - name: busybox
+ # image: arm64/busybox:latest
+ image: busybox:latest
+ command: ["/bin/sh"]
+ args: ["-c", "sleep 100000"]
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/busybox/service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/busybox/service.yaml
new file mode 100644
index 00000000..7a23a4d9
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/busybox/service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: busybox-service
+spec:
+ selector:
+ app: busybox
+ type: ClusterIP
+ ports:
+ - name: busybox-port
+ port: 8080
+ targetPort: 80
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..9e284876
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/resources/logback-test.xml
@@ -0,0 +1,14 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/Fabric8IstioIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/Fabric8IstioIT.java
index 9f2fbebf..0af50c41 100644
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/Fabric8IstioIT.java
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-istio-it/src/test/java/org/springframework/cloud/kubernetes/fabric8/istio/Fabric8IstioIT.java
@@ -59,7 +59,7 @@ class Fabric8IstioIT {
private static final String ISTIO_PILOT = "istio/pilot";
- private static final String ISTIO_VERSION = "1.13.3";
+ private static final String ISTIO_VERSION = "1.16.0";
private static final String LOCAL_ISTIO_BIN_PATH = "../../istio-cli/istio-" + ISTIO_VERSION + "/bin";
diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java
index fb913376..9d9e21ba 100644
--- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java
+++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Commons.java
@@ -46,7 +46,7 @@ public final class Commons {
/**
* Rancher version to use for test-containers.
*/
- public static final String RANCHER = "rancher/k3s:v1.24.2-k3s2";
+ public static final String RANCHER = "rancher/k3s:v1.25.4-k3s1";
/**
* Command to use when starting rancher. Without "server" option, traefik is not
diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java
index ecf2de17..1b07b018 100644
--- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java
+++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java
@@ -19,12 +19,15 @@ package org.springframework.cloud.kubernetes.integration.tests.commons;
import java.io.InputStream;
import java.time.Duration;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.Endpoints;
import io.fabric8.kubernetes.api.model.LoadBalancerIngress;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceAccount;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
@@ -174,6 +177,42 @@ public final class Fabric8Utils {
});
}
+ /**
+ * delete a deployment and every pod by spec.select.matchLabels, waits until
+ * everything is deleted.
+ */
+ public static void deleteDeployment(KubernetesClient client, String namespace, String name) {
+
+ Deployment deployment = client.apps().deployments().inNamespace(namespace).withName(name).get();
+ Map matchLabels = deployment.getSpec().getSelector().getMatchLabels();
+
+ client.apps().deployments().inNamespace(namespace).delete(deployment);
+
+ await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> {
+ Deployment inner = client.apps().deployments().inNamespace(namespace).withName(name).get();
+ return inner == null;
+ });
+
+ await().pollInterval(Duration.ofSeconds(1)).atMost(60, TimeUnit.SECONDS).until(() -> {
+ List podList = client.pods().inNamespace(namespace).withLabels(matchLabels).list().getItems();
+ return podList == null || podList.isEmpty();
+ });
+
+ }
+
+ /**
+ * delete the service and wait for it to be deleted.
+ */
+ public static void deleteService(KubernetesClient client, String namespace, String name) {
+
+ client.services().inNamespace(namespace).withName(name).delete();
+
+ await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> {
+ Service service = client.services().inNamespace(namespace).withName(name).get();
+ return service == null;
+ });
+ }
+
private static void innerSetup(KubernetesClient client, String namespace, InputStream serviceAccountAsStream,
InputStream roleBindingAsStream, InputStream roleAsStream) {
ServiceAccount serviceAccountFromStream = client.serviceAccounts().load(serviceAccountAsStream).get();