Complete alignment between fabric8 and k8s discovery clients (#1500)

This commit is contained in:
erabii
2023-11-14 21:27:24 +02:00
committed by GitHub
parent 901089dff9
commit ecfb80509d
28 changed files with 1906 additions and 486 deletions

View File

@@ -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<InstanceIdHostPodName> {
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);
}
}

View File

@@ -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<String, PodLabelsAndAnnotations> {
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()));
}
}

View File

@@ -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. <pre>
* - labels (if requested)
* - annotations (if requested)
* - metadata
* - service type
* </pre>
*/
static Map<String, String> serviceMetadata(KubernetesDiscoveryProperties properties, V1Service service,
String serviceId) {
Map<String, String> serviceMetadata = new HashMap<>();
KubernetesDiscoveryProperties.Metadata metadataProps = properties.metadata();
if (metadataProps.addLabels()) {
Map<String, String> labelMetadata = keysWithPrefix(service.getMetadata().getLabels(),
metadataProps.labelsPrefix());
LOG.debug(() -> "Adding labels metadata: " + labelMetadata + " for serviceId: " + serviceId);
serviceMetadata.putAll(labelMetadata);
}
if (metadataProps.addAnnotations()) {
Map<String, String> 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<V1Service> filter(KubernetesDiscoveryProperties properties) {
String spelExpression = properties.filter();
Predicate<V1Service> 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<String, Integer> endpointSubsetsPortData(List<V1EndpointSubset> 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<V1EndpointAddress> addresses(V1EndpointSubset endpointSubset,
KubernetesDiscoveryProperties properties) {
List<V1EndpointAddress> addresses = Optional.ofNullable(endpointSubset.getAddresses()).map(ArrayList::new)
.orElse(new ArrayList<>());
if (properties.includeNotReadyAddresses()) {
List<V1EndpointAddress> notReadyAddresses = endpointSubset.getNotReadyAddresses();
if (CollectionUtils.isEmpty(notReadyAddresses)) {
return addresses;
}
addresses.addAll(notReadyAddresses);
}
return addresses;
}
}

View File

@@ -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<V1Service> 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<V1Service> serviceLister, Lister<V1Endpoints> 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<SharedInformerFactory> 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<ServiceInstance> getInstances(String serviceId) {
Objects.requireNonNull(serviceId, "serviceId must be provided");
List<V1Service> services = serviceListers.stream().flatMap(x -> x.list().stream())
List<V1Service> 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<ServiceInstance> getServiceInstanceDetails(V1Service service, String serviceId) {
Map<String, String> serviceMetadata = serviceMetadata(properties, service, serviceId);
List<ServiceInstance> serviceInstances = allServices.stream().filter(filter)
.flatMap(service -> serviceInstances(service, serviceId).stream())
.collect(Collectors.toCollection(ArrayList::new));
List<V1Endpoints> 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<V1Service> 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<String, String> serviceInstanceMetadata = serviceInstanceMetadata(Map.of(), serviceMetadata,
properties);
Optional<String> 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<String, String> metadata = new HashMap<>(serviceMetadata);
List<CoreV1EndpointPort> 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<V1EndpointAddress> 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<String> 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<CoreV1EndpointPort> endpointPorts, String primaryPortName, String serviceId) {
if (endpointPorts.size() == 1) {
return endpointPorts.get(0).getPort();
}
else {
Map<String, Integer> 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<ServiceInstance> serviceInstances(V1Service service, String serviceId) {
List<ServiceInstance> instances = new ArrayList<>();
List<V1Endpoints> allEndpoints = endpointsListers.stream()
.map(endpointsLister -> endpointsLister.namespace(service.getMetadata().getNamespace()).get(serviceId))
.filter(Objects::nonNull).toList();
for (V1Endpoints endpoints : allEndpoints) {
List<V1EndpointSubset> subsets = endpoints.getSubsets();
if (subsets == null || subsets.isEmpty()) {
LOG.debug(() -> "serviceId : " + serviceId + " does not have any subsets");
}
else {
ServiceMetadata serviceMetadata = serviceMetadata(service);
Map<String, Integer> portsData = endpointSubsetsPortData(subsets);
Map<String, String> serviceInstanceMetadata = serviceInstanceMetadata(portsData, serviceMetadata,
properties);
for (V1EndpointSubset endpointSubset : subsets) {
Map<String, Integer> endpointsPortData = endpointSubsetsPortData(List.of(endpointSubset));
ServicePortNameAndNumber portData = endpointsPort(endpointsPortData, serviceMetadata, properties);
List<V1EndpointAddress> 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

View File

@@ -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");
}
}

View File

@@ -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"));
}
}

View File

@@ -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);

View File

@@ -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<V1Service> SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>();
private static final SharedInformerStub<V1Endpoints> ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>();
private Cache<V1Service> servicesCache;
private Lister<V1Service> servicesLister;
private Cache<V1Endpoints> endpointsCache;
private Lister<V1Endpoints> 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<ServiceInstance> 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<ServiceInstance> 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<ServiceInstance> 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<ServiceInstance> 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<ServiceInstance> 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<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).containsOnly(entry("http", "80"), entry("k8s_namespace", "test"),
entry("<unset>", "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<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
assertThat(instances).hasSize(1);
assertThat(instances.get(0).getMetadata()).containsOnly(entry("p_http", "80"), entry("k8s_namespace", "ns"),
entry("p_<unset>", "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<ServiceInstance> 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_<unset>", "5555"));
}
private void setup(String serviceId, String namespace, Map<String, String> labels, Map<String, String> annotations,
Map<Integer, String> 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<V1ServicePort> getServicePorts(Map<Integer, String> 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<CoreV1EndpointPort> getEndpointPorts(Map<Integer, String> 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());
}
}

View File

@@ -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 {
/**
* <pre>
* - labels are not added
* - annotations are not added
* </pre>
*/
@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<String, String> result = KubernetesDiscoveryClientUtils.serviceMetadata(properties, service, "my-service");
Assertions.assertEquals(result.size(), 2);
Assertions.assertEquals(result, Map.of("k8s_namespace", "default", "type", "ClusterIP"));
}
/**
* <pre>
* - labels are added without a prefix
* - annotations are not added
* </pre>
*/
@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<String, String> 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"));
}
/**
* <pre>
* - labels are added with prefix
* - annotations are not added
* </pre>
*/
@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<String, String> 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"));
}
/**
* <pre>
* - labels are not added
* - annotations are added without prefix
* </pre>
*/
@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<String, String> 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"));
}
/**
* <pre>
* - labels are not added
* - annotations are added with prefix
* </pre>
*/
@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<String, String> 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"));
}
/**
* <pre>
* - labels are added with prefix
* - annotations are added with prefix
* </pre>
*/
@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<String, String> 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<String, String> 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();
}
}

