diff --git a/docs/modules/ROOT/pages/spring-cloud-kubernetes.adoc b/docs/modules/ROOT/pages/spring-cloud-kubernetes.adoc index 90188d245..142406134 100644 --- a/docs/modules/ROOT/pages/spring-cloud-kubernetes.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-kubernetes.adoc @@ -35,20 +35,6 @@ To see the list of all Kubernetes related configuration properties please check Click https://docs.spring.io/spring-cloud-build/reference/building.html[here] for basic building instructions. -[[building-docker-images-on-arm64]] -=== Building Docker Images On ARM64 - -If you run the Spring Cloud Kuberentes build on an ARM64 machine the docker images -used for the integration tests will fail to run due to using the wrong architecture. -This is because the Paketo build pack does not yet support ARM64. To work around this you -can run the build by passing `-Dspring-boot.build-image.builder=dashaun/builder:tiny` to Maven. - -For example: -``` -./mvnw clean install -Dspring-boot.build-image.builder=dashaun/builder:tiny -``` - - [[contributing]] == Contributing diff --git a/pom.xml b/pom.xml index 97bc416d3..b03924a51 100644 --- a/pom.xml +++ b/pom.xml @@ -376,5 +376,18 @@ + + + build-image-aarch64 + + + aarch64 + + + + dashaun/builder:tiny + + diff --git a/spring-cloud-kubernetes-dependencies/pom.xml b/spring-cloud-kubernetes-dependencies/pom.xml index d43cee2f8..2b7342ab6 100644 --- a/spring-cloud-kubernetes-dependencies/pom.xml +++ b/spring-cloud-kubernetes-dependencies/pom.xml @@ -34,7 +34,7 @@ 0.13.0 6.9.2 - 19.0.0 + 19.0.1 3.4.2 4.4 diff --git a/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8Utils.java b/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8Utils.java index 0e65a87ef..21171bdd0 100644 --- a/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8Utils.java +++ b/spring-cloud-kubernetes-fabric8-autoconfig/src/main/java/org/springframework/cloud/kubernetes/fabric8/Fabric8Utils.java @@ -16,12 +16,16 @@ package org.springframework.cloud.kubernetes.fabric8; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceSpec; import io.fabric8.kubernetes.client.KubernetesClient; import jakarta.annotation.Nullable; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException; +import org.springframework.cloud.kubernetes.commons.discovery.ServiceMetadata; import org.springframework.core.log.LogAccessor; import org.springframework.util.StringUtils; @@ -37,6 +41,13 @@ private Fabric8Utils() { } + public static ServiceMetadata serviceMetadata(Service service) { + ObjectMeta metadata = service.getMetadata(); + ServiceSpec serviceSpec = service.getSpec(); + return new ServiceMetadata(metadata.getName(), metadata.getNamespace(), serviceSpec.getType(), + metadata.getLabels(), metadata.getAnnotations()); + } + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8Utils.class)); /** diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java index 7895f8cf9..e21de1eee 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java @@ -29,10 +29,8 @@ import io.fabric8.kubernetes.api.model.EndpointSubset; import io.fabric8.kubernetes.api.model.Endpoints; import io.fabric8.kubernetes.api.model.EndpointsList; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceList; -import io.fabric8.kubernetes.api.model.ServiceSpec; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.FilterNested; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; @@ -43,7 +41,6 @@ import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.cloud.kubernetes.commons.discovery.ServiceMetadata; import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; import org.springframework.core.log.LogAccessor; import org.springframework.util.CollectionUtils; @@ -198,13 +195,6 @@ static Map endpointSubsetsPortData(List endpoin EndpointPort::getPort)); } - static ServiceMetadata serviceMetadata(Service service) { - ObjectMeta metadata = service.getMetadata(); - ServiceSpec serviceSpec = service.getSpec(); - return new ServiceMetadata(metadata.getName(), metadata.getNamespace(), serviceSpec.getType(), - metadata.getLabels(), metadata.getAnnotations()); - } - /** * serviceName can be null, in which case, such a filter will not be applied. */ diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java index 6a5a786cf..5d8fcc1b9 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java @@ -44,12 +44,12 @@ import static org.springframework.cloud.kubernetes.commons.discovery.DiscoveryClientUtils.serviceInstance; import static org.springframework.cloud.kubernetes.commons.discovery.DiscoveryClientUtils.serviceInstanceMetadata; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.EXTERNAL_NAME; +import static org.springframework.cloud.kubernetes.fabric8.Fabric8Utils.serviceMetadata; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8InstanceIdHostPodNameSupplier.externalName; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8InstanceIdHostPodNameSupplier.nonExternalName; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8KubernetesDiscoveryClientUtils.addresses; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8KubernetesDiscoveryClientUtils.endpointSubsetsPortData; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8KubernetesDiscoveryClientUtils.endpoints; -import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8KubernetesDiscoveryClientUtils.serviceMetadata; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8KubernetesDiscoveryClientUtils.services; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8PodLabelsAndAnnotationsSupplier.externalName; import static org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8PodLabelsAndAnnotationsSupplier.nonExternalName; diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java index 9c4d2eda8..521c11782 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/main/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapper.java @@ -16,7 +16,6 @@ package org.springframework.cloud.kubernetes.fabric8.loadbalancer; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -27,10 +26,13 @@ import io.fabric8.kubernetes.client.utils.Utils; import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.DiscoveryClientUtils; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.ServiceMetadata; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesLoadBalancerProperties; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; /** * Class for mapping Kubernetes Service object into {@link KubernetesServiceInstance}. @@ -39,6 +41,11 @@ */ public class Fabric8ServiceInstanceMapper implements KubernetesServiceInstanceMapper { + /** + * empty on purpose, load balancer implementation does not need them. + */ + private static final Map PORTS_DATA = Map.of(); + private final KubernetesLoadBalancerProperties properties; private final KubernetesDiscoveryProperties discoveryProperties; @@ -57,7 +64,7 @@ public KubernetesServiceInstance map(Service service) { if (ports.size() == 1) { port = ports.get(0); } - else if (ports.size() > 1 && Utils.isNotNullOrEmpty(this.properties.getPortName())) { + else if (ports.size() > 1 && Utils.isNotNullOrEmpty(properties.getPortName())) { Optional optPort = ports.stream().filter(it -> properties.getPortName().endsWith(it.getName())) .findAny(); if (optPort.isPresent()) { @@ -72,24 +79,12 @@ else if (ports.size() > 1 && Utils.isNotNullOrEmpty(this.properties.getPortName( boolean secure = KubernetesServiceInstanceMapper.isSecure(service.getMetadata().getLabels(), service.getMetadata().getAnnotations(), port.getName(), port.getPort()); return new DefaultKubernetesServiceInstance(meta.getUid(), meta.getName(), host, port.getPort(), - getServiceMetadata(service), secure); + serviceMetadata(service), secure); } - private Map getServiceMetadata(Service service) { - Map serviceMetadata = new HashMap<>(); - KubernetesDiscoveryProperties.Metadata metadataProps = this.discoveryProperties.metadata(); - if (metadataProps.addLabels()) { - Map labelMetadata = KubernetesServiceInstanceMapper - .getMapWithPrefixedKeys(service.getMetadata().getLabels(), metadataProps.labelsPrefix()); - serviceMetadata.putAll(labelMetadata); - } - if (metadataProps.addAnnotations()) { - Map annotationMetadata = KubernetesServiceInstanceMapper - .getMapWithPrefixedKeys(service.getMetadata().getAnnotations(), metadataProps.annotationsPrefix()); - serviceMetadata.putAll(annotationMetadata); - } - - return serviceMetadata; + Map serviceMetadata(Service service) { + ServiceMetadata serviceMetadata = Fabric8Utils.serviceMetadata(service); + return DiscoveryClientUtils.serviceInstanceMetadata(PORTS_DATA, serviceMetadata, discoveryProperties); } } 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 0c6282f27..74ce4b09a 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 @@ -27,6 +27,7 @@ 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.KubernetesServiceInstanceMapper; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServicesListSupplier; import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; @@ -64,7 +65,22 @@ public Flux> get() { LOG.debug(() -> "discovering services in all namespaces"); List services = kubernetesClient.services().inAnyNamespace() .withField("metadata.name", serviceName).list().getItems(); - services.forEach(service -> result.add(mapper.map(service))); + services.forEach(service -> addMappedService(mapper, result, service)); + } + else if (!discoveryProperties.namespaces().isEmpty()) { + List selectiveNamespaces = discoveryProperties.namespaces().stream().sorted().toList(); + LOG.debug(() -> "discovering services in selective namespaces : " + selectiveNamespaces); + selectiveNamespaces.forEach(selectiveNamespace -> { + Service service = kubernetesClient.services().inNamespace(selectiveNamespace).withName(serviceName) + .get(); + if (service != null) { + addMappedService(mapper, result, service); + } + else { + LOG.debug(() -> "did not find service with name : " + serviceName + " in namespace : " + + selectiveNamespace); + } + }); } else { String namespace = Fabric8Utils.getApplicationNamespace(kubernetesClient, null, "loadbalancer-service", @@ -72,7 +88,7 @@ public Flux> get() { LOG.debug(() -> "discovering services in namespace : " + namespace); Service service = kubernetesClient.services().inNamespace(namespace).withName(serviceName).get(); if (service != null) { - result.add(mapper.map(service)); + addMappedService(mapper, result, service); } else { LOG.debug(() -> "did not find service with name : " + serviceName + " in namespace : " + namespace); @@ -83,4 +99,9 @@ public Flux> get() { return Flux.defer(() -> Flux.just(result)); } + private void addMappedService(KubernetesServiceInstanceMapper mapper, List services, + Service service) { + services.add(mapper.map(service)); + } + } diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java index 14b1b945d..a4a37c3fe 100644 --- a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.java +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/Fabric8ServiceInstanceMapperTests.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. @@ -38,7 +38,7 @@ class Fabric8ServiceInstanceMapperTests { @Test void testMapperSimple() { KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); - Service service = buildService("test", "abc", 8080, null, Map.of()); + Service service = buildService("test", "test-namespace", "abc", 8080, null, Map.of()); KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, KubernetesDiscoveryProperties.DEFAULT).map(service); Assertions.assertNotNull(instance); @@ -53,7 +53,7 @@ void testMapperMultiplePorts() { List ports = new ArrayList<>(); ports.add(new ServicePortBuilder().withPort(8080).withName("web").build()); ports.add(new ServicePortBuilder().withPort(9000).withName("http").build()); - Service service = buildService("test", "abc", ports, Map.of()); + Service service = buildService("test", "test-namespace", "abc", ports, Map.of()); KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, KubernetesDiscoveryProperties.DEFAULT).map(service); Assertions.assertNotNull(instance); @@ -65,7 +65,7 @@ void testMapperMultiplePorts() { @Test void testMapperSecure() { KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); - Service service = buildService("test", "abc", 443, null, Map.of()); + Service service = buildService("test", "test-namespace", "abc", 443, null, Map.of()); KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, KubernetesDiscoveryProperties.DEFAULT).map(service); Assertions.assertNotNull(instance); @@ -82,7 +82,7 @@ void testMapperSecureNullLabelsAndAnnotations() { false); List ports = new ArrayList<>(); ports.add(new ServicePortBuilder().withPort(443).build()); - Service service = buildService("test", "abc", ports, null, null); + Service service = buildService("test", "test-namespace", "abc", ports, null, null); KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, discoveryProperties) .map(service); Assertions.assertNotNull(instance); @@ -95,29 +95,56 @@ void testMapperSecureNullLabelsAndAnnotations() { void testMapperSecureWithLabels() { KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties(); Map labels = Map.of("secured", "true", "label1", "123"); - Service service = buildService("test", "abc", 8080, null, labels); + Service service = buildService("test", "test-namespace", "abc", 8080, null, labels); KubernetesServiceInstance instance = new Fabric8ServiceInstanceMapper(properties, KubernetesDiscoveryProperties.DEFAULT).map(service); Assertions.assertNotNull(instance); Assertions.assertEquals("test", instance.getServiceId()); Assertions.assertEquals("abc", instance.getInstanceId()); Assertions.assertTrue(instance.isSecure()); - Assertions.assertEquals(2, instance.getMetadata().keySet().size()); + Assertions.assertEquals(4, instance.getMetadata().keySet().size()); } - private Service buildService(String name, String uid, int port, String portName, Map labels) { + @Test + void serviceMetadataTest() { + + KubernetesLoadBalancerProperties loadBalancerProperties = new KubernetesLoadBalancerProperties(); + KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(true, false, Set.of(), + true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + + List ports = new ArrayList<>(); + ports.add(new ServicePortBuilder().withPort(443).build()); + + Map labels = Map.of("one", "1"); + Map annotations = Map.of("two", "2"); + + Service service = buildService("test", "test-namespace", "abc", ports, labels, annotations); + Map result = new Fabric8ServiceInstanceMapper(loadBalancerProperties, discoveryProperties) + .serviceMetadata(service); + Assertions.assertEquals(result.size(), 4); + Assertions.assertEquals(result.get("k8s_namespace"), "test-namespace"); + Assertions.assertEquals(result.get("type"), "ClusterIP"); + Assertions.assertEquals(result.get("one"), "1"); + Assertions.assertEquals(result.get("two"), "2"); + } + + private Service buildService(String name, String namespace, String uid, int port, String portName, + Map labels) { ServicePort servicePort = new ServicePortBuilder().withPort(port).withName(portName).build(); - return buildService(name, uid, Collections.singletonList(servicePort), labels); + return buildService(name, namespace, uid, Collections.singletonList(servicePort), labels); } - private Service buildService(String name, String uid, List ports, Map labels, - Map annotations) { - return new ServiceBuilder().withNewMetadata().withName(name).withUid(uid).addToLabels(labels) - .withAnnotations(annotations).endMetadata().withNewSpec().addAllToPorts(ports).endSpec().build(); + private Service buildService(String name, String namespace, String uid, List ports, + Map labels, Map annotations) { + return new ServiceBuilder().withNewMetadata().withNamespace(namespace).withName(name).withUid(uid) + .addToLabels(labels).withAnnotations(annotations).endMetadata().withNewSpec().addAllToPorts(ports) + .withType("ClusterIP").endSpec().build(); } - private Service buildService(String name, String uid, List ports, Map labels) { - return buildService(name, uid, ports, labels, Map.of()); + private Service buildService(String name, String namespace, String uid, List ports, + Map labels) { + return buildService(name, namespace, uid, ports, labels, Map.of()); } } 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 index 9219c47ff..460af3885 100644 --- 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 @@ -88,18 +88,18 @@ void testAllNamespaces(CapturedOutput output) { 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.assertEquals(serviceInstancesSorted.get(0).getServiceId(), "service-a"); + Assertions.assertEquals(serviceInstancesSorted.get(0).getHost(), "service-a.a.svc.cluster.local"); + Assertions.assertEquals(serviceInstancesSorted.get(0).getPort(), 8887); + + Assertions.assertEquals(serviceInstancesSorted.get(1).getServiceId(), "service-a"); + Assertions.assertEquals(serviceInstancesSorted.get(1).getHost(), "service-a.c.svc.cluster.local"); + Assertions.assertEquals(serviceInstancesSorted.get(1).getPort(), 8889); Assertions.assertTrue(output.getOut().contains("discovering services in all namespaces")); } @@ -138,6 +138,42 @@ void testOneNamespace(CapturedOutput output) { Assertions.assertTrue(output.getOut().contains("discovering services in namespace : c")); } + @Test + void testSelectiveNamespaces(CapturedOutput output) { + + createService("a", "my-service", 8887); + createService("b", "my-service", 8888); + createService("c", "my-service", 8889); + + Environment environment = new MockEnvironment().withProperty("loadbalancer.client.name", "my-service"); + boolean allNamespaces = false; + Set selectiveNamespaces = Set.of("a", "b"); + + 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 serviceInstancesSorted = serviceInstances.get(0).stream() + .sorted(Comparator.comparing(ServiceInstance::getPort)).toList(); + Assertions.assertEquals(serviceInstancesSorted.size(), 2); + Assertions.assertEquals(serviceInstancesSorted.get(0).getServiceId(), "my-service"); + Assertions.assertEquals(serviceInstancesSorted.get(0).getHost(), "my-service.a.svc.cluster.local"); + Assertions.assertEquals(serviceInstancesSorted.get(0).getPort(), 8887); + + Assertions.assertEquals(serviceInstancesSorted.get(1).getServiceId(), "my-service"); + Assertions.assertEquals(serviceInstancesSorted.get(1).getHost(), "my-service.b.svc.cluster.local"); + Assertions.assertEquals(serviceInstancesSorted.get(1).getPort(), 8888); + + Assertions.assertTrue(output.getOut().contains("discovering services in selective namespaces : [a, b]")); + } + private void createService(String namespace, String name, int port) { Service service = new ServiceBuilder().withNewMetadata().withNamespace(namespace).withName(name).endMetadata() .withSpec(new ServiceSpecBuilder() 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 index 76ca6097e..92dfebaec 100644 --- 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 @@ -16,25 +16,65 @@ package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it; +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.ObjectMetaBuilder; 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 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 */ -final class Util { +public final class Util { private Util() { } - static Service createService(String namespace, String name, int port) { + public static Service service(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(); } + public static Endpoints endpoints(int port, String host, String namespace) { + return new EndpointsBuilder() + .withSubsets(new EndpointSubsetBuilder().withPorts(new EndpointPortBuilder().withPort(port).build()) + .withAddresses(new EndpointAddressBuilder().withIp(host).build()).build()) + .withMetadata(new ObjectMetaBuilder().withName("random-name").withNamespace(namespace).build()).build(); + } + + @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-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/mode/pod/AllNamespacesTest.java similarity index 78% rename from spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeAllNamespacesTest.java rename to spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/pod/AllNamespacesTest.java index 98a5611c8..c58b8c3c2 100644 --- 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/mode/pod/AllNamespacesTest.java @@ -14,17 +14,12 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it; +package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.mode.pod; 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; @@ -37,23 +32,19 @@ 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.it.Util; 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; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.LoadBalancerConfiguration; /** * @author wind57 @@ -62,7 +53,7 @@ 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 { +class AllNamespacesTest { private static final String SERVICE_A_URL = "http://service-a"; @@ -137,22 +128,11 @@ static void afterAll() { @Test void test() { - Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); - Service serviceB = Util.createService("b", "service-b", SERVICE_B_PORT); + Service serviceA = Util.service("a", "service-a", SERVICE_A_PORT); + Service serviceB = Util.service("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(); + Endpoints endpointsA = Util.endpoints(SERVICE_A_PORT, "127.0.0.1", "a"); + Endpoints endpointsB = Util.endpoints(SERVICE_B_PORT, "127.0.0.1", "b"); String endpointsAListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsA).build()); String endpointsBListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsB).build()); @@ -192,26 +172,18 @@ void test() { .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(); - } - - } + wireMockServer.verify(WireMock.exactly(1), WireMock + .getRequestedFor(WireMock.urlEqualTo("/api/v1/endpoints?fieldSelector=metadata.name%3Dservice-a"))); - @SpringBootApplication - static class Configuration { + wireMockServer.verify(WireMock.exactly(1), WireMock + .getRequestedFor(WireMock.urlEqualTo("/api/v1/endpoints?fieldSelector=metadata.name%3Dservice-b"))); - public static void main(String[] args) { - SpringApplication.run(ServiceModeAllNamespacesTest.Configuration.class); - } + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/a/services/service-a"))); + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/b/services/service-b"))); } } diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/pod/SelectiveNamespacesTest.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/pod/SelectiveNamespacesTest.java new file mode 100644 index 000000000..c41c1fe0e --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/pod/SelectiveNamespacesTest.java @@ -0,0 +1,233 @@ +/* + * 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.mode.pod; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsListBuilder; +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.test.context.SpringBootTest; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util; +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.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.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); + + @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); + + 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"); + + // 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(); + serviceCMockServer.stop(); + MOCKED_STATIC.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() { + + Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + Service serviceB = Util.service("b", "my-service", SERVICE_B_PORT); + Service serviceC = Util.service("c", "my-service", SERVICE_C_PORT); + + Endpoints endpointsA = Util.endpoints(SERVICE_A_PORT, "127.0.0.1", "a"); + Endpoints endpointsB = Util.endpoints(SERVICE_B_PORT, "127.0.0.1", "b"); + Endpoints endpointsC = Util.endpoints(SERVICE_C_PORT, "127.0.0.1", "c"); + + String serviceAJson = Serialization.asJson(serviceA); + String serviceBJson = Serialization.asJson(serviceB); + String serviceCJson = Serialization.asJson(serviceC); + + String endpointsAListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsA).build()); + String endpointsBListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsB).build()); + String endpointsCListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsC).build()); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/a/services/my-service")) + .willReturn(WireMock.aResponse().withBody(serviceAJson).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/my-service")) + .willReturn(WireMock.aResponse().withBody(serviceBJson).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/c/services/my-service")) + .willReturn(WireMock.aResponse().withBody(serviceCJson).withStatus(200))); + + wireMockServer.stubFor(WireMock + .get(WireMock.urlEqualTo("/api/v1/namespaces/a/endpoints?fieldSelector=metadata.name%3Dmy-service")) + .willReturn(WireMock.aResponse().withBody(endpointsAListAsString).withStatus(200))); + + wireMockServer.stubFor(WireMock + .get(WireMock.urlEqualTo("/api/v1/namespaces/b/endpoints?fieldSelector=metadata.name%3Dmy-service")) + .willReturn(WireMock.aResponse().withBody(endpointsBListAsString).withStatus(200))); + + wireMockServer.stubFor(WireMock + .get(WireMock.urlEqualTo("/api/v1/namespaces/c/endpoints?fieldSelector=metadata.name%3Dmy-service")) + .willReturn(WireMock.aResponse().withBody(endpointsCListAsString).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))); + + 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); + + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/a/services/my-service"))); + + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/b/services/my-service"))); + + // not triggered in namespace 'c' since that is not a selective namespace + wireMockServer.verify(WireMock.exactly(0), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/c/services/my-service"))); + + wireMockServer.verify(WireMock.exactly(1), WireMock.getRequestedFor( + WireMock.urlEqualTo("/api/v1/namespaces/a/endpoints?fieldSelector=metadata.name%3Dmy-service"))); + + wireMockServer.verify(WireMock.exactly(1), WireMock.getRequestedFor( + WireMock.urlEqualTo("/api/v1/namespaces/b/endpoints?fieldSelector=metadata.name%3Dmy-service"))); + + // not triggered in namespace 'c' since that is not a selective namespace + wireMockServer.verify(WireMock.exactly(0), WireMock.getRequestedFor( + WireMock.urlEqualTo("/api/v1/namespaces/c/endpoints?fieldSelector=metadata.name%3Dmy-service"))); + } + +} 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/mode/pod/SpecificNamespaceTest.java similarity index 62% rename from spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/PodModeSpecificNamespaceTest.java rename to spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/pod/SpecificNamespaceTest.java index fc5692de8..53c84fc75 100644 --- 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/mode/pod/SpecificNamespaceTest.java @@ -14,17 +14,12 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it; +package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.mode.pod; 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; @@ -37,34 +32,29 @@ 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.it.Util; 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; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.LoadBalancerConfiguration; /** * @author wind57 */ -@SpringBootTest( - properties = { "spring.cloud.kubernetes.loadbalancer.mode=POD", "spring.main.cloud-platform=KUBERNETES", +@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 { + classes = { LoadBalancerConfiguration.class, Configuration.class }) +class SpecificNamespaceTest { - private static final String SERVICE_A_URL = "http://service-a"; + private static final String SERVICE_A_URL = "http://my-service"; private static final int SERVICE_A_PORT = 8888; @@ -77,7 +67,7 @@ class PodModeSpecificNamespaceTest { private static WireMockServer serviceBMockServer; private static final MockedStatic MOCKED_STATIC = Mockito - .mockStatic(KubernetesServiceInstanceMapper.class); + .mockStatic(KubernetesServiceInstanceMapper.class); @Autowired private WebClient.Builder builder; @@ -102,11 +92,11 @@ static void beforeAll() { // 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("my-service", "a", "cluster.local")) + .thenReturn("localhost"); - MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-b", "b", "cluster.local")) - .thenReturn("localhost"); + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "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()); @@ -127,34 +117,23 @@ static void afterAll() { /** *
-	 *      - service-a is present in 'a' namespace
-	 *      - service-b is present in 'b' namespace
+	 *      - 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 service-a service is load balanced
+	 *      - 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() { - Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); - Service serviceB = Util.createService("b", "service-a", SERVICE_B_PORT); + Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + Service serviceB = Util.service("b", "my-service", 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(); + Endpoints endpointsA = Util.endpoints(SERVICE_A_PORT, "127.0.0.1", "a"); + Endpoints endpointsB = Util.endpoints(SERVICE_B_PORT, "127.0.0.1", "b"); String endpointsAListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsA).build()); String endpointsBListAsString = Serialization.asJson(new EndpointsListBuilder().withItems(endpointsB).build()); @@ -162,55 +141,46 @@ void test() { 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")) + wireMockServer.stubFor(WireMock + .get(WireMock.urlEqualTo("/api/v1/namespaces/a/endpoints?fieldSelector=metadata.name%3Dmy-service")) .willReturn(WireMock.aResponse().withBody(endpointsAListAsString).withStatus(200))); - wireMockServer - .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/endpoints?fieldSelector=metadata.name%3Dservice-b")) + wireMockServer.stubFor(WireMock + .get(WireMock.urlEqualTo("/api/v1/namespaces/b/endpoints?fieldSelector=metadata.name%3Dmy-service")) .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/a/services/my-service")) + .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))); + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/my-service")) + .willReturn(WireMock.aResponse().withBody(serviceBString).withStatus(200))); serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) - .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) - .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + .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(); + .bodyToMono(String.class).block(); Assertions.assertThat(serviceAResult).isEqualTo("service-a-reached"); CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory - .getIfAvailable().getProvider("service-a", ServiceInstanceListSupplier.class).getIfAvailable(); + .getIfAvailable().getProvider("my-service", ServiceInstanceListSupplier.class).getIfAvailable(); Assertions.assertThat(supplier.getDelegate().getClass()) - .isSameAs(DiscoveryClientServiceInstanceListSupplier.class); - } - - @TestConfiguration - static class LoadBalancerConfiguration { + .isSameAs(DiscoveryClientServiceInstanceListSupplier.class); - @Bean - @LoadBalanced - WebClient.Builder client() { - return WebClient.builder(); - } + wireMockServer.verify(WireMock.exactly(1), WireMock.getRequestedFor( + WireMock.urlEqualTo("/api/v1/namespaces/a/endpoints?fieldSelector=metadata.name%3Dmy-service"))); - } - - @SpringBootApplication - static class Configuration { + wireMockServer.verify(WireMock.exactly(0), WireMock.getRequestedFor( + WireMock.urlEqualTo("/api/v1/namespaces/b/endpoints?fieldSelector=metadata.name%3Dmy-service"))); - public static void main(String[] args) { - SpringApplication.run(ServiceModeAllNamespacesTest.Configuration.class); - } + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/a/services/my-service"))); + wireMockServer.verify(WireMock.exactly(0), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/b/services/me-service"))); } - } 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/mode/service/AllNamespacesTest.java similarity index 79% rename from spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeAllNamespacesTest.java rename to spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/service/AllNamespacesTest.java index cf5750e65..77831735b 100644 --- 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/mode/service/AllNamespacesTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it; +package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.mode.service; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; @@ -26,28 +26,27 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; 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.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; import org.springframework.cloud.kubernetes.fabric8.loadbalancer.Fabric8ServicesListSupplier; +import org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util; 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; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.LoadBalancerConfiguration; /** * @author wind57 @@ -56,7 +55,8 @@ 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 { +@ExtendWith(OutputCaptureExtension.class) +class AllNamespacesTest { private static final String SERVICE_A_URL = "http://service-a"; @@ -129,10 +129,10 @@ static void afterAll() { * */ @Test - void test() { + void test(CapturedOutput output) { - Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); - Service serviceB = Util.createService("b", "service-b", SERVICE_B_PORT); + Service serviceA = Util.service("a", "service-a", SERVICE_A_PORT); + Service serviceB = Util.service("b", "service-b", SERVICE_B_PORT); String serviceListAJson = Serialization.asJson(new ServiceListBuilder().withItems(serviceA).build()); String serviceListBJson = Serialization.asJson(new ServiceListBuilder().withItems(serviceB).build()); @@ -159,29 +159,23 @@ void test() { .bodyToMono(String.class).block(); Assertions.assertThat(serviceBResult).isEqualTo("service-b-reached"); - CachingServiceInstanceListSupplier supplier = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + CachingServiceInstanceListSupplier supplierA = (CachingServiceInstanceListSupplier) loadBalancerClientFactory .getIfAvailable().getProvider("service-a", ServiceInstanceListSupplier.class).getIfAvailable(); - Assertions.assertThat(supplier.getDelegate().getClass()).isSameAs(Fabric8ServicesListSupplier.class); - } - - @TestConfiguration - static class LoadBalancerConfiguration { + Assertions.assertThat(supplierA.getDelegate().getClass()).isSameAs(Fabric8ServicesListSupplier.class); - @Bean - @LoadBalanced - WebClient.Builder client() { - return WebClient.builder(); - } - - } + CachingServiceInstanceListSupplier supplierB = (CachingServiceInstanceListSupplier) loadBalancerClientFactory + .getIfAvailable().getProvider("service-b", ServiceInstanceListSupplier.class).getIfAvailable(); + Assertions.assertThat(supplierB.getDelegate().getClass()).isSameAs(Fabric8ServicesListSupplier.class); - @SpringBootApplication - static class Configuration { + Assertions.assertThat(output.getOut()).contains("serviceID : service-a"); + Assertions.assertThat(output.getOut()).contains("serviceID : service-b"); + Assertions.assertThat(output.getOut()).contains("discovering services in all namespaces"); - public static void main(String[] args) { - SpringApplication.run(Configuration.class); - } + wireMockServer.verify(WireMock.exactly(1), WireMock + .getRequestedFor(WireMock.urlEqualTo("/api/v1/services?fieldSelector=metadata.name%3Dservice-a"))); + wireMockServer.verify(WireMock.exactly(1), WireMock + .getRequestedFor(WireMock.urlEqualTo("/api/v1/services?fieldSelector=metadata.name%3Dservice-b"))); } } diff --git a/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/service/SelectiveNamespacesTest.java b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/service/SelectiveNamespacesTest.java new file mode 100644 index 000000000..efeb89010 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/service/SelectiveNamespacesTest.java @@ -0,0 +1,207 @@ +/* + * 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.mode.service; + +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.junit.jupiter.api.extension.ExtendWith; +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.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; +import org.springframework.cloud.kubernetes.fabric8.loadbalancer.Fabric8ServicesListSupplier; +import org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util; +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.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.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 }) +@ExtendWith(OutputCaptureExtension.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); + + @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); + + 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"); + + // 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(); + serviceCMockServer.stop(); + MOCKED_STATIC.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 'SERVICE'
+	 *
+	 *      - as such, only service in namespace a and b are load balanced
+	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the SERVICE mode.
+	 * 
+ */ + @Test + void test(CapturedOutput output) { + + Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + Service serviceB = Util.service("b", "my-service", SERVICE_B_PORT); + Service serviceC = Util.service("c", "my-service", SERVICE_C_PORT); + + String serviceAJson = Serialization.asJson(serviceA); + String serviceBJson = Serialization.asJson(serviceB); + String serviceCJson = Serialization.asJson(serviceC); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/a/services/my-service")) + .willReturn(WireMock.aResponse().withBody(serviceAJson).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/my-service")) + .willReturn(WireMock.aResponse().withBody(serviceBJson).withStatus(200))); + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/c/services/my-service")) + .willReturn(WireMock.aResponse().withBody(serviceCJson).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))); + + 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(Fabric8ServicesListSupplier.class); + + Assertions.assertThat(output.getOut()).contains("serviceID : my-service"); + Assertions.assertThat(output.getOut()).contains("discovering services in selective namespaces : [a, b]"); + + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/a/services/my-service"))); + + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/b/services/my-service"))); + + // not triggered in namespace 'c' since that is not a selective namespace + wireMockServer.verify(WireMock.exactly(0), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/c/services/my-service"))); + } + +} 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/mode/service/SpecificNamespaceTest.java similarity index 67% rename from spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/ServiceModeSpecificNamespaceTest.java rename to spring-cloud-kubernetes-fabric8-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/fabric8/loadbalancer/it/mode/service/SpecificNamespaceTest.java index 5369e6130..687a3cbb1 100644 --- 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/mode/service/SpecificNamespaceTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it; +package org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.mode.service; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; @@ -25,39 +25,40 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; 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.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.loadbalancer.KubernetesServiceInstanceMapper; import org.springframework.cloud.kubernetes.fabric8.loadbalancer.Fabric8ServicesListSupplier; +import org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util; 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; +import static org.springframework.cloud.kubernetes.fabric8.loadbalancer.it.Util.Configuration; +import static org.springframework.cloud.kubernetes.fabric8.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 ServiceModeSpecificNamespaceTest { + 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 }) +@ExtendWith(OutputCaptureExtension.class) +class SpecificNamespaceTest { - private static final String SERVICE_A_URL = "http://service-a"; + private static final String MY_SERVICE_URL = "http://my-service"; private static final int SERVICE_A_PORT = 8888; @@ -70,7 +71,7 @@ class ServiceModeSpecificNamespaceTest { private static WireMockServer serviceBMockServer; private static final MockedStatic MOCKED_STATIC = Mockito - .mockStatic(KubernetesServiceInstanceMapper.class); + .mockStatic(KubernetesServiceInstanceMapper.class); @Autowired private WebClient.Builder builder; @@ -95,11 +96,11 @@ static void beforeAll() { // 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("my-service", "a", "cluster.local")) + .thenReturn("localhost"); - MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("service-b", "b", "cluster.local")) - .thenReturn("localhost"); + MOCKED_STATIC.when(() -> KubernetesServiceInstanceMapper.createHost("my-service", "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()); @@ -120,64 +121,54 @@ static void afterAll() { /** *
-	 *      - service-a is present in 'a' namespace
-	 *      - service-a is present in 'b' namespace
+	 *      - my-service is present in 'a' namespace
+	 *      - my-service 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
+	 *      - as such, only my-service in namespace a is load balanced
 	 *      - we also assert the type of ServiceInstanceListSupplier corresponding to the SERVICE mode.
 	 * 
*/ @Test - void test() { + void test(CapturedOutput output) { - Service serviceA = Util.createService("a", "service-a", SERVICE_A_PORT); - Service serviceB = Util.createService("b", "service-a", SERVICE_B_PORT); + Service serviceA = Util.service("a", "my-service", SERVICE_A_PORT); + Service serviceB = Util.service("b", "my-service", 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")) + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/a/services/my-service")) .willReturn(WireMock.aResponse().withBody(serviceAJson).withStatus(200))); - wireMockServer - .stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/service-a")) + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/api/v1/namespaces/b/services/my-service")) .willReturn(WireMock.aResponse().withBody(serviceBJson).withStatus(200))); serviceAMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) - .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); + .willReturn(WireMock.aResponse().withBody("service-a-reached").withStatus(200))); serviceBMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/")) - .willReturn(WireMock.aResponse().withBody("service-b-reached").withStatus(200))); + .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(); + String serviceAResult = builder.baseUrl(MY_SERVICE_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(); + .getIfAvailable().getProvider("my-service", ServiceInstanceListSupplier.class).getIfAvailable(); Assertions.assertThat(supplier.getDelegate().getClass()).isSameAs(Fabric8ServicesListSupplier.class); - } - - @TestConfiguration - static class LoadBalancerConfiguration { - @Bean - @LoadBalanced - WebClient.Builder client() { - return WebClient.builder(); - } - - } + Assertions.assertThat(output.getOut()).contains("serviceID : my-service"); + Assertions.assertThat(output.getOut()).contains("discovering services in namespace : a"); - @SpringBootApplication - static class Configuration { + // was called in namespace 'a' + wireMockServer.verify(WireMock.exactly(1), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/a/services/my-service"))); - public static void main(String[] args) { - SpringApplication.run(ServiceModeAllNamespacesTest.Configuration.class); - } + // was not called in namespace 'b' + wireMockServer.verify(WireMock.exactly(0), + WireMock.getRequestedFor(WireMock.urlEqualTo("/api/v1/namespaces/b/services/my-service"))); }