diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/TestAssertions.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/TestAssertions.java index 1fdd8272b9..8c034d6989 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/TestAssertions.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/client/discovery/TestAssertions.java @@ -150,49 +150,6 @@ static void assertBlockingConfiguration(CapturedOutput output, int port) { } - /** - * Both blocking and reactive are enabled. - */ - static void testDefaultConfiguration(CapturedOutput output, int port) { - - waitForLogStatement(output, "Will publish InstanceRegisteredEvent from blocking implementation"); - waitForLogStatement(output, "Will publish InstanceRegisteredEvent from reactive implementation"); - waitForLogStatement(output, "publishing InstanceRegisteredEvent"); - waitForLogStatement(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("$.components.discoveryComposite.status") - .isEqualTo("UP"); - - assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.discoveryComposite.components.discoveryClient.status") - .isEqualTo("UP"); - - assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathArrayValue("$.components.discoveryComposite.components.discoveryClient.details.services") - .containsExactlyInAnyOrder("kubernetes", "busybox-service"); - - assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.reactiveDiscoveryClients.status") - .isEqualTo("UP"); - - assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathStringValue( - "$.components.reactiveDiscoveryClients.components.['Fabric8 Kubernetes Reactive Discovery Client'].status") - .isEqualTo("UP"); - - assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue( - "$.components.reactiveDiscoveryClients.components.['Fabric8 Kubernetes Reactive Discovery Client'].details.services") - .containsExactlyInAnyOrder("kubernetes", "busybox-service"); - } - /** * Reactive is enabled, blocking is disabled. As such, * KubernetesInformerDiscoveryClientAutoConfiguration::indicatorInitializer will post diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml index bab2d79db5..dcfdd87fa7 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/pom.xml @@ -11,6 +11,11 @@ spring-cloud-kubernetes-k8s-client-discovery + + true + true + + org.springframework.cloud @@ -41,17 +46,5 @@ - - - - ../src/main/resources - true - - - src/main/resources - true - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApp.java index af9e0fdba2..aa35167c8e 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApp.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApp.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * 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. @@ -23,7 +23,7 @@ * @author wind57 */ @SpringBootApplication -public class DiscoveryApp { +class DiscoveryApp { public static void main(String[] args) { SpringApplication.run(DiscoveryApp.class, args); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApplicationListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApplicationListener.java deleted file mode 100644 index 37af2c7082..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryApplicationListener.java +++ /dev/null @@ -1,44 +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 io.kubernetes.client.openapi.models.V1Pod; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.core.log.LogAccessor; -import org.springframework.stereotype.Component; - -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer.RegisteredEventSource; - -/** - * @author wind57 - */ -@Component -public class DiscoveryApplicationListener implements ApplicationListener> { - - private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(DiscoveryApplicationListener.class)); - - @Override - public void onApplicationEvent(InstanceRegisteredEvent event) { - V1Pod pod = (V1Pod) ((RegisteredEventSource) event.getSource()).pod(); - LOG.info(() -> "received InstanceRegisteredEvent from pod with 'app' label value : " - + pod.getMetadata().getLabels().get("app")); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryController.java deleted file mode 100644 index 189b24bdd0..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/DiscoveryController.java +++ /dev/null @@ -1,52 +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.util.List; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author wind57 - */ -@RestController -public class DiscoveryController { - - private final KubernetesInformerDiscoveryClient discoveryClient; - - public DiscoveryController(ObjectProvider discoveryClient) { - KubernetesInformerDiscoveryClient[] local = new KubernetesInformerDiscoveryClient[1]; - discoveryClient.ifAvailable(x -> local[0] = x); - this.discoveryClient = local[0]; - } - - @GetMapping("/services") - public List allServices() { - return discoveryClient.getServices(); - } - - @GetMapping("/service-instances/{serviceId}") - public List serviceInstances(@PathVariable("serviceId") String serviceId) { - return discoveryClient.getInstances(serviceId); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/ReactiveDiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/ReactiveDiscoveryController.java deleted file mode 100644 index bd79be6566..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/main/java/org/springframework/cloud/kubernetes/k8s/client/discovery/ReactiveDiscoveryController.java +++ /dev/null @@ -1,55 +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.util.List; - -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author wind57 - */ -@RestController -public class ReactiveDiscoveryController { - - private final KubernetesInformerReactiveDiscoveryClient reactiveDiscoveryClient; - - public ReactiveDiscoveryController( - ObjectProvider reactiveDiscoveryClient) { - KubernetesInformerReactiveDiscoveryClient[] local = new KubernetesInformerReactiveDiscoveryClient[1]; - reactiveDiscoveryClient.ifAvailable(x -> local[0] = x); - this.reactiveDiscoveryClient = local[0]; - } - - @GetMapping("/reactive/services") - public Mono> allServices() { - return reactiveDiscoveryClient.getServices().collectList(); - } - - @GetMapping("reactive/service-instances/{serviceId}") - public Mono> serviceInstances(@PathVariable("serviceId") String serviceId) { - return reactiveDiscoveryClient.getInstances(serviceId).collectList(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientBlockingIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientBlockingIT.java new file mode 100644 index 0000000000..7ff3463492 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientBlockingIT.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.assertBlockingConfiguration; +import static org.springframework.cloud.kubernetes.k8s.client.discovery.TestAssertions.assertPodMetadata; + +/** + * @author wind57 + */ +@SpringBootTest(classes = { DiscoveryApp.class, KubernetesClientBlockingIT.TestConfig.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource( + properties = { "spring.cloud.discovery.reactive.enabled=false", "spring.cloud.discovery.blocking.enabled=true", + "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 KubernetesClientBlockingIT 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 disabled, only blocking is active. As such,
+	 * 	 	We assert for logs and call '/health' endpoint to see that blocking discovery
+	 * 	 	client was initialized.
+	 *
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + assertBlockingConfiguration(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/KubernetesClientDiscoveryBase.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryBase.java new file mode 100644 index 0000000000..ec8ab9177e --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryBase.java @@ -0,0 +1,81 @@ +/* + * 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.io.IOException; +import java.io.StringReader; +import java.util.Map; +import java.util.Set; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.util.Config; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.k3s.K3sContainer; + +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.test.context.TestPropertySource; + +/** + * @author wind57 + */ +@TestPropertySource(properties = { "spring.main.cloud-platform=kubernetes", + "spring.cloud.config.import-check.enabled=false", "spring.cloud.kubernetes.client.namespace=default", + "spring.cloud.kubernetes.discovery.metadata.add-pod-labels=true", + "spring.cloud.kubernetes.discovery.metadata.add-pod-annotations=true", + "logging.level.org.springframework.cloud.kubernetes.client.discovery=debug" }) +@ExtendWith(OutputCaptureExtension.class) +abstract class KubernetesClientDiscoveryBase { + + protected static final String NAMESPACE = "default"; + + protected static final K3sContainer K3S = Commons.container(); + + protected static Util util; + + @BeforeAll + protected static void beforeAll() { + K3S.start(); + util = new Util(K3S); + } + + protected static ApiClient apiClient() { + String kubeConfigYaml = K3S.getKubeConfigYaml(); + + ApiClient client; + try { + client = Config.fromConfig(new StringReader(kubeConfigYaml)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + return new CoreV1Api(client).getApiClient(); + } + + protected static KubernetesDiscoveryProperties discoveryProperties(boolean useEndpointSlices, + Set namespaces, String filter) { + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, null, true, + null, true, "port.", true, true); + return new KubernetesDiscoveryProperties(true, false, namespaces, true, 60, false, filter, Set.of(443, 8443), + Map.of(), null, metadata, 0, useEndpointSlices, true, 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/KubernetesClientDiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java deleted file mode 100644 index 470b381c1e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java +++ /dev/null @@ -1,519 +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.Map; -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.k3s.K3sContainer; -import reactor.netty.http.client.HttpClient; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - -import org.springframework.cloud.client.ServiceInstance; -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 KubernetesClientDiscoveryClientIT { - - 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 final String NAMESPACE_A_UAT = "a-uat"; - - private static final String NAMESPACE_B_UAT = "b-uat"; - - 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); - Images.loadBusybox(K3S); - - util = new Util(K3S); - util.setUp(NAMESPACE); - manifests(Phase.CREATE); - } - - @AfterAll - static void afterAll() { - manifests(Phase.DELETE); - } - - /** - * Three services are deployed in the default namespace. We do not configure any - * explicit namespace and 'default' must be picked-up. - */ - @Test - @Order(1) - void testSimple() throws Exception { - - util.busybox(NAMESPACE, Phase.CREATE); - - // 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"); - - Commons.waitForLogStatement("serviceSharedInformer will use namespace : default", K3S, IMAGE_NAME); - - 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(), 4); - Assertions.assertTrue(servicesResult.contains("kubernetes")); - Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); - Assertions.assertTrue(servicesResult.contains("busybox-service")); - Assertions.assertTrue(servicesResult.contains("external-name-service")); - - WebClient ourServiceClient = builder() - .baseUrl("http://localhost/service-instances/spring-cloud-kubernetes-k8s-client-discovery") - .build(); - - List ourServiceInstances = ourServiceClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .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("app", "spring-cloud-kubernetes-k8s-client-discovery", "custom-spring-k8s", "spring-k8s", - "port.http", "8080", "k8s_namespace", "default", "type", "ClusterIP")); - Assertions.assertEquals(serviceInstance.getPort(), 8080); - Assertions.assertEquals(serviceInstance.getNamespace(), "default"); - - WebClient busyBoxServiceClient = builder().baseUrl("http://localhost/service-instances/busybox-service") - .build(); - List busyBoxServiceInstances = busyBoxServiceClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(busyBoxServiceInstances.size(), 2); - - DefaultKubernetesServiceInstance withCustomLabel = busyBoxServiceInstances.stream() - .filter(x -> x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()) - .toList() - .get(0); - Assertions.assertEquals(withCustomLabel.getServiceId(), "busybox-service"); - Assertions.assertNotNull(withCustomLabel.getInstanceId()); - Assertions.assertNotNull(withCustomLabel.getHost()); - Assertions.assertEquals(withCustomLabel.getMetadata(), - Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80")); - Assertions.assertTrue(withCustomLabel.podMetadata() - .get("labels") - .entrySet() - .stream() - .anyMatch(x -> x.getKey().equals("custom-label") && x.getValue().equals("custom-label-value"))); - - DefaultKubernetesServiceInstance withCustomAnnotation = busyBoxServiceInstances.stream() - .filter(x -> !x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()) - .toList() - .get(0); - Assertions.assertEquals(withCustomAnnotation.getServiceId(), "busybox-service"); - Assertions.assertNotNull(withCustomAnnotation.getInstanceId()); - Assertions.assertNotNull(withCustomAnnotation.getHost()); - Assertions.assertEquals(withCustomAnnotation.getMetadata(), - Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80")); - Assertions.assertTrue(withCustomAnnotation.podMetadata() - .get("annotations") - .entrySet() - .stream() - .anyMatch(x -> x.getKey().equals("custom-annotation") && x.getValue().equals("custom-annotation-value"))); - - // enforces this : - // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286 - WebClient clientForNonExistentService = builder().baseUrl("http://localhost/service-instances/non-existent") - .build(); - List resultForNonExistentService = clientForNonExistentService.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(resultForNonExistentService.size(), 0); - - // clean-up - util.busybox(NAMESPACE, Phase.DELETE); - } - - /** - *
-	 *     - config server is enabled for all namespaces
-	 *     - wiremock service is deployed in namespace-a
-	 *     - busybox service is deployed in namespace-b
-	 *     - external-name-service is deployed in namespace "default" and such a service type is requested,
-	 *       thus found also.
-	 *
-	 *     Our discovery searches in all namespaces, thus finds them both.
-	 * 
- */ - @Test - @Order(2) - void testAllNamespaces() { - util.createNamespace(NAMESPACE_A); - util.createNamespace(NAMESPACE_B); - util.setUpClusterWideClusterRoleBinding(NAMESPACE); - util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE, false); - util.busybox(NAMESPACE_B, Phase.CREATE); - - KubernetesClientDiscoveryClientUtils.patchForAllNamespaces(DEPLOYMENT_NAME, NAMESPACE); - - Commons.waitForLogStatement("serviceSharedInformer will use all-namespaces", K3S, IMAGE_NAME); - - 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(), 8); - Assertions.assertTrue(servicesResult.contains("kubernetes")); - Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); - Assertions.assertTrue(servicesResult.contains("busybox-service")); - Assertions.assertTrue(servicesResult.contains("service-wiremock")); - Assertions.assertTrue(servicesResult.contains("external-name-service")); - - // enforces this : - // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286 - WebClient clientForNonExistentService = builder().baseUrl("http://localhost/service-instances/non-existent") - .build(); - List resultForNonExistentService = clientForNonExistentService.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(resultForNonExistentService.size(), 0); - - // test ExternalName fields - WebClient externalNameClient = builder().baseUrl("http://localhost/service-instances/external-name-service") - .build(); - List externalNameServices = externalNameClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - DefaultKubernetesServiceInstance externalNameService = externalNameServices.get(0); - Assertions.assertNotNull(externalNameService.getInstanceId()); - Assertions.assertEquals(externalNameService.getHost(), "spring.io"); - Assertions.assertEquals(externalNameService.getPort(), -1); - Assertions.assertEquals(externalNameService.getMetadata(), - Map.of("k8s_namespace", "default", "type", "ExternalName")); - Assertions.assertFalse(externalNameService.isSecure()); - Assertions.assertEquals(externalNameService.getUri().toASCIIString(), "spring.io"); - Assertions.assertEquals(externalNameService.getScheme(), "http"); - - // do not remove wiremock in namespace a, it is required in the next test - util.busybox(NAMESPACE_B, Phase.DELETE); - util.deleteClusterWideClusterRoleBinding(NAMESPACE); - } - - /** - *
-	 *     - config server is enabled for namespace-a
-	 *     - wiremock service is deployed in namespace-a
-	 *     - wiremock service is deployed in namespace-b
-	 *
-	 *     Only service in namespace-a is found.
-	 * 
- */ - @Test - @Order(3) - void testSpecificNamespace() { - util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A)); - util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE, false); - - KubernetesClientDiscoveryClientUtils.patchForSingleNamespace(DEPLOYMENT_NAME, NAMESPACE); - - // first check that wiremock service is present in both namespaces a and b - assertServicePresentInNamespaces(List.of("a", "b"), "service-wiremock", "service-wiremock"); - - Commons.waitForLogStatement("using selective namespaces : [a]", K3S, IMAGE_NAME); - Commons.waitForLogStatement("reading pod in namespace : default", 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); - - 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 wiremockInNamespaceAClient = builder().baseUrl("http://localhost/service-instances/service-wiremock") - .build(); - - List wiremockInNamespaceA = wiremockInNamespaceAClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(wiremockInNamespaceA.size(), 1); - - DefaultKubernetesServiceInstance serviceInstance = wiremockInNamespaceA.get(0); - Assertions.assertEquals(serviceInstance.getNamespace(), "a"); - - // enforces this : - // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286 - WebClient clientForNonExistentService = builder().baseUrl("http://localhost/service-instances/non-existent") - .build(); - List resultForNonExistentService = clientForNonExistentService.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(resultForNonExistentService.size(), 0); - - util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE, false); - util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE, false); - util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A)); - util.deleteNamespace(NAMESPACE_A); - util.deleteNamespace(NAMESPACE_B); - } - - @Test - @Order(4) - void testSimplePodMetadata() { - util.setUp(NAMESPACE); - String imageName = "docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery:" + Commons.pomVersion(); - KubernetesClientDiscoveryClientUtils.patchForPodMetadata(imageName, DEPLOYMENT_NAME, NAMESPACE); - new KubernetesClientDiscoveryPodMetadataITDelegate().testSimple(); - } - - @Test - @Order(5) - void filterMatchesOneNamespaceViaThePredicate() { - String imageName = "docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery:" + Commons.pomVersion(); - KubernetesClientDiscoveryClientUtils.patchForUATNamespacesTests(imageName, DEPLOYMENT_NAME, NAMESPACE); - new KubernetesClientDiscoveryFilterITDelegate().filterMatchesOneNamespaceViaThePredicate(util); - - } - - /** - *
-	 *     - service "wiremock" is present in namespace "a-uat"
-	 *     - service "wiremock" is present in namespace "b-uat"
-	 *
-	 *     - we search with a predicate : "#root.metadata.namespace matches '^uat.*$'"
-	 *
-	 *     As such, both services are found via 'getInstances' call.
-	 * 
- */ - @Test - @Order(6) - void filterMatchesBothNamespacesViaThePredicate() { - - // patch the deployment to change what namespaces are take into account - KubernetesClientDiscoveryClientUtils.patchForTwoNamespacesMatchViaThePredicate(DEPLOYMENT_NAME, NAMESPACE); - - new KubernetesClientDiscoveryFilterITDelegate().filterMatchesBothNamespacesViaThePredicate(); - } - - @Test - @Order(7) - void testBlockingConfiguration() { - - // filter tests are done, clean-up a bit to prepare everything for health tests - deleteNamespacesAndWiremock(); - - String imageName = "docker.io/springcloud/spring-cloud-kubernetes-k8s-client-discovery:" + Commons.pomVersion(); - KubernetesClientDiscoveryClientUtils.patchForBlockingHealth(imageName, DEPLOYMENT_NAME, NAMESPACE); - - new KubernetesClientDiscoveryHealthITDelegate().testBlockingConfiguration(K3S); - } - - @Test - @Order(8) - void testReactiveConfiguration() { - - KubernetesClientDiscoveryClientUtils.patchForReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); - - new KubernetesClientDiscoveryHealthITDelegate().testReactiveConfiguration(K3S); - } - - @Test - @Order(9) - void testDefaultConfiguration() { - - KubernetesClientDiscoveryClientUtils.patchForBlockingAndReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); - - new KubernetesClientDiscoveryHealthITDelegate().testDefaultConfiguration(K3S); - } - - private void deleteNamespacesAndWiremock() { - util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.DELETE, false); - util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.DELETE, false); - util.deleteNamespace(NAMESPACE_A_UAT); - util.deleteNamespace(NAMESPACE_B_UAT); - } - - private static void manifests(Phase phase) { - V1Deployment deployment = (V1Deployment) util.yaml("kubernetes-discovery-deployment.yaml"); - V1Service service = (V1Service) util.yaml("kubernetes-discovery-service.yaml"); - V1Service externalNameService = (V1Service) util.yaml("external-name-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("kubernetes-discovery-ingress.yaml"); - - if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - util.deleteAndWait(NAMESPACE, null, externalNameService, null); - 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 commonsLevel = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_DISCOVERY") - .value("DEBUG"); - - V1EnvVar debugLevelForClient = new V1EnvVar() - .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT") - .value("DEBUG"); - - V1EnvVar addLabels = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODLABELS") - .value("TRUE"); - - V1EnvVar addAnnotations = new V1EnvVar() - .name("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODANNOTATIONS") - .value("TRUE"); - - envVars.add(debugLevel); - envVars.add(debugLevelForClient); - envVars.add(addLabels); - envVars.add(addAnnotations); - envVars.add(commonsLevel); - deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); - - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - util.createAndWait(NAMESPACE, null, null, externalNameService, null, true); - } - - } - - 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); - } - - private void assertServicePresentInNamespaces(List namespaces, String value, String serviceName) { - namespaces.forEach(x -> { - try { - String service = K3S - .execInContainer("sh", "-c", - "kubectl get services -n " + x + " -l app=" + value + " -o=name --no-headers | tr -d '\n'") - .getStdout(); - Assertions.assertEquals(service, "service/" + serviceName); - } - catch (Exception e) { - throw new RuntimeException(e); - } - - }); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java deleted file mode 100644 index 7e18bbe72e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java +++ /dev/null @@ -1,415 +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.util.Map; - -import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithMerge; -import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithReplace; - -/** - * @author wind57 - */ -final class KubernetesClientDiscoveryClientUtils { - - private static final Map POD_LABELS = Map.of("app", "spring-cloud-kubernetes-k8s-client-discovery"); - - // patch the filter so that it matches both namespaces - private static final String BODY_ONE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [{ - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_FILTER", - "value": "#root.metadata.namespace matches '^.*uat$'" - }] - }] - } - } - } - } - """; - - private static final String BODY_TWO = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [ - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_DISCOVERY", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_CLIENT_DISCOVERY_HEALTH_REACTIVE", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY_REACTIVE", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY", - "value": "DEBUG" - }, - { - "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", - "value": "FALSE" - }, - { - "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - // this one patches on top of BODY_TWO, so it essentially enables both blocking and - // reactive implementations - // and adds proper packages in DEBUG mode, so that we could assert logs. - private static final String BODY_THREE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [{ - "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", - "value": "TRUE" - }] - }] - } - } - } - } - """; - - // this one patches on top of BODY_TWO, so it essentially enables both blocking and - // reactive implementations - // and adds proper packages in DEBUG mode, so that we could assert logs. - private static final String BODY_FOUR = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "image": "image_name_here", - "env": [ - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_DISCOVERY", - "value": "DEBUG" - }, - { - "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", - "value": "FALSE" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_CLIENT_DISCOVERY_HEALTH", - "value": "DEBUG" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY", - "value": "DEBUG" - } - ] - }] - } - } - } - } - """; - - // patch to include all namespaces + external name services - private static final String BODY_FIVE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [ - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES", - "value": "TRUE" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - // disable all namespaces and include a single namespace to be discoverable - private static final String BODY_SIX = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [ - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0", - "value": "a" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES", - "value": "FALSE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_SEVEN = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "image": "image_name_here", - "env": [ - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY", - "value": "DEBUG" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDLABELS", - "value": "TRUE" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_LABELSPREFIX", - "value": "label-" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDANNOTATIONS", - "value": "TRUE" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ANNOTATIONSPREFIX", - "value": "annotation-" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_EIGHT = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [ - { - "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", - "value": "TRUE" - }, - { - "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", - "value": "FALSE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_NINE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [ - { - "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", - "value": "TRUE" - }, - { - "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_TEN = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [ - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1", - "value": "b" - }, - { - "name": "SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED", - "value": "FALSE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_ELEVEN = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [ - { - "name": "SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED", - "value": "TRUE" - } - ] - }] - } - } - } - } - """; - - private static final String BODY_TWELVE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-discovery", - "image": "image_name_here", - "env": [ - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0", - "value": "a-uat" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1", - "value": "b-uat" - }, - { - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_FILTER", - "value": "#root.metadata.namespace matches 'a-uat$'" - }, - { - "name": "LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY", - "value": "DEBUG" - } - ] - }] - } - } - } - } - """; - - private KubernetesClientDiscoveryClientUtils() { - - } - - static void patchForTwoNamespacesMatchViaThePredicate(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_ONE, POD_LABELS); - } - - static void patchForReactiveHealth(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_TWO, POD_LABELS); - } - - static void patchForBlockingAndReactiveHealth(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_THREE, POD_LABELS); - } - - // notice the usage of 'PATCH_FORMAT_JSON_MERGE_PATCH' here, it will not merge - // env variables - static void patchForBlockingHealth(String image, String deploymentName, String namespace) { - patchWithReplace(image, deploymentName, namespace, BODY_FOUR, POD_LABELS); - } - - // add SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES=TRUE - // and SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES=TRUE - static void patchForAllNamespaces(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_FIVE, POD_LABELS); - } - - static void patchForSingleNamespace(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_SIX, POD_LABELS); - } - - static void patchForPodMetadata(String imageName, String deploymentName, String namespace) { - patchWithReplace(imageName, deploymentName, namespace, BODY_SEVEN, POD_LABELS); - } - - static void patchForReactiveOnly(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_EIGHT, POD_LABELS); - } - - static void patchForBlockingAndReactive(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_NINE, POD_LABELS); - } - - static void patchForTwoNamespacesBlockingOnly(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_TEN, POD_LABELS); - } - - static void patchToAddBlockingSupport(String deploymentName, String namespace) { - patchWithMerge(deploymentName, namespace, BODY_ELEVEN, POD_LABELS); - } - - static void patchForUATNamespacesTests(String image, String deploymentName, String namespace) { - patchWithReplace(image, deploymentName, namespace, BODY_TWELVE, POD_LABELS); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterIT.java new file mode 100644 index 0000000000..189dd3d729 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterIT.java @@ -0,0 +1,138 @@ +/* + * 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.Comparator; +import java.util.List; +import java.util.Map; +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.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; + +/** + * @author wind57 + */ +@SpringBootTest(classes = { DiscoveryApp.class, KubernetesClientDiscoveryFilterIT.TestConfig.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { "spring.cloud.kubernetes.discovery.namespaces[0]=a-uat", + "spring.cloud.kubernetes.discovery.namespaces[1]=b-uat" }) +class KubernetesClientDiscoveryFilterIT extends KubernetesClientDiscoveryBase { + + private static final String NAMESPACE_A_UAT = "a-uat"; + + private static final String NAMESPACE_B_UAT = "b-uat"; + + @Autowired + private DiscoveryClient discoveryClient; + + @BeforeEach + void beforeEach() { + util.createNamespace(NAMESPACE_A_UAT); + util.createNamespace(NAMESPACE_B_UAT); + + Images.loadWiremock(K3S); + util.wiremock(NAMESPACE_A_UAT, "/", Phase.CREATE); + util.wiremock(NAMESPACE_B_UAT, "/", Phase.CREATE); + } + + @AfterEach + void afterEach() { + util.wiremock(NAMESPACE_A_UAT, "/", Phase.DELETE); + util.wiremock(NAMESPACE_B_UAT, "/", Phase.DELETE); + + util.deleteNamespace(NAMESPACE_A_UAT); + util.deleteNamespace(NAMESPACE_B_UAT); + } + + /** + *
+	 *     - service "wiremock" is present in namespace "a-uat"
+	 *     - service "wiremock" is present in namespace "b-uat"
+	 *
+	 *     - we search with a predicate : "#root.metadata.namespace matches '^uat.*$'"
+	 *
+	 *     As such, both services are found via 'getInstances' call.
+	 * 
+ */ + @Test + void test() { + List services = discoveryClient.getServices(); + List serviceInstances = discoveryClient.getInstances("service-wiremock"); + + assertThat(services.size()).isEqualTo(1); + assertThat(services).contains("service-wiremock"); + assertThat(serviceInstances.size()).isEqualTo(2); + + List sorted = serviceInstances.stream() + .map(x -> (DefaultKubernetesServiceInstance) x) + .sorted(Comparator.comparing(DefaultKubernetesServiceInstance::getNamespace)) + .toList(); + + DefaultKubernetesServiceInstance first = sorted.get(0); + assertThat(first.getServiceId()).isEqualTo("service-wiremock"); + assertThat(first.getInstanceId()).isNotNull(); + assertThat(first.getPort()).isEqualTo(8080); + assertThat(first.getNamespace()).isEqualTo("a-uat"); + assertThat(first.getMetadata()).containsAllEntriesOf( + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); + + DefaultKubernetesServiceInstance second = sorted.get(1); + assertThat(second.getServiceId()).isEqualTo("service-wiremock"); + assertThat(second.getInstanceId()).isNotNull(); + assertThat(second.getPort()).isEqualTo(8080); + assertThat(second.getNamespace()).isEqualTo("b-uat"); + assertThat(second.getMetadata()).containsAllEntriesOf( + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "b-uat", "type", "ClusterIP")); + } + + @TestConfiguration + static class TestConfig { + + @Bean + @Primary + ApiClient client() { + return apiClient(); + } + + @Bean + @Primary + KubernetesDiscoveryProperties kubernetesDiscoveryProperties() { + return discoveryProperties(false, Set.of(NAMESPACE_A_UAT, NAMESPACE_B_UAT), + "#root.metadata.namespace matches '^.*uat$'"); + } + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java deleted file mode 100644 index eee7807ff5..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java +++ /dev/null @@ -1,161 +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.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.junit.jupiter.api.Assertions; -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.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 - */ -class KubernetesClientDiscoveryFilterITDelegate { - - private static final String NAMESPACE_A_UAT = "a-uat"; - - private static final String NAMESPACE_B_UAT = "b-uat"; - - private static final String NAMESPACE = "default"; - - private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; - - void filterMatchesOneNamespaceViaThePredicate(Util util) { - - // set-up for this test and the next one - util.createNamespace(NAMESPACE_A_UAT); - util.createNamespace(NAMESPACE_B_UAT); - util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A_UAT, NAMESPACE_B_UAT)); - util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.CREATE, false); - util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.CREATE, false); - - WebClient clientServices = builder().baseUrl("http://localhost/services").build(); - - @SuppressWarnings("unchecked") - List services = (List) clientServices.method(HttpMethod.GET) - .retrieve() - .bodyToMono(List.class) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(services.size(), 1); - Assertions.assertTrue(services.contains("service-wiremock")); - - WebClient client = builder().baseUrl("http://localhost/service-instances/service-wiremock").build(); - List serviceInstances = client.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(serviceInstances.size(), 1); - - DefaultKubernetesServiceInstance first = serviceInstances.get(0); - Assertions.assertEquals(first.getServiceId(), "service-wiremock"); - Assertions.assertNotNull(first.getInstanceId()); - Assertions.assertEquals(first.getPort(), 8080); - Assertions.assertEquals(first.getNamespace(), "a-uat"); - Assertions.assertEquals(first.getMetadata(), - Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); - - } - - /** - *
-	 *     - service "wiremock" is present in namespace "a-uat"
-	 *     - service "wiremock" is present in namespace "b-uat"
-	 *
-	 *     - we search with a predicate : "#root.metadata.namespace matches '^uat.*$'"
-	 *
-	 *     As such, both services are found via 'getInstances' call.
-	 * 
- */ - void filterMatchesBothNamespacesViaThePredicate() { - - // patch the deployment to change what namespaces are take into account - KubernetesClientDiscoveryClientUtils.patchForTwoNamespacesMatchViaThePredicate(DEPLOYMENT_NAME, NAMESPACE); - - WebClient clientServices = builder().baseUrl("http://localhost/services").build(); - - @SuppressWarnings("unchecked") - List services = (List) clientServices.method(HttpMethod.GET) - .retrieve() - .bodyToMono(List.class) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(services.size(), 1); - Assertions.assertTrue(services.contains("service-wiremock")); - - WebClient client = builder().baseUrl("http://localhost/service-instances/service-wiremock").build(); - List serviceInstances = client.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertEquals(serviceInstances.size(), 2); - List sorted = serviceInstances.stream() - .sorted(Comparator.comparing(DefaultKubernetesServiceInstance::getNamespace)) - .toList(); - - DefaultKubernetesServiceInstance first = sorted.get(0); - Assertions.assertEquals(first.getServiceId(), "service-wiremock"); - Assertions.assertNotNull(first.getInstanceId()); - Assertions.assertEquals(first.getPort(), 8080); - Assertions.assertEquals(first.getNamespace(), "a-uat"); - Assertions.assertEquals(first.getMetadata(), - Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); - - DefaultKubernetesServiceInstance second = sorted.get(1); - Assertions.assertEquals(second.getServiceId(), "service-wiremock"); - Assertions.assertNotNull(second.getInstanceId()); - Assertions.assertEquals(second.getPort(), 8080); - Assertions.assertEquals(second.getNamespace(), "b-uat"); - Assertions.assertEquals(second.getMetadata(), - Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "b-uat", "type", "ClusterIP")); - - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private RetryBackoffSpec retrySpec() { - return Retry.fixedDelay(15, Duration.ofSeconds(2)).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/KubernetesClientDiscoveryHealthITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java deleted file mode 100644 index 8d11b5a29e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java +++ /dev/null @@ -1,318 +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.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -import org.assertj.core.api.Assertions; -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.boot.test.json.BasicJsonTester; -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; - -import static org.awaitility.Awaitility.await; - -/** - * @author wind57 - */ -class KubernetesClientDiscoveryHealthITDelegate { - - KubernetesClientDiscoveryHealthITDelegate() { - - } - - private static final String REACTIVE_STATUS = "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].status"; - - private static final String BLOCKING_STATUS = "$.components.discoveryComposite.components.discoveryClient.status"; - - private static final String NAMESPACE = "default"; - - private static final String DEPLOYMENT_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; - - private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester( - KubernetesClientDiscoveryHealthITDelegate.class); - - /** - * 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. - */ - void testBlockingConfiguration(K3sContainer container) { - - assertLogStatement(container, "Will publish InstanceRegisteredEvent from blocking implementation"); - assertLogStatement(container, "publishing InstanceRegisteredEvent"); - assertLogStatement(container, "Discovery Client has been initialized"); - assertLogStatement(container, - "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-k8s-client-discovery"); - - WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); - - String healthResult = healthClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.discoveryComposite.status") - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue(BLOCKING_STATUS) - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathArrayValue("$.components.discoveryComposite.components.discoveryClient.details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", - "external-name-service"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(REACTIVE_STATUS); - - } - - /** - * Reactive is enabled, blocking is disabled. As such, - * KubernetesInformerDiscoveryClientAutoConfiguration::indicatorInitializer will post - * an InstanceRegisteredEvent. - * - * We assert for logs and call '/health' endpoint to see that blocking discovery - * client was initialized. - */ - void testReactiveConfiguration(K3sContainer container) { - - KubernetesClientDiscoveryClientUtils.patchForReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); - - assertLogStatement(container, "Will publish InstanceRegisteredEvent from reactive implementation"); - assertLogStatement(container, "publishing InstanceRegisteredEvent"); - assertLogStatement(container, "Discovery Client has been initialized"); - assertLogStatement(container, - "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-k8s-client-discovery"); - - WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); - - String healthResult = healthClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.reactiveDiscoveryClients.status") - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue(REACTIVE_STATUS) - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathArrayValue( - "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", - "external-name-service"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(BLOCKING_STATUS); - - // test for services also: - - WebClient servicesClient = builder().baseUrl("http://localhost/reactive/services").build(); - - List servicesResult = servicesClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertThat(servicesResult).contains("spring-cloud-kubernetes-k8s-client-discovery"); - Assertions.assertThat(servicesResult).contains("kubernetes"); - - } - - /** - * Both blocking and reactive are enabled. - */ - void testDefaultConfiguration(K3sContainer container) { - - KubernetesClientDiscoveryClientUtils.patchForBlockingAndReactiveHealth(DEPLOYMENT_NAME, NAMESPACE); - - assertLogStatement(container, "Will publish InstanceRegisteredEvent from blocking implementation"); - assertLogStatement(container, "publishing InstanceRegisteredEvent"); - assertLogStatement(container, "Discovery Client has been initialized"); - assertLogStatement(container, - "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-k8s-client-discovery"); - - WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); - WebClient infoClient = builder().baseUrl("http://localhost/actuator/info").build(); - - String healthResult = healthClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - String infoResult = infoClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(String.class) - .retryWhen(retrySpec()) - .block(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.discoveryComposite.status") - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.discoveryComposite.components.discoveryClient.status") - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathArrayValue("$.components.discoveryComposite.components.discoveryClient.details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", - "external-name-service"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.reactiveDiscoveryClients.status") - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue( - "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].status") - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathArrayValue( - "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", - "external-name-service"); - - // assert health/info also - assertHealth(healthResult); - assertInfo(infoResult); - } - - private void assertHealth(String healthResult) { - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.status") - .isEqualTo("UP"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.details.hostIp") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathBooleanValue("$.components.kubernetes.details.inside") - .isEqualTo(true); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.details.labels.app") - .isEqualTo("spring-cloud-kubernetes-k8s-client-discovery"); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.details.namespace") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.details.nodeName") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.details.podIp") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.details.podName") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) - .extractingJsonPathStringValue("$.components.kubernetes.details.serviceAccount") - .isNotEmpty(); - } - - private void assertInfo(String infoResult) { - Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) - .extractingJsonPathStringValue("$.kubernetes.hostIp") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) - .extractingJsonPathBooleanValue("$.kubernetes.inside") - .isEqualTo(true); - - Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) - .extractingJsonPathStringValue("$.kubernetes.namespace") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) - .extractingJsonPathStringValue("$.kubernetes.nodeName") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) - .extractingJsonPathStringValue("$.kubernetes.podIp") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) - .extractingJsonPathStringValue("$.kubernetes.podName") - .isNotEmpty(); - - Assertions.assertThat(BASIC_JSON_TESTER.from(infoResult)) - .extractingJsonPathStringValue("$.kubernetes.serviceAccount") - .isNotEmpty(); - } - - 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); - } - - private void assertLogStatement(K3sContainer container, String message) { - try { - String appPodName = container - .execInContainer("sh", "-c", - "kubectl get pods -l app=" + DEPLOYMENT_NAME + " -o=name --no-headers | tr -d '\n'") - .getStdout(); - - await().pollDelay(Duration.ofSeconds(4)) - .pollInterval(Duration.ofSeconds(1)) - .atMost(20, TimeUnit.SECONDS) - .until(() -> { - Container.ExecResult execResult = container.execInContainer("sh", "-c", - "kubectl logs " + appPodName.trim()); - String ok = execResult.getStdout(); - return ok.contains(message); - }); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java deleted file mode 100644 index 2fcf0b3a1b..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate.java +++ /dev/null @@ -1,237 +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.Comparator; -import java.util.List; -import java.util.Objects; - -import org.junit.jupiter.api.Assertions; -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.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author wind57 - */ -class KubernetesClientDiscoveryMultipleSelectiveNamespacesITDelegate { - - 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 IMAGE_NAME = "spring-cloud-kubernetes-k8s-client-discovery"; - - /** - * Deploy wiremock in 3 namespaces: default, a, b. Search in selective namespaces 'a' - * and 'b' with blocking enabled and reactive disabled, as such find services and it's - * instances. - */ - void testTwoNamespacesBlockingOnly(K3sContainer container) { - - Commons.waitForLogStatement("using selective namespaces : [a, b]", container, IMAGE_NAME); - Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a, b]", - container, IMAGE_NAME); - Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a, b]", - container, IMAGE_NAME); - Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a, b]", - container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for services) in namespace : a", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for services) in namespace : b", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for endpoints) in namespace : a", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for endpoints) in namespace : b", container, IMAGE_NAME); - - // this tiny checks makes sure that blocking is enabled and reactive is disabled. - Commons.waitForLogStatement(BLOCKING_PUBLISH, container, IMAGE_NAME); - Assertions.assertFalse(logs(container).contains(REACTIVE_PUBLISH)); - - blockingCheck(); - - } - - /** - * Deploy wiremock in 3 namespaces: default, a, b. Search in selective namespaces 'a' - * and 'b' with blocking disabled and reactive enabled, as such find services and it's - * instances. - */ - void testTwoNamespaceReactiveOnly(K3sContainer container) { - - Commons.waitForLogStatement("using selective namespaces : [a, b]", container, IMAGE_NAME); - Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a, b]", - container, IMAGE_NAME); - Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a, b]", - container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for services) in namespace : a", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for services) in namespace : b", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for endpoints) in namespace : a", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for endpoints) in namespace : b", container, IMAGE_NAME); - - // this tiny checks makes sure that blocking is disabled and reactive is enabled. - Commons.waitForLogStatement(REACTIVE_PUBLISH, container, IMAGE_NAME); - Assertions.assertFalse(logs(container).contains(BLOCKING_PUBLISH)); - - reactiveCheck(); - - } - - /** - * Deploy wiremock in 3 namespaces: default, a, b. Search in selective namespaces 'a' - * and 'b' with blocking enabled and reactive enabled, as such find services and its - * service instances. - */ - void testTwoNamespacesBothBlockingAndReactive(K3sContainer container) { - - Commons.waitForLogStatement("using selective namespaces : [a, b]", container, IMAGE_NAME); - Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a, b]", - container, IMAGE_NAME); - Commons.waitForLogStatement("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a, b]", - container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for services) in namespace : a", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for services) in namespace : b", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for endpoints) in namespace : a", container, IMAGE_NAME); - Commons.waitForLogStatement("registering lister (for endpoints) in namespace : b", container, IMAGE_NAME); - - // this tiny checks makes sure that blocking is enabled and reactive is enabled. - Commons.waitForLogStatement(BLOCKING_PUBLISH, container, IMAGE_NAME); - Assertions.assertTrue(logs(container).contains(REACTIVE_PUBLISH)); - - blockingCheck(); - reactiveCheck(); - - } - - private String logs(K3sContainer container) { - try { - String appPodName = container - .execInContainer("sh", "-c", - "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'") - .getStdout(); - - Container.ExecResult execResult = container.execInContainer("sh", "-c", - "kubectl logs " + appPodName.trim()); - return execResult.getStdout(); - } - catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - 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(); - - // we get two here, but since there is 'distinct' call, only 1 will be reported - // but service instances will report 2 nevertheless - 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(), 2); - ourServiceInstances = ourServiceInstances.stream() - .sorted(Comparator.comparing(DefaultKubernetesServiceInstance::namespace)) - .toList(); - - DefaultKubernetesServiceInstance serviceInstanceA = ourServiceInstances.get(0); - // we only care about namespace here, as all other fields are tested in various - // other tests. - Assertions.assertEquals(serviceInstanceA.getNamespace(), "a"); - - DefaultKubernetesServiceInstance serviceInstanceB = ourServiceInstances.get(1); - // we only care about namespace here, as all other fields are tested in various - // other tests. - Assertions.assertEquals(serviceInstanceB.getNamespace(), "b"); - } - - private void blockingCheck() { - WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); - - List servicesResult = servicesClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .retryWhen(retrySpec()) - .block(); - - // we get two here, but since there is 'distinct' call, only 1 will be reported - // but service instances will report 2 nevertheless - 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(), 2); - ourServiceInstances = ourServiceInstances.stream() - .sorted(Comparator.comparing(DefaultKubernetesServiceInstance::namespace)) - .toList(); - - DefaultKubernetesServiceInstance serviceInstanceA = ourServiceInstances.get(0); - // we only care about namespace here, as all other fields are tested in various - // other tests. - Assertions.assertEquals(serviceInstanceA.getNamespace(), "a"); - - DefaultKubernetesServiceInstance serviceInstanceB = ourServiceInstances.get(1); - // we only care about namespace here, as all other fields are tested in various - // other tests. - Assertions.assertEquals(serviceInstanceB.getNamespace(), "b"); - } - - 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/KubernetesClientDiscoveryPodMetadataITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java deleted file mode 100644 index 940e3fc75e..0000000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java +++ /dev/null @@ -1,95 +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.List; -import java.util.Map; -import java.util.Objects; - -import org.junit.jupiter.api.Assertions; -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.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author wind57 - */ -class KubernetesClientDiscoveryPodMetadataITDelegate { - - /** - * Three services are deployed in the default namespace. We do not configure any - * explicit namespace and 'default' must be picked-up. - */ - void testSimple() { - - 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(), 3); - Assertions.assertTrue(servicesResult.contains("kubernetes")); - Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); - Assertions.assertTrue(servicesResult.contains("external-name-service")); - - WebClient ourServiceClient = builder() - .baseUrl("http://localhost//service-instances/spring-cloud-kubernetes-k8s-client-discovery") - .build(); - - List ourServiceInstances = ourServiceClient.method(HttpMethod.GET) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - - }) - .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