View File

@@ -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<V1Service> SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>();
private static final SharedInformerStub<V1Endpoints> ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>();
private Cache<V1Service> servicesCache;
private Lister<V1Service> servicesLister;
private Cache<V1Endpoints> endpointsCache;
private Lister<V1Endpoints> 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<ServiceInstance> serviceInstances = discoveryClient.getInstances("no-port-name-service");
Assertions.assertEquals(serviceInstances.size(), 1);
Assertions.assertEquals(serviceInstances.get(0).getMetadata(),
Map.of("port.<unset>", "8080", "k8s_namespace", "spring-k8s", "type", "ClusterIP"));
}
}

View File

@@ -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<V1Service> SERVICE_SHARED_INFORMER_STUB = new SharedInformerStub<>();
private static final SharedInformerStub<V1Endpoints> ENDPOINTS_SHARED_INFORMER_STUB = new SharedInformerStub<>();
private Cache<V1Service> servicesCache;
private Lister<V1Service> servicesLister;
private Cache<V1Endpoints> endpointsCache;
private Lister<V1Endpoints> endpointsLister;
@BeforeEach
void beforeEach() {
servicesCache = new Cache<>();
servicesLister = new Lister<>(servicesCache);
endpointsCache = new Cache<>();
endpointsLister = new Lister<>(endpointsCache);
}
@Test
void getInstancesShouldBeAbleToHandleEndpointsSingleAddress() {
Map<String, String> labels = Map.of("l", "v");
String serviceId = "id";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("10");
List<String> names = List.of("http");
List<String> protocols = List.of("TCP");
List<Integer> ports = List.of(80);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of("l2", "v2");
String serviceId = "endpoint";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("20");
List<String> names = List.of("http", "mgmt");
List<String> protocols = List.of("TCP", "TCP");
List<Integer> ports = List.of(80, 900);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of("l1", "v1");
String serviceId = "endpoint";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1", "ip2");
List<String> uuids = List.of("40", "50");
List<String> names = List.of("https");
List<String> protocols = List.of("TCP");
List<Integer> ports = List.of(443);
List<String> 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<ServiceInstance> instances = discoveryClient.getInstances("endpoint");
assertThat(instances).hasSize(2).filteredOn(ServiceInstance::isSecure).extracting(ServiceInstance::getHost)
.containsOnly("ip1", "ip2");
}
@Test
void getInstancesShouldBeAbleToHandleEndpointsFromMultipleNamespaces() {
Map<String, String> labels = Map.of("l", "v");
String serviceId = "endpoint";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("60");
List<String> names = List.of("http");
List<String> protocols = List.of("TCP");
List<Integer> ports = List.of(80);
List<String> 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<ServiceInstance> 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<ServiceInstance> instances = discoveryClient.getInstances("endpoint1");
assertThat(instances).isEmpty();
}
@Test
void getInstancesShouldBeAbleToHandleEndpointsSingleAddressAndMultiplePortsUsingPrimaryPortNameLabel() {
Map<String, String> labels = Map.of("primary-port-name", "https");
String serviceId = "endpoint2";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("80");
List<String> names = List.of("http", "https");
List<String> protocols = List.of("TCP", "TCP");
List<Integer> ports = List.of(80, 443);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of("primary-port-name", "oops");
String serviceId = "endpoint3";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("90");
List<String> names = List.of("httpA", "httpB", "httpC", "httpD");
List<String> protocols = List.of("TCP", "TCP", "TCP", "TCP");
List<Integer> ports = List.of(8443, 443, 80, 8080);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of();
String serviceId = "endpoint4";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("100");
List<String> names = List.of("httpA", "httpB", "httpC", "httpD");
List<String> protocols = List.of("TCP", "TCP", "TCP", "TCP");
List<Integer> ports = List.of(8443, 443, 80, 8080);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of();
String serviceId = "endpoint5";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("110");
List<String> names = List.of("httpA", "httpB");
List<String> protocols = List.of("TCP", "TCP");
List<Integer> ports = List.of(443, 80);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of();
String serviceId = "endpoint5";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("120");
List<String> names = List.of("httpA", "httpB", "httpC");
List<String> protocols = List.of("http", "http", "http");
List<Integer> ports = List.of(80, 8443, 80);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of();
String serviceId = "endpoint5";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("130");
List<String> names = List.of("http", "https");
List<String> protocols = List.of("http", "https");
List<Integer> ports = List.of(80, 443);
List<String> 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<ServiceInstance> 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<String, String> labels = Map.of();
String serviceId = "endpoint5";
String serviceType = "ExternalName";
String namespace = "test";
List<String> ips = List.of("ip1");
List<String> uuids = List.of("130");
List<String> names = List.of();
List<String> protocols = List.of();
List<Integer> ports = List.of();
List<String> 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<ServiceInstance> 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<String, String> labels,
List<String> ips, List<String> uuids, List<String> names, List<String> protocols, List<Integer> ports,
List<String> 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<V1EndpointAddress> 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<CoreV1EndpointPort> 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);
}
}

