From 0bd7bc5580e5c6b0e6f0022a52b59612a21c8294 Mon Sep 17 00:00:00 2001 From: erabii Date: Mon, 18 Mar 2024 15:27:14 +0200 Subject: [PATCH] Add namespace provider to fabric8 loadbalancer (#1597) --- ...ernetesClientServicesFunctionProvider.java | 3 +- .../pom.xml | 23 ++ .../Fabric8ServicesListSupplier.java | 30 ++- .../Fabric8ServiceListSupplierTests.java | 4 +- ...c8ServicesListSupplierMockClientTests.java | 149 ++++++++++++ .../it/PodModeAllNamespacesTest.java | 217 ++++++++++++++++++ .../it/PodModeSpecificNamespaceTest.java | 216 +++++++++++++++++ .../it/ServiceModeAllNamespacesTest.java | 187 +++++++++++++++ .../it/ServiceModeSpecificNamespaceTest.java | 184 +++++++++++++++ .../fabric8/loadbalancer/it/Util.java | 40 ++++ .../src/test/resources/logback-test.xml | 7 + .../pom.xml | 4 +- 12 files changed, 1052 insertions(+), 12 deletions(-) create mode 100644 spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplierMockClientTests.java create mode 100644 spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeAllNamespacesTest.java create mode 100644 spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeSpecificNamespaceTest.java create mode 100644 spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeAllNamespacesTest.java create mode 100644 spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeSpecificNamespaceTest.java create mode 100644 spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/Util.java create mode 100644 spring-cloud-kubernetes-fabric8-loadbalancer/src/test/resources/logback-test.xml diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java index 07cbf1a8de..2a65d110dc 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * 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. @@ -46,6 +46,7 @@ public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscov } + @Deprecated(forRemoval = true) public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties, Binder binder, BindHandler bindHandler) { return servicesFunction(properties, new KubernetesNamespaceProvider(binder, bindHandler)); diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml b/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml index 5e2e3fc1ca..65f2aade9c 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/pom.xml @@ -40,12 +40,35 @@ org.springframework.boot spring-boot-starter-test test + + + org.mokito + mockito-core + + + + + org.mockito + mockito-inline + test + + io.fabric8 kubernetes-server-mock test + + org.springframework.boot + spring-boot-starter-webflux + test + + + org.wiremock + wiremock-standalone + test + org.springframework.cloud spring-cloud-kubernetes-test-support diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java index 0972271c56..0c6282f278 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * 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. @@ -21,14 +21,17 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.client.KubernetesClient; +import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier; +import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; +import org.springframework.core.log.LogAccessor; /** * Implementation of {@link ServiceInstanceListSupplier} for load balancer in SERVICE @@ -38,30 +41,45 @@ */ public class Fabric8ServicesListSupplier extends KubernetesServicesListSupplier { + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8ServicesListSupplier.class)); + private final KubernetesClient kubernetesClient; + private final KubernetesNamespaceProvider namespaceProvider; + Fabric8ServicesListSupplier(Environment environment, KubernetesClient kubernetesClient, Fabric8ServiceInstanceMapper mapper, KubernetesDiscoveryProperties discoveryProperties) { super(environment, mapper, discoveryProperties); this.kubernetesClient = kubernetesClient; + namespaceProvider = new KubernetesNamespaceProvider(environment); } @Override public Flux> get() { List result = new ArrayList<>(); + String serviceName = getServiceId(); + LOG.debug(() -> "serviceID : " + serviceName); + if (discoveryProperties.allNamespaces()) { + LOG.debug(() -> "discovering services in all namespaces"); List services = kubernetesClient.services().inAnyNamespace() - .withField("metadata.name", getServiceId()).list().getItems(); + .withField("metadata.name", serviceName).list().getItems(); services.forEach(service -> result.add(mapper.map(service))); } else { - Service service = StringUtils.hasText(kubernetesClient.getNamespace()) ? kubernetesClient.services() - .inNamespace(kubernetesClient.getNamespace()).withName(getServiceId()).get() - : kubernetesClient.services().withName(getServiceId()).get(); + String namespace = Fabric8Utils.getApplicationNamespace(kubernetesClient, null, "loadbalancer-service", + namespaceProvider); + LOG.debug(() -> "discovering services in namespace : " + namespace); + Service service = kubernetesClient.services().inNamespace(namespace).withName(serviceName).get(); if (service != null) { result.add(mapper.map(service)); } + else { + LOG.debug(() -> "did not find service with name : " + serviceName + " in namespace : " + namespace); + } } + + LOG.debug(() -> "found services : " + result); return Flux.defer(() -> Flux.just(result)); } diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceListSupplierTests.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceListSupplierTests.java index 1e41a432ab..2462417f12 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceListSupplierTests.java +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceListSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * 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. @@ -78,7 +78,6 @@ void testPositiveMatch() { KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper, KubernetesDiscoveryProperties.DEFAULT); List instances = supplier.get().blockFirst(); - assert instances != null; Assertions.assertEquals(1, instances.size()); } @@ -98,7 +97,6 @@ void testPositiveMatchAllNamespaces() { KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper, discoveryProperties); List instances = supplier.get().blockFirst(); - assert instances != null; Assertions.assertEquals(1, instances.size()); } diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplierMockClientTests.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplierMockClientTests.java new file mode 100644 index 0000000000..9219c47ff5 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServicesListSupplierMockClientTests.java @@ -0,0 +1,149 @@ +/* + * 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.fabric8.loadbalancer; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +@ExtendWith(OutputCaptureExtension.class) +class Fabric8ServicesListSupplierMockClientTests { + + private static KubernetesClient mockClient; + + @BeforeAll + static void setUpBeforeClass() { + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @AfterEach + void afterEach() { + mockClient.services().inAnyNamespace().delete(); + } + + @Test + void testAllNamespaces(CapturedOutput output) { + + createService("a", "service-a", 8887); + createService("b", "service-b", 8888); + createService("c", "service-a", 8889); + + Environment environment = new MockEnvironment().withProperty("loadbalancer.client.name", "service-a"); + boolean allNamespaces = true; + Set selectiveNamespaces = Set.of(); + + KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(true, allNamespaces, + selectiveNamespaces, true, 60, false, null, Set.of(), Map.of(), null, + KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, null); + + Fabric8ServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, mockClient, + new Fabric8ServiceInstanceMapper(loadBalancerProperties, discoveryProperties), discoveryProperties); + + List> serviceInstances = supplier.get().collectList().block(); + Assertions.assertEquals(serviceInstances.size(), 1); + List inner = serviceInstances.get(0); + + List serviceInstancesSorted = serviceInstances.get(0).stream() + .sorted(Comparator.comparing(ServiceInstance::getServiceId)).toList(); + Assertions.assertEquals(serviceInstancesSorted.size(), 2); + Assertions.assertEquals(inner.get(0).getServiceId(), "service-a"); + Assertions.assertEquals(inner.get(0).getHost(), "service-a.a.svc.cluster.local"); + Assertions.assertEquals(inner.get(0).getPort(), 8887); + + Assertions.assertEquals(inner.get(1).getServiceId(), "service-a"); + Assertions.assertEquals(inner.get(1).getHost(), "service-a.c.svc.cluster.local"); + Assertions.assertEquals(inner.get(1).getPort(), 8889); + + Assertions.assertTrue(output.getOut().contains("discovering services in all namespaces")); + } + + @Test + void testOneNamespace(CapturedOutput output) { + + createService("a", "service-c", 8887); + createService("b", "service-b", 8888); + createService("c", "service-c", 8889); + + Environment environment = new MockEnvironment().withProperty("spring.cloud.kubernetes.client.namespace", "c") + .withProperty("loadbalancer.client.name", "service-c"); + boolean allNamespaces = false; + Set selectiveNamespaces = Set.of(); + + KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(true, allNamespaces, + selectiveNamespaces, true, 60, false, null, Set.of(), Map.of(), null, + KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, false, null); + + Fabric8ServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, mockClient, + new Fabric8ServiceInstanceMapper(loadBalancerProperties, discoveryProperties), discoveryProperties); + + List> serviceInstances = supplier.get().collectList().block(); + Assertions.assertEquals(serviceInstances.size(), 1); + List inner = serviceInstances.get(0); + + List serviceInstancesSorted = serviceInstances.get(0).stream() + .sorted(Comparator.comparing(ServiceInstance::getServiceId)).toList(); + Assertions.assertEquals(serviceInstancesSorted.size(), 1); + Assertions.assertEquals(inner.get(0).getServiceId(), "service-c"); + Assertions.assertEquals(inner.get(0).getHost(), "service-c.c.svc.cluster.local"); + Assertions.assertEquals(inner.get(0).getPort(), 8889); + + Assertions.assertTrue(output.getOut().contains("discovering services in namespace : c")); + } + + private void createService(String namespace, String name, int port) { + Service service = new ServiceBuilder().withNewMetadata().withNamespace(namespace).withName(name).endMetadata() + .withSpec(new ServiceSpecBuilder() + .withPorts(new ServicePortBuilder().withName("http").withPort(port).build()).build()) + .build(); + mockClient.services().resource(service).create(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeAllNamespacesTest.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeAllNamespacesTest.java new file mode 100644 index 0000000000..98a5611c86 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeAllNamespacesTest.java @@ -0,0 +1,217 @@ +/* + * 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.fabric8.loadbalancer.it; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.fabric8.kubernetes.api.model.EndpointAddressBuilder; +import io.fabric8.kubernetes.api.model.EndpointPortBuilder; +import io.fabric8.kubernetes.api.model.EndpointSubsetBuilder; +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsBuilder; +import io.fabric8.kubernetes.api.model.EndpointsListBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.utils.Serialization; +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.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +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.context.annotation.Bean; +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.springframework.cloud.kubernetes.fabric8.loadbalancer.it.PodModeAllNamespacesTest.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.PodModeAllNamespacesTest.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 PodModeAllNamespacesTest { + + 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 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()); + + 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"); + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, "http://localhost:" + wireMockServer.port()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.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() { + + Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); + Service serviceB = Util.createService("b", "service-b", SERVICE_B_PORT); + + Endpoints endpointsA = new EndpointsBuilder() + .withSubsets(new EndpointSubsetBuilder() + .withPorts(new EndpointPortBuilder().withPort(SERVICE_A_PORT).build()) + .withAddresses(new EndpointAddressBuilder().withIp("127.0.0.1").build()).build()) + .withMetadata(new ObjectMetaBuilder().withName("no-port-name-service").withNamespace("a").build()) + .build(); + + Endpoints endpointsB = new EndpointsBuilder() + .withSubsets(new EndpointSubsetBuilder() + .withPorts(new EndpointPortBuilder().withPort(SERVICE_B_PORT).build()) + .withAddresses(new EndpointAddressBuilder().withIp("127.0.0.1").build()).build()) + .withMetadata(new ObjectMetaBuilder().withName("no-port-name-service").withNamespace("b").build()) + .build(); + + String endpointsAListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsA).build()); + String endpointsBListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsB).build()); + + String serviceAString = Serialization.asJson(serviceA); + String serviceBString = Serialization.asJson(serviceB); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/endpoints?fieldSelector=metadata.name%3Dservice-a")) + .willReturn(WireMock.aResponse().withBody(endpointsAListAsString).withStatus(200))); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/endpoints?fieldSelector=metadata.name%3Dservice-b")) + .willReturn(WireMock.aResponse().withBody(endpointsBListAsString).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/a/services/service-a")) + .willReturn(WireMock.aResponse().withBody(serviceAString).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/service-b")) + .willReturn(WireMock.aResponse().withBody(serviceBString).withStatus(200))); + + 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); + } + + @TestConfiguration + static class LoadBalancerConfiguration { + + @Bean + @LoadBalanced + WebClient.Builder client() { + return WebClient.builder(); + } + + } + + @SpringBootApplication + static class Configuration { + + public static void main(String[] args) { + SpringApplication.run(ServiceModeAllNamespacesTest.Configuration.class); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeSpecificNamespaceTest.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeSpecificNamespaceTest.java new file mode 100644 index 0000000000..fc5692de83 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeSpecificNamespaceTest.java @@ -0,0 +1,216 @@ +/* + * 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.fabric8.loadbalancer.it; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.fabric8.kubernetes.api.model.EndpointAddressBuilder; +import io.fabric8.kubernetes.api.model.EndpointPortBuilder; +import io.fabric8.kubernetes.api.model.EndpointSubsetBuilder; +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsBuilder; +import io.fabric8.kubernetes.api.model.EndpointsListBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.utils.Serialization; +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.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +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.context.annotation.Bean; +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.springframework.cloud.kubernetes.fabric8.loadbalancer.it.PodModeSpecificNamespaceTest.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.PodModeSpecificNamespaceTest.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 PodModeSpecificNamespaceTest { + + private static final String SERVICE_A_URL = "http://service-a"; + + 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); + + @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()); + + 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"); + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, "http://localhost:" + wireMockServer.port()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.close(); + } + + /** + *
+	 *      - service-a is present in 'a' namespace
+	 *      - service-b is present in 'b' namespace
+	 *      - we enable search in namespace 'a'
+	 *      - load balancer mode is 'POD'
+	 *
+	 *      - as such, only service-a service is load balanced
+	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the POD mode.
+	 * 
+ */ + @Test + void test() { + + Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); + Service serviceB = Util.createService("b", "service-a", SERVICE_B_PORT); + + Endpoints endpointsA = new EndpointsBuilder() + .withSubsets(new EndpointSubsetBuilder() + .withPorts(new EndpointPortBuilder().withPort(SERVICE_A_PORT).build()) + .withAddresses(new EndpointAddressBuilder().withIp("127.0.0.1").build()).build()) + .withMetadata(new ObjectMetaBuilder().withName("no-port-name-service").withNamespace("a").build()) + .build(); + + Endpoints endpointsB = new EndpointsBuilder() + .withSubsets(new EndpointSubsetBuilder() + .withPorts(new EndpointPortBuilder().withPort(SERVICE_B_PORT).build()) + .withAddresses(new EndpointAddressBuilder().withIp("127.0.0.1").build()).build()) + .withMetadata(new ObjectMetaBuilder().withName("no-port-name-service").withNamespace("b").build()) + .build(); + + String endpointsAListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsA).build()); + String endpointsBListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsB).build()); + + String serviceAString = Serialization.asJson(serviceA); + String serviceBString = Serialization.asJson(serviceB); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/a/endpoints?fieldSelector=metadata.name%3Dservice-a")) + .willReturn(WireMock.aResponse().withBody(endpointsAListAsString).withStatus(200))); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/endpoints?fieldSelector=metadata.name%3Dservice-b")) + .willReturn(WireMock.aResponse().withBody(endpointsBListAsString).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/a/services/service-a")) + .willReturn(WireMock.aResponse().withBody(serviceAString).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/service-a")) + .willReturn(WireMock.aResponse().withBody(serviceBString).withStatus(200))); + + 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("service-a", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()) + .isSameAs(DiscoveryClientServiceInstanceListSupplier.class); + } + + @TestConfiguration + static class LoadBalancerConfiguration { + + @Bean + @LoadBalanced + WebClient.Builder client() { + return WebClient.builder(); + } + + } + + @SpringBootApplication + static class Configuration { + + public static void main(String[] args) { + SpringApplication.run(ServiceModeAllNamespacesTest.Configuration.class); + } + + } + + +} diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeAllNamespacesTest.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeAllNamespacesTest.java new file mode 100644 index 0000000000..cf5750e653 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeAllNamespacesTest.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.fabric8.loadbalancer.it; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceListBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.utils.Serialization; +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.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.kubernetes.fabric8.loadbalancer.Fabric8ServicesListSupplier; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.context.annotation.Bean; +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.springframework.cloud.kubernetes.fabric8.loadbalancer.it.ServiceModeAllNamespacesTest.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.ServiceModeAllNamespacesTest.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 ServiceModeAllNamespacesTest { + + 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 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()); + + 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"); + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, "http://localhost:" + wireMockServer.port()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.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() { + + Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); + Service serviceB = Util.createService("b", "service-b", SERVICE_B_PORT); + + String serviceListAJson = Serialization.asJson(new ServiceListBuilder().withItems(serviceA).build()); + String serviceListBJson = Serialization.asJson(new ServiceListBuilder().withItems(serviceB).build()); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/services?fieldSelector=metadata.name%3Dservice-a")) + .willReturn(WireMock.aResponse().withBody(serviceListAJson).withStatus(200))); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/services?fieldSelector=metadata.name%3Dservice-b")) + .willReturn(WireMock.aResponse().withBody(serviceListBJson).withStatus(200))); + + 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(Fabric8ServicesListSupplier.class); + } + + @TestConfiguration + static class LoadBalancerConfiguration { + + @Bean + @LoadBalanced + WebClient.Builder client() { + return WebClient.builder(); + } + + } + + @SpringBootApplication + static class Configuration { + + public static void main(String[] args) { + SpringApplication.run(Configuration.class); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeSpecificNamespaceTest.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeSpecificNamespaceTest.java new file mode 100644 index 0000000000..5369e61304 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeSpecificNamespaceTest.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.fabric8.loadbalancer.it; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.utils.Serialization; +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.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.kubernetes.fabric8.loadbalancer.Fabric8ServicesListSupplier; +import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.context.annotation.Bean; +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.springframework.cloud.kubernetes.fabric8.loadbalancer.it.ServiceModeSpecificNamespaceTest.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.ServiceModeSpecificNamespaceTest.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 ServiceModeSpecificNamespaceTest { + + private static final String SERVICE_A_URL = "http://service-a"; + + 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); + + @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()); + + 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"); + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, "http://localhost:" + wireMockServer.port()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + serviceAMockServer.stop(); + serviceBMockServer.stop(); + MOCKED_STATIC.close(); + } + + /** + *
+	 *      - service-a is present in 'a' namespace
+	 *      - service-a is present in 'b' namespace
+	 *      - we enable search in namespace 'a'
+	 *      - load balancer mode is 'SERVICE'
+	 *
+	 *      - as such, only service-a service is load balanced
+	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the SERVICE mode.
+	 * 
+ */ + @Test + void test() { + + Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); + Service serviceB = Util.createService("b", "service-a", SERVICE_B_PORT); + + String serviceAJson = Serialization.asJson(serviceA); + String serviceBJson = Serialization.asJson(serviceB); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/a/services/service-a")) + .willReturn(WireMock.aResponse().withBody(serviceAJson).withStatus(200))); + + wireMockServer + .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/service-a")) + .willReturn(WireMock.aResponse().withBody(serviceBJson).withStatus(200))); + + 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("service-a", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplier.getDelegate().getClass()).isSameAs(Fabric8ServicesListSupplier.class); + } + + @TestConfiguration + static class LoadBalancerConfiguration { + + @Bean + @LoadBalanced + WebClient.Builder client() { + return WebClient.builder(); + } + + } + + @SpringBootApplication + static class Configuration { + + public static void main(String[] args) { + SpringApplication.run(ServiceModeAllNamespacesTest.Configuration.class); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/Util.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/Util.java new file mode 100644 index 0000000000..76ca6097e6 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/Util.java @@ -0,0 +1,40 @@ +/* + * 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.fabric8.loadbalancer.it; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; + +/** + * @author wind57 + */ +final class Util { + + private Util() { + + } + + static Service createService(String namespace, String name, int port) { + return new ServiceBuilder().withNewMetadata().withNamespace(namespace).withName(name).endMetadata() + .withSpec(new ServiceSpecBuilder() + .withPorts(new ServicePortBuilder().withName("http").withPort(port).build()).build()) + .build(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..047e3076eb --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/resources/logback-test.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml index 4e47756483..22a6f84198 100644 --- a/spring-cloud-kubernetes-integration-tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/pom.xml @@ -128,7 +128,7 @@ spring-cloud-kubernetes-k8s-client-configuration-watcher - + spring-cloud-kubernetes-k8s-client-loadbalancer @@ -137,5 +137,5 @@ spring-cloud-kubernetes-k8s-client-rabbitmq-secret-reload-multiple-apps - +