>() {
-
- })
- .retryWhen(retrySpec())
- .block();
-
- Assertions.assertEquals(ourServiceInstances.size(), 1);
-
- DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0);
- Assertions.assertNotNull(serviceInstance.getInstanceId());
- Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-k8s-client-discovery");
- Assertions.assertNotNull(serviceInstance.getHost());
- Assertions.assertEquals(serviceInstance.getMetadata(),
- Map.of("port.http", "8080", "k8s_namespace", "default", "type", "ClusterIP", "label-app",
- "spring-cloud-kubernetes-k8s-client-discovery", "annotation-custom-spring-k8s", "spring-k8s"));
- Assertions.assertEquals(serviceInstance.getPort(), 8080);
- Assertions.assertEquals(serviceInstance.getNamespace(), "default");
-
- }
-
- 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-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySelectiveNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySelectiveNamespacesIT.java
deleted file mode 100644
index ecb9513df7..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySelectiveNamespacesIT.java
+++ /dev/null
@@ -1,364 +0,0 @@
-/*
- * Copyright 2013-2023 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.k8s.client.discovery;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-
-import io.kubernetes.client.openapi.models.V1Deployment;
-import io.kubernetes.client.openapi.models.V1EnvVar;
-import io.kubernetes.client.openapi.models.V1Ingress;
-import io.kubernetes.client.openapi.models.V1Service;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.MethodOrderer;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestMethodOrder;
-import org.testcontainers.containers.Container;
-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.DefaultKubernetesServiceInstance;
-import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
-import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
-import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
-import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.client.reactive.ReactorClientHttpConnector;
-import org.springframework.web.reactive.function.client.WebClient;
-
-/**
- * @author wind57
- */
-@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
-class KubernetesClientDiscoverySelectiveNamespacesIT {
-
- private static final String BLOCKING_PUBLISH = "Will publish InstanceRegisteredEvent from blocking implementation";
-
- private static final String REACTIVE_PUBLISH = "Will publish InstanceRegisteredEvent from reactive implementation";
-
- private static final String NAMESPACE = "default";
-
- private static final String NAMESPACE_A = "a";
-
- private static final String NAMESPACE_B = "b";
-
- private static final String IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-discovery";
-
- private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-k8s-client-discovery";
-
- private static Util util;
-
- private static final K3sContainer K3S = Commons.container();
-
- @BeforeAll
- static void beforeAll() throws Exception {
- K3S.start();
- Commons.validateImage(IMAGE_NAME, K3S);
- Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);
-
- Images.loadWiremock(K3S);
-
- util = new Util(K3S);
-
- util.createNamespace(NAMESPACE_A);
- util.createNamespace(NAMESPACE_B);
- util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B));
- util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE, false);
- util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE, false);
- util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE, false);
- manifests(Phase.CREATE);
- }
-
- @AfterAll
- static void afterAll() {
- util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE, false);
- util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE, false);
- util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE, false);
- util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B));
- util.deleteNamespace(NAMESPACE_A);
- util.deleteNamespace(NAMESPACE_B);
- manifests(Phase.DELETE);
- }
-
- /**
- * Deploy wiremock in 3 namespaces: default, a, b. Search only in selective namespace
- * 'a' with blocking enabled and reactive disabled, as such find a single service and
- * its service instance.
- */
- @Test
- @Order(1)
- void testOneNamespaceBlockingOnly() {
-
- Commons.waitForLogStatement("using selective namespaces : [a]", K3S, IMAGE_NAME);
- Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a]", K3S,
- IMAGE_NAME);
- Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a]", K3S,
- IMAGE_NAME);
- Commons.waitForLogStatement("registering lister (for services) in namespace : a", K3S, IMAGE_NAME);
- Commons.waitForLogStatement("registering lister (for endpoints) in namespace : a", K3S, IMAGE_NAME);
-
- // this tiny checks makes sure that blocking is enabled and reactive is disabled.
- Commons.waitForLogStatement(BLOCKING_PUBLISH, K3S, IMAGE_NAME);
- Assertions.assertFalse(logs().contains(REACTIVE_PUBLISH));
-
- blockingCheck();
-
- }
-
- /**
- * Deploy wiremock in 3 namespaces: default, a, b. Search only in selective namespace
- * 'a' with blocking disabled and reactive enabled, as such find a single service and
- * its service instance.
- */
- @Test
- @Order(2)
- void testOneNamespaceReactiveOnly() {
-
- KubernetesClientDiscoveryClientUtils.patchForReactiveOnly(DEPLOYMENT_NAME, NAMESPACE);
-
- Commons.waitForLogStatement("using selective namespaces : [a]", K3S, IMAGE_NAME);
- Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a]", K3S,
- IMAGE_NAME);
- Commons.waitForLogStatement("registering lister (for services) in namespace : a", K3S, IMAGE_NAME);
- Commons.waitForLogStatement("registering lister (for endpoints) in namespace : a", K3S, IMAGE_NAME);
-
- // this tiny checks makes sure that reactive is enabled and blocking is disabled.
- Commons.waitForLogStatement(REACTIVE_PUBLISH, K3S, IMAGE_NAME);
- Assertions.assertFalse(logs().contains(BLOCKING_PUBLISH));
-
- reactiveCheck();
-
- }
-
- /**
- * Deploy wiremock in 3 namespaces: default, a, b. Search only in selective namespace
- * 'a' with blocking enabled and reactive enabled, as such find a single service and
- * its service instance.
- */
- @Test
- @Order(3)
- void testOneNamespaceBothBlockingAndReactive() {
-
- KubernetesClientDiscoveryClientUtils.patchForBlockingAndReactive(DEPLOYMENT_NAME, NAMESPACE);
-
- Commons.waitForLogStatement("using selective namespaces : [a]", K3S, IMAGE_NAME);
- Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a]", K3S,
- IMAGE_NAME);
- Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a]", K3S,
- IMAGE_NAME);
- Commons.waitForLogStatement("registering lister (for services) in namespace : a", K3S, IMAGE_NAME);
- Commons.waitForLogStatement("registering lister (for endpoints) in namespace : a", K3S, IMAGE_NAME);
-
- // this tiny checks makes sure that blocking and reactive is enabled.
- Commons.waitForLogStatement(BLOCKING_PUBLISH, K3S, IMAGE_NAME);
- Commons.waitForLogStatement(REACTIVE_PUBLISH, K3S, IMAGE_NAME);
-
- blockingCheck();
- reactiveCheck();
-
- }
-
- /**
- * previous test already has:
- * - SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0 = a
- * - SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED = TRUE
- * - SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED = TRUE
- *
- * All we need to patch for is:
- * - add one more namespace to track, via SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1 = b
- * - disable reactive, via SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED = FALSE
- *
- * As such, two namespaces + blocking only, is achieved.
- *
- */
- @Test
- @Order(4)
- void testTwoNamespacesBlockingOnly() {
- KubernetesClientDiscoveryClientUtils.patchForTwoNamespacesBlockingOnly(DEPLOYMENT_NAME, NAMESPACE);
- new KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate().testTwoNamespacesBlockingOnly(K3S);
- }
-
- /**
- * previous test already has:
- * - SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0 = a
- * - SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1 = b
- * - SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED = FALSE
- * - SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED = TRUE
- *
- * We invert the reactive and blocking in this test via patching.
- *
- * As such, two namespaces + reactive only, is achieved.
- *
- */
- @Test
- @Order(5)
- void testTwoNamespacesReactiveOnly() {
- KubernetesClientDiscoveryClientUtils.patchForReactiveOnly(DEPLOYMENT_NAME, NAMESPACE);
- new KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate().testTwoNamespaceReactiveOnly(K3S);
- }
-
- /**
- * previous test already has:
- * - SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0 = a
- * - SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1 = b
- * - SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED = TRUE
- * - SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED = FALSE
- *
- * We invert the blocking support.
- *
- * As such, two namespaces + blocking and reactive, is achieved.
- *
- */
- @Test
- @Order(6)
- void testTwoNamespacesBothBlockingAndReactive() {
- KubernetesClientDiscoveryClientUtils.patchToAddBlockingSupport(DEPLOYMENT_NAME, NAMESPACE);
- new KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate()
- .testTwoNamespacesBothBlockingAndReactive(K3S);
- }
-
- private static void manifests(Phase phase) {
- V1Deployment deployment = (V1Deployment) util.yaml("kubernetes-discovery-deployment.yaml");
- V1Service service = (V1Service) util.yaml("kubernetes-discovery-service.yaml");
- V1Ingress ingress = (V1Ingress) util.yaml("kubernetes-discovery-ingress.yaml");
-
- if (phase.equals(Phase.DELETE)) {
- util.deleteAndWait(NAMESPACE, deployment, service, ingress);
- return;
- }
-
- if (phase.equals(Phase.CREATE)) {
- List envVars = new ArrayList<>(
- Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv())
- .orElse(List.of()));
- V1EnvVar debugLevel = new V1EnvVar()
- .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY")
- .value("DEBUG");
- V1EnvVar selectiveNamespaceA = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0")
- .value(NAMESPACE_A);
-
- V1EnvVar disableReactiveEnvVar = new V1EnvVar().name("SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED")
- .value("FALSE");
- envVars.add(disableReactiveEnvVar);
-
- envVars.add(debugLevel);
- envVars.add(selectiveNamespaceA);
- deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars);
- util.createAndWait(NAMESPACE, null, deployment, service, ingress, true);
- }
- }
-
- private void reactiveCheck() {
- WebClient servicesClient = builder().baseUrl("http://localhost/reactive/services").build();
-
- List servicesResult = servicesClient.method(HttpMethod.GET)
- .retrieve()
- .bodyToMono(new ParameterizedTypeReference>() {
-
- })
- .retryWhen(retrySpec())
- .block();
-
- Assertions.assertEquals(servicesResult.size(), 1);
- Assertions.assertTrue(servicesResult.contains("service-wiremock"));
-
- WebClient ourServiceClient = builder().baseUrl("http://localhost/reactive/service-instances/service-wiremock")
- .build();
-
- List ourServiceInstances = ourServiceClient.method(HttpMethod.GET)
- .retrieve()
- .bodyToMono(new ParameterizedTypeReference>() {
-
- })
- .retryWhen(retrySpec())
- .block();
-
- Assertions.assertEquals(ourServiceInstances.size(), 1);
-
- DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0);
- // we only care about namespace here, as all other fields are tested in various
- // other tests.
- Assertions.assertEquals(serviceInstance.getNamespace(), "a");
- }
-
- private void blockingCheck() {
- WebClient servicesClient = builder().baseUrl("http://localhost/services").build();
-
- List servicesResult = servicesClient.method(HttpMethod.GET)
- .retrieve()
- .bodyToMono(new ParameterizedTypeReference>() {
-
- })
- .retryWhen(retrySpec())
- .block();
-
- Assertions.assertEquals(servicesResult.size(), 1);
- Assertions.assertTrue(servicesResult.contains("service-wiremock"));
-
- WebClient ourServiceClient = builder().baseUrl("http://localhost/service-instances/service-wiremock").build();
-
- List ourServiceInstances = ourServiceClient.method(HttpMethod.GET)
- .retrieve()
- .bodyToMono(new ParameterizedTypeReference>() {
-
- })
- .retryWhen(retrySpec())
- .block();
-
- Assertions.assertEquals(ourServiceInstances.size(), 1);
-
- DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0);
- // we only care about namespace here, as all other fields are tested in various
- // other tests.
- Assertions.assertEquals(serviceInstance.getNamespace(), "a");
- }
-
- private String logs() {
- try {
- String appPodName = K3S
- .execInContainer("sh", "-c",
- "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'")
- .getStdout();
-
- Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim());
- return execResult.getStdout();
- }
- catch (Exception e) {
- e.printStackTrace();
- throw new RuntimeException(e);
- }
- }
-
- 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-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySimpleIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySimpleIT.java
new file mode 100644
index 0000000000..e00c3b0140
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoverySimpleIT.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2013-2025 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.k8s.client.discovery;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.V1Service;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
+import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.cloud.kubernetes.k8s.client.discovery.TestAssertions.assertLogStatement;
+
+/**
+ * @author wind57
+ */
+@SpringBootTest(classes = { DiscoveryApp.class, KubernetesClientDiscoverySimpleIT.TestConfig.class },
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@TestPropertySource(properties = { "spring.cloud.kubernetes.discovery.namespaces[0]=default",
+ "org.springframework.cloud.kubernetes.client.discovery=debug" })
+class KubernetesClientDiscoverySimpleIT extends KubernetesClientDiscoveryBase {
+
+ @Autowired
+ private DiscoveryClient discoveryClient;
+
+ private static V1Service externalNameService;
+
+ @BeforeEach
+ void beforeEach() {
+ Images.loadBusybox(K3S);
+ util.busybox(NAMESPACE, Phase.CREATE);
+
+ externalNameService = (V1Service) util.yaml("external-name-service.yaml");
+ util.createAndWait(NAMESPACE, null, null, externalNameService, null, true);
+ }
+
+ @AfterEach
+ void afterEach() {
+ util.busybox(NAMESPACE, Phase.DELETE);
+ util.deleteAndWait(NAMESPACE, null, externalNameService, null);
+ }
+
+ @Test
+ void test(CapturedOutput output) throws Exception {
+
+ // find both pods
+ String[] both = K3S.execInContainer("sh", "-c", "kubectl get pods -l app=busybox -o=name --no-headers")
+ .getStdout()
+ .split("\n");
+ // add a label to first pod
+ K3S.execInContainer("sh", "-c",
+ "kubectl label pods " + both[0].split("/")[1] + " custom-label=custom-label-value");
+
+ // add annotation to the second pod
+ K3S.execInContainer("sh", "-c",
+ "kubectl annotate pods " + both[1].split("/")[1] + " custom-annotation=custom-annotation-value");
+
+ assertLogStatement(output, "using selective namespaces : [default]");
+
+ List services = discoveryClient.getServices();
+ List instances = discoveryClient.getInstances("busybox-service");
+
+ Assertions.assertThat(services)
+ .containsExactlyInAnyOrder("kubernetes", "busybox-service", "external-name-service");
+ testCustomLabel(instances);
+ testCustomAnnotation(instances);
+ testUnExistentService(discoveryClient);
+ testExternalNameService(discoveryClient);
+ }
+
+ // pod where annotations are not present
+ private void testCustomLabel(List instances) {
+ DefaultKubernetesServiceInstance withCustomLabel = instances.stream()
+ .map(serviceInstance -> (DefaultKubernetesServiceInstance) serviceInstance)
+ .filter(x -> x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty())
+ .toList()
+ .get(0);
+ assertThat(withCustomLabel.getServiceId()).isEqualTo("busybox-service");
+ assertThat(withCustomLabel.getInstanceId()).isNotNull();
+ assertThat(withCustomLabel.getHost()).isNotNull();
+ assertThat(withCustomLabel.getMetadata())
+ .containsAllEntriesOf(Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80"));
+ }
+
+ // pod where annotations are present
+ private void testCustomAnnotation(List instances) {
+ DefaultKubernetesServiceInstance withCustomAnnotation = instances.stream()
+ .map(serviceInstance -> (DefaultKubernetesServiceInstance) serviceInstance)
+ .filter(x -> !x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty())
+ .toList()
+ .get(0);
+ assertThat(withCustomAnnotation.getServiceId()).isEqualTo("busybox-service");
+ assertThat(withCustomAnnotation.getInstanceId()).isNotNull();
+ assertThat(withCustomAnnotation.getHost()).isNotNull();
+ assertThat(withCustomAnnotation.getMetadata())
+ .containsAllEntriesOf(Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80"));
+
+ Map annotations = withCustomAnnotation.podMetadata().get("annotations");
+ assertThat(annotations).containsEntry("custom-annotation", "custom-annotation-value");
+ }
+
+ private void testExternalNameService(DiscoveryClient discoveryClient) {
+ DefaultKubernetesServiceInstance externalNameService = (DefaultKubernetesServiceInstance) discoveryClient
+ .getInstances("external-name-service")
+ .get(0);
+
+ assertThat(externalNameService.getInstanceId()).isNotNull();
+ assertThat(externalNameService.getHost()).isEqualTo("spring.io");
+ assertThat(externalNameService.getPort()).isEqualTo(-1);
+ assertThat(externalNameService.getMetadata())
+ .containsAllEntriesOf(Map.of("k8s_namespace", "default", "type", "ExternalName"));
+ assertThat(externalNameService.isSecure()).isFalse();
+ assertThat(externalNameService.getUri().toASCIIString()).isEqualTo("spring.io");
+ assertThat(externalNameService.getScheme()).isEqualTo("http");
+ }
+
+ // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286
+ private void testUnExistentService(DiscoveryClient discoveryClient) {
+ List serviceInstances = discoveryClient.getInstances("non-existent");
+ assertThat(serviceInstances).isEmpty();
+ }
+
+ @TestConfiguration
+ static class TestConfig {
+
+ @Bean
+ @Primary
+ ApiClient client() {
+ return apiClient();
+ }
+
+ @Bean
+ @Primary
+ KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
+ return discoveryProperties(false, Set.of(NAMESPACE), null);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientReactiveIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientReactiveIT.java
new file mode 100644
index 0000000000..cf609142ef
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientReactiveIT.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2013-2025 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.k8s.client.discovery;
+
+import java.util.Set;
+
+import io.kubernetes.client.openapi.ApiClient;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.boot.test.web.server.LocalManagementPort;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Images;
+import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.test.context.TestPropertySource;
+
+import static org.springframework.cloud.kubernetes.k8s.client.discovery.TestAssertions.assertPodMetadata;
+import static org.springframework.cloud.kubernetes.k8s.client.discovery.TestAssertions.assertReactiveConfiguration;
+
+/**
+ * @author wind57
+ */
+@SpringBootTest(classes = { DiscoveryApp.class, KubernetesClientReactiveIT.TestConfig.class },
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@TestPropertySource(
+ properties = { "spring.cloud.discovery.reactive.enabled=true", "spring.cloud.discovery.blocking.enabled=false",
+ "logging.level.org.springframework.cloud.kubernetes.commons.discovery=debug",
+ "logging.level.org.springframework.cloud.client.discovery.health=debug",
+ "logging.level.org.springframework.cloud.kubernetes.client.discovery=debug" })
+class KubernetesClientReactiveIT extends KubernetesClientDiscoveryBase {
+
+ @LocalManagementPort
+ private int port;
+
+ @Autowired
+ private DiscoveryClient discoveryClient;
+
+ @BeforeEach
+ void beforeEach() {
+ Images.loadWiremock(K3S);
+ util.wiremock(NAMESPACE, "/", Phase.CREATE);
+ }
+
+ @AfterEach
+ void afterEach() {
+ util.wiremock(NAMESPACE, "/", Phase.DELETE);
+ }
+
+ /**
+ *
+ *
+ * Reactive is enabled, only blocking is disabled. As such,
+ * We assert for logs and call '/health' endpoint to see that blocking discovery
+ * client was initialized.
+ *
+ *
+ */
+ @Test
+ void test(CapturedOutput output) {
+ assertReactiveConfiguration(output, port);
+ assertPodMetadata(discoveryClient);
+ }
+
+ @TestConfiguration
+ static class TestConfig {
+
+ @Bean
+ @Primary
+ ApiClient client() {
+ return apiClient();
+ }
+
+ @Bean
+ @Primary
+ KubernetesDiscoveryProperties kubernetesDiscoveryProperties() {
+ return discoveryProperties(false, Set.of(NAMESPACE), null);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/TestAssertions.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/TestAssertions.java
new file mode 100644
index 0000000000..dd86ad7437
--- /dev/null
+++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/TestAssertions.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2013-2025 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.k8s.client.discovery;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.boot.test.json.BasicJsonTester;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.builder;
+import static org.springframework.cloud.kubernetes.integration.tests.commons.Commons.retrySpec;
+
+final class TestAssertions {
+
+ private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester(TestAssertions.class);
+
+ private static final String REACTIVE = "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client']";
+
+ private static final String REACTIVE_STATUS = REACTIVE + ".status";
+
+ private static final String REACTIVE_SERVICES = REACTIVE + ".details.services";
+
+ private static final String BLOCKING = "$.components.discoveryComposite.components.discoveryClient";
+
+ private static final String BLOCKING_STATUS = BLOCKING + ".status";
+
+ private static final String BLOCKING_SERVICES = BLOCKING + ".details.services";
+
+ private TestAssertions() {
+
+ }
+
+ static void assertLogStatement(CapturedOutput output, String textToAssert) {
+ await().atMost(Duration.ofSeconds(60))
+ .pollInterval(Duration.ofMillis(200))
+ .untilAsserted(() -> assertThat(output.getOut()).contains(textToAssert));
+ }
+
+ /**
+ * Reactive is disabled, only blocking is active. As such,
+ * KubernetesInformerDiscoveryClientAutoConfiguration::indicatorInitializer will post
+ * an InstanceRegisteredEvent.
+ *
+ * We assert for logs and call '/health' endpoint to see that blocking discovery
+ * client was initialized.
+ */
+ static void assertBlockingConfiguration(CapturedOutput output, int port) {
+
+ assertLogStatement(output, "Will publish InstanceRegisteredEvent from blocking implementation");
+ assertLogStatement(output, "publishing InstanceRegisteredEvent");
+ assertLogStatement(output, "Discovery Client has been initialized");
+
+ WebClient healthClient = builder().baseUrl("http://localhost:" + port + "/actuator/health").build();
+
+ String healthResult = healthClient.method(HttpMethod.GET)
+ .retrieve()
+ .bodyToMono(String.class)
+ .retryWhen(retrySpec())
+ .block();
+
+ assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathStringValue(BLOCKING_STATUS).isEqualTo("UP");
+
+ assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathStringValue(BLOCKING_STATUS).isEqualTo("UP");
+
+ assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue(BLOCKING_SERVICES)
+ .containsExactlyInAnyOrder("kubernetes", "service-wiremock");
+
+ assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(REACTIVE_STATUS);
+
+ }
+
+ /**
+ * Reactive is disabled, only blocking is active. As such,
+ * KubernetesInformerDiscoveryClientAutoConfiguration::indicatorInitializer will post
+ * an InstanceRegisteredEvent.
+ *
+ * We assert for logs and call '/health' endpoint to see that blocking discovery
+ * client was initialized.
+ */
+ static void assertReactiveConfiguration(CapturedOutput output, int port) {
+
+ assertLogStatement(output, "Will publish InstanceRegisteredEvent from reactive implementation");
+ assertLogStatement(output, "publishing InstanceRegisteredEvent");
+ assertLogStatement(output, "Discovery Client has been initialized");
+
+ WebClient healthClient = builder().baseUrl("http://localhost:" + port + "/actuator/health").build();
+
+ String healthResult = healthClient.method(HttpMethod.GET)
+ .retrieve()
+ .bodyToMono(String.class)
+ .retryWhen(retrySpec())
+ .block();
+
+ assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathStringValue(REACTIVE_STATUS).isEqualTo("UP");
+
+ assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue(REACTIVE_SERVICES)
+ .containsExactlyInAnyOrder("kubernetes", "service-wiremock");
+
+ assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(BLOCKING_STATUS);
+
+ }
+
+ static void assertPodMetadata(DiscoveryClient discoveryClient) {
+
+ List serviceInstances = discoveryClient.getInstances("service-wiremock");
+ assertThat(serviceInstances).hasSize(1);
+ DefaultKubernetesServiceInstance wiremockInstance = (DefaultKubernetesServiceInstance) serviceInstances.get(0);
+
+ assertThat(wiremockInstance.getServiceId()).isEqualTo("service-wiremock");
+ assertThat(wiremockInstance.getInstanceId()).isNotNull();
+ assertThat(wiremockInstance.getHost()).isNotNull();
+ assertThat(wiremockInstance.getMetadata()).isEqualTo(Map.of("k8s_namespace", "default", "type", "ClusterIP",
+ "port.http", "8080", "app", "service-wiremock"));
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-deployment.yaml
deleted file mode 100644
index 27045945fc..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-deployment.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: spring-cloud-kubernetes-k8s-client-discovery
-spec:
- selector:
- matchLabels:
- app: spring-cloud-kubernetes-k8s-client-discovery
- template:
- metadata:
- labels:
- app: spring-cloud-kubernetes-k8s-client-discovery
- spec:
- serviceAccountName: spring-cloud-kubernetes-serviceaccount
- containers:
- - name: spring-cloud-kubernetes-k8s-client-discovery
- image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery
- imagePullPolicy: IfNotPresent
- readinessProbe:
- httpGet:
- port: 8080
- path: /actuator/health/readiness
- initialDelaySeconds: 15
- periodSeconds: 2
- failureThreshold: 5
- livenessProbe:
- httpGet:
- port: 8080
- path: /actuator/health/liveness
- initialDelaySeconds: 15
- periodSeconds: 2
- failureThreshold: 5
- ports:
- - containerPort: 8080
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-ingress.yaml
deleted file mode 100644
index 27e0242960..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-ingress.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- name: spring-cloud-kubernetes-k8s-client-discovery
- namespace: default
-spec:
- rules:
- - http:
- paths:
- - path: /
- pathType: Prefix
- backend:
- service:
- name: spring-cloud-kubernetes-k8s-client-discovery
- port:
- number: 8080
diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-service.yaml
deleted file mode 100644
index 4f19d879c0..0000000000
--- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/kubernetes-discovery-service.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
- labels:
- app: spring-cloud-kubernetes-k8s-client-discovery
- annotations:
- custom-spring-k8s: spring-k8s
- name: spring-cloud-kubernetes-k8s-client-discovery
-spec:
- ports:
- - name: http
- port: 8080
- targetPort: 8080
- selector:
- app: spring-cloud-kubernetes-k8s-client-discovery
- type: ClusterIP