View File

@@ -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<V1EndpointSubset> 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<String, Integer> portsData = endpointSubsetsPortData(endpointSubsets);
Assertions.assertEquals(portsData.size(), 2);
Assertions.assertEquals(portsData.get("https"), 8080);
Assertions.assertEquals(portsData.get("<unset>"), 8081);
}
@Test
void testPortsDataTwo() {
List<V1EndpointSubset> 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<String, Integer> 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<String, Integer> 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<String, Integer> 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<String, Integer> result = endpointSubsetsPortData(List.of(endpointSubset));
Assertions.assertEquals(result.size(), 1);
Assertions.assertEquals(result.get("<unset>"), 80);
}
/**
* <pre>
* - ready addresses are empty
* - not ready addresses are not included
* </pre>
*/
@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<V1EndpointAddress> addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties);
Assertions.assertEquals(addresses.size(), 0);
}
/**
* <pre>
* - ready addresses has two entries
* - not ready addresses are not included
* </pre>
*/
@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<V1EndpointAddress> addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties);
Assertions.assertEquals(addresses.size(), 2);
}
/**
* <pre>
* - ready addresses has two entries
* - not ready addresses has a single entry, but we do not take it
* </pre>
*/
@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<V1EndpointAddress> addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties);
Assertions.assertEquals(addresses.size(), 2);
List<String> hostNames = addresses.stream().map(V1EndpointAddress::getHostname).sorted().toList();
Assertions.assertEquals(hostNames, List.of("one", "two"));
}
/**
* <pre>
* - ready addresses has two entries
* - not ready addresses has a single entry, but we do not take it
* </pre>
*/
@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<V1EndpointAddress> addresses = KubernetesDiscoveryClientUtils.addresses(endpointSubset, properties);
Assertions.assertEquals(addresses.size(), 3);
List<String> hostNames = addresses.stream().map(V1EndpointAddress::getHostname).sorted().toList();
Assertions.assertEquals(hostNames, List.of("one", "three", "two"));
}
// preserve order for testing reasons
private Map<String, String> ordered(Map<String, String> input) {
return input.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(

View File

@@ -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("<unset>", "80", "k8s_namespace", "namespace1", "type", "ClusterIP"), false,
.containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "1.1.1.1", 80,
Map.of("port.<unset>", "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", "<unset>", "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.<unset>", "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("<unset>", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false,
.containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080,
Map.of("port.<unset>", "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("<unset>", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false,
.containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080,
Map.of("port.<unset>", "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("<unset>", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false,
.containsOnly(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080,
Map.of("port.<unset>", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false,
"namespace1", null));
}
@@ -250,7 +267,7 @@ class KubernetesInformerDiscoveryClientTests {
}
@Test
void instanceWithoutPortsShouldBeSkipped() {
void instanceWithoutPortsWillNotBeSkipped() {
Lister<V1Service> serviceLister = setupServiceLister(SERVICE_1);
Lister<V1Endpoints> 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("<unset>", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false,
new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080,
Map.of("port.<unset>", "8080", "k8s_namespace", "namespace1", "type", "ClusterIP"), false,
"namespace1", null),
new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080,
Map.of("<unset>", "8080", "k8s_namespace", "namespace2", "type", "ClusterIP"), false,
new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080,
Map.of("port.<unset>", "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<V1Service> serviceLister = setupServiceLister(externalNameService);
Lister<V1Endpoints> 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<ServiceInstance> 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<V1Service> serviceLister = setupServiceLister(nonExternalNameService);
Lister<V1Endpoints> 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<ServiceInstance> 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<unset>", "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<V1Service> setupServiceLister(V1Service... services) {
Cache<V1Service> serviceCache = new Cache<>();
Lister<V1Service> serviceLister = new Lister<>(serviceCache);

View File

@@ -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() {
}
}

View File

@@ -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<T extends KubernetesObject> implements SharedInformer<T> {
@Override
public void addEventHandler(ResourceEventHandler<T> handler) {
}
@Override
public void addEventHandlerWithResyncPeriod(ResourceEventHandler<T> 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) {
}
}

View File

@@ -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", "<unset>", "8080", "k8s_namespace", "namespace1"), false,
.expectNext(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080,
Map.of("type", "ClusterIP", "port.<unset>", "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", "<unset>", "8080", "k8s_namespace", "namespace1"), false,
.expectNext(new DefaultKubernetesServiceInstance(null, "test-svc-1", "2.2.2.2", 8080,
Map.of("type", "ClusterIP", "port.<unset>", "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);

View File

@@ -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);

View File

@@ -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<String, Map<String, String>> result = new HashMap<>();
if (properties.metadata().addPodLabels() && !both.labels().isEmpty()) {

View File

@@ -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();

View File

@@ -292,7 +292,7 @@ class Fabric8KubernetesDiscoveryClientUtilsTests {
new EndpointSubsetBuilder()
.withPorts(new EndpointPortBuilder().withPort(8080).withName("https").build()).build());
Map<String, Integer> portsData = Fabric8KubernetesDiscoveryClientUtils.endpointSubsetsPortData(endpointSubsets);
Map<String, Integer> portsData = endpointSubsetsPortData(endpointSubsets);
Assertions.assertEquals(portsData.size(), 2);
Assertions.assertEquals(portsData.get("https"), 8080);
Assertions.assertEquals(portsData.get("<unset>"), 8081);
@@ -306,7 +306,7 @@ class Fabric8KubernetesDiscoveryClientUtilsTests {
new EndpointSubsetBuilder()
.withPorts(new EndpointPortBuilder().withPort(8080).withName("https").build()).build());
Map<String, Integer> portsData = Fabric8KubernetesDiscoveryClientUtils.endpointSubsetsPortData(endpointSubsets);
Map<String, Integer> portsData = endpointSubsetsPortData(endpointSubsets);
Assertions.assertEquals(portsData.size(), 2);
Assertions.assertEquals(portsData.get("https"), 8080);
Assertions.assertEquals(portsData.get("http"), 8081);

View File

@@ -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 {
/**

View File

@@ -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.
* </pre>
@@ -187,11 +220,12 @@ class KubernetesClientDiscoveryClientIT {
.bodyToMono(new ParameterizedTypeReference<List<String>>() {
}).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<DefaultKubernetesServiceInstance> externalNameServices = externalNameClient.method(HttpMethod.GET)
.retrieve().bodyToMono(new ParameterizedTypeReference<List<DefaultKubernetesServiceInstance>>() {
}).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);
}
}

View File

@@ -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);
}

View File

@@ -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"));
}

View File

@@ -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);

View File

@@ -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");

View File

@@ -0,0 +1,7 @@
kind: Service
apiVersion: v1
metadata:
name: external-name-service
spec:
type: ExternalName
externalName: spring.io

View File

@@ -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<String, String> 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<String, String> 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);