diff --git a/docs/src/main/asciidoc/discovery-client.adoc b/docs/src/main/asciidoc/discovery-client.adoc index 441866ac..53b8df5e 100644 --- a/docs/src/main/asciidoc/discovery-client.adoc +++ b/docs/src/main/asciidoc/discovery-client.adoc @@ -6,6 +6,8 @@ This client lets you query Kubernetes endpoints (see https://kubernetes.io/docs/ A service is typically exposed by the Kubernetes API server as a collection of endpoints that represent `http` and `https` addresses and that a client can access from a Spring Boot application running as a pod. +DiscoveryClient can also find services of type `ExternalName` (see https://kubernetes.io/docs/concepts/services-networking/service/#externalname[ExternalName services]). At the moment, external name support type of services is only available if the following property `spring.cloud.kubernetes.discovery.include-external-name-services` is set to `true` and only in the `fabric8` implementation. In a later release, support will be added for the kubernetes native client also. + This is something that you get for free by adding the following dependency inside your project: ==== @@ -112,6 +114,8 @@ Please make sure to configure your service and/or application accordingly. By default all of the ports and their names will be added to the metadata of the `ServiceInstance`. +As said before, if you want to get the list of `ServiceInstance` to also include the `ExternalName` type services, you need to enable that support via: `spring.cloud.kubernetes.discovery.include-external-name-services=true`. As such, when calling `DiscoveryClient::getInstances` those will be returned also. You can distinguish between `ExternalName` and any other types by inspecting `ServiceInstance::getMetadata` and lookup for a field called `type`. This will be the type of the service returned : `ExternalName`/`ClusterIP`, etc. + If, for any reason, you need to disable the `DiscoveryClient`, you can set the following property in `application.properties`: ==== diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DefaultKubernetesServiceInstance.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DefaultKubernetesServiceInstance.java index 5f55233c..e73342c7 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DefaultKubernetesServiceInstance.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DefaultKubernetesServiceInstance.java @@ -102,6 +102,10 @@ public record DefaultKubernetesServiceInstance(String instanceId, String service } private URI createUri(String scheme, String host, int port) { + // assume ExternalName type of service + if (port == -1) { + return URI.create(host); + } return URI.create(scheme + "://" + host + ":" + port); } } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java index ba725854..a6565d07 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryConstants.java @@ -67,4 +67,14 @@ public final class KubernetesDiscoveryConstants { */ public static final String ENDPOINT_SLICE = "EndpointSlice"; + /** + * ExternalName type of service. + */ + public static final String EXTERNAL_NAME = "ExternalName"; + + /** + * Type of the service. + */ + public static final String SERVICE_TYPE = "type"; + } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java index 6001e79d..69d3b101 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryProperties.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.Set; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; import org.springframework.boot.context.properties.bind.DefaultValue; import static org.springframework.cloud.client.discovery.DiscoveryClient.DEFAULT_ORDER; @@ -43,6 +44,8 @@ import static org.springframework.cloud.client.discovery.DiscoveryClient.DEFAULT * @param primaryPortName If set then the port with a given name is used as primary when * multiple ports are defined for a service. * @param useEndpointSlices use EndpointSlice instead of Endpoints + * @param includeExternalNameServices should the discovery also search for services that + * have "type: ExternalName" in their spec. */ // @formatter:off @ConfigurationProperties("spring.cloud.kubernetes.discovery") @@ -56,14 +59,31 @@ public record KubernetesDiscoveryProperties( @DefaultValue Map serviceLabels, String primaryPortName, @DefaultValue Metadata metadata, @DefaultValue("" + DEFAULT_ORDER) int order, - boolean useEndpointSlices) { + boolean useEndpointSlices, + boolean includeExternalNameServices) { // @formatter:on + @ConstructorBinding + public KubernetesDiscoveryProperties { + + } + + public KubernetesDiscoveryProperties(@DefaultValue("true") boolean enabled, boolean allNamespaces, + @DefaultValue Set namespaces, @DefaultValue("true") boolean waitCacheReady, + @DefaultValue("60") long cacheLoadingTimeoutSeconds, boolean includeNotReadyAddresses, String filter, + @DefaultValue({ "443", "8443" }) Set knownSecurePorts, + @DefaultValue Map serviceLabels, String primaryPortName, @DefaultValue Metadata metadata, + @DefaultValue("" + DEFAULT_ORDER) int order, boolean useEndpointSlices) { + this(enabled, allNamespaces, namespaces, waitCacheReady, cacheLoadingTimeoutSeconds, includeNotReadyAddresses, + filter, knownSecurePorts, serviceLabels, primaryPortName, metadata, order, useEndpointSlices, false); + } + /** * Default instance. */ public static final KubernetesDiscoveryProperties DEFAULT = new KubernetesDiscoveryProperties(true, false, Set.of(), - true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); + true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false, + false); /** * @param addLabels include labels as metadata diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java index 55c29f64..f3d62f1c 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryPropertiesTests.java @@ -53,6 +53,7 @@ class KubernetesDiscoveryPropertiesTests { assertThat(props.primaryPortName()).isNull(); assertThat(props.order()).isZero(); assertThat(props.useEndpointSlices()).isFalse(); + assertThat(props.includeExternalNameServices()).isFalse(); }); } @@ -64,7 +65,8 @@ class KubernetesDiscoveryPropertiesTests { "spring.cloud.kubernetes.discovery.metadata.labelsPrefix=labelsPrefix", "spring.cloud.kubernetes.discovery.use-endpoint-slices=true", "spring.cloud.kubernetes.discovery.namespaces[0]=ns1", - "spring.cloud.kubernetes.discovery.namespaces[1]=ns2") + "spring.cloud.kubernetes.discovery.namespaces[1]=ns2", + "spring.cloud.kubernetes.discovery.include-external-name-services=true") .run(context -> { KubernetesDiscoveryProperties props = context.getBean(KubernetesDiscoveryProperties.class); assertThat(props).isNotNull(); @@ -84,6 +86,7 @@ class KubernetesDiscoveryPropertiesTests { assertThat(props.primaryPortName()).isNull(); assertThat(props.order()).isZero(); assertThat(props.useEndpointSlices()).isTrue(); + assertThat(props.includeExternalNameServices()).isTrue(); }); } 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 48a42a2a..00751474 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 @@ -37,14 +37,16 @@ import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.core.log.LogAccessor; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.EXTERNAL_NAME; import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.addresses; import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.endpoints; import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.endpointsPort; import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.serviceInstance; import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.serviceMetadata; +import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.services; /** - * Kubernetes implementation of {@link DiscoveryClient}. + * Fabric8 Kubernetes implementation of {@link DiscoveryClient}. * * @author Ioannis Canellos * @author Tim Ysewyn @@ -110,6 +112,20 @@ public class KubernetesDiscoveryClient implements DiscoveryClient, EnvironmentAw instances.addAll(getNamespaceServiceInstances(es, serviceId)); } + if (properties.includeExternalNameServices()) { + LOG.debug(() -> "Searching for 'ExternalName' type of services with serviceId : " + serviceId); + List services = services(properties, client, namespaceProvider, + s -> s.getSpec().getType().equals(EXTERNAL_NAME), Map.of("metadata.name", serviceId), + "fabric8-discovery"); + for (Service service : services) { + Map serviceMetadata = serviceMetadata(serviceId, service, properties, List.of(), + service.getMetadata().getNamespace()); + ServiceInstance externalNameServiceInstance = serviceInstance(null, service, null, -1, serviceId, + serviceMetadata, service.getMetadata().getNamespace()); + instances.add(externalNameServiceInstance); + } + } + return instances; } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtils.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtils.java index d84b1cf3..7cb5dd77 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtils.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.EndpointAddress; @@ -30,10 +31,12 @@ import io.fabric8.kubernetes.api.model.Endpoints; import io.fabric8.kubernetes.api.model.EndpointsList; import io.fabric8.kubernetes.api.model.ObjectReference; import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.FilterNested; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; import jakarta.annotation.Nullable; import org.apache.commons.logging.LogFactory; @@ -52,6 +55,7 @@ import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesD import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.HTTPS; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.NAMESPACE_METADATA_KEY; import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.PRIMARY_PORT_NAME_LABEL_KEY; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.SERVICE_TYPE; /** * @author wind57 @@ -159,11 +163,14 @@ final class KubernetesDiscoveryClientUtils { .filter(port -> StringUtils.hasText(port.getName())) .collect(toMap(EndpointPort::getName, port -> Integer.toString(port.getPort()))); Map portMetadata = keysWithPrefix(ports, properties.metadata().portsPrefix()); - LOG.debug(() -> "Adding port metadata: " + portMetadata + " for serviceId : " + serviceId); + if (!portMetadata.isEmpty()) { + LOG.debug(() -> "Adding port metadata: " + portMetadata + " for serviceId : " + serviceId); + } serviceMetadata.putAll(portMetadata); } serviceMetadata.put(NAMESPACE_METADATA_KEY, namespace); + serviceMetadata.put(SERVICE_TYPE, service.getSpec().getType()); return serviceMetadata; } @@ -227,19 +234,76 @@ final class KubernetesDiscoveryClientUtils { return addresses; } - static ServiceInstance serviceInstance(ServicePortSecureResolver servicePortSecureResolver, Service service, - EndpointAddress endpointAddress, int endpointPort, String serviceId, Map serviceMetadata, - String namespace) { + static ServiceInstance serviceInstance(@Nullable ServicePortSecureResolver servicePortSecureResolver, + Service service, @Nullable EndpointAddress endpointAddress, int endpointPort, String serviceId, + Map serviceMetadata, String namespace) { // instanceId is usually the pod-uid as seen in the .metadata.uid - String instanceId = Optional.ofNullable(endpointAddress.getTargetRef()).map(ObjectReference::getUid) - .orElse(null); + String instanceId = Optional.ofNullable(endpointAddress).map(EndpointAddress::getTargetRef) + .map(ObjectReference::getUid).orElseGet(() -> service.getMetadata().getUid()); - boolean secured = servicePortSecureResolver - .resolve(new ServicePortSecureResolver.Input(endpointPort, service.getMetadata().getName(), - service.getMetadata().getLabels(), service.getMetadata().getAnnotations())); + boolean secured; + if (servicePortSecureResolver == null) { + secured = false; + } + else { + secured = servicePortSecureResolver + .resolve(new ServicePortSecureResolver.Input(endpointPort, service.getMetadata().getName(), + service.getMetadata().getLabels(), service.getMetadata().getAnnotations())); + } + + String host = Optional.ofNullable(endpointAddress).map(EndpointAddress::getIp) + .orElseGet(() -> service.getSpec().getExternalName()); + + return new DefaultKubernetesServiceInstance(instanceId, serviceId, host, endpointPort, serviceMetadata, secured, + namespace, null); + } + + static List services(KubernetesDiscoveryProperties properties, KubernetesClient client, + KubernetesNamespaceProvider namespaceProvider, Predicate predicate, + Map fieldFilters, String target) { + + List services; + + if (properties.allNamespaces()) { + LOG.debug(() -> "discovering services in all namespaces"); + services = filteredServices(client.services().inAnyNamespace().withNewFilter(), properties, predicate, + fieldFilters); + } + else if (!properties.namespaces().isEmpty()) { + LOG.debug(() -> "discovering services in namespaces : " + properties.namespaces()); + List inner = new ArrayList<>(properties.namespaces().size()); + properties.namespaces().forEach( + namespace -> inner.addAll(filteredServices(client.services().inNamespace(namespace).withNewFilter(), + properties, predicate, fieldFilters))); + services = inner; + } + else { + String namespace = Fabric8Utils.getApplicationNamespace(client, null, target, namespaceProvider); + LOG.debug(() -> "discovering services in namespace : " + namespace); + services = filteredServices(client.services().inNamespace(namespace).withNewFilter(), properties, predicate, + fieldFilters); + } + + return services; + } + + /** + * serviceName can be null, in which case, such a filter will not be applied. + */ + private static List filteredServices( + FilterNested>> filterNested, + KubernetesDiscoveryProperties properties, Predicate predicate, + @Nullable Map fieldFilters) { + + FilterNested>> partial = filterNested + .withLabels(properties.serviceLabels()); + + if (fieldFilters != null) { + partial = partial.withFields(fieldFilters); + } + + return partial.endFilter().list().getItems().stream().filter(predicate).toList(); - return new DefaultKubernetesServiceInstance(instanceId, serviceId, endpointAddress.getIp(), endpointPort, - serviceMetadata, secured, namespace, null); } private static Optional fromMap(Map existingPorts, String key, String message) { diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java index a289b42c..52a1aa20 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java @@ -302,7 +302,7 @@ abstract class Fabric8EndpointsAndEndpointSlicesTests { Endpoints endpoints = new EndpointsBuilder() .withMetadata(new ObjectMetaBuilder().withLabels(labels).withName("endpoints-" + podName).build()) .withSubsets(List.of(endpointSubset)).build(); - mockClient().endpoints().inNamespace(namespace).create(endpoints); + mockClient().endpoints().inNamespace(namespace).resource(endpoints).create(); } static void endpointSlice(String namespace, Map labels, String podName) { diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsTests.java new file mode 100644 index 00000000..c9a01a52 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsTests.java @@ -0,0 +1,295 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +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.ServiceSpecBuilder; +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.Test; + +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.mock.env.MockEnvironment; + +import static org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientUtils.services; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +class Fabric8KubernetesDiscoveryClientUtilsTests { + + private static KubernetesClient client; + + @AfterEach + void afterEach() { + client.services().inAnyNamespace().delete(); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "B"
+	 *     - no filters are applied, so both are present
+	 * 
+ */ + @Test + void testServicesAllNamespacesNoFilters() { + boolean allNamespaces = true; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), + true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of()); + service("serviceB", "B", Map.of()); + + List result = services(properties, client, null, x -> true, null, "fabric8-discovery"); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.stream().map(s -> s.getMetadata().getName()).sorted().toList(), + List.of("serviceA", "serviceB")); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "B"
+	 *     - we search only for "serviceA" filter, so only one is returned
+	 * 
+ */ + @Test + void testServicesAllNamespacesNameFilter() { + boolean allNamespaces = true; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), + true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of()); + service("serviceB", "B", Map.of()); + + List result = services(properties, client, null, x -> true, Map.of("metadata.name", "serviceA"), + "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "B"
+	 *     - we search with a filter where a label with name "letter" and value "b" is present
+	 * 
+ */ + @Test + void testServicesAllNamespacesPredicateFilter() { + boolean allNamespaces = true; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), + true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of("letter", "a")); + service("serviceB", "B", Map.of("letter", "b")); + + List result = services(properties, client, null, + x -> x.getMetadata().getLabels().equals(Map.of("letter", "b")), null, "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceB"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - selective namespaces : [A, B]
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "B"
+	 *     - serviceC present in namespace "C"
+	 *     - we search in namespaces [A, B], as such two services are returned
+	 * 
+ */ + @Test + void testServicesSelectiveNamespacesNoFilters() { + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, + Set.of("A", "B"), true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of()); + service("serviceB", "B", Map.of()); + service("serviceC", "C", Map.of()); + + List result = services(properties, client, null, x -> true, null, "fabric8-discovery"); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.stream().map(x -> x.getMetadata().getName()).sorted().toList(), + List.of("serviceA", "serviceB")); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - selective namespaces : [A, B]
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "B"
+	 *     - serviceC present in namespace "C"
+	 *     - we search in namespaces [A, B] with name filter = "serviceA", so we get a single result
+	 * 
+ */ + @Test + void testServicesSelectiveNamespacesNameFilter() { + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, + Set.of("A", "B"), true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of()); + service("serviceB", "B", Map.of()); + service("serviceC", "C", Map.of()); + + List result = services(properties, client, null, x -> true, Map.of("metadata.name", "serviceA"), + "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - selective namespaces : [A, B]
+	 *     - serviceA present in namespace "A" with labels [letter, a]
+	 *     - serviceB present in namespace "B" with labels [letter, b]
+	 *     - serviceC present in namespace "C" with labels [letter, c]
+	 *     - we search in namespaces [A, B] with predicate filter = [letter, b], so we get a single result
+	 * 
+ */ + @Test + void testServicesSelectiveNamespacesPredicateFilter() { + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, + Set.of("A", "B"), true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of("letter", "a")); + service("serviceB", "B", Map.of("letter", "b")); + service("serviceC", "C", Map.of("letter", "c")); + + List result = services(properties, client, null, + x -> x.getMetadata().getLabels().equals(Map.of("letter", "b")), null, "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceB"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - selective namespaces : []
+	 *     - namespace from kubernetes namespace provider = [A]
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "B"
+	 *     - serviceC present in namespace "C"
+	 *     - we search in namespaces [A], as such we get one service
+	 * 
+ */ + @Test + void testServicesNamespaceProviderNoFilters() { + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), + true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of()); + service("serviceB", "B", Map.of()); + service("serviceC", "C", Map.of()); + + List result = services(properties, client, namespaceProvider("A"), x -> true, null, + "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - selective namespaces : []
+	 *     - namespace from kubernetes namespace provider = [A]
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "A"
+	 *     - we search in namespaces [A] with name filter = "serviceA", so we get a single result
+	 * 
+ */ + @Test + void testServicesNamespaceProviderNameFilter() { + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), + true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of()); + service("serviceB", "A", Map.of()); + + List result = services(properties, client, namespaceProvider("A"), x -> true, + Map.of("metadata.name", "serviceA"), "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - selective namespaces : []
+	 *     - namespace from kubernetes namespace provider = [A]
+	 *     - serviceA present in namespace "A"
+	 *     - serviceB present in namespace "A"
+	 *     - we search in namespaces [A] with predicate filter = [letter, b], so we get a single result
+	 * 
+ */ + @Test + void testServicesNamespaceProviderPredicateFilter() { + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), + true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + service("serviceA", "A", Map.of("letter", "a")); + service("serviceB", "A", Map.of("letter", "b")); + + List result = services(properties, client, namespaceProvider("A"), + x -> x.getMetadata().getLabels().equals(Map.of("letter", "b")), null, "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceB"); + } + + @Test + void testExternalName() { + Service service = new ServiceBuilder() + .withSpec(new ServiceSpecBuilder().withType("ExternalName").withExternalName("k8s-spring").build()) + .withNewMetadata().withName("external-name-service").and().build(); + client.services().inNamespace("test").resource(service).create(); + + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), + true, 60L, false, "", Set.of(), Map.of(), "", null, 0, false); + + List result = services(properties, client, namespaceProvider("test"), + x -> x.getSpec().getType().equals("ExternalName"), Map.of(), "fabric8-discovery"); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "external-name-service"); + } + + private void service(String name, String namespace, Map labels) { + Service service = new ServiceBuilder().withNewMetadata().withName(name).withLabels(labels) + .withNamespace(namespace).and().build(); + client.services().inNamespace(namespace).resource(service).create(); + } + + private KubernetesNamespaceProvider namespaceProvider(String namespace) { + MockEnvironment mockEnvironment = new MockEnvironment(); + mockEnvironment.setProperty("spring.cloud.kubernetes.client.namespace", namespace); + return new KubernetesNamespaceProvider(mockEnvironment); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java index 0e1390e6..19908c9f 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java @@ -32,6 +32,7 @@ import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.FilterNested; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; @@ -91,7 +92,7 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); - assertThat(instances.get(0).getMetadata()).isEqualTo(Map.of("k8s_namespace", "ns")); + assertThat(instances.get(0).getMetadata()).isEqualTo(Map.of("k8s_namespace", "ns", "type", "ClusterIP")); } @Test @@ -111,7 +112,7 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); assertThat(instances.get(0).getMetadata()).containsOnly(entry("l1", "v1"), entry("l2", "v2"), - entry("k8s_namespace", "ns")); + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); } @Test @@ -131,7 +132,7 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); assertThat(instances.get(0).getMetadata()).containsOnly(entry("l_l1", "v1"), entry("l_l2", "v2"), - entry("k8s_namespace", "ns")); + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); } @Test @@ -151,7 +152,7 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); assertThat(instances.get(0).getMetadata()).containsOnly(entry("a1", "v1"), entry("a2", "v2"), - entry("k8s_namespace", "ns")); + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); } @Test @@ -171,7 +172,7 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); assertThat(instances.get(0).getMetadata()).containsOnly(entry("a_a1", "v1"), entry("a_a2", "v2"), - entry("k8s_namespace", "ns")); + entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); } @Test @@ -190,7 +191,8 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); - assertThat(instances.get(0).getMetadata()).containsOnly(entry("http", "80"), entry("k8s_namespace", "test")); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("http", "80"), entry("k8s_namespace", "test"), + entry("type", "ClusterIP")); } @Test @@ -209,7 +211,8 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); - assertThat(instances.get(0).getMetadata()).containsOnly(entry("p_http", "80"), entry("k8s_namespace", "ns")); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("p_http", "80"), entry("k8s_namespace", "ns"), + entry("type", "ClusterIP")); } @Test @@ -229,14 +232,15 @@ class KubernetesDiscoveryClientFilterMetadataTest { List instances = discoveryClient.getInstances(serviceId); assertThat(instances).hasSize(1); assertThat(instances.get(0).getMetadata()).containsOnly(entry("a_a1", "an1"), entry("a_a2", "an2"), - entry("l_l1", "la1"), entry("p_http", "80"), entry("k8s_namespace", "ns")); + entry("l_l1", "la1"), entry("p_http", "80"), entry("k8s_namespace", "ns"), entry("type", "ClusterIP")); } private void setupServiceWithLabelsAndAnnotationsAndPorts(String serviceId, String namespace, Map labels, Map annotations, Map ports) { - Service service = new ServiceBuilder().withNewMetadata().withNamespace(namespace).withLabels(labels) - .withAnnotations(annotations).endMetadata().withNewSpec().withPorts(getServicePorts(ports)).endSpec() - .build(); + Service service = new ServiceBuilder() + .withSpec(new ServiceSpecBuilder().withType("ClusterIP").withPorts(getServicePorts(ports)).build()) + .withNewMetadata().withNamespace(namespace).withLabels(labels).withAnnotations(annotations) + .endMetadata().build(); when(serviceOperation.withName(serviceId)).thenReturn(serviceResource); when(serviceResource.get()).thenReturn(service); when(CLIENT.services()).thenReturn(serviceOperation); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java index 404cdcbc..3754a264 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java @@ -27,6 +27,7 @@ import io.fabric8.kubernetes.api.model.EndpointsBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; +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; @@ -73,8 +74,8 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace("test") - .withLabels(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint").withNamespace("test").withLabels(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -99,8 +100,9 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace("test") - .withLabels(labels).withAnnotations(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint").withNamespace("test").withLabels(labels).withAnnotations(labels) + .endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -207,8 +209,8 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace("test") - .withLabels(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint").withNamespace("test").withLabels(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -320,10 +322,12 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoints1).create(); mockClient.endpoints().inNamespace("test2").resource(endPoints2).create(); - Service service1 = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace("test") + Service service1 = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint").withNamespace("test") .withLabels(Collections.singletonMap("l", "v")).endMetadata().build(); - Service service2 = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace("test2") + Service service2 = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint").withNamespace("test2") .withLabels(Collections.singletonMap("l", "v")).endMetadata().build(); mockClient.services().inNamespace("test").resource(service1).create(); @@ -375,8 +379,9 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint1).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint2").withNamespace("test") - .withLabels(labels).withAnnotations(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint2").withNamespace("test").withLabels(labels) + .withAnnotations(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -405,8 +410,9 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint1).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint3").withNamespace("test") - .withLabels(labels).withAnnotations(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint3").withNamespace("test").withLabels(labels) + .withAnnotations(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -435,8 +441,9 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint1).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint4").withNamespace("test") - .withLabels(labels).withAnnotations(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint4").withNamespace("test").withLabels(labels) + .withAnnotations(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -464,8 +471,9 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint1).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint5").withNamespace("test") - .withLabels(labels).withAnnotations(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint5").withNamespace("test").withLabels(labels) + .withAnnotations(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -493,8 +501,9 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint1).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint5").withNamespace("test") - .withLabels(labels).withAnnotations(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint5").withNamespace("test").withLabels(labels) + .withAnnotations(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); @@ -520,8 +529,9 @@ class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint1).create(); - Service service = new ServiceBuilder().withNewMetadata().withName("endpoint5").withNamespace("test") - .withLabels(labels).withAnnotations(labels).endMetadata().build(); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()) + .withNewMetadata().withName("endpoint5").withNamespace("test").withLabels(labels) + .withAnnotations(labels).endMetadata().build(); mockClient.services().inNamespace("test").resource(service).create(); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTests.java index 1252746f..b6f4d32a 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTests.java @@ -24,6 +24,9 @@ import io.fabric8.kubernetes.api.model.Endpoints; import io.fabric8.kubernetes.api.model.EndpointsBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; 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.ServiceSpecBuilder; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; @@ -35,6 +38,8 @@ 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.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; /** @@ -60,6 +65,7 @@ class KubernetesDiscoveryClientTests { @AfterEach void afterEach() { client.endpoints().inAnyNamespace().delete(); + client.services().inAnyNamespace().delete(); } /** @@ -226,7 +232,7 @@ class KubernetesDiscoveryClientTests { Set namespaces = Set.of(); Map serviceLabels = Map.of(); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, namespaces, - true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false); + true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false, false); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null); List result = discoveryClient.getEndPointsList("serviceId"); @@ -276,7 +282,7 @@ class KubernetesDiscoveryClientTests { Set namespaces = Set.of(); Map serviceLabels = Map.of("color", "blue"); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, namespaces, - true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false); + true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false, false); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null); List result = discoveryClient.getEndPointsList("blue-service"); @@ -327,7 +333,7 @@ class KubernetesDiscoveryClientTests { Set namespaces = Set.of(); Map serviceLabels = Map.of("color", "blue"); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, namespaces, - true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false); + true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false, false); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null); List result = discoveryClient.getEndPointsList("service-one"); @@ -377,7 +383,7 @@ class KubernetesDiscoveryClientTests { Set namespaces = Set.of("test"); Map serviceLabels = Map.of(); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, namespaces, - true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false); + true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false, false); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null); List result = discoveryClient.getEndPointsList("serviceId"); @@ -459,7 +465,7 @@ class KubernetesDiscoveryClientTests { String namespacesAsString = namespaces.toString(); Map serviceLabels = Map.of("color", "blue"); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, namespaces, - true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false); + true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false, false); KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null); List result = discoveryClient.getEndPointsList("blue-service"); @@ -467,6 +473,62 @@ class KubernetesDiscoveryClientTests { Assertions.assertTrue(output.getOut().contains("discovering endpoints in namespaces : " + namespacesAsString)); } + /** + *
+	 *     - two services are present in two namespaces [a, b]
+	 *     - both are returned
+	 * 
+ */ + @Test + void testGetServicesWithExternalNameService() { + Service nonExternalNameService = new ServiceBuilder() + .withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()).withNewMetadata() + .withName("blue-service").withNamespace("a").endMetadata().build(); + client.services().inNamespace("a").resource(nonExternalNameService).create(); + + Service externalNameService = new ServiceBuilder() + .withSpec(new ServiceSpecBuilder().withType("ExternalName").withExternalName("k8s-spring").build()) + .withNewMetadata().withName("blue-service").withNamespace("b").endMetadata().build(); + client.services().inNamespace("b").resource(externalNameService).create(); + + // last argument is irrelevant, as getServices does not care about that flag + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of("a", "b"), true, + 60L, false, "", Set.of(), Map.of(), "", KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); + + KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null); + List result = discoveryClient.getServices(); + Assertions.assertEquals(result.size(), 2); + // this looks weird at the moment, but there is an issue that will fix this + Assertions.assertEquals(result.get(0), "blue-service"); + Assertions.assertEquals(result.get(1), "blue-service"); + } + + @Test + void testExternalNameService() { + Service externalNameService = new ServiceBuilder() + .withSpec(new ServiceSpecBuilder().withType("ExternalName").withExternalName("k8s-spring-b").build()) + .withNewMetadata().withLabels(Map.of("label-key", "label-value")).withAnnotations(Map.of("abc", "def")) + .withName("blue-service").withNamespace("b").endMetadata().build(); + client.services().inNamespace("b").resource(externalNameService).create(); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, + "labels-prefix-", true, "annotations-prefix-", true, "ports-prefix"); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of("a", "b"), true, + 60L, false, "", Set.of(), Map.of(), "", metadata, 0, false, true); + + KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null); + List result = discoveryClient.getInstances("blue-service"); + Assertions.assertEquals(result.size(), 1); + DefaultKubernetesServiceInstance externalNameServiceInstance = (DefaultKubernetesServiceInstance) result.get(0); + Assertions.assertEquals(externalNameServiceInstance.getServiceId(), "blue-service"); + Assertions.assertEquals(externalNameServiceInstance.getHost(), "k8s-spring-b"); + Assertions.assertEquals(externalNameServiceInstance.getPort(), -1); + Assertions.assertFalse(externalNameServiceInstance.isSecure()); + Assertions.assertEquals(externalNameServiceInstance.getUri().toASCIIString(), "k8s-spring-b"); + Assertions.assertEquals(externalNameServiceInstance.getMetadata(), Map.of("k8s_namespace", "b", + "labels-prefix-label-key", "label-value", "annotations-prefix-abc", "def", "type", "ExternalName")); + } + private void createEndpoints(String namespace, String name, Map labels) { client.endpoints().inNamespace(namespace) .resource(new EndpointsBuilder() diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java index 9d41b95f..db5f9c79 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientUtilsTests.java @@ -32,6 +32,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; 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.ServiceSpecBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -282,7 +283,7 @@ class KubernetesDiscoveryClientUtilsTests { String primaryPortName = "three"; KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), primaryPortName, null, 0, false); + true, "", Set.of(), Map.of(), primaryPortName, null, 0, false, false); Service service = new ServiceBuilder().withMetadata(new ObjectMeta()).build(); @@ -345,13 +346,13 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder().build(); + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()).build(); Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, List.of(), namespace); - Assertions.assertEquals(result.size(), 1); - Assertions.assertEquals(result, Map.of("k8s_namespace", "default")); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result, Map.of("k8s_namespace", "default", "type", "ClusterIP")); } /** @@ -374,15 +375,15 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder() + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()) .withMetadata(new ObjectMetaBuilder().withLabels(Map.of("a", "b")).build()).build(); Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, List.of(), namespace); - Assertions.assertEquals(result.size(), 2); - Assertions.assertEquals(result, Map.of("a", "b", "k8s_namespace", "default")); - String labelsMetadata = filterOnK8sNamespace(result); + Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result, Map.of("a", "b", "k8s_namespace", "default", "type", "ClusterIP")); + String labelsMetadata = filterOnK8sNamespaceAndType(result); Assertions.assertTrue( output.getOut().contains("Adding labels metadata: " + labelsMetadata + " for serviceId: my-service")); } @@ -407,16 +408,17 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder() + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()) .withMetadata(new ObjectMetaBuilder().withLabels(Map.of("a", "b", "c", "d")).build()).build(); Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, List.of(), namespace); - Assertions.assertEquals(result.size(), 3); - Assertions.assertEquals(result, Map.of("prefix-a", "b", "prefix-c", "d", "k8s_namespace", "default")); + Assertions.assertEquals(result.size(), 4); + Assertions.assertEquals(result, + Map.of("prefix-a", "b", "prefix-c", "d", "k8s_namespace", "default", "type", "ClusterIP")); // so that result is deterministic in assertion - String labelsMetadata = filterOnK8sNamespace(result); + String labelsMetadata = filterOnK8sNamespaceAndType(result); Assertions.assertTrue( output.getOut().contains("Adding labels metadata: " + labelsMetadata + " for serviceId: my-service")); } @@ -441,15 +443,16 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder().withMetadata( - new ObjectMetaBuilder().withAnnotations(Map.of("aa", "bb")).withLabels(Map.of("a", "b")).build()) + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()) + .withMetadata(new ObjectMetaBuilder().withAnnotations(Map.of("aa", "bb")).withLabels(Map.of("a", "b")) + .build()) .build(); Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, List.of(), namespace); - Assertions.assertEquals(result.size(), 2); - Assertions.assertEquals(result, Map.of("aa", "bb", "k8s_namespace", "default")); + Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result, Map.of("aa", "bb", "k8s_namespace", "default", "type", "ClusterIP")); Assertions .assertTrue(output.getOut().contains("Adding annotations metadata: {aa=bb} for serviceId: my-service")); } @@ -474,16 +477,19 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder().withMetadata(new ObjectMetaBuilder() - .withAnnotations(Map.of("aa", "bb", "cc", "dd")).withLabels(Map.of("a", "b")).build()).build(); + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()) + .withMetadata(new ObjectMetaBuilder().withAnnotations(Map.of("aa", "bb", "cc", "dd")) + .withLabels(Map.of("a", "b")).build()) + .build(); Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, List.of(), namespace); - Assertions.assertEquals(result.size(), 3); - Assertions.assertEquals(result, Map.of("prefix-aa", "bb", "prefix-cc", "dd", "k8s_namespace", "default")); + Assertions.assertEquals(result.size(), 4); + Assertions.assertEquals(result, + Map.of("prefix-aa", "bb", "prefix-cc", "dd", "k8s_namespace", "default", "type", "ClusterIP")); // so that result is deterministic in assertion - String annotations = filterOnK8sNamespace(result); + String annotations = filterOnK8sNamespaceAndType(result); Assertions.assertTrue( output.getOut().contains("Adding annotations metadata: " + annotations + " for serviceId: my-service")); } @@ -508,16 +514,17 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder().withMetadata(new ObjectMetaBuilder() - .withAnnotations(Map.of("aa", "bb", "cc", "dd")).withLabels(Map.of("a", "b", "c", "d")).build()) + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder() + .withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()).withMetadata(new ObjectMetaBuilder() + .withAnnotations(Map.of("aa", "bb", "cc", "dd")).withLabels(Map.of("a", "b", "c", "d")).build()) .build(); Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, List.of(), namespace); - Assertions.assertEquals(result.size(), 5); + Assertions.assertEquals(result.size(), 6); Assertions.assertEquals(result, Map.of("annotation-aa", "bb", "annotation-cc", "dd", "label-a", "b", "label-c", - "d", "k8s_namespace", "default")); + "d", "k8s_namespace", "default", "type", "ClusterIP")); // so that result is deterministic in assertion String labels = result.entrySet().stream().filter(en -> en.getKey().contains("label")) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).toString(); @@ -548,9 +555,11 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder().withMetadata(new ObjectMetaBuilder() - .withAnnotations(Map.of("aa", "bb", "cc", "dd")).withLabels(Map.of("a", "b")).build()).build(); + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()) + .withMetadata(new ObjectMetaBuilder().withAnnotations(Map.of("aa", "bb", "cc", "dd")) + .withLabels(Map.of("a", "b")).build()) + .build(); List endpointSubsets = List.of( new EndpointSubsetBuilder().withPorts(new EndpointPortBuilder().withPort(8081).withName("").build()) @@ -560,8 +569,8 @@ class KubernetesDiscoveryClientUtilsTests { Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, endpointSubsets, namespace); - Assertions.assertEquals(result.size(), 2); - Assertions.assertEquals(result, Map.of("https", "8080", "k8s_namespace", "default")); + Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result, Map.of("https", "8080", "k8s_namespace", "default", "type", "ClusterIP")); Assertions .assertTrue(output.getOut().contains("Adding port metadata: {https=8080} for serviceId : my-service")); } @@ -585,9 +594,11 @@ class KubernetesDiscoveryClientUtilsTests { KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(addLabels, labelsPrefix, addAnnotations, annotationsPrefix, addPorts, portsPrefix); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - true, "", Set.of(), Map.of(), "", metadata, 0, false); - Service service = new ServiceBuilder().withMetadata(new ObjectMetaBuilder() - .withAnnotations(Map.of("aa", "bb", "cc", "dd")).withLabels(Map.of("a", "b")).build()).build(); + true, "", Set.of(), Map.of(), "", metadata, 0, false, false); + Service service = new ServiceBuilder().withSpec(new ServiceSpecBuilder().withType("ClusterIP").build()) + .withMetadata(new ObjectMetaBuilder().withAnnotations(Map.of("aa", "bb", "cc", "dd")) + .withLabels(Map.of("a", "b")).build()) + .build(); List endpointSubsets = List.of( new EndpointSubsetBuilder().withPorts(new EndpointPortBuilder().withPort(8081).withName("http").build()) @@ -597,9 +608,9 @@ class KubernetesDiscoveryClientUtilsTests { Map result = KubernetesDiscoveryClientUtils.serviceMetadata("my-service", service, properties, endpointSubsets, namespace); - Assertions.assertEquals(result.size(), 3); + Assertions.assertEquals(result.size(), 4); Assertions.assertEquals(result, - Map.of("prefix-https", "8080", "prefix-http", "8081", "k8s_namespace", "default")); + Map.of("prefix-https", "8080", "prefix-http", "8081", "k8s_namespace", "default", "type", "ClusterIP")); Assertions.assertTrue(output.getOut() .contains("Adding port metadata: {prefix-http=8081, prefix-https=8080} for serviceId : my-service")); } @@ -614,7 +625,7 @@ class KubernetesDiscoveryClientUtilsTests { void testEmptyAddresses() { boolean includeNotReadyAddresses = false; KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false); + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false, false); EndpointSubset endpointSubset = new EndpointSubsetBuilder().build(); List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); Assertions.assertEquals(addresses.size(), 0); @@ -649,7 +660,7 @@ class KubernetesDiscoveryClientUtilsTests { void testReadyAddressesTakenNotReadyAddressesNotTaken() { boolean includeNotReadyAddresses = false; KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false); + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false, false); EndpointSubset endpointSubset = new EndpointSubsetBuilder() .withAddresses(new EndpointAddressBuilder().withHostname("one").build(), new EndpointAddressBuilder().withHostname("two").build()) @@ -684,7 +695,7 @@ class KubernetesDiscoveryClientUtilsTests { @Test void testServiceInstance() { KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, - false, "", Set.of(), Map.of(), "", null, 0, false); + false, "", Set.of(), Map.of(), "", null, 0, false, false); ServicePortSecureResolver resolver = new ServicePortSecureResolver(properties); Service service = new ServiceBuilder().withMetadata(new ObjectMeta()).build(); EndpointAddress address = new EndpointAddressBuilder().withNewTargetRef().withUid("123").endTargetRef() @@ -706,8 +717,31 @@ class KubernetesDiscoveryClientUtilsTests { Assertions.assertNull(defaultInstance.getCluster()); } - private String filterOnK8sNamespace(Map result) { + @Test + void testExternalNameServiceInstance() { + Service service = new ServiceBuilder() + .withSpec(new ServiceSpecBuilder().withExternalName("spring.io").withType("ExternalName").build()) + .withMetadata(new ObjectMetaBuilder().withUid("123").build()).build(); + + ServiceInstance serviceInstance = KubernetesDiscoveryClientUtils.serviceInstance(null, service, null, -1, + "my-service", Map.of("a", "b"), "k8s"); + Assertions.assertTrue(serviceInstance instanceof DefaultKubernetesServiceInstance); + DefaultKubernetesServiceInstance defaultInstance = (DefaultKubernetesServiceInstance) serviceInstance; + Assertions.assertEquals(defaultInstance.getInstanceId(), "123"); + Assertions.assertEquals(defaultInstance.getServiceId(), "my-service"); + Assertions.assertEquals(defaultInstance.getHost(), "spring.io"); + Assertions.assertEquals(defaultInstance.getPort(), -1); + Assertions.assertFalse(defaultInstance.isSecure()); + Assertions.assertEquals(defaultInstance.getUri().toASCIIString(), "spring.io"); + Assertions.assertEquals(defaultInstance.getMetadata(), Map.of("a", "b")); + Assertions.assertEquals(defaultInstance.getScheme(), "http"); + Assertions.assertEquals(defaultInstance.getNamespace(), "k8s"); + Assertions.assertNull(defaultInstance.getCluster()); + } + + private String filterOnK8sNamespaceAndType(Map result) { return result.entrySet().stream().filter(en -> !en.getKey().contains("k8s_namespace")) + .filter(en -> !en.getKey().equals("type")) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).toString(); } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java index aa7cbd8b..192db70f 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java @@ -27,6 +27,7 @@ import io.fabric8.kubernetes.api.model.EndpointsList; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.api.model.ServiceListBuilder; +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; @@ -136,7 +137,8 @@ class KubernetesReactiveDiscoveryClientTests { @Test void shouldReturnFlux() { ServiceList services = new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service") - .withNamespace("test").withLabels(Map.of("label", "value")).endMetadata().endItem().build(); + .withNamespace("test").withLabels(Map.of("label", "value")).endMetadata() + .withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).endItem().build(); Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("existing-service").withNamespace("test") .withLabels(Map.of("label", "value")).endMetadata().addNewSubset().addNewAddress().withIp("ip1") @@ -166,8 +168,10 @@ class KubernetesReactiveDiscoveryClientTests { @Test void shouldReturnFluxWithPrefixedMetadata() { kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") - .andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service") - .withLabels(Map.of("label", "value")).endMetadata().endItem().build()) + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service") + .withLabels(Map.of("label", "value")).endMetadata() + .withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).endItem().build()) .once(); Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace("test") @@ -184,12 +188,11 @@ class KubernetesReactiveDiscoveryClientTests { .withPath("/api/v1/namespaces/test/endpoints?fieldSelector=metadata.name%3Dexisting-service") .andReturn(200, endpoints).once(); - kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services/existing-service") - .andReturn(200, new ServiceBuilder().withNewMetadata().withName("existing-service") - .withLabels(Map.of("label", "value")).endMetadata().build()) + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services/existing-service").andReturn(200, + new ServiceBuilder().withNewMetadata().withName("existing-service").withLabels(Map.of("label", "value")) + .endMetadata().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).build()) .once(); - Metadata metadata = new Metadata(true, "label.", true, "annotation.", true, "port."); ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient(kubernetesClient, KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services); Flux instances = client.getInstances("existing-service"); @@ -199,8 +202,10 @@ class KubernetesReactiveDiscoveryClientTests { @Test void shouldReturnFluxWhenServiceHasMultiplePortsAndPrimaryPortNameIsSet() { kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") - .andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service") - .withLabels(Map.of("label", "value")).endMetadata().endItem().build()) + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service") + .withLabels(Map.of("label", "value")).endMetadata() + .withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).endItem().build()) .once(); Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace("test") @@ -218,9 +223,9 @@ class KubernetesReactiveDiscoveryClientTests { .withPath("/api/v1/namespaces/test/endpoints?fieldSelector=metadata.name%3Dexisting-service") .andReturn(200, endpoints).once(); - kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services/existing-service") - .andReturn(200, new ServiceBuilder().withNewMetadata().withName("existing-service") - .withLabels(Map.of("label", "value")).endMetadata().build()) + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services/existing-service").andReturn(200, + new ServiceBuilder().withNewMetadata().withName("existing-service").withLabels(Map.of("label", "value")) + .endMetadata().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).build()) .once(); ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient(kubernetesClient, @@ -232,8 +237,10 @@ class KubernetesReactiveDiscoveryClientTests { @Test void shouldReturnFluxOfServicesAcrossAllNamespaces() { kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") - .andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service") - .withLabels(Map.of("label", "value")).endMetadata().endItem().build()) + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service") + .withLabels(Map.of("label", "value")).endMetadata() + .withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).endItem().build()) .once(); Endpoints endpoints = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace("test") @@ -247,9 +254,9 @@ class KubernetesReactiveDiscoveryClientTests { kubernetesServer.expect().get().withPath("/api/v1/endpoints?fieldSelector=metadata.name%3Dexisting-service") .andReturn(200, endpointsList).once(); - kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services/existing-service") - .andReturn(200, new ServiceBuilder().withNewMetadata().withName("existing-service") - .withLabels(Map.of("label", "value")).endMetadata().build()) + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services/existing-service").andReturn(200, + new ServiceBuilder().withNewMetadata().withName("existing-service").withLabels(Map.of("label", "value")) + .endMetadata().withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).build()) .once(); KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryController.java index 7cf7d06e..430901f1 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryController.java @@ -20,6 +20,7 @@ import java.util.List; import io.fabric8.kubernetes.api.model.Endpoints; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -47,4 +48,9 @@ public class Fabric8DiscoveryController { return discoveryClient.getEndPointsList(serviceId); } + @GetMapping("/service-instances/{serviceId}") + public List serviceInstances(@PathVariable("serviceId") String serviceId) { + return discoveryClient.getInstances(serviceId); + } + } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryIT.java index cf84a372..93fe4d31 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryIT.java @@ -18,9 +18,13 @@ package org.springframework.cloud.kubernetes.fabric8.configmap; import java.io.InputStream; import java.time.Duration; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; @@ -34,9 +38,11 @@ import reactor.netty.http.client.HttpClient; import reactor.util.retry.Retry; import reactor.util.retry.RetryBackoffSpec; +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; import org.springframework.cloud.kubernetes.integration.tests.commons.fabric8_client.Util; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; @@ -78,18 +84,46 @@ class Fabric8DiscoveryIT { Commons.cleanUp(IMAGE_NAME, K3S); } + /** + * KubernetesDiscoveryClient::getServices call must include the external-name-service + * also. + */ @Test - void test() { + void testAllServices() { WebClient client = builder().baseUrl("localhost/services").build(); - @SuppressWarnings("unchecked") - List result = (List) client.method(HttpMethod.GET).retrieve().bodyToMono(List.class) - .retryWhen(retrySpec()).block(); + List result = client.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { - Assertions.assertEquals(result.size(), 3); + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(result.size(), 4); Assertions.assertTrue(result.contains("kubernetes")); Assertions.assertTrue(result.contains("spring-cloud-kubernetes-fabric8-client-discovery")); Assertions.assertTrue(result.contains("service-wiremock")); + Assertions.assertTrue(result.contains("external-name-service")); + } + + @Test + void testExternalNameServiceInstance() { + + WebClient client = builder().baseUrl("localhost/service-instances/external-name-service").build(); + List serviceInstances = client.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + DefaultKubernetesServiceInstance result = serviceInstances.get(0); + + Assertions.assertEquals(serviceInstances.size(), 1); + Assertions.assertEquals(result.getServiceId(), "external-name-service"); + Assertions.assertNotNull(result.getInstanceId()); + Assertions.assertEquals(result.getHost(), "spring.io"); + Assertions.assertEquals(result.getPort(), -1); + Assertions.assertEquals(result.getMetadata(), Map.of("k8s_namespace", "default", "type", "ExternalName")); + Assertions.assertFalse(result.isSecure()); + Assertions.assertEquals(result.getUri().toASCIIString(), "spring.io"); + Assertions.assertEquals(result.getScheme(), "http"); } private static void manifests(Phase phase) { @@ -97,16 +131,30 @@ class Fabric8DiscoveryIT { InputStream deploymentStream = util.inputStream("fabric8-discovery-deployment.yaml"); InputStream serviceStream = util.inputStream("fabric8-discovery-service.yaml"); InputStream ingressStream = util.inputStream("fabric8-discovery-ingress.yaml"); + InputStream externalNameServiceInputStream = util.inputStream("external-name-service.yaml"); Deployment deployment = client.apps().deployments().load(deploymentStream).get(); + + List existing = new ArrayList<>( + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); + existing.add(new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES") + .withValue("true").build()); + existing.add( + new EnvVarBuilder().withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY") + .withValue("DEBUG").build()); + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(existing); + Service service = client.services().load(serviceStream).get(); + Service externalNameService = client.services().load(externalNameServiceInputStream).get(); Ingress ingress = client.network().v1().ingresses().load(ingressStream).get(); if (phase.equals(Phase.CREATE)) { util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); + util.createAndWait(NAMESPACE, null, null, externalNameService, null, false); } else { util.deleteAndWait(NAMESPACE, deployment, service, ingress); + util.deleteAndWait(NAMESPACE, null, externalNameService, null); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/resources/external-name-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/resources/external-name-service.yaml new file mode 100644 index 00000000..649d5da7 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/resources/external-name-service.yaml @@ -0,0 +1,7 @@ +kind: Service +apiVersion: v1 +metadata: + name: external-name-service +spec: + type: ExternalName + externalName: spring.io diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java index 5e2469d8..748f6203 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/fabric8_client/Util.java @@ -74,24 +74,29 @@ public final class Util { * tight as possible, providing reasonable defaults. * */ - public void createAndWait(String namespace, String name, Deployment deployment, Service service, + public void createAndWait(String namespace, String name, @Nullable Deployment deployment, Service service, @Nullable Ingress ingress, boolean changeVersion) { try { - String imageFromDeployment = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage(); - if (changeVersion) { - deployment.getSpec().getTemplate().getSpec().getContainers().get(0) - .setImage(imageFromDeployment + ":" + pomVersion()); - } - else { - String[] image = imageFromDeployment.split(":", 2); - pullImage(image[0], image[1], container); - loadImage(image[0], image[1], name, container); + if (deployment != null) { + String imageFromDeployment = deployment.getSpec().getTemplate().getSpec().getContainers().get(0) + .getImage(); + if (changeVersion) { + deployment.getSpec().getTemplate().getSpec().getContainers().get(0) + .setImage(imageFromDeployment + ":" + pomVersion()); + } + else { + String[] image = imageFromDeployment.split(":", 2); + pullImage(image[0], image[1], container); + loadImage(image[0], image[1], name, container); + } + + client.apps().deployments().inNamespace(namespace).resource(deployment).create(); + waitForDeployment(namespace, deployment); } - client.apps().deployments().inNamespace(namespace).resource(deployment).create(); client.services().inNamespace(namespace).resource(service).create(); - waitForDeployment(namespace, deployment); + if (ingress != null) { client.network().v1().ingresses().inNamespace(namespace).resource(ingress).create(); waitForIngress(namespace, ingress); @@ -116,11 +121,16 @@ public final class Util { } } - public void deleteAndWait(String namespace, Deployment deployment, Service service, @Nullable Ingress ingress) { + public void deleteAndWait(String namespace, @Nullable Deployment deployment, Service service, + @Nullable Ingress ingress) { try { - client.apps().deployments().inNamespace(namespace).resource(deployment).delete(); + + if (deployment != null) { + client.apps().deployments().inNamespace(namespace).resource(deployment).delete(); + waitForDeploymentToBeDeleted(namespace, deployment); + } + client.services().inNamespace(namespace).resource(service).delete(); - waitForDeploymentToBeDeleted(namespace, deployment); if (ingress != null) { client.network().v1().ingresses().inNamespace(namespace).resource(ingress).delete();