diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplier.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplier.java new file mode 100644 index 00000000..49628ebd --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplier.java @@ -0,0 +1,78 @@ +/* + * 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.client.discovery; + +import java.util.Optional; +import java.util.function.Supplier; + +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1ObjectReference; +import io.kubernetes.client.openapi.models.V1Service; + +import org.springframework.cloud.kubernetes.commons.discovery.InstanceIdHostPodName; + +/** + * @author wind57 + */ +final class K8sInstanceIdHostPodNameSupplier implements Supplier { + + private final V1EndpointAddress endpointAddress; + + private final V1Service service; + + private K8sInstanceIdHostPodNameSupplier(V1EndpointAddress endpointAddress, V1Service service) { + this.endpointAddress = endpointAddress; + this.service = service; + } + + @Override + public InstanceIdHostPodName get() { + return new InstanceIdHostPodName(instanceId(), host(), podName()); + } + + /** + * to be used when .spec.type of the Service is != 'ExternalName'. + */ + static K8sInstanceIdHostPodNameSupplier nonExternalName(V1EndpointAddress endpointAddress, V1Service service) { + return new K8sInstanceIdHostPodNameSupplier(endpointAddress, service); + } + + /** + * to be used when .spec.type of the Service is == 'ExternalName'. + */ + static K8sInstanceIdHostPodNameSupplier externalName(V1Service service) { + return new K8sInstanceIdHostPodNameSupplier(null, service); + } + + // instanceId is usually the pod-uid as seen in the .metadata.uid + private String instanceId() { + return Optional.ofNullable(endpointAddress).map(V1EndpointAddress::getTargetRef).map(V1ObjectReference::getUid) + .orElseGet(() -> service.getMetadata().getUid()); + } + + private String host() { + return Optional.ofNullable(endpointAddress).map(V1EndpointAddress::getIp) + .orElseGet(() -> service.getSpec().getExternalName()); + } + + private String podName() { + return Optional.ofNullable(endpointAddress).map(V1EndpointAddress::getTargetRef) + .filter(objectReference -> "Pod".equals(objectReference.getKind())).map(V1ObjectReference::getName) + .orElse(null); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplier.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplier.java new file mode 100644 index 00000000..21c32f8f --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplier.java @@ -0,0 +1,80 @@ +/* + * 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.client.discovery; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.kubernetes.commons.discovery.PodLabelsAndAnnotations; +import org.springframework.core.log.LogAccessor; + +/** + * @author wind57 + */ +final class K8sPodLabelsAndAnnotationsSupplier implements Function { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(K8sPodLabelsAndAnnotationsSupplier.class)); + + private final CoreV1Api coreV1Api; + + private final String namespace; + + private K8sPodLabelsAndAnnotationsSupplier(CoreV1Api coreV1Api, String namespace) { + this.coreV1Api = coreV1Api; + this.namespace = namespace; + } + + /** + * to be used when .spec.type of the Service is != 'ExternalName'. + */ + static K8sPodLabelsAndAnnotationsSupplier nonExternalName(CoreV1Api coreV1Api, String namespace) { + return new K8sPodLabelsAndAnnotationsSupplier(coreV1Api, namespace); + } + + /** + * to be used when .spec.type of the Service is == 'ExternalName'. + */ + static K8sPodLabelsAndAnnotationsSupplier externalName() { + return new K8sPodLabelsAndAnnotationsSupplier(null, null); + } + + @Override + public PodLabelsAndAnnotations apply(String podName) { + + V1ObjectMeta objectMeta; + + try { + objectMeta = Optional.ofNullable(coreV1Api.readNamespacedPod(podName, namespace, null).getMetadata()) + .orElse(new V1ObjectMetaBuilder().withLabels(Map.of()).withAnnotations(Map.of()).build()); + } + catch (ApiException e) { + LOG.warn(e, "Could not get pod metadata"); + objectMeta = new V1ObjectMetaBuilder().withLabels(Map.of()).withAnnotations(Map.of()).build(); + } + + return new PodLabelsAndAnnotations(Optional.ofNullable(objectMeta.getLabels()).orElse(Map.of()), + Optional.ofNullable(objectMeta.getAnnotations()).orElse(Map.of())); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java index 9e0a88ae..864d469d 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtils.java @@ -17,15 +17,19 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.time.Duration; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.openapi.models.V1ServiceSpec; @@ -33,14 +37,15 @@ import io.kubernetes.client.util.wait.Wait; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.ServiceMetadata; import org.springframework.core.log.LogAccessor; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.util.CollectionUtils; -import static org.springframework.cloud.kubernetes.commons.config.ConfigUtils.keysWithPrefix; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.NAMESPACE_METADATA_KEY; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.SERVICE_TYPE; +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.UNSET_PORT_NAME; +import static org.springframework.util.StringUtils.hasText; /** * @author wind57 @@ -82,40 +87,6 @@ final class KubernetesDiscoveryClientUtils { } - /** - * This adds the following metadata.
-	 *     - labels (if requested)
-	 *     - annotations (if requested)
-	 *     - metadata
-	 *     - service type
-	 * 
- */ - static Map serviceMetadata(KubernetesDiscoveryProperties properties, V1Service service, - String serviceId) { - - Map serviceMetadata = new HashMap<>(); - KubernetesDiscoveryProperties.Metadata metadataProps = properties.metadata(); - if (metadataProps.addLabels()) { - Map labelMetadata = keysWithPrefix(service.getMetadata().getLabels(), - metadataProps.labelsPrefix()); - LOG.debug(() -> "Adding labels metadata: " + labelMetadata + " for serviceId: " + serviceId); - serviceMetadata.putAll(labelMetadata); - } - if (metadataProps.addAnnotations()) { - Map annotationMetadata = keysWithPrefix(service.getMetadata().getAnnotations(), - metadataProps.annotationsPrefix()); - LOG.debug(() -> "Adding annotations metadata: " + annotationMetadata + " for serviceId: " + serviceId); - serviceMetadata.putAll(annotationMetadata); - } - - serviceMetadata.put(NAMESPACE_METADATA_KEY, - Optional.ofNullable(service.getMetadata()).map(V1ObjectMeta::getNamespace).orElse(null)); - serviceMetadata.put(SERVICE_TYPE, - Optional.ofNullable(service.getSpec()).map(V1ServiceSpec::getType).orElse(null)); - - return serviceMetadata; - } - static Predicate filter(KubernetesDiscoveryProperties properties) { String spelExpression = properties.filter(); Predicate predicate; @@ -159,4 +130,38 @@ final class KubernetesDiscoveryClientUtils { } + static ServiceMetadata serviceMetadata(V1Service service) { + V1ObjectMeta metadata = service.getMetadata(); + V1ServiceSpec serviceSpec = service.getSpec(); + return new ServiceMetadata(metadata.getName(), metadata.getNamespace(), serviceSpec.getType(), + metadata.getLabels(), metadata.getAnnotations()); + } + + /** + * a service is allowed to have a single port defined without a name. + */ + static Map endpointSubsetsPortData(List endpointSubsets) { + return endpointSubsets.stream() + .flatMap(endpointSubset -> Optional.ofNullable(endpointSubset.getPorts()).orElse(List.of()).stream()) + .collect(Collectors.toMap( + endpointPort -> hasText(endpointPort.getName()) ? endpointPort.getName() : UNSET_PORT_NAME, + CoreV1EndpointPort::getPort)); + } + + static List addresses(V1EndpointSubset endpointSubset, + KubernetesDiscoveryProperties properties) { + List addresses = Optional.ofNullable(endpointSubset.getAddresses()).map(ArrayList::new) + .orElse(new ArrayList<>()); + + if (properties.includeNotReadyAddresses()) { + List notReadyAddresses = endpointSubset.getNotReadyAddresses(); + if (CollectionUtils.isEmpty(notReadyAddresses)) { + return addresses; + } + addresses.addAll(notReadyAddresses); + } + + return addresses; + } + } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java index 572ecd35..251d4a3a 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java @@ -17,43 +17,47 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import io.kubernetes.client.informer.SharedInformer; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; -import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; import io.kubernetes.client.openapi.models.V1Endpoints; import io.kubernetes.client.openapi.models.V1Service; import jakarta.annotation.PostConstruct; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.ServiceMetadata; +import org.springframework.cloud.kubernetes.commons.discovery.ServicePortNameAndNumber; +import org.springframework.cloud.kubernetes.commons.discovery.ServicePortSecureResolver; import org.springframework.core.log.LogAccessor; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; +import static org.springframework.cloud.kubernetes.client.discovery.K8sInstanceIdHostPodNameSupplier.externalName; +import static org.springframework.cloud.kubernetes.client.discovery.K8sInstanceIdHostPodNameSupplier.nonExternalName; +import static org.springframework.cloud.kubernetes.client.discovery.K8sPodLabelsAndAnnotationsSupplier.externalName; +import static org.springframework.cloud.kubernetes.client.discovery.K8sPodLabelsAndAnnotationsSupplier.nonExternalName; +import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.addresses; +import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.endpointSubsetsPortData; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.filter; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.matchesServiceLabels; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.postConstruct; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.serviceMetadata; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.HTTP; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.HTTPS; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.PRIMARY_PORT_NAME_LABEL_KEY; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.SECURED; -import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryConstants.UNSET_PORT_NAME; +import static org.springframework.cloud.kubernetes.commons.discovery.DiscoveryClientUtils.endpointsPort; +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; /** * @author Min Kim @@ -76,6 +80,13 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { private final Predicate filter; + private final ServicePortSecureResolver servicePortSecureResolver; + + // visible only for testing and + // must be constructor injected in a future release + @Autowired + CoreV1Api coreV1Api; + @Deprecated(forRemoval = true) public KubernetesInformerDiscoveryClient(String namespace, SharedInformerFactory sharedInformerFactory, Lister serviceLister, Lister endpointsLister, @@ -87,6 +98,7 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); this.properties = properties; filter = filter(properties); + servicePortSecureResolver = new ServicePortSecureResolver(properties); } public KubernetesInformerDiscoveryClient(SharedInformerFactory sharedInformerFactory, @@ -99,6 +111,7 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); this.properties = properties; filter = filter(properties); + servicePortSecureResolver = new ServicePortSecureResolver(properties); } public KubernetesInformerDiscoveryClient(List sharedInformerFactories, @@ -119,6 +132,7 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { this.properties = properties; filter = filter(properties); + servicePortSecureResolver = new ServicePortSecureResolver(properties); } @Override @@ -130,104 +144,76 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { public List getInstances(String serviceId) { Objects.requireNonNull(serviceId, "serviceId must be provided"); - List services = serviceListers.stream().flatMap(x -> x.list().stream()) + List allServices = serviceListers.stream().flatMap(x -> x.list().stream()) .filter(scv -> scv.getMetadata() != null).filter(svc -> serviceId.equals(svc.getMetadata().getName())) - .filter(scv -> matchesServiceLabels(scv, properties)).filter(filter).toList(); - return services.stream().flatMap(service -> getServiceInstanceDetails(service, serviceId)).toList(); - } + .filter(scv -> matchesServiceLabels(scv, properties)).toList(); - private Stream getServiceInstanceDetails(V1Service service, String serviceId) { - Map serviceMetadata = serviceMetadata(properties, service, serviceId); + List serviceInstances = allServices.stream().filter(filter) + .flatMap(service -> serviceInstances(service, serviceId).stream()) + .collect(Collectors.toCollection(ArrayList::new)); - List endpoints = endpointsListers.stream() - .map(endpointsLister -> endpointsLister.namespace(service.getMetadata().getNamespace()) - .get(service.getMetadata().getName())) - .filter(Objects::nonNull).filter(ep -> ep.getSubsets() != null).toList(); + if (properties.includeExternalNameServices()) { + LOG.debug(() -> "Searching for 'ExternalName' type of services with serviceId : " + serviceId); + List externalNameServices = allServices.stream().filter(s -> s.getSpec() != null) + .filter(s -> EXTERNAL_NAME.equals(s.getSpec().getType())).toList(); + for (V1Service service : externalNameServices) { + ServiceMetadata serviceMetadata = serviceMetadata(service); + Map serviceInstanceMetadata = serviceInstanceMetadata(Map.of(), serviceMetadata, + properties); - Optional discoveredPrimaryPortName = Optional.empty(); - if (service.getMetadata() != null && service.getMetadata().getLabels() != null) { - discoveredPrimaryPortName = Optional - .ofNullable(service.getMetadata().getLabels().get(PRIMARY_PORT_NAME_LABEL_KEY)); - } - final String primaryPortName = discoveredPrimaryPortName.orElse(properties.primaryPortName()); + K8sInstanceIdHostPodNameSupplier supplierOne = externalName(service); + K8sPodLabelsAndAnnotationsSupplier supplierTwo = externalName(); - final boolean secured = isSecured(service); - - return endpoints.stream() - .flatMap(ep -> ep.getSubsets().stream() - .filter(subset -> subset.getPorts() != null && subset.getPorts().size() > 0) // safeguard - .flatMap(subset -> { - Map metadata = new HashMap<>(serviceMetadata); - List endpointPorts = subset.getPorts(); - if (properties.metadata() != null && properties.metadata().addPorts()) { - endpointPorts.forEach(p -> metadata.put( - StringUtils.hasText(p.getName()) ? p.getName() : UNSET_PORT_NAME, - Integer.toString(p.getPort()))); - } - List addresses = subset.getAddresses(); - if (addresses == null) { - addresses = new ArrayList<>(); - } - if (properties.includeNotReadyAddresses() - && !CollectionUtils.isEmpty(subset.getNotReadyAddresses())) { - addresses.addAll(subset.getNotReadyAddresses()); - } - - final int port = findEndpointPort(endpointPorts, primaryPortName, serviceId); - return addresses.stream() - .map(addr -> new DefaultKubernetesServiceInstance( - addr.getTargetRef() != null ? addr.getTargetRef().getUid() : "", serviceId, - addr.getIp(), port, metadata, secured, service.getMetadata().getNamespace(), - // TODO find out how to get cluster name - // possibly from - // KubeConfig - null)); - })); - } - - private static boolean isSecured(V1Service service) { - Optional securedOpt = Optional.empty(); - if (service.getMetadata() != null && service.getMetadata().getAnnotations() != null) { - securedOpt = Optional.ofNullable(service.getMetadata().getAnnotations().get(SECURED)); - } - if (!securedOpt.isPresent() && service.getMetadata() != null && service.getMetadata().getLabels() != null) { - securedOpt = Optional.ofNullable(service.getMetadata().getLabels().get(SECURED)); - } - return Boolean.parseBoolean(securedOpt.orElse("false")); - } - - private int findEndpointPort(List endpointPorts, String primaryPortName, String serviceId) { - if (endpointPorts.size() == 1) { - return endpointPorts.get(0).getPort(); - } - else { - Map ports = endpointPorts.stream().filter(p -> StringUtils.hasText(p.getName())) - .collect(Collectors.toMap(CoreV1EndpointPort::getName, CoreV1EndpointPort::getPort)); - // This oneliner is looking for a port with a name equal to the primary port - // name specified in the service label - // or in spring.cloud.kubernetes.discovery.primary-port-name, equal to https, - // or equal to http. - // In case no port has been found return -1 to log a warning and fall back to - // the first port in the list. - int discoveredPort = ports.getOrDefault(primaryPortName, - ports.getOrDefault(HTTPS, ports.getOrDefault(HTTP, -1))); - - if (discoveredPort == -1) { - if (StringUtils.hasText(primaryPortName)) { - LOG.warn(() -> "Could not find a port named '" + primaryPortName - + "', 'https', or 'http' for service '" + serviceId + "'."); - } - else { - LOG.warn(() -> "Could not find a port named 'https' or 'http' for service '" + serviceId + "'."); - } - LOG.warn( - () -> "Make sure that either the primary-port-name label has been added to the service, or that spring.cloud.kubernetes.discovery.primary-port-name has been configured."); - LOG.warn(() -> "Alternatively name the primary port 'https' or 'http'"); - LOG.warn(() -> "An incorrect configuration may result in non-deterministic behaviour."); - discoveredPort = endpointPorts.get(0).getPort(); + ServiceInstance externalNameServiceInstance = serviceInstance(null, serviceMetadata, supplierOne, + supplierTwo, new ServicePortNameAndNumber(-1, null), serviceInstanceMetadata, properties); + serviceInstances.add(externalNameServiceInstance); } - return discoveredPort; } + + return serviceInstances; + } + + private List serviceInstances(V1Service service, String serviceId) { + + List instances = new ArrayList<>(); + + List allEndpoints = endpointsListers.stream() + .map(endpointsLister -> endpointsLister.namespace(service.getMetadata().getNamespace()).get(serviceId)) + .filter(Objects::nonNull).toList(); + + for (V1Endpoints endpoints : allEndpoints) { + List subsets = endpoints.getSubsets(); + if (subsets == null || subsets.isEmpty()) { + LOG.debug(() -> "serviceId : " + serviceId + " does not have any subsets"); + } + else { + ServiceMetadata serviceMetadata = serviceMetadata(service); + Map portsData = endpointSubsetsPortData(subsets); + Map serviceInstanceMetadata = serviceInstanceMetadata(portsData, serviceMetadata, + properties); + + for (V1EndpointSubset endpointSubset : subsets) { + + Map endpointsPortData = endpointSubsetsPortData(List.of(endpointSubset)); + ServicePortNameAndNumber portData = endpointsPort(endpointsPortData, serviceMetadata, properties); + + List addresses = addresses(endpointSubset, properties); + for (V1EndpointAddress endpointAddress : addresses) { + + K8sInstanceIdHostPodNameSupplier supplierOne = nonExternalName(endpointAddress, service); + K8sPodLabelsAndAnnotationsSupplier supplierTwo = nonExternalName(coreV1Api, + service.getMetadata().getNamespace()); + + ServiceInstance serviceInstance = serviceInstance(servicePortSecureResolver, serviceMetadata, + supplierOne, supplierTwo, portData, serviceInstanceMetadata, properties); + instances.add(serviceInstance); + } + } + + } + } + + return instances; } @Override diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplierTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplierTests.java new file mode 100644 index 00000000..fd9ce80b --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sInstanceIdHostPodNameSupplierTests.java @@ -0,0 +1,135 @@ +/* + * 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.client.discovery; + +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.kubernetes.commons.discovery.InstanceIdHostPodName; + +/** + * @author wind57 + */ +class K8sInstanceIdHostPodNameSupplierTests { + + @Test + void instanceIdNoEndpointAddress() { + V1Service service = new V1ServiceBuilder().withSpec(new V1ServiceSpecBuilder().build()) + .withMetadata(new V1ObjectMetaBuilder().withUid("123").build()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.externalName(service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.instanceId(), "123"); + } + + @Test + void instanceIdWithEndpointAddress() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder() + .withTargetRef(new V1ObjectReferenceBuilder().withUid("456").build()).build(); + V1Service service = new V1ServiceBuilder().withSpec(new V1ServiceSpecBuilder().build()) + .withMetadata(new V1ObjectMetaBuilder().withUid("123").build()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.instanceId(), "456"); + } + + @Test + void hostNoEndpointAddress() { + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.externalName(service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.host(), "external-name"); + } + + @Test + void hostWithEndpointAddress() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder().withIp("127.0.0.1").build(); + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.host(), "127.0.0.1"); + } + + @Test + void testPodNameIsNull() { + V1Service service = new V1ServiceBuilder().withMetadata(new V1ObjectMetaBuilder().withUid("123").build()) + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()).build(); + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.externalName(service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertNull(result.podName()); + } + + @Test + void podNameKindNotPod() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder() + .withTargetRef(new V1ObjectReferenceBuilder().withKind("Service").build()).build(); + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertNull(result.podName()); + } + + @Test + void podNameKindIsPod() { + V1EndpointAddress endpointAddress = new V1EndpointAddressBuilder() + .withTargetRef(new V1ObjectReferenceBuilder().withKind("Pod").withName("my-pod").build()).build(); + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withExternalName("external-name").build()) + .withMetadata(new V1ObjectMeta()).build(); + + K8sInstanceIdHostPodNameSupplier supplier = K8sInstanceIdHostPodNameSupplier.nonExternalName(endpointAddress, + service); + InstanceIdHostPodName result = supplier.get(); + + Assertions.assertNotNull(result); + Assertions.assertEquals(result.podName(), "my-pod"); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplierTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplierTests.java new file mode 100644 index 00000000..8b9ad135 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/K8sPodLabelsAndAnnotationsSupplierTests.java @@ -0,0 +1,74 @@ +/* + * 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.client.discovery; + +import java.util.Map; + +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1PodBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.cloud.kubernetes.commons.discovery.PodLabelsAndAnnotations; + +/** + * @author wind57 + */ +class K8sPodLabelsAndAnnotationsSupplierTests { + + private static final String NAMESPACE = "spring-k8s"; + + private static final String POD_NAME = "my-pod"; + + private final CoreV1Api coreV1Api = Mockito.mock(CoreV1Api.class); + + @AfterEach + void afterEach() { + Mockito.reset(coreV1Api); + } + + @Test + void noObjetMeta() throws Exception { + + Mockito.when(coreV1Api.readNamespacedPod(POD_NAME, NAMESPACE, null)).thenReturn( + new V1PodBuilder().withMetadata(new V1ObjectMetaBuilder().withName(POD_NAME).build()).build()); + + PodLabelsAndAnnotations result = K8sPodLabelsAndAnnotationsSupplier.nonExternalName(coreV1Api, NAMESPACE) + .apply(POD_NAME); + Assertions.assertNotNull(result); + Assertions.assertTrue(result.labels().isEmpty()); + Assertions.assertTrue(result.annotations().isEmpty()); + } + + @Test + void labelsAndAnnotationsPresent() throws Exception { + + Mockito.when(coreV1Api.readNamespacedPod(POD_NAME, NAMESPACE, null)) + .thenReturn(new V1PodBuilder().withMetadata(new V1ObjectMetaBuilder().withName(POD_NAME) + .withLabels(Map.of("a", "b")).withAnnotations(Map.of("c", "d")).build()).build()); + + PodLabelsAndAnnotations result = K8sPodLabelsAndAnnotationsSupplier.nonExternalName(coreV1Api, NAMESPACE) + .apply(POD_NAME); + Assertions.assertNotNull(result); + Assertions.assertEquals(result.labels(), Map.of("a", "b")); + Assertions.assertEquals(result.annotations(), Map.of("c", "d")); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java index 312f396f..f04a3b92 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests.java @@ -20,6 +20,7 @@ import java.util.Collections; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; import okhttp3.OkHttpClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -91,6 +92,11 @@ public class KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests { @Configuration(proxyBeanMethods = false) protected static class EnvironmentKnobbler { + @Bean + CoreV1Api coreV1Api(ApiClient apiClient) { + return new CoreV1Api(apiClient); + } + @Bean ApiClient apiClient() { ApiClient apiClient = mock(ApiClient.class); diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientFilterMetadataTest.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientFilterMetadataTest.java new file mode 100644 index 00000000..3071b367 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientFilterMetadataTest.java @@ -0,0 +1,277 @@ +/* + * 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.client.discovery; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.kubernetes.client.informer.cache.Cache; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServicePort; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.assertj.core.util.Strings; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; + +import static java.util.Map.entry; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesDiscoveryClientFilterMetadataTest { + + private static final SharedInformerFactoryStub STUB = new SharedInformerFactoryStub(); + + private static final SharedInformerStub SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private static final SharedInformerStub ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private Cache servicesCache; + + private Lister servicesLister; + + private Cache endpointsCache; + + private Lister endpointsLister; + + @BeforeEach + void beforeEach() { + servicesCache = new Cache<>(); + servicesLister = new Lister<>(servicesCache); + + endpointsCache = new Cache<>(); + endpointsLister = new Lister<>(endpointsCache); + } + + @Test + void testAllExtraMetadataDisabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "lab"), Map.of("l1", "lab"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).isEqualTo(Map.of("k8s_namespace", "ns", "type", "ClusterIP")); + } + + @Test + void testLabelsEnabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, null, false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1", "l2", "v2"), Map.of("l1", "lab"), Map.of(80, "http", 5555, "")); + + 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("type", "ClusterIP")); + } + + @Test + void testLabelsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, "l_", false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1", "l2", "v2"), Map.of("l1", "lab"), Map.of(80, "http", 5555, "")); + + 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("type", "ClusterIP")); + } + + @Test + void testAnnotationsEnabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, true, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + 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("type", "ClusterIP")); + } + + @Test + void testAnnotationsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, true, + "a_", false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + 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("type", "ClusterIP")); + } + + @Test + void testPortsEnabled() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, true, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "test", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("http", "80"), entry("k8s_namespace", "test"), + entry("", "5555"), entry("type", "ClusterIP")); + } + + @Test + void testPortsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, true, "p_"); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "v1"), Map.of("a1", "v1", "a2", "v2"), Map.of(80, "http", 5555, "")); + + List instances = discoveryClient.getInstances(serviceId); + assertThat(instances).hasSize(1); + assertThat(instances.get(0).getMetadata()).containsOnly(entry("p_http", "80"), entry("k8s_namespace", "ns"), + entry("p_", "5555"), entry("type", "ClusterIP")); + } + + @Test + void testLabelsAndAnnotationsAndPortsEnabledWithPrefix() { + String serviceId = "s"; + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, "l_", true, + "a_", true, "p_"); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + setup(serviceId, "ns", Map.of("l1", "la1"), Map.of("a1", "an1", "a2", "an2"), Map.of(80, "http", 5555, "")); + + 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("type", "ClusterIP"), + entry("p_", "5555")); + } + + private void setup(String serviceId, String namespace, Map labels, Map annotations, + Map ports) { + + V1Service service = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withType("ClusterIP").withPorts(getServicePorts(ports)).build()) + .withNewMetadata().withName(serviceId).withNamespace(namespace).withLabels(labels) + .withAnnotations(annotations).endMetadata().build(); + + servicesCache.add(service); + + V1ObjectMeta objectMeta = new V1ObjectMeta(); + objectMeta.setNamespace(namespace); + objectMeta.setName(serviceId); + + V1Endpoints endpoints = new V1EndpointsBuilder().withMetadata(objectMeta).addNewSubset() + .addAllToPorts(getEndpointPorts(ports)).addNewAddress().endAddress().endSubset().build(); + + endpointsCache.add(endpoints); + + } + + private List getServicePorts(Map ports) { + return ports.entrySet().stream().map(e -> { + V1ServicePortBuilder servicePortBuilder = new V1ServicePortBuilder(); + servicePortBuilder.withPort(e.getKey()); + if (!Strings.isNullOrEmpty(e.getValue())) { + servicePortBuilder.withName(e.getValue()); + } + return servicePortBuilder.build(); + }).collect(toList()); + } + + private List getEndpointPorts(Map ports) { + return ports.entrySet().stream().map(e -> { + CoreV1EndpointPortBuilder endpointPortBuilder = new CoreV1EndpointPortBuilder(); + endpointPortBuilder.withPort(e.getKey()); + if (!Strings.isNullOrEmpty(e.getValue())) { + endpointPortBuilder.withName(e.getValue()); + } + return endpointPortBuilder.build(); + }).collect(toList()); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceMetadataTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceMetadataTests.java deleted file mode 100644 index 5d6b904a..00000000 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceMetadataTests.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2013-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.kubernetes.client.discovery; - -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import io.kubernetes.client.openapi.models.V1ObjectMeta; -import io.kubernetes.client.openapi.models.V1Service; -import io.kubernetes.client.openapi.models.V1ServiceSpec; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; - -/** - * @author wind57 - */ -@ExtendWith(OutputCaptureExtension.class) -class KubernetesDiscoveryClientServiceMetadataTests { - - /** - *
-	 *     - labels are not added
-	 *     - annotations are not added
-	 * 
- */ - @Test - void testServiceMetadataEmpty() { - boolean addLabels = false; - String labelsPrefix = ""; - boolean addAnnotations = false; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - 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, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")) - .metadata(new V1ObjectMeta().namespace("default")); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - Assertions.assertEquals(result.size(), 2); - Assertions.assertEquals(result, Map.of("k8s_namespace", "default", "type", "ClusterIP")); - } - - /** - *
-	 *     - labels are added without a prefix
-	 *     - annotations are not added
-	 * 
- */ - @Test - void testServiceMetadataAddLabelsNoPrefix(CapturedOutput output) { - boolean addLabels = true; - String labelsPrefix = ""; - boolean addAnnotations = false; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - 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, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")) - .metadata(new V1ObjectMeta().namespace("default").labels(Map.of("a", "b"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - 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")); - } - - /** - *
-	 *     - labels are added with prefix
-	 *     - annotations are not added
-	 * 
- */ - @Test - void testServiceMetadataAddLabelsWithPrefix(CapturedOutput output) { - boolean addLabels = true; - String labelsPrefix = "prefix-"; - boolean addAnnotations = false; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - 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, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")) - .metadata(new V1ObjectMeta().namespace("default").labels(Map.of("a", "b", "c", "d"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - 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 = filterOnK8sNamespaceAndType(result); - Assertions.assertTrue( - output.getOut().contains("Adding labels metadata: " + labelsMetadata + " for serviceId: my-service")); - } - - /** - *
-	 *     - labels are not added
-	 *     - annotations are added without prefix
-	 * 
- */ - @Test - void testServiceMetadataAddAnnotationsNoPrefix(CapturedOutput output) { - boolean addLabels = false; - String labelsPrefix = ""; - boolean addAnnotations = true; - String annotationsPrefix = ""; - boolean addPorts = false; - String portsPrefix = ""; - - 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, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")).metadata( - new V1ObjectMeta().namespace("default").labels(Map.of("a", "b")).annotations(Map.of("aa", "bb"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - 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")); - } - - /** - *
-	 *     - labels are not added
-	 *     - annotations are added with prefix
-	 * 
- */ - @Test - void testServiceMetadataAddAnnotationsWithPrefix(CapturedOutput output) { - boolean addLabels = false; - String labelsPrefix = ""; - boolean addAnnotations = true; - String annotationsPrefix = "prefix-"; - boolean addPorts = false; - String portsPrefix = ""; - - 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, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")).metadata(new V1ObjectMeta() - .namespace("default").labels(Map.of("a", "b")).annotations(Map.of("aa", "bb", "cc", "dd"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - 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 = filterOnK8sNamespaceAndType(result); - Assertions.assertTrue( - output.getOut().contains("Adding annotations metadata: " + annotations + " for serviceId: my-service")); - } - - /** - *
-	 *     - labels are added with prefix
-	 *     - annotations are added with prefix
-	 * 
- */ - @Test - void testServiceMetadataAddLabelsAndAnnotationsWithPrefix(CapturedOutput output) { - boolean addLabels = true; - String labelsPrefix = "label-"; - boolean addAnnotations = true; - String annotationsPrefix = "annotation-"; - boolean addPorts = false; - String portsPrefix = ""; - - 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, false); - V1Service service = new V1Service().spec(new V1ServiceSpec().type("ClusterIP")).metadata(new V1ObjectMeta() - .namespace("default").labels(Map.of("a", "b", "c", "d")).annotations(Map.of("aa", "bb", "cc", "dd"))); - - Map result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service"); - 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", "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(); - String annotations = result.entrySet().stream().filter(en -> en.getKey().contains("annotation")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).toString(); - Assertions.assertTrue( - output.getOut().contains("Adding labels metadata: " + labels + " for serviceId: my-service")); - Assertions.assertTrue( - output.getOut().contains("Adding annotations metadata: " + annotations + " for serviceId: my-service")); - } - - 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-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceWithoutPortNameTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceWithoutPortNameTests.java new file mode 100644 index 00000000..5bfcc509 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientServiceWithoutPortNameTests.java @@ -0,0 +1,104 @@ +/* + * 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.client.discovery; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.kubernetes.client.informer.cache.Cache; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; + +/** + * @author wind57 + */ +class KubernetesDiscoveryClientServiceWithoutPortNameTests { + + private static final String NAMESPACE = "spring-k8s"; + + private static final SharedInformerFactoryStub STUB = new SharedInformerFactoryStub(); + + private static final SharedInformerStub SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private static final SharedInformerStub ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private Cache servicesCache; + + private Lister servicesLister; + + private Cache endpointsCache; + + private Lister endpointsLister; + + @BeforeEach + void beforeEach() { + servicesCache = new Cache<>(); + servicesLister = new Lister<>(servicesCache); + + endpointsCache = new Cache<>(); + endpointsLister = new Lister<>(endpointsCache); + } + + @Test + void testDiscoveryWithoutAServicePortName() { + + V1Endpoints endpoints = new V1EndpointsBuilder() + .withSubsets( + new V1EndpointSubsetBuilder().withPorts(new CoreV1EndpointPortBuilder().withPort(8080).build()) + .withAddresses(new V1EndpointAddressBuilder().withIp("127.0.0.1").build()).build()) + .withMetadata( + new V1ObjectMetaBuilder().withName("no-port-name-service").withNamespace(NAMESPACE).build()) + .build(); + endpointsCache.add(endpoints); + + V1Service service = new V1ServiceBuilder() + .withSpec( + new V1ServiceSpecBuilder().withPorts(new V1ServicePortBuilder().withPort(8080).build()).build()) + .withMetadata( + new V1ObjectMetaBuilder().withName("no-port-name-service").withNamespace(NAMESPACE).build()) + .withSpec(new V1ServiceSpecBuilder().withType("ClusterIP").build()).build(); + servicesCache.add(service); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(NAMESPACE), + true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List serviceInstances = discoveryClient.getInstances("no-port-name-service"); + Assertions.assertEquals(serviceInstances.size(), 1); + Assertions.assertEquals(serviceInstances.get(0).getMetadata(), + Map.of("port.", "8080", "k8s_namespace", "spring-k8s", "type", "ClusterIP")); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientTests.java new file mode 100644 index 00000000..b531bf30 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientTests.java @@ -0,0 +1,457 @@ +/* + * 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.client.discovery; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.kubernetes.client.informer.cache.Cache; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesServiceInstance; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author wind57 + */ +class KubernetesDiscoveryClientTests { + + private static final SharedInformerFactoryStub STUB = new SharedInformerFactoryStub(); + + private static final SharedInformerStub SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private static final SharedInformerStub ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>(); + + private Cache servicesCache; + + private Lister servicesLister; + + private Cache endpointsCache; + + private Lister endpointsLister; + + @BeforeEach + void beforeEach() { + servicesCache = new Cache<>(); + servicesLister = new Lister<>(servicesCache); + + endpointsCache = new Cache<>(); + endpointsLister = new Lister<>(endpointsCache); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsSingleAddress() { + Map labels = Map.of("l", "v"); + String serviceId = "id"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("10"); + List names = List.of("http"); + List protocols = List.of("TCP"); + List ports = List.of(80); + List appProtocols = List.of("appTCP"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, false, null); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("id"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("10")).hasSize(1); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsSingleAddressAndMultiplePorts() { + Map labels = Map.of("l2", "v2"); + String serviceId = "endpoint"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("20"); + List names = List.of("http", "mgmt"); + List protocols = List.of("TCP", "TCP"); + List ports = List.of(80, 900); + List appProtocols = List.of("http_tcp", "mgmt_tcp"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), labels, "http_tcp", KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("20")).hasSize(1).filteredOn(s -> 80 == s.getPort()) + .hasSize(1); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsMultipleAddresses() { + Map labels = Map.of("l1", "v1"); + String serviceId = "endpoint"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1", "ip2"); + List uuids = List.of("40", "50"); + List names = List.of("https"); + List protocols = List.of("TCP"); + List ports = List.of(443); + List appProtocols = List.of("https_tcp"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(false, null, false, + null, true, "port."); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(443, 8443), labels, null, metadata, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint"); + + assertThat(instances).hasSize(2).filteredOn(ServiceInstance::isSecure).extracting(ServiceInstance::getHost) + .containsOnly("ip1", "ip2"); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsFromMultipleNamespaces() { + + Map labels = Map.of("l", "v"); + String serviceId = "endpoint"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("60"); + List names = List.of("http"); + List protocols = List.of("TCP"); + List ports = List.of(80); + List appProtocols = List.of("https_tcp"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + ips = List.of("ip2"); + uuids = List.of("70"); + namespace = "test2"; + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint"); + + assertThat(instances).hasSize(2); + assertThat(instances).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1); + assertThat(instances).filteredOn(s -> s.getHost().equals("ip2") && !s.isSecure()).hasSize(1); + assertThat(instances).filteredOn(s -> s.getServiceId().contains("endpoint") + && ((KubernetesServiceInstance) s).getNamespace().equals("test")).hasSize(1); + assertThat(instances).filteredOn(s -> s.getServiceId().contains("endpoint") + && ((KubernetesServiceInstance) s).getNamespace().equals("test2")).hasSize(1); + assertThat(instances).filteredOn(s -> s.getInstanceId().equals("60")).hasSize(1); + assertThat(instances).filteredOn(s -> s.getInstanceId().equals("70")).hasSize(1); + } + + @Test + void instanceWithoutSubsetsShouldBeSkipped() { + V1Endpoints endpoints = new V1EndpointsBuilder().withNewMetadata().withName("endpoint1").withNamespace("test") + .withLabels(Collections.emptyMap()).endMetadata().build(); + endpointsCache.add(endpoints); + + V1Service service = new V1ServiceBuilder().withNewMetadata().withName("endpoint1").withNamespace("test").and() + .build(); + servicesCache.add(service); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint1"); + + assertThat(instances).isEmpty(); + } + + @Test + void getInstancesShouldBeAbleToHandleEndpointsSingleAddressAndMultiplePortsUsingPrimaryPortNameLabel() { + Map labels = Map.of("primary-port-name", "https"); + String serviceId = "endpoint2"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("80"); + List names = List.of("http", "https"); + List protocols = List.of("TCP", "TCP"); + List ports = List.of(80, 443); + List appProtocols = List.of("http", "https"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint2"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("80")).hasSize(1).filteredOn(s -> 443 == s.getPort()) + .hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndMisconfiguredPrimaryPortNameInLabelWithoutFallbackShouldLogWarning() { + Map labels = Map.of("primary-port-name", "oops"); + String serviceId = "endpoint3"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("90"); + List names = List.of("httpA", "httpB", "httpC", "httpD"); + List protocols = List.of("TCP", "TCP", "TCP", "TCP"); + List ports = List.of(8443, 443, 80, 8080); + List appProtocols = List.of("https1", "https2", "http1", "http2"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint3"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("90")).hasSize(1).hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndMisconfiguredGenericPrimaryPortNameWithoutFallbackShouldLogWarning() { + Map labels = Map.of(); + String serviceId = "endpoint4"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("100"); + List names = List.of("httpA", "httpB", "httpC", "httpD"); + List protocols = List.of("TCP", "TCP", "TCP", "TCP"); + List ports = List.of(8443, 443, 80, 8080); + List appProtocols = List.of("https1", "https2", "http1", "http2"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), "oops", KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint4"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("100")).hasSize(1).hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedShouldFallBackToHttps() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("110"); + List names = List.of("httpA", "httpB"); + List protocols = List.of("TCP", "TCP"); + List ports = List.of(443, 80); + List appProtocols = List.of("http", "https"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, + false, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, + true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint5"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("110")).hasSize(1).filteredOn(s -> 443 == s.getPort()) + .hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedOrHttpsPortShouldFallBackToHttp() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("120"); + List names = List.of("httpA", "httpB", "httpC"); + List protocols = List.of("http", "http", "http"); + List ports = List.of(80, 8443, 80); + List appProtocols = List.of("TCP", "TCP", "TCP"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, + KubernetesDiscoveryProperties.DEFAULT); + + List instances = discoveryClient.getInstances("endpoint5"); + + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("120")).hasSize(1).filteredOn(s -> 80 == s.getPort()) + .hasSize(1); + } + + @Test + void instanceWithMultiplePortsAndWithoutPrimaryPortNameSpecifiedShouldLogWarning() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("130"); + List names = List.of("http", "https"); + List protocols = List.of("http", "https"); + List ports = List.of(80, 443); + List appProtocols = List.of("TCP", "TCP"); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + true, null, Set.of(443, 8443), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint5"); + + // We're returning the first discovered port to not change previous behaviour + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && s.isSecure()).hasSize(1) + .filteredOn(s -> s.getInstanceId().equals("130")).hasSize(1).filteredOn(s -> 443 == s.getPort()) + .hasSize(1); + } + + @Test + public void instanceWithoutPorts() { + Map labels = Map.of(); + String serviceId = "endpoint5"; + String serviceType = "ExternalName"; + String namespace = "test"; + List ips = List.of("ip1"); + List uuids = List.of("130"); + List names = List.of(); + List protocols = List.of(); + List ports = List.of(); + List appProtocols = List.of(); + + setup(serviceId, serviceType, namespace, labels, ips, uuids, names, protocols, ports, appProtocols); + + KubernetesDiscoveryProperties properties = KubernetesDiscoveryProperties.DEFAULT; + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(STUB, servicesLister, + endpointsLister, SERVICE_SHARED_INFORMER_STUB, ENDPOINTS_SHARED_INFORMER_STUB, properties); + + List instances = discoveryClient.getInstances("endpoint5"); + + // We're returning the first discovered port to not change previous behaviour + assertThat(instances).hasSize(1).filteredOn(s -> s.getHost().equals("ip1") && !s.isSecure()).hasSize(1) + .filteredOn(s -> s.getUri().toASCIIString().equals("http://ip1")) + .filteredOn(s -> s.getInstanceId().equals("130")).hasSize(1).filteredOn(s -> 0 == s.getPort()) + .hasSize(1); + } + + private void setup(String serviceId, String serviceType, String namespace, Map labels, + List ips, List uuids, List names, List protocols, List ports, + List appProtocols) { + + V1Service service = new V1ServiceBuilder().withSpec(new V1ServiceSpecBuilder().withType(serviceType).build()) + .withNewMetadata().withName(serviceId).withNamespace(namespace).withLabels(labels).endMetadata() + .build(); + + servicesCache.add(service); + + V1ObjectMeta objectMeta = new V1ObjectMeta(); + objectMeta.setNamespace(namespace); + objectMeta.setName(serviceId); + + V1Endpoints endpoints = new V1EndpointsBuilder().withNewMetadata().withName(serviceId).withNamespace(namespace) + .withLabels(labels).endMetadata().build(); + + List addresses = new ArrayList<>(); + for (int i = 0; i < ips.size(); ++i) { + V1EndpointAddress address = new V1EndpointAddressBuilder().withIp(ips.get(i)).withNewTargetRef() + .withUid(uuids.get(i)).endTargetRef().build(); + addresses.add(address); + } + + V1EndpointSubset subset = new V1EndpointSubset(); + subset.setAddresses(addresses); + + List corePorts = new ArrayList<>(); + for (int i = 0; i < names.size(); ++i) { + CoreV1EndpointPort port = new CoreV1EndpointPortBuilder().withName(names.get(i)) + .withProtocol(protocols.get(i)).withPort(ports.get(i)).withAppProtocol(appProtocols.get(i)).build(); + corePorts.add(port); + } + subset.setPorts(corePorts); + endpoints.setSubsets(List.of(subset)); + + endpointsCache.add(endpoints); + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java index 4f3551e6..4cf2620b 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesDiscoveryClientUtilsTests.java @@ -17,10 +17,16 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.openapi.models.V1ServiceBuilder; @@ -32,6 +38,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.endpointSubsetsPortData; import static org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientUtils.matchesServiceLabels; /** @@ -161,6 +168,139 @@ class KubernetesDiscoveryClientUtilsTests { Assertions.assertTrue(output.getOut().contains("Service labels from service : {a=b, c=d}")); } + @Test + void testPortsDataOne() { + List endpointSubsets = List.of( + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8081).withName("").build()).build(), + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8080).withName("https").build()).build()); + + Map portsData = endpointSubsetsPortData(endpointSubsets); + Assertions.assertEquals(portsData.size(), 2); + Assertions.assertEquals(portsData.get("https"), 8080); + Assertions.assertEquals(portsData.get(""), 8081); + } + + @Test + void testPortsDataTwo() { + List endpointSubsets = List.of( + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8081).withName("http").build()).build(), + new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(8080).withName("https").build()).build()); + + Map portsData = endpointSubsetsPortData(endpointSubsets); + Assertions.assertEquals(portsData.size(), 2); + Assertions.assertEquals(portsData.get("https"), 8080); + Assertions.assertEquals(portsData.get("http"), 8081); + } + + @Test + void endpointSubsetPortsDataWithoutPorts() { + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder().build(); + Map result = endpointSubsetsPortData(List.of(endpointSubset)); + + Assertions.assertEquals(result.size(), 0); + } + + @Test + void endpointSubsetPortsDataSinglePort() { + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withName("name").withPort(80).build()).build(); + Map result = endpointSubsetsPortData(List.of(endpointSubset)); + + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get("name"), 80); + } + + @Test + void endpointSubsetPortsDataSinglePortNoName() { + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withPorts(new CoreV1EndpointPortBuilder().withPort(80).build()).build(); + Map result = endpointSubsetsPortData(List.of(endpointSubset)); + + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(""), 80); + } + + /** + *
+	 *      - ready addresses are empty
+	 *      - not ready addresses are not included
+	 * 
+ */ + @Test + void testEmptyAddresses() { + boolean includeNotReadyAddresses = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder().build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 0); + } + + /** + *
+	 *      - ready addresses has two entries
+	 *      - not ready addresses are not included
+	 * 
+ */ + @Test + void testReadyAddressesOnly() { + boolean includeNotReadyAddresses = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withAddresses(new V1EndpointAddressBuilder().withHostname("one").build(), + new V1EndpointAddressBuilder().withHostname("two").build()) + .build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 2); + } + + /** + *
+	 *      - ready addresses has two entries
+	 *      - not ready addresses has a single entry, but we do not take it
+	 * 
+ */ + @Test + void testReadyAddressesTakenNotReadyAddressesNotTaken() { + boolean includeNotReadyAddresses = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withAddresses(new V1EndpointAddressBuilder().withHostname("one").build(), + new V1EndpointAddressBuilder().withHostname("two").build()) + .withNotReadyAddresses(new V1EndpointAddressBuilder().withHostname("three").build()).build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 2); + List hostNames = addresses.stream().map(V1EndpointAddress::getHostname).sorted().toList(); + Assertions.assertEquals(hostNames, List.of("one", "two")); + } + + /** + *
+	 *      - ready addresses has two entries
+	 *      - not ready addresses has a single entry, but we do not take it
+	 * 
+ */ + @Test + void testBothAddressesTaken() { + boolean includeNotReadyAddresses = true; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60L, + includeNotReadyAddresses, "", Set.of(), Map.of(), "", null, 0, false); + V1EndpointSubset endpointSubset = new V1EndpointSubsetBuilder() + .withAddresses(new V1EndpointAddressBuilder().withHostname("one").build(), + new V1EndpointAddressBuilder().withHostname("two").build()) + .withNotReadyAddresses(new V1EndpointAddressBuilder().withHostname("three").build()).build(); + List addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties); + Assertions.assertEquals(addresses.size(), 3); + List hostNames = addresses.stream().map(V1EndpointAddress::getHostname).sorted().toList(); + Assertions.assertEquals(hostNames, List.of("one", "three", "two")); + } + // preserve order for testing reasons private Map ordered(Map input) { return input.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect( diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java index 626009fa..a83aeeac 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java @@ -21,16 +21,33 @@ import java.util.List; import java.util.Map; import java.util.Set; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Cache; import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.CoreV1EndpointPortBuilder; import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointAddressBuilder; import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder; import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsBuilder; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodBuilder; import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; import io.kubernetes.client.openapi.models.V1ServiceSpec; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -38,6 +55,7 @@ import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.assertj.core.api.Assertions.assertThat; class KubernetesInformerDiscoveryClientTests { @@ -89,8 +107,8 @@ class KubernetesInformerDiscoveryClientTests { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1").toArray()) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("", "80", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "1.1.1.1", 80, + Map.of("port.", "80", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -136,11 +154,10 @@ class KubernetesInformerDiscoveryClientTests { assertThat(discoveryClient.getInstances("test-svc-1").toArray()).isEmpty(); assertThat(discoveryClient.getInstances("test-svc-3").toArray()) - .containsOnly( - new DefaultKubernetesServiceInstance( - "", "test-svc-3", "2.2.2.2", 8080, Map.of("spring", "true", "", "8080", "k8s", - "true", "k8s_namespace", "namespace1", "type", "ClusterIP"), - false, "namespace1", null)); + .containsOnly(new DefaultKubernetesServiceInstance( + null, "test-svc-3", "2.2.2.2", 8080, Map.of("spring", "true", "port.", "8080", "k8s", + "true", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -188,8 +205,8 @@ class KubernetesInformerDiscoveryClientTests { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -202,8 +219,8 @@ class KubernetesInformerDiscoveryClientTests { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -232,8 +249,8 @@ class KubernetesInformerDiscoveryClientTests { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @@ -250,7 +267,7 @@ class KubernetesInformerDiscoveryClientTests { } @Test - void instanceWithoutPortsShouldBeSkipped() { + void instanceWithoutPortsWillNotBeSkipped() { Lister serviceLister = setupServiceLister(SERVICE_1); Lister endpointsLister = setupEndpointsLister(ENDPOINTS_NO_PORTS); @@ -258,7 +275,9 @@ class KubernetesInformerDiscoveryClientTests { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, KubernetesDiscoveryProperties.DEFAULT); - assertThat(discoveryClient.getInstances("test-svc-1")).isEmpty(); + assertThat(discoveryClient.getInstances("test-svc-1")) + .containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "1.1.1.1", 0, + Map.of("k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null)); } @Test @@ -269,12 +288,10 @@ class KubernetesInformerDiscoveryClientTests { KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly( - new DefaultKubernetesServiceInstance( - "", "test-svc-1", "1.1.1.1", 443, Map.of("http", "80", "primary-port-name", "https", - "https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), - false, "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance( + null, "test-svc-1", "1.1.1.1", 443, Map.of("port.http", "80", "primary-port-name", "https", + "port.https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + true, "namespace1", null)); } @Test @@ -287,11 +304,10 @@ class KubernetesInformerDiscoveryClientTests { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly( - new DefaultKubernetesServiceInstance( - "", "test-svc-1", "1.1.1.1", 80, Map.of("tcp1", "80", "primary-port-name", "oops", - "tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), - false, "namespace1", null)); + .containsOnly(new DefaultKubernetesServiceInstance( + null, "test-svc-1", "1.1.1.1", 80, Map.of("port.tcp1", "80", "primary-port-name", "oops", + "port.tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -302,10 +318,10 @@ class KubernetesInformerDiscoveryClientTests { KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 443, - Map.of("http", "80", "https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 443, + Map.of("port.http", "80", "port.https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + true, "namespace1", null)); } @Test @@ -317,10 +333,10 @@ class KubernetesInformerDiscoveryClientTests { KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("tcp1", "80", "tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 80, + Map.of("port.tcp1", "80", "port.tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -331,10 +347,10 @@ class KubernetesInformerDiscoveryClientTests { KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 443, - Map.of("http", "80", "https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 443, + Map.of("port.http", "80", "port.https", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + true, "namespace1", null)); } @Test @@ -345,10 +361,10 @@ class KubernetesInformerDiscoveryClientTests { KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("http", "80", "tcp", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 80, + Map.of("port.http", "80", "port.tcp", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + "namespace1", null)); } @Test @@ -360,10 +376,10 @@ class KubernetesInformerDiscoveryClientTests { KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, NOT_ALL_NAMESPACES); - assertThat(discoveryClient.getInstances("test-svc-1")) - .containsOnly(new DefaultKubernetesServiceInstance("", "test-svc-1", "1.1.1.1", 80, - Map.of("tcp1", "80", "tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, - "namespace1", null)); + assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly(new DefaultKubernetesServiceInstance(null, + "test-svc-1", "1.1.1.1", 80, + Map.of("port.tcp1", "80", "port.tcp2", "443", "k8s_namespace", "namespace1", "type", "ClusterIP"), + false, "namespace1", null)); } @Test @@ -375,11 +391,11 @@ class KubernetesInformerDiscoveryClientTests { SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, ALL_NAMESPACES); assertThat(discoveryClient.getInstances("test-svc-1")).containsOnly( - new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, + new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false, "namespace1", null), - new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("", "8080", "k8s_namespace", "namespace2", "type", "ClusterIP"), false, + new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("port.", "8080", "k8s_namespace", "namespace2", "type", "ClusterIP"), false, "namespace2", null)); } @@ -496,6 +512,94 @@ class KubernetesInformerDiscoveryClientTests { assertThat(serviceInstances.get(1).getMetadata().get("k8s_namespace")).isEqualTo("namespaceB"); } + @Test + void testExternalNameService() { + V1Service externalNameService = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().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(); + + V1Endpoints endpoints = new V1EndpointsBuilder().withMetadata(new V1ObjectMeta().namespace("irrelevant")) + .build(); + + Lister serviceLister = setupServiceLister(externalNameService); + Lister endpointsLister = setupEndpointsLister(endpoints); + + 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); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( + SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, properties); + + 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")); + } + + @Test + void testPodMetadata() { + V1Service nonExternalNameService = new V1ServiceBuilder() + .withSpec(new V1ServiceSpecBuilder().withType("ClusterIP").build()).withNewMetadata() + .withName("blue-service").withNamespace("a").endMetadata().build(); + + V1Endpoints endpoints = new V1EndpointsBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("blue-service").withNamespace("a").build()) + .withSubsets( + new V1EndpointSubsetBuilder().withPorts(new CoreV1EndpointPortBuilder().withPort(8080).build()) + .withAddresses(new V1EndpointAddressBuilder().withIp("127.0.0.1").withTargetRef( + new V1ObjectReferenceBuilder().withKind("Pod").withName("my-pod").build()) + .build()) + .build()) + .build(); + + Lister serviceLister = setupServiceLister(nonExternalNameService); + Lister endpointsLister = setupEndpointsLister(endpoints); + + WireMockServer server = new WireMockServer(options().dynamicPort()); + server.start(); + WireMock.configureFor("localhost", server.port()); + ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + server.port()).build(); + + V1Pod pod = new V1PodBuilder().withNewMetadata().withName("my-pod").withLabels(Map.of("a", "b")) + .withAnnotations(Map.of("c", "d")).endMetadata().build(); + + WireMock.stubFor(WireMock.get("/api/v1/namespaces/a/pods/my-pod") + .willReturn(WireMock.aResponse().withStatus(200).withBody(new JSON().serialize(pod)))); + + KubernetesDiscoveryProperties.Metadata metadata = new KubernetesDiscoveryProperties.Metadata(true, + "labels-prefix-", true, "annotations-prefix-", true, "ports-prefix", true, true); + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of("a", "b"), + true, 60L, false, "", Set.of(), Map.of(), "", metadata, 0, false, true); + + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( + SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, properties); + discoveryClient.coreV1Api = new CoreV1Api(apiClient); + + List result = discoveryClient.getInstances("blue-service"); + Assertions.assertEquals(result.size(), 1); + DefaultKubernetesServiceInstance serviceInstance = (DefaultKubernetesServiceInstance) result.get(0); + Assertions.assertEquals(serviceInstance.getServiceId(), "blue-service"); + Assertions.assertEquals(serviceInstance.getHost(), "127.0.0.1"); + Assertions.assertEquals(serviceInstance.getPort(), 8080); + Assertions.assertFalse(serviceInstance.isSecure()); + Assertions.assertEquals(serviceInstance.getUri().toASCIIString(), "http://127.0.0.1:8080"); + Assertions.assertEquals(serviceInstance.getMetadata(), + Map.of("k8s_namespace", "a", "type", "ClusterIP", "ports-prefix", "8080")); + Assertions.assertEquals(serviceInstance.podMetadata().get("labels"), Map.of("a", "b")); + Assertions.assertEquals(serviceInstance.podMetadata().get("annotations"), Map.of("c", "d")); + + server.shutdown(); + } + private Lister setupServiceLister(V1Service... services) { Cache serviceCache = new Cache<>(); Lister serviceLister = new Lister<>(serviceCache); diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerFactoryStub.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerFactoryStub.java new file mode 100644 index 00000000..cae63d6e --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerFactoryStub.java @@ -0,0 +1,31 @@ +/* + * 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.client.discovery; + +import io.kubernetes.client.informer.SharedInformerFactory; + +/** + * @author wind57 + */ +final class SharedInformerFactoryStub extends SharedInformerFactory { + + @Override + public void startAllRegisteredInformers() { + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerStub.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerStub.java new file mode 100644 index 00000000..1aea2b60 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/SharedInformerStub.java @@ -0,0 +1,65 @@ +/* + * 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.client.discovery; + +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.informer.ResourceEventHandler; +import io.kubernetes.client.informer.SharedInformer; +import io.kubernetes.client.informer.TransformFunc; + +/** + * @author wind57 + */ +final class SharedInformerStub implements SharedInformer { + + @Override + public void addEventHandler(ResourceEventHandler handler) { + + } + + @Override + public void addEventHandlerWithResyncPeriod(ResourceEventHandler handler, long resyncPeriod) { + + } + + @Override + public void run() { + + } + + @Override + public void stop() { + + } + + // this is the only method we care about + @Override + public boolean hasSynced() { + return true; + } + + @Override + public String lastSyncResourceVersion() { + return null; + } + + @Override + public void setTransform(TransformFunc transformFunc) { + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java index 16d98059..a5b9e321 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java @@ -30,6 +30,7 @@ import io.kubernetes.client.openapi.models.V1Endpoints; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.openapi.models.V1ServiceSpec; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -128,8 +129,8 @@ class KubernetesInformerReactiveDiscoveryClientTests { kubernetesDiscoveryProperties)); StepVerifier.create(discoveryClient.getInstances("test-svc-1")) - .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("type", "ClusterIP", "", "8080", "k8s_namespace", "namespace1"), false, + .expectNext(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("type", "ClusterIP", "port.", "8080", "k8s_namespace", "namespace1"), false, "namespace1", null)) .expectComplete().verify(); @@ -149,8 +150,8 @@ class KubernetesInformerReactiveDiscoveryClientTests { kubernetesDiscoveryProperties)); StepVerifier.create(discoveryClient.getInstances("test-svc-1")) - .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, - Map.of("type", "ClusterIP", "", "8080", "k8s_namespace", "namespace1"), false, + .expectNext(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080, + Map.of("type", "ClusterIP", "port.", "8080", "k8s_namespace", "namespace1"), false, "namespace1", null)) .expectComplete().verify(); @@ -237,9 +238,11 @@ class KubernetesInformerReactiveDiscoveryClientTests { boolean allNamespaces = true; V1Service serviceXNamespaceA = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); V1Service serviceXNamespaceB = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); serviceCache.add(serviceXNamespaceA); serviceCache.add(serviceXNamespaceB); @@ -286,9 +289,11 @@ class KubernetesInformerReactiveDiscoveryClientTests { boolean allNamespaces = true; V1Service serviceXNamespaceA = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); V1Service serviceXNamespaceB = new V1Service() - .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")); + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")) + .spec(new V1ServiceSpecBuilder().withType("ClusterIP").build()); serviceCache.add(serviceXNamespaceA); serviceCache.add(serviceXNamespaceB); diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java index bfc95cf5..68b8471e 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerPodModeTests.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Collections; import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.util.ClientBuilder; import org.junit.jupiter.api.Test; @@ -52,13 +53,13 @@ import static org.mockito.Mockito.when; */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KubernetesClientLoadBalancerPodModeTests.App.class) -public class KubernetesClientLoadBalancerPodModeTests { +class KubernetesClientLoadBalancerPodModeTests { @Autowired private RestTemplate restTemplate; @Test - public void testLoadBalancer() { + void testLoadBalancer() { String resp = restTemplate.getForObject("http://servicea-wiremock", String.class); assertThat(resp).isEqualTo("hello"); } @@ -67,12 +68,17 @@ public class KubernetesClientLoadBalancerPodModeTests { static class App { @Bean - public ApiClient apiClient() { + CoreV1Api coreV1Api(ApiClient apiClient) { + return new CoreV1Api(apiClient); + } + + @Bean + ApiClient apiClient() { return new ClientBuilder().build(); } @Bean - public BlockingLoadBalancerClient blockingLoadBalancerClient() { + BlockingLoadBalancerClient blockingLoadBalancerClient() { BlockingLoadBalancerClient client = mock(BlockingLoadBalancerClient.class); try { ClientHttpResponse response = new MockClientHttpResponse("hello".getBytes(), HttpStatus.OK); @@ -87,14 +93,14 @@ public class KubernetesClientLoadBalancerPodModeTests { } @Bean - public KubernetesNamespaceProvider kubernetesNamespaceProvider() { + KubernetesNamespaceProvider kubernetesNamespaceProvider() { KubernetesNamespaceProvider provider = mock(KubernetesNamespaceProvider.class); when(provider.getNamespace()).thenReturn("test"); return provider; } @Bean - public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { + KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient() { KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class); ServiceInstance instance = new DefaultServiceInstance("servicea-wiremock1", "servicea-wiremock", "fake", 8888, false); diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java index 35ebfcea..62d1d0a0 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/DiscoveryClientUtils.java @@ -185,7 +185,10 @@ public final class DiscoveryClientUtils { if (!EXTERNAL_NAME.equals(serviceMetadata.get(SERVICE_TYPE))) { if (properties.metadata().addPodLabels() || properties.metadata().addPodAnnotations()) { + LOG.debug(() -> "Pod labels/annotations were requested"); + if (podName != null) { + LOG.debug(() -> "getting labels/annotation for pod: " + podName); PodLabelsAndAnnotations both = podLabelsAndMetadata.apply(podName); Map> result = new HashMap<>(); if (properties.metadata().addPodLabels() && !both.labels().isEmpty()) { diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java index 16e0bc53..fcf276a4 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java @@ -375,7 +375,7 @@ class Fabric8KubernetesDiscoveryClientTest { } @Test - void instanceWithoutPortsShouldBeSkipped() { + void instanceWithoutSubsetsShouldBeSkipped() { Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint1").withNamespace("test") .withLabels(Collections.emptyMap()).endMetadata().build(); 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 index 5dcb172a..a19db4f5 100644 --- 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 @@ -292,7 +292,7 @@ class Fabric8KubernetesDiscoveryClientUtilsTests { new EndpointSubsetBuilder() .withPorts(new EndpointPortBuilder().withPort(8080).withName("https").build()).build()); - Map portsData = Fabric8KubernetesDiscoveryClientUtils.endpointSubsetsPortData(endpointSubsets); + Map portsData = endpointSubsetsPortData(endpointSubsets); Assertions.assertEquals(portsData.size(), 2); Assertions.assertEquals(portsData.get("https"), 8080); Assertions.assertEquals(portsData.get(""), 8081); @@ -306,7 +306,7 @@ class Fabric8KubernetesDiscoveryClientUtilsTests { new EndpointSubsetBuilder() .withPorts(new EndpointPortBuilder().withPort(8080).withName("https").build()).build()); - Map portsData = Fabric8KubernetesDiscoveryClientUtils.endpointSubsetsPortData(endpointSubsets); + Map portsData = endpointSubsetsPortData(endpointSubsets); Assertions.assertEquals(portsData.size(), 2); Assertions.assertEquals(portsData.get("https"), 8080); Assertions.assertEquals(portsData.get("http"), 8081); 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 f1068dbc..f7abe74f 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 @@ -26,15 +26,12 @@ import io.fabric8.kubernetes.api.model.EndpointSubset; import io.fabric8.kubernetes.api.model.EndpointSubsetBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; /** * @author wind57 */ -@ExtendWith(OutputCaptureExtension.class) class KubernetesDiscoveryClientUtilsTests { /** diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java index 031922d6..a844f475 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientIT.java @@ -98,10 +98,20 @@ class KubernetesClientDiscoveryClientIT { */ @Test @Order(1) - void testSimple() { + void testSimple() throws Exception { util.busybox(NAMESPACE, Phase.CREATE); + // find both pods + String[] both = K3S.execInContainer("sh", "-c", "kubectl get pods -l app=busybox -o=name --no-headers") + .getStdout().split("\n"); + // add a label to first pod + K3S.execInContainer("sh", "-c", + "kubectl label pods " + both[0].split("/")[1] + " custom-label=custom-label-value"); + // add annotation to the second pod + K3S.execInContainer("sh", "-c", + "kubectl annotate pods " + both[1].split("/")[1] + " custom-annotation=custom-annotation-value"); + Commons.waitForLogStatement("serviceSharedInformer will use namespace : default", K3S, IMAGE_NAME); WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); @@ -111,10 +121,11 @@ class KubernetesClientDiscoveryClientIT { }).retryWhen(retrySpec()).block(); - Assertions.assertEquals(servicesResult.size(), 3); + Assertions.assertEquals(servicesResult.size(), 4); Assertions.assertTrue(servicesResult.contains("kubernetes")); Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); Assertions.assertTrue(servicesResult.contains("busybox-service")); + Assertions.assertTrue(servicesResult.contains("external-name-service")); WebClient ourServiceClient = builder() .baseUrl("http://localhost/service-instances/spring-cloud-kubernetes-k8s-client-discovery").build(); @@ -131,8 +142,8 @@ class KubernetesClientDiscoveryClientIT { Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-k8s-client-discovery"); Assertions.assertNotNull(serviceInstance.getHost()); Assertions.assertEquals(serviceInstance.getMetadata(), - Map.of("app", "spring-cloud-kubernetes-k8s-client-discovery", "custom-spring-k8s", "spring-k8s", "http", - "8080", "k8s_namespace", "default", "type", "ClusterIP")); + Map.of("app", "spring-cloud-kubernetes-k8s-client-discovery", "custom-spring-k8s", "spring-k8s", + "port.http", "8080", "k8s_namespace", "default", "type", "ClusterIP")); Assertions.assertEquals(serviceInstance.getPort(), 8080); Assertions.assertEquals(serviceInstance.getNamespace(), "default"); @@ -145,6 +156,26 @@ class KubernetesClientDiscoveryClientIT { Assertions.assertEquals(busyBoxServiceInstances.size(), 2); + DefaultKubernetesServiceInstance withCustomLabel = busyBoxServiceInstances.stream() + .filter(x -> x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()).toList().get(0); + Assertions.assertEquals(withCustomLabel.getServiceId(), "busybox-service"); + Assertions.assertNotNull(withCustomLabel.getInstanceId()); + Assertions.assertNotNull(withCustomLabel.getHost()); + Assertions.assertEquals(withCustomLabel.getMetadata(), + Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80")); + Assertions.assertTrue(withCustomLabel.podMetadata().get("labels").entrySet().stream() + .anyMatch(x -> x.getKey().equals("custom-label") && x.getValue().equals("custom-label-value"))); + + DefaultKubernetesServiceInstance withCustomAnnotation = busyBoxServiceInstances.stream() + .filter(x -> !x.podMetadata().getOrDefault("annotations", Map.of()).isEmpty()).toList().get(0); + Assertions.assertEquals(withCustomAnnotation.getServiceId(), "busybox-service"); + Assertions.assertNotNull(withCustomAnnotation.getInstanceId()); + Assertions.assertNotNull(withCustomAnnotation.getHost()); + Assertions.assertEquals(withCustomAnnotation.getMetadata(), + Map.of("k8s_namespace", "default", "type", "ClusterIP", "port.busybox-port", "80")); + Assertions.assertTrue(withCustomAnnotation.podMetadata().get("annotations").entrySet().stream().anyMatch( + x -> x.getKey().equals("custom-annotation") && x.getValue().equals("custom-annotation-value"))); + // enforces this : // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286 WebClient clientForNonExistentService = builder().baseUrl("http://localhost/service-instances/non-existent") @@ -165,6 +196,8 @@ class KubernetesClientDiscoveryClientIT { * - config server is enabled for all namespaces * - wiremock service is deployed in namespace-a * - busybox service is deployed in namespace-b + * - external-name-service is deployed in namespace "default" and such a service type is requested, + * thus found also. * * Our discovery searches in all namespaces, thus finds them both. * @@ -187,11 +220,12 @@ class KubernetesClientDiscoveryClientIT { .bodyToMono(new ParameterizedTypeReference>() { }).retryWhen(retrySpec()).block(); - Assertions.assertEquals(servicesResult.size(), 7); + Assertions.assertEquals(servicesResult.size(), 8); Assertions.assertTrue(servicesResult.contains("kubernetes")); Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); Assertions.assertTrue(servicesResult.contains("busybox-service")); Assertions.assertTrue(servicesResult.contains("service-wiremock")); + Assertions.assertTrue(servicesResult.contains("external-name-service")); // enforces this : // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1286 @@ -204,6 +238,23 @@ class KubernetesClientDiscoveryClientIT { Assertions.assertEquals(resultForNonExistentService.size(), 0); + // test ExternalName fields + WebClient externalNameClient = builder().baseUrl("http://localhost/service-instances/external-name-service") + .build(); + List externalNameServices = externalNameClient.method(HttpMethod.GET) + .retrieve().bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + DefaultKubernetesServiceInstance externalNameService = externalNameServices.get(0); + Assertions.assertNotNull(externalNameService.getInstanceId()); + Assertions.assertEquals(externalNameService.getHost(), "spring.io"); + Assertions.assertEquals(externalNameService.getPort(), -1); + Assertions.assertEquals(externalNameService.getMetadata(), + Map.of("k8s_namespace", "default", "type", "ExternalName")); + Assertions.assertFalse(externalNameService.isSecure()); + Assertions.assertEquals(externalNameService.getUri().toASCIIString(), "spring.io"); + Assertions.assertEquals(externalNameService.getScheme(), "http"); + // do not remove wiremock in namespace a, it is required in the next test util.busybox(NAMESPACE_B, Phase.DELETE); util.deleteClusterWideClusterRoleBinding(NAMESPACE); @@ -352,10 +403,12 @@ class KubernetesClientDiscoveryClientIT { private static void manifests(Phase phase) { V1Deployment deployment = (V1Deployment) util.yaml("kubernetes-discovery-deployment.yaml"); V1Service service = (V1Service) util.yaml("kubernetes-discovery-service.yaml"); + V1Service externalNameService = (V1Service) util.yaml("external-name-service.yaml"); V1Ingress ingress = (V1Ingress) util.yaml("kubernetes-discovery-ingress.yaml"); if (phase.equals(Phase.DELETE)) { util.deleteAndWait(NAMESPACE, deployment, service, ingress); + util.deleteAndWait(NAMESPACE, null, externalNameService, null); return; } @@ -366,15 +419,27 @@ class KubernetesClientDiscoveryClientIT { .orElse(List.of())); V1EnvVar debugLevel = new V1EnvVar() .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY").value("DEBUG"); + V1EnvVar commonsLevel = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_DISCOVERY").value("DEBUG"); V1EnvVar debugLevelForClient = new V1EnvVar() .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT").value("DEBUG"); + V1EnvVar addLabels = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODLABELS") + .value("TRUE"); + + V1EnvVar addAnnotations = new V1EnvVar() + .name("SPRING_CLOUD_KUBERNETES_DISCOVERY_METADATA_ADDPODANNOTATIONS").value("TRUE"); + envVars.add(debugLevel); envVars.add(debugLevelForClient); + envVars.add(addLabels); + envVars.add(addAnnotations); + envVars.add(commonsLevel); deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); + util.createAndWait(NAMESPACE, null, null, externalNameService, null, true); } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java index b127e58f..7e18bbe7 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryClientUtils.java @@ -144,7 +144,7 @@ final class KubernetesClientDiscoveryClientUtils { } """; - // patch to include all namespaces + // patch to include all namespaces + external name services private static final String BODY_FIVE = """ { "spec": { @@ -152,10 +152,16 @@ final class KubernetesClientDiscoveryClientUtils { "spec": { "containers": [{ "name": "spring-cloud-kubernetes-k8s-client-discovery", - "env": [{ - "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES", - "value": "TRUE" - }] + "env": [ + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES", + "value": "TRUE" + }, + { + "name": "SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES", + "value": "TRUE" + } + ] }] } } @@ -373,6 +379,7 @@ final class KubernetesClientDiscoveryClientUtils { } // add SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES=TRUE + // and SPRING_CLOUD_KUBERNETES_DISCOVERY_INCLUDEEXTERNALNAMESERVICES=TRUE static void patchForAllNamespaces(String deploymentName, String namespace) { patchWithMerge(deploymentName, namespace, BODY_FIVE, POD_LABELS); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java index 1a09df4b..5e6c6f4b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryFilterITDelegate.java @@ -81,7 +81,7 @@ class KubernetesClientDiscoveryFilterITDelegate { Assertions.assertEquals(first.getPort(), 8080); Assertions.assertEquals(first.getNamespace(), "a-uat"); Assertions.assertEquals(first.getMetadata(), - Map.of("app", "service-wiremock", "http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); } @@ -125,7 +125,7 @@ class KubernetesClientDiscoveryFilterITDelegate { Assertions.assertEquals(first.getPort(), 8080); Assertions.assertEquals(first.getNamespace(), "a-uat"); Assertions.assertEquals(first.getMetadata(), - Map.of("app", "service-wiremock", "http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); DefaultKubernetesServiceInstance second = sorted.get(1); Assertions.assertEquals(second.getServiceId(), "service-wiremock"); @@ -133,7 +133,7 @@ class KubernetesClientDiscoveryFilterITDelegate { Assertions.assertEquals(second.getPort(), 8080); Assertions.assertEquals(second.getNamespace(), "b-uat"); Assertions.assertEquals(second.getMetadata(), - Map.of("app", "service-wiremock", "http", "8080", "k8s_namespace", "b-uat", "type", "ClusterIP")); + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "b-uat", "type", "ClusterIP")); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java index e5e534dc..f38f2f20 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryHealthITDelegate.java @@ -41,6 +41,10 @@ import static org.awaitility.Awaitility.await; */ class KubernetesClientDiscoveryHealthITDelegate { + KubernetesClientDiscoveryHealthITDelegate() { + + } + private static final String REACTIVE_STATUS = "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].status"; private static final String BLOCKING_STATUS = "$.components.discoveryComposite.components.discoveryClient.status"; @@ -82,7 +86,8 @@ class KubernetesClientDiscoveryHealthITDelegate { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) .extractingJsonPathArrayValue( "$.components.discoveryComposite.components.discoveryClient.details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(REACTIVE_STATUS); @@ -119,7 +124,8 @@ class KubernetesClientDiscoveryHealthITDelegate { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue( "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(BLOCKING_STATUS); @@ -167,7 +173,8 @@ class KubernetesClientDiscoveryHealthITDelegate { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) .extractingJsonPathArrayValue( "$.components.discoveryComposite.components.discoveryClient.details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) .extractingJsonPathStringValue("$.components.reactiveDiscoveryClients.status").isEqualTo("UP"); @@ -178,7 +185,8 @@ class KubernetesClientDiscoveryHealthITDelegate { Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue( "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") - .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes"); + .containsExactlyInAnyOrder("spring-cloud-kubernetes-k8s-client-discovery", "kubernetes", + "external-name-service"); // assert health/info also assertHealth(healthResult); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java index f1d1f931..a67dc95c 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/java/org/springframework/cloud/kubernetes/k8s/client/discovery/KubernetesClientDiscoveryPodMetadataITDelegate.java @@ -50,9 +50,10 @@ class KubernetesClientDiscoveryPodMetadataITDelegate { }).retryWhen(retrySpec()).block(); - Assertions.assertEquals(servicesResult.size(), 2); + Assertions.assertEquals(servicesResult.size(), 3); Assertions.assertTrue(servicesResult.contains("kubernetes")); Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-k8s-client-discovery")); + Assertions.assertTrue(servicesResult.contains("external-name-service")); WebClient ourServiceClient = builder() .baseUrl("http://localhost//service-instances/spring-cloud-kubernetes-k8s-client-discovery").build(); @@ -69,7 +70,7 @@ class KubernetesClientDiscoveryPodMetadataITDelegate { Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-k8s-client-discovery"); Assertions.assertNotNull(serviceInstance.getHost()); Assertions.assertEquals(serviceInstance.getMetadata(), - Map.of("http", "8080", "k8s_namespace", "default", "type", "ClusterIP", "label-app", + Map.of("port.http", "8080", "k8s_namespace", "default", "type", "ClusterIP", "label-app", "spring-cloud-kubernetes-k8s-client-discovery", "annotation-custom-spring-k8s", "spring-k8s")); Assertions.assertEquals(serviceInstance.getPort(), 8080); Assertions.assertEquals(serviceInstance.getNamespace(), "default"); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-client-discovery/src/test/resources/external-name-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-k8s-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-k8s-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/native_client/Util.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java index e9d3dceb..9ad2fe9a 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java @@ -116,20 +116,25 @@ public final class Util { @Nullable V1Ingress 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); + coreV1Api.createNamespacedService(namespace, service, null, null, null, null); + + 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); + } + + appsV1Api.createNamespacedDeployment(namespace, deployment, null, null, null, null); + waitForDeployment(namespace, deployment); } - appsV1Api.createNamespacedDeployment(namespace, deployment, null, null, null, null); - coreV1Api.createNamespacedService(namespace, service, null, null, null, null); - waitForDeployment(namespace, deployment); if (ingress != null) { networkingV1Api.createNamespacedIngress(namespace, ingress, null, null, null, null); waitForIngress(namespace, ingress); @@ -193,20 +198,27 @@ public final class Util { public void deleteAndWait(String namespace, V1Deployment deployment, V1Service service, @Nullable V1Ingress ingress) { - String deploymentName = deploymentName(deployment); + + if (deployment != null) { + try { + String deploymentName = deploymentName(deployment); + Map podLabels = appsV1Api.readNamespacedDeployment(deploymentName, namespace, null) + .getSpec().getTemplate().getMetadata().getLabels(); + appsV1Api.deleteNamespacedDeployment(deploymentName, namespace, null, null, null, null, null, null); + coreV1Api.deleteCollectionNamespacedPod(namespace, null, null, null, null, null, + labelSelector(podLabels), null, null, null, null, null, null, null); + waitForDeploymentToBeDeleted(deploymentName, namespace); + waitForDeploymentPodsToBeDeleted(podLabels, namespace); + } + catch (Exception e) { + throw new RuntimeException(e); + } + + } + String serviceName = serviceName(service); try { - - Map podLabels = appsV1Api.readNamespacedDeployment(deploymentName, namespace, null) - .getSpec().getTemplate().getMetadata().getLabels(); - - appsV1Api.deleteNamespacedDeployment(deploymentName, namespace, null, null, null, null, null, null); coreV1Api.deleteNamespacedService(serviceName, namespace, null, null, null, null, null, null); - coreV1Api.deleteCollectionNamespacedPod(namespace, null, null, null, null, null, labelSelector(podLabels), - null, null, null, null, null, null, null); - waitForDeploymentToBeDeleted(deploymentName, namespace); - waitForDeploymentPodsToBeDeleted(podLabels, namespace); - if (ingress != null) { String ingressName = ingressName(ingress); networkingV1Api.deleteNamespacedIngress(ingressName, namespace, null, null, null, null, null, null);