From 0acf32f7c78aa810d23a61c42fa01e53f4c9a9ce Mon Sep 17 00:00:00 2001 From: erabii Date: Wed, 3 Apr 2024 20:01:24 +0300 Subject: [PATCH] Cleanup k8s client loadbalancer part 2 (#1613) * test * fix @Nested tests not running * trigger again * Add renovate.json * Delete renovate.json * Delete delme.sh * dirty * first * checkstyle * dirty * dirty * add tests * fix test * fix tests * checkstyle --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../pom.xml | 18 ++ .../client/loadbalancer/it/Util.java | 160 +++++++++++++ .../it/mode/pod/AllNamespacesTest.java | 176 ++++++++++++++ .../it/mode/pod/SelectiveNamespacesTest.java | 220 ++++++++++++++++++ .../it/mode/pod/SpecificNamespaceTest.java | 183 +++++++++++++++ .../it/mode/service/AllNamespacesTest.java | 187 +++++++++++++++ .../mode/service/SelectiveNamespacesTest.java | 219 +++++++++++++++++ .../mode/service/SpecificNamespaceTest.java | 184 +++++++++++++++ .../src/test/resources/logback-test.xml | 2 + .../pom.xml | 3 - ...bernetesClientLoadBalancerApplication.java | 73 ------ ...rnetesClientLoadBalancerConfiguration.java | 36 --- .../src/main/resources/application.yaml | 1 - .../client/loadbalancer/LoadBalancerIT.java | 177 -------------- .../src/test/resources/logback-test.xml | 14 -- ...es-k8s-client-loadbalancer-deployment.yaml | 31 --- ...netes-k8s-client-loadbalancer-ingress.yaml | 19 -- ...netes-k8s-client-loadbalancer-service.yaml | 14 -- 18 files changed, 1349 insertions(+), 368 deletions(-) create mode 100644 spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/Util.java create mode 100644 spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/AllNamespacesTest.java create mode 100644 spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SelectiveNamespacesTest.java create mode 100644 spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SpecificNamespaceTest.java create mode 100644 spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/AllNamespacesTest.java create mode 100644 spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SelectiveNamespacesTest.java create mode 100644 spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SpecificNamespaceTest.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerApplication.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerConfiguration.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/resources/application.yaml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/LoadBalancerIT.java delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/logback-test.xml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml delete mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml diff --git a/spring-cloud-kubernetes-client-loadbalancer/pom.xml b/spring-cloud-kubernetes-client-loadbalancer/pom.xml index d2c9517e..685cef2d 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/pom.xml +++ b/spring-cloud-kubernetes-client-loadbalancer/pom.xml @@ -35,7 +35,20 @@ org.springframework.boot spring-boot-starter-test test + + + org.mokito + mockito-core + + + + + org.mockito + mockito-inline + test + + org.wiremock wiremock-standalone @@ -51,6 +64,11 @@ spring-boot-starter-web test + + org.springframework.boot + spring-boot-starter-webflux + test + org.springframework.cloud spring-cloud-kubernetes-test-support diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/Util.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/Util.java new file mode 100644 index 00000000..29825e16 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/Util.java @@ -0,0 +1,160 @@ +/* + * Copyright 2013-2024 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.client.loadbalancer.it; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author wind57 + */ +public final class Util { + + private Util() { + + } + + public static V1Service service(String namespace, String name, int port) { + return new V1ServiceBuilder().withNewMetadata().withNamespace(namespace).withName(name).endMetadata() + .withSpec(new V1ServiceSpecBuilder() + .withPorts(new V1ServicePortBuilder().withName("http").withPort(port).build()).build()) + .build(); + } + + public static V1Endpoints endpoints(String namespace, String name, int port, String host) { + return new V1EndpointsBuilder() + .withSubsets( + new V1EndpointSubsetBuilder().withPorts(new CoreV1EndpointPortBuilder().withPort(port).build()) + .withAddresses(new V1EndpointAddressBuilder().withIp(host).build()).build()) + .withMetadata(new V1ObjectMetaBuilder().withName(name).withNamespace(namespace).build()).build(); + } + + public static void servicesPodMode(WireMockServer server, V1ServiceList serviceList) { + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/services*")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200))); + } + + public static void servicesServiceMode(WireMockServer server, V1ServiceList serviceList, String serviceName) { + // resourceVersion=0 is passed only from the watcher, so this mocks the + // 'postConstruct' in the KubernetesInformerDiscoveryClient + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/services*")) + .withQueryParam("resourceVersion", WireMock.equalTo("0")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200))); + + server.stubFor( + WireMock.get(WireMock.urlEqualTo("/api/v1/services?fieldSelector=metadata.name%3D" + serviceName)) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200))); + } + + public static void endpointsPodMode(WireMockServer server, V1EndpointsList endpointsList) { + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/endpoints*")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200))); + } + + public static void endpointsServiceMode(WireMockServer server, V1EndpointsList endpointsList, + String endpointsName) { + // resourceVersion=0 is passed only from the watcher, so this mocks the + // 'postConstruct' in the KubernetesInformerDiscoveryClient + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/endpoints*")) + .withQueryParam("resourceVersion", WireMock.equalTo("0")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200))); + + server.stubFor(WireMock + .get(WireMock.urlEqualTo("/api/v1/endpoints?fieldSelector=metadata.name%3D" + endpointsName)) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200))); + } + + public static void servicesInNamespacePodMode(WireMockServer server, V1ServiceList serviceList, String namespace) { + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/services*")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200))); + } + + public static void servicesInNamespaceServiceMode(WireMockServer server, V1ServiceList serviceList, + String namespace, String serviceName) { + // resourceVersion=0 is passed only from the watcher, so this mocks the + // 'postConstruct' in the KubernetesInformerDiscoveryClient + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/services*")) + .withQueryParam("resourceVersion", WireMock.equalTo("0")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200))); + + server.stubFor(WireMock + .get(WireMock.urlEqualTo( + "/api/v1/namespaces/" + namespace + "/services?fieldSelector=metadata.name%3D" + serviceName)) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(serviceList)).withStatus(200))); + } + + public static void endpointsInNamespacePodMode(WireMockServer server, V1EndpointsList endpointsList, + String namespace) { + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/endpoints*")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200))); + } + + public static void endpointsInNamespaceServiceMode(WireMockServer server, V1EndpointsList endpointsList, + String namespace, String endpointsName) { + // resourceVersion=0 is passed only from the watcher, so this mocks the + // 'postConstruct' in the KubernetesInformerDiscoveryClient + server.stubFor(WireMock.get(WireMock.urlPathMatching("^/api/v1/namespaces/" + namespace + "/endpoints*")) + .withQueryParam("resourceVersion", WireMock.equalTo("0")) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200))); + + server.stubFor(WireMock + .get(WireMock.urlEqualTo("/api/v1/namespaces/" + namespace + "/endpoints?fieldSelector=metadata.name%3D" + + endpointsName)) + .willReturn(WireMock.aResponse().withBody(new JSON().serialize(endpointsList)).withStatus(200))); + } + + @TestConfiguration + public static class LoadBalancerConfiguration { + + @Bean + @LoadBalanced + WebClient.Builder client() { + return WebClient.builder(); + } + + } + + @SpringBootApplication + public static class Configuration { + + public static void main(String[] args) { + SpringApplication.run(Configuration.class); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/AllNamespacesTest.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/AllNamespacesTest.java new file mode 100644 index 00000000..e8738f1d --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/AllNamespacesTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2013-2024 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.client.loadbalancer.it.mode.pod; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.loadbalancer.it.Util; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.LoadBalancerConfiguration; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.cloud.kubernetes.loadbalancer.mode=POD", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.discovery.all-namespaces=true" }, + classes = { LoadBalancerConfiguration.class, Configuration.class }) +class AllNamespacesTest { + + private static final String SERVICE_A_URL = "http://service-a"; + + private static final String SERVICE_B_URL = "http://service-b"; + + private static final int SERVICE_A_PORT = 8888; + + private static final int SERVICE_B_PORT = 8889; + + private static WireMockServer wireMockServer; + + private static WireMockServer serviceAMockServer; + + private static WireMockServer serviceBMockServer; + + private static MockedStatic clientUtils; + + private static final MockedStatic MOCKED_STATIC = Mockito + .mockStatic(KubernetesServiceInstanceMapper.class); + + @Autowired + private WebClient.Builder builder; + + @Autowired + private ObjectProvider loadBalancerClientFactory; + + @BeforeAll + static void beforeAll() { + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + mockWatchers(); + + serviceAMockServer = new WireMockServer(SERVICE_A_PORT); + serviceAMockServer.start(); + WireMock.configureFor("localhost", SERVICE_A_PORT); + + serviceBMockServer = new WireMockServer(SERVICE_B_PORT); + serviceBMockServer.start(); + WireMock.configureFor("localhost", SERVICE_B_PORT); + + // we mock host creation so that it becomes something like : localhost:8888 + // then wiremock can catch this request, and we can assert for the result + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-a", "a", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-b", "b", "cluster.local")) + .thenReturn("localhost"); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + clientUtils = mockStatic(KubernetesClientUtils.class); + clientUtils.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(client); + + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.close(); + clientUtils.close(); + } + + /** + *
+	 *      - service-a is present in namespace a with exposed port 8888
+	 *      - service-b is present in namespace b with exposed port 8889
+	 *      - we make two calls to them via the load balancer
+	 * 
+ */ + @Test + void test() { + + serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + + serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + + String serviceAResult = builder.baseUrl(SERVICE_A_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + Assertions.assertThat(serviceAResult).isEqualTo("service-a-reached"); + + String serviceBResult = builder.baseUrl(SERVICE_B_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + Assertions.assertThat(serviceBResult).isEqualTo("service-b-reached"); + + CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + .getIfAvailable().getProvider("service-a", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()) + .isSameAs(DiscoveryClientServiceInstanceListSupplier.class); + + } + + private static void mockWatchers() { + V1Service serviceA = Util.service("a", "service-a", SERVICE_A_PORT); + V1Service serviceB = Util.service("b", "service-b", SERVICE_B_PORT); + V1ServiceList serviceList = new V1ServiceListBuilder().withKind("V1ServiceList") + .withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build()) + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceA, serviceB).build(); + Util.servicesPodMode(wireMockServer, serviceList); + + V1Endpoints endpointsA = Util.endpoints("a", "service-a", SERVICE_A_PORT, "127.0.0.1"); + V1Endpoints endpointsB = Util.endpoints("b", "service-b", SERVICE_B_PORT, "127.0.0.1"); + V1EndpointsList endpointsList = new V1EndpointsListBuilder().withKind("V1EndpointsList") + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsA, endpointsB).build(); + Util.endpointsPodMode(wireMockServer, endpointsList); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SelectiveNamespacesTest.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SelectiveNamespacesTest.java new file mode 100644 index 00000000..563fff08 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SelectiveNamespacesTest.java @@ -0,0 +1,220 @@ +/* + * Copyright 2013-2024 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.client.loadbalancer.it.mode.pod; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.loadbalancer.it.Util; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.LoadBalancerConfiguration; + +/** + * @author wind57 + */ +@SpringBootTest(properties = { "spring.cloud.kubernetes.loadbalancer.mode=POD", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.discovery.all-namespaces=false", "spring.cloud.kubernetes.discovery.namespaces.[0]=a", + "spring.cloud.kubernetes.discovery.namespaces.[1]=b" }, + classes = { LoadBalancerConfiguration.class, Configuration.class }) +class SelectiveNamespacesTest { + + private static final String MY_SERVICE_URL = "http://my-service"; + + private static final int SERVICE_A_PORT = 8887; + + private static final int SERVICE_B_PORT = 8888; + + private static final int SERVICE_C_PORT = 8889; + + private static WireMockServer wireMockServer; + + private static WireMockServer serviceAMockServer; + + private static WireMockServer serviceBMockServer; + + private static WireMockServer serviceCMockServer; + + private static final MockedStatic MOCKED_STATIC = Mockito + .mockStatic(KubernetesServiceInstanceMapper.class); + + private static MockedStatic clientUtils; + + @Autowired + private WebClient.Builder builder; + + @Autowired + private ObjectProvider loadBalancerClientFactory; + + @BeforeAll + static void beforeAll() { + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + mockWatchers(); + + serviceAMockServer = new WireMockServer(SERVICE_A_PORT); + serviceAMockServer.start(); + WireMock.configureFor("localhost", SERVICE_A_PORT); + + serviceBMockServer = new WireMockServer(SERVICE_B_PORT); + serviceBMockServer.start(); + WireMock.configureFor("localhost", SERVICE_B_PORT); + + serviceCMockServer = new WireMockServer(SERVICE_C_PORT); + serviceCMockServer.start(); + WireMock.configureFor("localhost", SERVICE_C_PORT); + + // we mock host creation so that it becomes something like : localhost:8888 + // then wiremock can catch this request, and we can assert for the result + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "a", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "b", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "c", "cluster.local")) + .thenReturn("localhost"); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + clientUtils = mockStatic(KubernetesClientUtils.class); + clientUtils.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(client); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + serviceCMockServer.stop(); + MOCKED_STATIC.close(); + clientUtils.close(); + } + + /** + *
+	 *      - my-service is present in 'a' namespace
+	 *      - my-service is present in 'b' namespace
+	 *      - my-service is present in 'c' namespace
+	 *      - we enable search in selective namespaces [a, b]
+	 *      - load balancer mode is 'POD'
+	 *
+	 *      - as such, only service in namespace a and b are load balanced
+	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the POD mode.
+	 * 
+ */ + @Test + void test() { + + serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + + serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + + serviceCMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-c-reached").withStatus(200))); + + String firstCallResult = builder.baseUrl(MY_SERVICE_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + + String secondCallResult = builder.baseUrl(MY_SERVICE_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + + // since selective namespaces is a Set, we need to be careful with assertion order + if (firstCallResult.equals("service-a-reached")) { + Assertions.assertThat(secondCallResult).isEqualTo("service-b-reached"); + } + else { + Assertions.assertThat(firstCallResult).isEqualTo("service-b-reached"); + Assertions.assertThat(secondCallResult).isEqualTo("service-a-reached"); + } + + CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + .getIfAvailable().getProvider("my-service", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()) + .isSameAs(DiscoveryClientServiceInstanceListSupplier.class); + } + + private static void mockWatchers() { + V1Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + V1Service serviceB = Util.service("b", "my-service", SERVICE_B_PORT); + V1Service serviceC = Util.service("c", "my-service", SERVICE_C_PORT); + + V1ServiceList serviceListA = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceA).build(); + V1ServiceList serviceListB = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceB).build(); + V1ServiceList serviceListC = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceC).build(); + + Util.servicesInNamespacePodMode(wireMockServer, serviceListA, "a"); + Util.servicesInNamespacePodMode(wireMockServer, serviceListB, "b"); + Util.servicesInNamespacePodMode(wireMockServer, serviceListC, "c"); + + V1Endpoints endpointsA = Util.endpoints("a", "my-service", SERVICE_A_PORT, "127.0.0.1"); + V1Endpoints endpointsB = Util.endpoints("b", "my-service", SERVICE_B_PORT, "127.0.0.1"); + V1Endpoints endpointsC = Util.endpoints("c", "my-service", SERVICE_C_PORT, "127.0.0.1"); + + V1EndpointsList endpointsListA = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsA).build(); + V1EndpointsList endpointsListB = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsB).build(); + V1EndpointsList endpointsListC = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsC).build(); + + Util.endpointsInNamespacePodMode(wireMockServer, endpointsListA, "a"); + Util.endpointsInNamespacePodMode(wireMockServer, endpointsListB, "b"); + Util.endpointsInNamespacePodMode(wireMockServer, endpointsListC, "c"); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SpecificNamespaceTest.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SpecificNamespaceTest.java new file mode 100644 index 00000000..c24400c6 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/pod/SpecificNamespaceTest.java @@ -0,0 +1,183 @@ +/* + * Copyright 2013-2024 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.client.loadbalancer.it.mode.pod; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.loadbalancer.it.Util; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.LoadBalancerConfiguration; + +/** + * @author wind57 + */ +@SpringBootTest(properties = { "spring.cloud.kubernetes.loadbalancer.mode=POD", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.discovery.all-namespaces=false", "spring.cloud.kubernetes.client.namespace=a" }, + classes = { LoadBalancerConfiguration.class, Configuration.class }) +class SpecificNamespaceTest { + + private static final String SERVICE_A_URL = "http://my-service"; + + private static final int SERVICE_A_PORT = 8888; + + private static final int SERVICE_B_PORT = 8889; + + private static WireMockServer wireMockServer; + + private static WireMockServer serviceAMockServer; + + private static WireMockServer serviceBMockServer; + + private static final MockedStatic MOCKED_STATIC = Mockito + .mockStatic(KubernetesServiceInstanceMapper.class); + + private static MockedStatic clientUtils; + + @Autowired + private WebClient.Builder builder; + + @Autowired + private ObjectProvider loadBalancerClientFactory; + + @BeforeAll + static void beforeAll() { + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + mockWatchers(); + + serviceAMockServer = new WireMockServer(SERVICE_A_PORT); + serviceAMockServer.start(); + WireMock.configureFor("localhost", SERVICE_A_PORT); + + serviceBMockServer = new WireMockServer(SERVICE_B_PORT); + serviceBMockServer.start(); + WireMock.configureFor("localhost", SERVICE_B_PORT); + + // we mock host creation so that it becomes something like : localhost:8888 + // then wiremock can catch this request, and we can assert for the result + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "a", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "b", "cluster.local")) + .thenReturn("localhost"); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + // we need to not mock 'getApplicationNamespace' + clientUtils = mockStatic(KubernetesClientUtils.class, Mockito.CALLS_REAL_METHODS); + clientUtils.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(client); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.close(); + clientUtils.close(); + } + + /** + *
+	 *      - my-service is present in 'a' namespace
+	 *      - my-service is present in 'b' namespace
+	 *      - we enable search in namespace 'a'
+	 *      - load balancer mode is 'POD'
+	 *
+	 *      - as such, only my-service in namespace a is load balanced
+	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the POD mode.
+	 * 
+ */ + @Test + void test() { + + serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + + serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + + String serviceAResult = builder.baseUrl(SERVICE_A_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + Assertions.assertThat(serviceAResult).isEqualTo("service-a-reached"); + + CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + .getIfAvailable().getProvider("my-service", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()) + .isSameAs(DiscoveryClientServiceInstanceListSupplier.class); + } + + private static void mockWatchers() { + V1Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + V1Service serviceB = Util.service("b", "my-service", SERVICE_B_PORT); + + V1ServiceList serviceListA = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceA).build(); + V1ServiceList serviceListB = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceB).build(); + + Util.servicesInNamespacePodMode(wireMockServer, serviceListA, "a"); + Util.servicesInNamespacePodMode(wireMockServer, serviceListB, "b"); + + V1Endpoints endpointsA = Util.endpoints("a", "my-service", SERVICE_A_PORT, "127.0.0.1"); + V1Endpoints endpointsB = Util.endpoints("b", "my-service", SERVICE_B_PORT, "127.0.0.1"); + + V1EndpointsList endpointsListA = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsA).build(); + V1EndpointsList endpointsListB = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsB).build(); + + Util.endpointsInNamespacePodMode(wireMockServer, endpointsListA, "a"); + Util.endpointsInNamespacePodMode(wireMockServer, endpointsListB, "b"); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/AllNamespacesTest.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/AllNamespacesTest.java new file mode 100644 index 00000000..27ec8b26 --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/AllNamespacesTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2013-2024 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.client.loadbalancer.it.mode.service; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.ConsoleNotifier; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.loadbalancer.KubernetesClientServicesListSupplier; +import org.springframework.cloud.kubernetes.client.loadbalancer.it.Util; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.LoadBalancerConfiguration; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.cloud.kubernetes.loadbalancer.mode=SERVICE", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.discovery.all-namespaces=true" }, + classes = { LoadBalancerConfiguration.class, Configuration.class }) +class AllNamespacesTest { + + private static final String SERVICE_A_URL = "http://service-a"; + + private static final String SERVICE_B_URL = "http://service-b"; + + private static final int SERVICE_A_PORT = 8888; + + private static final int SERVICE_B_PORT = 8889; + + private static WireMockServer wireMockServer; + + private static WireMockServer serviceAMockServer; + + private static WireMockServer serviceBMockServer; + + private static MockedStatic clientUtils; + + private static final MockedStatic MOCKED_STATIC = Mockito + .mockStatic(KubernetesServiceInstanceMapper.class); + + @Autowired + private WebClient.Builder builder; + + @Autowired + private ObjectProvider loadBalancerClientFactory; + + @BeforeAll + static void beforeAll() { + + wireMockServer = new WireMockServer(options().dynamicPort().notifier(new ConsoleNotifier(true))); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + mockWatchers(); + + serviceAMockServer = new WireMockServer(SERVICE_A_PORT); + serviceAMockServer.start(); + WireMock.configureFor("localhost", SERVICE_A_PORT); + + serviceBMockServer = new WireMockServer(SERVICE_B_PORT); + serviceBMockServer.start(); + WireMock.configureFor("localhost", SERVICE_B_PORT); + + // we mock host creation so that it becomes something like : localhost:8888 + // then wiremock can catch this request, and we can assert for the result + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-a", "a", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-b", "b", "cluster.local")) + .thenReturn("localhost"); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + clientUtils = mockStatic(KubernetesClientUtils.class); + clientUtils.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(client); + + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.close(); + clientUtils.close(); + } + + /** + *
+	 *      - service-a is present in namespace a with exposed port 8888
+	 *      - service-b is present in namespace b with exposed port 8889
+	 *      - we make two calls to them via the load balancer
+	 * 
+ */ + @Test + void test() { + + serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + + serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + + String serviceAResult = builder.baseUrl(SERVICE_A_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + Assertions.assertThat(serviceAResult).isEqualTo("service-a-reached"); + + String serviceBResult = builder.baseUrl(SERVICE_B_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + Assertions.assertThat(serviceBResult).isEqualTo("service-b-reached"); + + CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + .getIfAvailable().getProvider("service-a", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()).isSameAs(KubernetesClientServicesListSupplier.class); + + } + + private static void mockWatchers() { + V1Service serviceA = Util.service("a", "service-a", SERVICE_A_PORT); + V1ServiceList serviceListA = new V1ServiceListBuilder().withKind("V1ServiceList") + .withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build()) + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceA).build(); + Util.servicesServiceMode(wireMockServer, serviceListA, "service-a"); + + V1Service serviceB = Util.service("b", "service-b", SERVICE_B_PORT); + V1ServiceList serviceListB = new V1ServiceListBuilder().withKind("V1ServiceList") + .withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build()) + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceB).build(); + Util.servicesServiceMode(wireMockServer, serviceListB, "service-b"); + + V1Endpoints endpointsA = Util.endpoints("a", "service-a", SERVICE_A_PORT, "127.0.0.1"); + V1EndpointsList endpointsListA = new V1EndpointsListBuilder().withKind("V1EndpointsList") + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsA).build(); + Util.endpointsServiceMode(wireMockServer, endpointsListA, "service-a"); + + V1Endpoints endpointsB = Util.endpoints("b", "service-b", SERVICE_B_PORT, "127.0.0.1"); + V1EndpointsList endpointsListB = new V1EndpointsListBuilder().withKind("V1EndpointsList") + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsB).build(); + Util.endpointsServiceMode(wireMockServer, endpointsListB, "service-b"); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SelectiveNamespacesTest.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SelectiveNamespacesTest.java new file mode 100644 index 00000000..cc23253b --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SelectiveNamespacesTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2013-2024 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.client.loadbalancer.it.mode.service; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.loadbalancer.KubernetesClientServicesListSupplier; +import org.springframework.cloud.kubernetes.client.loadbalancer.it.Util; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.LoadBalancerConfiguration; + +/** + * @author wind57 + */ +@SpringBootTest(properties = { "spring.cloud.kubernetes.loadbalancer.mode=SERVICE", + "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.discovery.all-namespaces=false", + "spring.cloud.kubernetes.discovery.namespaces.[0]=a", "spring.cloud.kubernetes.discovery.namespaces.[1]=b" }, + classes = { LoadBalancerConfiguration.class, Configuration.class }) +class SelectiveNamespacesTest { + + private static final String MY_SERVICE_URL = "http://my-service"; + + private static final int SERVICE_A_PORT = 8887; + + private static final int SERVICE_B_PORT = 8888; + + private static final int SERVICE_C_PORT = 8889; + + private static WireMockServer wireMockServer; + + private static WireMockServer serviceAMockServer; + + private static WireMockServer serviceBMockServer; + + private static WireMockServer serviceCMockServer; + + private static final MockedStatic MOCKED_STATIC = Mockito + .mockStatic(KubernetesServiceInstanceMapper.class); + + private static MockedStatic clientUtils; + + @Autowired + private WebClient.Builder builder; + + @Autowired + private ObjectProvider loadBalancerClientFactory; + + @BeforeAll + static void beforeAll() { + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + mockWatchers(); + + serviceAMockServer = new WireMockServer(SERVICE_A_PORT); + serviceAMockServer.start(); + WireMock.configureFor("localhost", SERVICE_A_PORT); + + serviceBMockServer = new WireMockServer(SERVICE_B_PORT); + serviceBMockServer.start(); + WireMock.configureFor("localhost", SERVICE_B_PORT); + + serviceCMockServer = new WireMockServer(SERVICE_C_PORT); + serviceCMockServer.start(); + WireMock.configureFor("localhost", SERVICE_C_PORT); + + // we mock host creation so that it becomes something like : localhost:8888 + // then wiremock can catch this request, and we can assert for the result + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "a", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "b", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "c", "cluster.local")) + .thenReturn("localhost"); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + clientUtils = mockStatic(KubernetesClientUtils.class); + clientUtils.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(client); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + serviceCMockServer.stop(); + MOCKED_STATIC.close(); + clientUtils.close(); + } + + /** + *
+	 *      - my-service is present in 'a' namespace
+	 *      - my-service is present in 'b' namespace
+	 *      - my-service is present in 'c' namespace
+	 *      - we enable search in selective namespaces [a, b]
+	 *      - load balancer mode is 'POD'
+	 *
+	 *      - as such, only service in namespace a and b are load balanced
+	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the POD mode.
+	 * 
+ */ + @Test + void test() { + + serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + + serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + + serviceCMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-c-reached").withStatus(200))); + + String firstCallResult = builder.baseUrl(MY_SERVICE_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + + String secondCallResult = builder.baseUrl(MY_SERVICE_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + + // since selective namespaces is a Set, we need to be careful with assertion order + if (firstCallResult.equals("service-a-reached")) { + Assertions.assertThat(secondCallResult).isEqualTo("service-b-reached"); + } + else { + Assertions.assertThat(firstCallResult).isEqualTo("service-b-reached"); + Assertions.assertThat(secondCallResult).isEqualTo("service-a-reached"); + } + + CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + .getIfAvailable().getProvider("my-service", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()).isSameAs(KubernetesClientServicesListSupplier.class); + } + + private static void mockWatchers() { + V1Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + V1Service serviceB = Util.service("b", "my-service", SERVICE_B_PORT); + V1Service serviceC = Util.service("c", "my-service", SERVICE_C_PORT); + + V1ServiceList serviceListA = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceA).build(); + V1ServiceList serviceListB = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceB).build(); + V1ServiceList serviceListC = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceC).build(); + + Util.servicesInNamespaceServiceMode(wireMockServer, serviceListA, "a", "my-service"); + Util.servicesInNamespaceServiceMode(wireMockServer, serviceListB, "b", "my-service"); + Util.servicesInNamespaceServiceMode(wireMockServer, serviceListC, "c", "my-service"); + + V1Endpoints endpointsA = Util.endpoints("a", "my-service", SERVICE_A_PORT, "127.0.0.1"); + V1Endpoints endpointsB = Util.endpoints("b", "my-service", SERVICE_B_PORT, "127.0.0.1"); + V1Endpoints endpointsC = Util.endpoints("c", "my-service", SERVICE_C_PORT, "127.0.0.1"); + + V1EndpointsList endpointsListA = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsA).build(); + V1EndpointsList endpointsListB = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsB).build(); + V1EndpointsList endpointsListC = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsC).build(); + + Util.endpointsInNamespaceServiceMode(wireMockServer, endpointsListA, "a", "my-service"); + Util.endpointsInNamespaceServiceMode(wireMockServer, endpointsListB, "b", "my-service"); + Util.endpointsInNamespaceServiceMode(wireMockServer, endpointsListC, "c", "my-service"); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SpecificNamespaceTest.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SpecificNamespaceTest.java new file mode 100644 index 00000000..1d0675bf --- /dev/null +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/it/mode/service/SpecificNamespaceTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2013-2024 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.client.loadbalancer.it.mode.service; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.client.KubernetesClientUtils; +import org.springframework.cloud.kubernetes.client.loadbalancer.KubernetesClientServicesListSupplier; +import org.springframework.cloud.kubernetes.client.loadbalancer.it.Util; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.client.WebClient; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.client.loadbalancer.it.Util.LoadBalancerConfiguration; + +/** + * @author wind57 + */ +@SpringBootTest( + properties = { "spring.cloud.kubernetes.loadbalancer.mode=SERVICE", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.kubernetes.discovery.all-namespaces=false", + "spring.cloud.kubernetes.client.namespace=a" }, + classes = { LoadBalancerConfiguration.class, Configuration.class }) +class SpecificNamespaceTest { + + private static final String SERVICE_A_URL = "http://my-service"; + + private static final int SERVICE_A_PORT = 8888; + + private static final int SERVICE_B_PORT = 8889; + + private static WireMockServer wireMockServer; + + private static WireMockServer serviceAMockServer; + + private static WireMockServer serviceBMockServer; + + private static final MockedStatic MOCKED_STATIC = Mockito + .mockStatic(KubernetesServiceInstanceMapper.class); + + private static MockedStatic clientUtils; + + @Autowired + private WebClient.Builder builder; + + @Autowired + private ObjectProvider loadBalancerClientFactory; + + @BeforeAll + static void beforeAll() { + + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + mockWatchers(); + + serviceAMockServer = new WireMockServer(SERVICE_A_PORT); + serviceAMockServer.start(); + WireMock.configureFor("localhost", SERVICE_A_PORT); + + serviceBMockServer = new WireMockServer(SERVICE_B_PORT); + serviceBMockServer.start(); + WireMock.configureFor("localhost", SERVICE_B_PORT); + + // we mock host creation so that it becomes something like : localhost:8888 + // then wiremock can catch this request, and we can assert for the result + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "a", "cluster.local")) + .thenReturn("localhost"); + + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "b", "cluster.local")) + .thenReturn("localhost"); + + ApiClient client = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + // we need to not mock 'getApplicationNamespace' + clientUtils = mockStatic(KubernetesClientUtils.class, Mockito.CALLS_REAL_METHODS); + clientUtils.when(KubernetesClientUtils::kubernetesApiClient).thenReturn(client); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.close(); + clientUtils.close(); + } + + /** + *
+	 *      - my-service is present in 'a' namespace
+	 *      - my-service is present in 'b' namespace
+	 *      - we enable search in namespace 'a'
+	 *      - load balancer mode is 'POD'
+	 *
+	 *      - as such, only my-service in namespace a is load balanced
+	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the POD mode.
+	 * 
+ */ + @Test + void test() { + + serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + + serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + + String serviceAResult = builder.baseUrl(SERVICE_A_URL).build().method(HttpMethod.GET).retrieve() + .bodyToMono(String.class).block(); + Assertions.assertThat(serviceAResult).isEqualTo("service-a-reached"); + + CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + .getIfAvailable().getProvider("my-service", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()).isSameAs(KubernetesClientServicesListSupplier.class); + } + + private static void mockWatchers() { + V1Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + V1Service serviceB = Util.service("b", "my-service", SERVICE_B_PORT); + + V1ServiceList serviceListA = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceA).build(); + V1ServiceList serviceListB = new V1ServiceListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(serviceB).build(); + + Util.servicesInNamespaceServiceMode(wireMockServer, serviceListA, "a", "my-service"); + Util.servicesInNamespaceServiceMode(wireMockServer, serviceListB, "b", "my-service"); + + V1Endpoints endpointsA = Util.endpoints("a", "my-service", SERVICE_A_PORT, "127.0.0.1"); + V1Endpoints endpointsB = Util.endpoints("b", "my-service", SERVICE_B_PORT, "127.0.0.1"); + + V1EndpointsList endpointsListA = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsA).build(); + V1EndpointsList endpointsListB = new V1EndpointsListBuilder() + .withNewMetadataLike(new V1ListMetaBuilder().withResourceVersion("0").build()).endMetadata() + .withItems(endpointsB).build(); + + Util.endpointsInNamespaceServiceMode(wireMockServer, endpointsListA, "a", "my-service"); + Util.endpointsInNamespaceServiceMode(wireMockServer, endpointsListB, "b", "my-service"); + } + +} diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-client-loadbalancer/src/test/resources/logback-test.xml index 7c31c5ab..7b3a49d3 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/test/resources/logback-test.xml +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/resources/logback-test.xml @@ -3,4 +3,6 @@ + + diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml index b6fb4499..d4dd4696 100644 --- a/spring-cloud-kubernetes-integration-tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/pom.xml @@ -128,9 +128,6 @@ spring-cloud-kubernetes-k8s-client-configuration-watcher - - spring-cloud-kubernetes-k8s-client-loadbalancer - spring-cloud-kubernetes-k8s-client-kafka-configmap-reload-multiple-apps diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerApplication.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerApplication.java deleted file mode 100644 index 9b1f6144..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerApplication.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2013-2020 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.loadbalancer; - -import java.util.Map; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; -import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; -import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; -import org.springframework.http.HttpMethod; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author Ryan Baxter - */ - -@SpringBootApplication -@RestController -class KubernetesClientLoadBalancerApplication { - - private static final String URL = "http://service-wiremock/__admin/mappings"; - - private final WebClient.Builder client; - - private final ObjectProvider loadBalancerClientFactory; - - KubernetesClientLoadBalancerApplication(WebClient.Builder client, - ObjectProvider loadBalancerClientFactory) { - this.client = client; - this.loadBalancerClientFactory = loadBalancerClientFactory; - } - - public static void main(String[] args) { - SpringApplication.run(KubernetesClientLoadBalancerApplication.class, args); - } - - @GetMapping("/loadbalancer-it/service") - @SuppressWarnings("unchecked") - Map greeting() { - return (Map) client.baseUrl(URL).build().method(HttpMethod.GET).retrieve().bodyToMono(Map.class) - .block(); - } - - @GetMapping("/loadbalancer-it/supplier") - String supplier() { - ServiceInstanceListSupplier supplier = loadBalancerClientFactory.getIfAvailable() - .getProvider("service-wiremock", ServiceInstanceListSupplier.class).getIfAvailable(); - if (supplier instanceof CachingServiceInstanceListSupplier cachingSupplier) { - return cachingSupplier.getDelegate().getClass().getSimpleName(); - } - return supplier.getClass().getSimpleName(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerConfiguration.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerConfiguration.java deleted file mode 100644 index 2c029aa3..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/KubernetesClientLoadBalancerConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2013-2024 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.loadbalancer; - -import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * @author wind57 - */ -@Configuration -class KubernetesClientLoadBalancerConfiguration { - - @Bean - @LoadBalanced - WebClient.Builder client() { - return WebClient.builder(); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/resources/application.yaml deleted file mode 100644 index 98b2266e..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/main/resources/application.yaml +++ /dev/null @@ -1 +0,0 @@ -debug: true diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/LoadBalancerIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/LoadBalancerIT.java deleted file mode 100644 index 2911bfe4..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/k8s/client/loadbalancer/LoadBalancerIT.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2013-2020 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.loadbalancer; - -import java.util.Map; - -import io.kubernetes.client.openapi.models.V1Deployment; -import io.kubernetes.client.openapi.models.V1Ingress; -import io.kubernetes.client.openapi.models.V1Service; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.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 org.springframework.boot.test.json.BasicJsonTester; -import org.springframework.cloud.kubernetes.client.loadbalancer.KubernetesClientServicesListSupplier; -import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; -import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; -import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; -import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier; -import org.springframework.http.HttpMethod; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util.patchWithMerge; - -/** - * @author Ryan Baxter - */ -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class LoadBalancerIT { - - private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester(LoadBalancerIT.class); - - private static final String BODY_FOR_MERGE = """ - { - "spec": { - "template": { - "spec": { - "containers": [{ - "name": "spring-cloud-kubernetes-k8s-client-loadbalancer", - "env": [ - { - "name": "SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE", - "value": "SERVICE" - } - ] - }] - } - } - } - } - """; - - private static final Map POD_LABELS = Map.of("app", - "spring-cloud-kubernetes-k8s-client-loadbalancer"); - - private static final String SERVICE_URL = "http://localhost:80/loadbalancer-it/service"; - - private static final String SERVICE_INSTANCE_LIST_SUPPLIER_URL = "http://localhost:80/loadbalancer-it/supplier"; - - private static final String SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME = "spring-cloud-kubernetes-k8s-client-loadbalancer"; - - private static final String NAMESPACE = "default"; - - private static final K3sContainer K3S = Commons.container(); - - private static Util util; - - @BeforeAll - static void beforeAll() throws Exception { - K3S.start(); - Commons.validateImage(SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME, K3S); - Commons.loadSpringCloudKubernetesImage(SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME, K3S); - util = new Util(K3S); - util.setUp(NAMESPACE); - loadbalancerIt(Phase.CREATE); - } - - @AfterAll - static void afterAll() throws Exception { - loadbalancerIt(Phase.DELETE); - Commons.cleanUp(SPRING_CLOUD_K8S_LOADBALANCER_APP_NAME, K3S); - Commons.systemPrune(); - } - - @BeforeEach - void setup() { - util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE, false); - } - - @AfterEach - void afterEach() { - util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE, false); - } - - @Test - @Order(1) - void testLoadBalancerPodMode() { - testLoadBalancer(true); - } - - @Test - @Order(2) - void testLoadBalancerServiceMode() { - patchForServiceMode(); - testLoadBalancer(false); - } - - private void testLoadBalancer(boolean podMode) { - - WebClient.Builder builder = builder(); - WebClient serviceClient = builder.baseUrl(SERVICE_URL).build(); - - String result = serviceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class).block(); - Assertions.assertThat(BASIC_JSON_TESTER.from(result)).extractingJsonPathArrayValue("$.mappings").isEmpty(); - Assertions.assertThat(BASIC_JSON_TESTER.from(result)).extractingJsonPathNumberValue("$.meta.total") - .isEqualTo(0); - - WebClient.Builder supplierBuilder = builder(); - WebClient supplierServiceClient = supplierBuilder.baseUrl(SERVICE_INSTANCE_LIST_SUPPLIER_URL).build(); - String supplierResult = supplierServiceClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) - .block(); - if (podMode) { - Assertions.assertThat(supplierResult) - .isEqualTo(DiscoveryClientServiceInstanceListSupplier.class.getSimpleName()); - } - else { - Assertions.assertThat(supplierResult).isEqualTo(KubernetesClientServicesListSupplier.class.getSimpleName()); - } - } - - private static void loadbalancerIt(Phase phase) { - V1Deployment deployment = (V1Deployment) util - .yaml("spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml"); - V1Service service = (V1Service) util.yaml("spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml"); - V1Ingress ingress = (V1Ingress) util.yaml("spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml"); - - if (phase.equals(Phase.CREATE)) { - util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); - } - else if (phase.equals(Phase.DELETE)) { - util.deleteAndWait(NAMESPACE, deployment, service, ingress); - } - } - - private WebClient.Builder builder() { - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); - } - - private static void patchForServiceMode() { - patchWithMerge("spring-cloud-kubernetes-k8s-client-loadbalancer", LoadBalancerIT.NAMESPACE, BODY_FOR_MERGE, - POD_LABELS); - } - -} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/logback-test.xml deleted file mode 100644 index 9e284876..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/logback-test.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n - - - - - - - - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml deleted file mode 100644 index a0aab26b..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-deployment.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: spring-cloud-kubernetes-k8s-client-loadbalancer -spec: - selector: - matchLabels: - app: spring-cloud-kubernetes-k8s-client-loadbalancer - template: - metadata: - labels: - app: spring-cloud-kubernetes-k8s-client-loadbalancer - spec: - serviceAccountName: spring-cloud-kubernetes-serviceaccount - containers: - - name: spring-cloud-kubernetes-k8s-client-loadbalancer - env: - - name: SPRING_CLOUD_KUBERNETES_LOADBALANCER_MODE - value: POD - image: docker.io/springcloud/spring-cloud-kubernetes-k8s-client-loadbalancer - imagePullPolicy: IfNotPresent - readinessProbe: - httpGet: - port: 8080 - path: /actuator/health/readiness - livenessProbe: - httpGet: - port: 8080 - path: /actuator/health/liveness - ports: - - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml deleted file mode 100644 index 35c5d8da..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-ingress.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: it-ingress - namespace: default -spec: - rules: - - http: - paths: - - path: /loadbalancer-it/ - pathType: Prefix - backend: - service: - name: spring-cloud-kubernetes-k8s-client-loadbalancer - port: - number: 8080 - - - diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml deleted file mode 100644 index 6fe86a0a..00000000 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-loadbalancer/src/test/resources/spring-cloud-kubernetes-k8s-client-loadbalancer-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: spring-cloud-kubernetes-k8s-client-loadbalancer - name: spring-cloud-kubernetes-k8s-client-loadbalancer -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - selector: - app: spring-cloud-kubernetes-k8s-client-loadbalancer - type: ClusterIP