diff --git a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java index 220d3bc3..f50394db 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/main/java/org/springframework/cloud/kubernetes/client/KubernetesClientPodUtils.java @@ -19,6 +19,7 @@ package org.springframework.cloud.kubernetes.client; import java.nio.file.Paths; import java.util.function.Supplier; +import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1Pod; import io.kubernetes.client.util.Config; @@ -82,10 +83,14 @@ public class KubernetesClientPodUtils implements PodUtils { private V1Pod internalGetPod() { try { if (isServiceHostEnvVarPresent() && isHostNameEnvVarPresent() && isServiceAccountFound()) { + LOG.debug("reading pod in namespace : " + namespace); return client.readNamespacedPod(hostName, namespace, null); } } catch (Throwable t) { + if (t instanceof ApiException apiException) { + LOG.warn("error reading pod, with error : " + apiException.getResponseBody()); + } LOG.warn("Failed to get pod with name:[" + hostName + "]. You should look into this if things aren't" + " working as you expect. Are you missing serviceaccount permissions?", t); } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesMissing.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesMissing.java new file mode 100644 index 00000000..5f1815d0 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesMissing.java @@ -0,0 +1,61 @@ +/* + * 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.Set; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.log.LogAccessor; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Conditional that checks if our discovery is _not_ based on selective namespaces, i.e.: + * 'spring.cloud.kubernetes.discovery.namespaces' is not set. + * + * @author wind57 + */ +public final class ConditionalOnSelectiveNamespacesMissing implements ConfigurationCondition { + + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(ConditionalOnSelectiveNamespacesMissing.class)); + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + + Set selectiveNamespaces = Binder.get(context.getEnvironment()) + .bind("spring.cloud.kubernetes.discovery.namespaces", Bindable.setOf(String.class)).orElse(Set.of()); + boolean selectiveNamespacesMissing = selectiveNamespaces.isEmpty(); + if (selectiveNamespacesMissing) { + LOG.debug(() -> "selective namespaces not present"); + } + else { + LOG.debug(() -> "found selective namespaces : " + selectiveNamespaces.stream().sorted().toList()); + } + return selectiveNamespacesMissing; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesPresent.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesPresent.java new file mode 100644 index 00000000..6832287d --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesPresent.java @@ -0,0 +1,61 @@ +/* + * 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.Set; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.log.LogAccessor; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Conditional that checks if our discovery is based on selective namespaces, i.e.: + * 'spring.cloud.kubernetes.discovery.namespaces' is set. + * + * @author wind57 + */ +public final class ConditionalOnSelectiveNamespacesPresent implements ConfigurationCondition { + + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(ConditionalOnSelectiveNamespacesPresent.class)); + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + + Set selectiveNamespaces = Binder.get(context.getEnvironment()) + .bind("spring.cloud.kubernetes.discovery.namespaces", Bindable.setOf(String.class)).orElse(Set.of()); + boolean selectiveNamespacesPresent = !selectiveNamespaces.isEmpty(); + if (selectiveNamespacesPresent) { + LOG.debug(() -> "found selective namespaces : " + selectiveNamespaces.stream().sorted().toList()); + } + else { + LOG.debug(() -> "selective namespaces not present"); + } + return selectiveNamespacesPresent; + } + + @Override + public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java index 132c0f88..2b736210 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java @@ -42,6 +42,7 @@ import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKuber import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.log.LogAccessor; @@ -57,6 +58,7 @@ import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils. @ConditionalOnKubernetesDiscoveryEnabled @ConditionalOnBlockingOrReactiveEnabled @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@Conditional(ConditionalOnSelectiveNamespacesMissing.class) @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class }) public class KubernetesClientInformerAutoConfiguration { @@ -67,6 +69,7 @@ public class KubernetesClientInformerAutoConfiguration { @Bean @ConditionalOnMissingBean public SharedInformerFactory sharedInformerFactory(ApiClient client) { + LOG.debug(() -> "registering sharedInformerFactory for non-selective namespaces"); return new SharedInformerFactory(client); } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfiguration.java new file mode 100644 index 00000000..5f44ed9b --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerSelectiveNamespacesAutoConfiguration.java @@ -0,0 +1,166 @@ +/* + * 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.List; + +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.util.generic.GenericKubernetesApi; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.cloud.client.CommonsClientAutoConfiguration; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; +import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; +import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; +import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.log.LogAccessor; + +/** + * Auto-configuration to be used when "spring.cloud.kubernetes.discovery.namespaces" is + * defined. + * + * @author wind57 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnDiscoveryEnabled +@ConditionalOnKubernetesDiscoveryEnabled +@ConditionalOnBlockingOrReactiveEnabled +@Conditional(ConditionalOnSelectiveNamespacesPresent.class) +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) +@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class }) +public class KubernetesClientInformerSelectiveNamespacesAutoConfiguration { + + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class)); + + // we rely on the order of namespaces to enable listers, as such provide a bean of + // namespaces + // as a list, instead of the incoming Set. + @Bean + @ConditionalOnMissingBean + public List selectiveNamespaces(KubernetesDiscoveryProperties properties) { + List selectiveNamespaces = properties.namespaces().stream().sorted().toList(); + LOG.debug(() -> "using selective namespaces : " + selectiveNamespaces); + return selectiveNamespaces; + } + + @Bean + @ConditionalOnMissingBean(value = SharedInformerFactory.class, parameterizedContainer = List.class) + public List sharedInformerFactories(ApiClient apiClient, List selectiveNamespaces) { + + int howManyNamespaces = selectiveNamespaces.size(); + List sharedInformerFactories = new ArrayList<>(howManyNamespaces); + for (int i = 0; i < howManyNamespaces; ++i) { + sharedInformerFactories.add(new SharedInformerFactory(apiClient)); + } + return sharedInformerFactories; + } + + @Bean + @ConditionalOnMissingBean(value = V1Service.class, + parameterizedContainer = { List.class, SharedIndexInformer.class }) + public List> serviceSharedIndexInformers( + List sharedInformerFactories, List selectiveNamespaces, + ApiClient apiClient) { + + int howManyNamespaces = selectiveNamespaces.size(); + List> serviceSharedIndexedInformers = new ArrayList<>(howManyNamespaces); + for (int i = 0; i < howManyNamespaces; ++i) { + GenericKubernetesApi servicesApi = new GenericKubernetesApi<>(V1Service.class, + V1ServiceList.class, "", "v1", "services", apiClient); + SharedIndexInformer sharedIndexInformer = sharedInformerFactories.get(i) + .sharedIndexInformerFor(servicesApi, V1Service.class, 0L, selectiveNamespaces.get(i)); + serviceSharedIndexedInformers.add(sharedIndexInformer); + } + return serviceSharedIndexedInformers; + } + + @Bean + @ConditionalOnMissingBean(value = V1Service.class, parameterizedContainer = { List.class, Lister.class }) + public List> serviceListers(List selectiveNamespaces, + List> serviceSharedIndexInformers) { + + int howManyNamespaces = selectiveNamespaces.size(); + List> serviceListers = new ArrayList<>(howManyNamespaces); + + for (int i = 0; i < howManyNamespaces; ++i) { + String namespace = selectiveNamespaces.get(i); + Lister lister = new Lister<>(serviceSharedIndexInformers.get(i).getIndexer(), namespace); + LOG.debug(() -> "registering lister (for services) in namespace : " + namespace); + serviceListers.add(lister); + } + + return serviceListers; + } + + @Bean + @ConditionalOnMissingBean(value = V1Endpoints.class, + parameterizedContainer = { List.class, SharedIndexInformer.class }) + public List> endpointsSharedIndexInformers( + List sharedInformerFactories, List selectiveNamespaces, + ApiClient apiClient) { + + int howManyNamespaces = selectiveNamespaces.size(); + List> endpointsSharedIndexedInformers = new ArrayList<>(howManyNamespaces); + for (int i = 0; i < howManyNamespaces; ++i) { + GenericKubernetesApi endpointsApi = new GenericKubernetesApi<>( + V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient); + SharedIndexInformer sharedIndexInformer = sharedInformerFactories.get(i) + .sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, selectiveNamespaces.get(i)); + endpointsSharedIndexedInformers.add(sharedIndexInformer); + } + return endpointsSharedIndexedInformers; + } + + @Bean + @ConditionalOnMissingBean(value = V1Endpoints.class, parameterizedContainer = { List.class, Lister.class }) + public List> endpointsListers(List selectiveNamespaces, + List> serviceSharedIndexInformers) { + + int howManyNamespaces = selectiveNamespaces.size(); + List> endpointsListers = new ArrayList<>(howManyNamespaces); + + for (int i = 0; i < howManyNamespaces; ++i) { + String namespace = selectiveNamespaces.get(i); + Lister lister = new Lister<>(serviceSharedIndexInformers.get(i).getIndexer()); + LOG.debug(() -> "registering lister (for endpoints) in namespace : " + namespace); + endpointsListers.add(lister); + } + + return endpointsListers; + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java index c7792e6e..e504eb32 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerAutoConfiguration.java @@ -41,6 +41,7 @@ import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKuber import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.log.LogAccessor; @@ -59,6 +60,7 @@ import static io.kubernetes.client.util.Namespaces.NAMESPACE_DEFAULT; @ConditionalOnKubernetesDiscoveryEnabled @ConditionalOnBlockingOrReactiveEnabled @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@Conditional(ConditionalOnSelectiveNamespacesMissing.class) @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class }) public class KubernetesInformerAutoConfiguration { diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java index 09e75ac2..f4965d6b 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java @@ -63,14 +63,14 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesInformerDiscoveryClient.class)); - private final SharedInformerFactory sharedInformerFactory; + private final List sharedInformerFactories; - private final Lister serviceLister; + private final List> serviceListers; + + private final List> endpointsListers; private final Supplier informersReadyFunc; - private final Lister endpointsLister; - private final KubernetesDiscoveryProperties properties; @Deprecated(forRemoval = true) @@ -78,9 +78,9 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { Lister serviceLister, Lister endpointsLister, SharedInformer serviceInformer, SharedInformer endpointsInformer, KubernetesDiscoveryProperties properties) { - this.sharedInformerFactory = sharedInformerFactory; - this.serviceLister = serviceLister; - this.endpointsLister = endpointsLister; + this.sharedInformerFactories = List.of(sharedInformerFactory); + this.serviceListers = List.of(serviceLister); + this.endpointsListers = List.of(endpointsLister); this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); this.properties = properties; } @@ -89,13 +89,32 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { Lister serviceLister, Lister endpointsLister, SharedInformer serviceInformer, SharedInformer endpointsInformer, KubernetesDiscoveryProperties properties) { - this.sharedInformerFactory = sharedInformerFactory; - this.serviceLister = serviceLister; - this.endpointsLister = endpointsLister; + this.sharedInformerFactories = List.of(sharedInformerFactory); + this.serviceListers = List.of(serviceLister); + this.endpointsListers = List.of(endpointsLister); this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); this.properties = properties; } + public KubernetesInformerDiscoveryClient(List sharedInformerFactories, + List> serviceListers, List> endpointsListers, + List> serviceInformers, List> endpointsInformers, + KubernetesDiscoveryProperties properties) { + this.sharedInformerFactories = sharedInformerFactories; + + this.serviceListers = serviceListers; + this.endpointsListers = endpointsListers; + this.informersReadyFunc = () -> { + boolean serviceInformersReady = serviceInformers.isEmpty() || serviceInformers.stream() + .map(SharedInformer::hasSynced).reduce(Boolean::logicalAnd).orElse(false); + boolean endpointsInformersReady = endpointsInformers.isEmpty() || endpointsInformers.stream() + .map(SharedInformer::hasSynced).reduce(Boolean::logicalAnd).orElse(false); + return serviceInformersReady && endpointsInformersReady; + }; + + this.properties = properties; + } + @Override public String description() { return "Kubernetes Client Discovery"; @@ -105,11 +124,11 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { public List getInstances(String serviceId) { Objects.requireNonNull(serviceId, "serviceId must be provided"); - List services = serviceLister.list().stream() - .filter(svc -> serviceId.equals(svc.getMetadata().getName())).toList(); - if (services.size() == 0 || !services.stream().anyMatch(service -> matchesServiceLabels(service, properties))) { - // no such service present in the cluster - return new ArrayList<>(); + List services = serviceListers.stream().flatMap(x -> x.list().stream()) + .filter(scv -> scv.getMetadata() != null).filter(svc -> serviceId.equals(svc.getMetadata().getName())) + .toList(); + if (services.size() == 0 || services.stream().noneMatch(service -> matchesServiceLabels(service, properties))) { + return List.of(); } return services.stream().flatMap(s -> getServiceInstanceDetails(s, serviceId)).toList(); } @@ -117,12 +136,10 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { private Stream getServiceInstanceDetails(V1Service service, String serviceId) { Map serviceMetadata = serviceMetadata(properties, service, serviceId); - V1Endpoints ep = endpointsLister.namespace(service.getMetadata().getNamespace()) - .get(service.getMetadata().getName()); - if (ep == null || ep.getSubsets() == null) { - // no available endpoints in the cluster - return Stream.empty(); - } + List endpoints = endpointsListers.stream() + .map(endpointsLister -> endpointsLister.namespace(service.getMetadata().getNamespace()) + .get(service.getMetadata().getName())) + .filter(Objects::nonNull).filter(ep -> ep.getSubsets() != null).toList(); Optional discoveredPrimaryPortName = Optional.empty(); if (service.getMetadata() != null && service.getMetadata().getLabels() != null) { @@ -133,33 +150,36 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { final boolean secured = isSecured(service); - return ep.getSubsets().stream().filter(subset -> subset.getPorts() != null && subset.getPorts().size() > 0) // safeguard - .flatMap(subset -> { - Map metadata = new HashMap<>(serviceMetadata); - List endpointPorts = subset.getPorts(); - if (properties.metadata() != null && properties.metadata().addPorts()) { - endpointPorts.forEach( - p -> metadata.put(StringUtils.hasText(p.getName()) ? p.getName() : UNSET_PORT_NAME, + return endpoints.stream() + .flatMap(ep -> ep.getSubsets().stream() + .filter(subset -> subset.getPorts() != null && subset.getPorts().size() > 0) // safeguard + .flatMap(subset -> { + Map metadata = new HashMap<>(serviceMetadata); + List endpointPorts = subset.getPorts(); + if (properties.metadata() != null && properties.metadata().addPorts()) { + endpointPorts.forEach(p -> metadata.put( + StringUtils.hasText(p.getName()) ? p.getName() : UNSET_PORT_NAME, Integer.toString(p.getPort()))); - } - List addresses = subset.getAddresses(); - if (addresses == null) { - addresses = new ArrayList<>(); - } - if (properties.includeNotReadyAddresses() - && !CollectionUtils.isEmpty(subset.getNotReadyAddresses())) { - addresses.addAll(subset.getNotReadyAddresses()); - } + } + List addresses = subset.getAddresses(); + if (addresses == null) { + addresses = new ArrayList<>(); + } + if (properties.includeNotReadyAddresses() + && !CollectionUtils.isEmpty(subset.getNotReadyAddresses())) { + addresses.addAll(subset.getNotReadyAddresses()); + } - final int port = findEndpointPort(endpointPorts, primaryPortName, serviceId); - return addresses.stream() - .map(addr -> new DefaultKubernetesServiceInstance( - addr.getTargetRef() != null ? addr.getTargetRef().getUid() : "", serviceId, - addr.getIp(), port, metadata, secured, service.getMetadata().getNamespace(), - // TODO find out how to get cluster name possibly from - // KubeConfig - null)); - }); + 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) { @@ -209,17 +229,16 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { @Override public List getServices() { - List services = serviceLister.list().stream() + List services = serviceListers.stream().flatMap(serviceLister -> serviceLister.list().stream()) .filter(service -> matchesServiceLabels(service, properties)).map(s -> s.getMetadata().getName()) .distinct().toList(); - LOG.debug(() -> "will return services : " + services); return services; } @PostConstruct public void afterPropertiesSet() { - sharedInformerFactory.startAllRegisteredInformers(); + sharedInformerFactories.forEach(SharedInformerFactory::startAllRegisteredInformers); if (!Wait.poll(Duration.ofSeconds(1), Duration.ofSeconds(properties.cacheLoadingTimeoutSeconds()), () -> { LOG.info(() -> "Waiting for the cache of informers to be fully loaded.."); return informersReadyFunc.get(); @@ -233,8 +252,8 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { () -> "Timeout waiting for informers cache to be ready, ignoring the failure because waitForInformerCacheReady property is false"); } } - LOG.info(() -> "Cache fully loaded (total " + serviceLister.list().size() - + " services) , discovery client is now available"); + LOG.info(() -> "Cache fully loaded (total " + serviceListers.stream().mapToLong(x -> x.list().size()).sum() + + " services), discovery client is now available"); } @Override diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java index eebacc1e..57341b29 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.cloud.kubernetes.client.discovery; +import java.util.List; + import io.kubernetes.client.informer.SharedInformer; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; @@ -44,6 +46,7 @@ import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscover import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.log.LogAccessor; @@ -57,7 +60,8 @@ import org.springframework.core.log.LogAccessor; @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesClientInformerAutoConfiguration.class }) + KubernetesClientInformerAutoConfiguration.class, + KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class }) public class KubernetesInformerDiscoveryClientAutoConfiguration { private static final LogAccessor LOG = new LogAccessor( @@ -90,7 +94,8 @@ public class KubernetesInformerDiscoveryClientAutoConfiguration { @Bean @ConditionalOnMissingBean - public KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient( + @Conditional(ConditionalOnSelectiveNamespacesMissing.class) + KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient( SharedInformerFactory sharedInformerFactory, Lister serviceLister, Lister endpointsLister, SharedInformer serviceInformer, SharedInformer endpointsInformer, KubernetesDiscoveryProperties properties) { @@ -98,4 +103,15 @@ public class KubernetesInformerDiscoveryClientAutoConfiguration { serviceInformer, endpointsInformer, properties); } + @Bean + @ConditionalOnMissingBean + @Conditional(ConditionalOnSelectiveNamespacesPresent.class) + KubernetesInformerDiscoveryClient selectiveNamespacesKubernetesInformerDiscoveryClient( + List sharedInformerFactories, List> serviceListers, + List> endpointsListers, List> serviceInformers, + List> endpointsInformers, KubernetesDiscoveryProperties properties) { + return new KubernetesInformerDiscoveryClient(sharedInformerFactories, serviceListers, endpointsListers, + serviceInformers, endpointsInformers, properties); + } + } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java index 3828afd8..13248a2a 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-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. @@ -49,6 +49,8 @@ public class KubernetesInformerReactiveDiscoveryClient implements ReactiveDiscov serviceInformer, endpointsInformer, properties); } + // this is either kubernetesClientInformerDiscoveryClient + // or selectiveNamespacesKubernetesClientInformerDiscoveryClient KubernetesInformerReactiveDiscoveryClient(KubernetesInformerDiscoveryClient kubernetesDiscoveryClient) { this.kubernetesDiscoveryClient = kubernetesDiscoveryClient; } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java index ea67908f..62f73939 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.cloud.kubernetes.client.discovery.reactive; +import java.util.List; + import io.kubernetes.client.informer.SharedInformer; import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; @@ -39,7 +41,10 @@ import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIn import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; import org.springframework.cloud.kubernetes.client.KubernetesClientPodUtils; +import org.springframework.cloud.kubernetes.client.discovery.ConditionalOnSelectiveNamespacesMissing; +import org.springframework.cloud.kubernetes.client.discovery.ConditionalOnSelectiveNamespacesPresent; import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerSelectiveNamespacesAutoConfiguration; import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.PodUtils; @@ -49,6 +54,7 @@ import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscover import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.log.LogAccessor; @@ -66,7 +72,8 @@ import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesD @AutoConfigureBefore({ SimpleReactiveDiscoveryClientAutoConfiguration.class, ReactiveCommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ ReactiveCompositeDiscoveryClientAutoConfiguration.class, - KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class }) + KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class, + KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class }) public class KubernetesInformerReactiveDiscoveryClientAutoConfiguration { private static final LogAccessor LOG = new LogAccessor( @@ -127,6 +134,7 @@ public class KubernetesInformerReactiveDiscoveryClientAutoConfiguration { @Bean @ConditionalOnMissingBean + @Conditional(ConditionalOnSelectiveNamespacesMissing.class) KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient( SharedInformerFactory sharedInformerFactory, Lister serviceLister, Lister endpointsLister, SharedInformer serviceInformer, @@ -135,4 +143,15 @@ public class KubernetesInformerReactiveDiscoveryClientAutoConfiguration { serviceInformer, endpointsInformer, properties); } + @Bean + @ConditionalOnMissingBean + @Conditional(ConditionalOnSelectiveNamespacesPresent.class) + KubernetesInformerDiscoveryClient selectiveNamespacesKubernetesClientInformerDiscoveryClient( + List sharedInformerFactories, List> serviceListers, + List> endpointsListers, List> serviceInformers, + List> endpointsInformers, KubernetesDiscoveryProperties properties) { + return new KubernetesInformerDiscoveryClient(sharedInformerFactories, serviceListers, endpointsListers, + serviceInformers, endpointsInformers, properties); + } + } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index d7f1ac9f..a2e5e613 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -2,3 +2,4 @@ org.springframework.cloud.kubernetes.client.discovery.catalog.KubernetesCatalogW org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClientAutoConfiguration org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClientAutoConfiguration +org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerSelectiveNamespacesAutoConfiguration diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesDisabledTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesDisabledTests.java new file mode 100644 index 00000000..0f3b8a30 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesDisabledTests.java @@ -0,0 +1,62 @@ +/* + * 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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.context.annotation.ConditionContext; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ + +public class ConditionalOnSelectiveNamespacesDisabledTests { + + private static final ConditionalOnSelectiveNamespacesMissing TO_TEST = new ConditionalOnSelectiveNamespacesMissing(); + + private static final ConditionContext CONDITION_CONTEXT = Mockito.mock(ConditionContext.class); + + @Test + void testSelectiveNamespacesNotPresent() { + MockEnvironment environment = new MockEnvironment(); + Mockito.when(CONDITION_CONTEXT.getEnvironment()).thenReturn(environment); + boolean result = TO_TEST.matches(CONDITION_CONTEXT, null); + Assertions.assertTrue(result); + } + + @Test + void testSelectiveNamespacesPresentEmpty() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.cloud.kubernetes.discovery.namespaces", ""); + Mockito.when(CONDITION_CONTEXT.getEnvironment()).thenReturn(environment); + boolean result = TO_TEST.matches(CONDITION_CONTEXT, null); + Assertions.assertTrue(result); + } + + @Test + void testSelectiveNamespacesPresentNonEmpty() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.cloud.kubernetes.discovery.namespaces", "default"); + Mockito.when(CONDITION_CONTEXT.getEnvironment()).thenReturn(environment); + boolean result = TO_TEST.matches(CONDITION_CONTEXT, null); + Assertions.assertFalse(result); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesEnabledTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesEnabledTests.java new file mode 100644 index 00000000..69f79fb2 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/ConditionalOnSelectiveNamespacesEnabledTests.java @@ -0,0 +1,61 @@ +/* + * 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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.context.annotation.ConditionContext; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +class ConditionalOnSelectiveNamespacesEnabledTests { + + private static final ConditionalOnSelectiveNamespacesPresent TO_TEST = new ConditionalOnSelectiveNamespacesPresent(); + + private static final ConditionContext CONDITION_CONTEXT = Mockito.mock(ConditionContext.class); + + @Test + void testSelectiveNamespacesNotPresent() { + MockEnvironment environment = new MockEnvironment(); + Mockito.when(CONDITION_CONTEXT.getEnvironment()).thenReturn(environment); + boolean result = TO_TEST.matches(CONDITION_CONTEXT, null); + Assertions.assertFalse(result); + } + + @Test + void testSelectiveNamespacesPresentEmpty() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.cloud.kubernetes.discovery.namespaces", ""); + Mockito.when(CONDITION_CONTEXT.getEnvironment()).thenReturn(environment); + boolean result = TO_TEST.matches(CONDITION_CONTEXT, null); + Assertions.assertFalse(result); + } + + @Test + void testSelectiveNamespacesPresentNonEmpty() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("spring.cloud.kubernetes.discovery.namespaces", "default"); + Mockito.when(CONDITION_CONTEXT.getEnvironment()).thenReturn(environment); + boolean result = TO_TEST.matches(CONDITION_CONTEXT, null); + Assertions.assertTrue(result); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests.java index 28de3bbf..58c7329f 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests.java @@ -18,9 +18,6 @@ package org.springframework.cloud.kubernetes.client.discovery; import java.io.StringReader; -import io.kubernetes.client.informer.SharedIndexInformer; -import io.kubernetes.client.informer.SharedInformerFactory; -import io.kubernetes.client.informer.cache.Lister; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.util.Config; import org.junit.jupiter.api.AfterAll; @@ -31,7 +28,9 @@ import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; @@ -40,6 +39,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertNonSelectiveNamespacesBeansMissing; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertNonSelectiveNamespacesBeansPresent; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertSelectiveNamespacesBeansMissing; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertSelectiveNamespacesBeansPresent; /** * Test various conditionals for @@ -62,11 +65,30 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests void discoveryEnabledDefault() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void discoveryEnabledDefaultWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.namespaces=a,b,c"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 3); }); } @@ -75,11 +97,30 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.enabled=true"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void discoveryEnabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.enabled=true", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -88,11 +129,30 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).doesNotHaveBean(SharedInformerFactory.class); - assertThat(context).doesNotHaveBean(SharedIndexInformer.class); - assertThat(context).doesNotHaveBean(Lister.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void discoveryDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.enabled=false", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); }); } @@ -101,11 +161,31 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=true"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryEnabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.enabled=true", + "spring.cloud.kubernetes.discovery.namespaces=a,b,c,d"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 4); }); } @@ -114,11 +194,31 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).doesNotHaveBean(SharedInformerFactory.class); - assertThat(context).doesNotHaveBean(SharedIndexInformer.class); - assertThat(context).doesNotHaveBean(Lister.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.enabled=false", + "spring.cloud.kubernetes.discovery.namespaces=a,b,c,d"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); }); } @@ -127,11 +227,30 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.blocking.enabled=true"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryBlockingEnabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=true", "spring.cloud.kubernetes.discovery.namespaces=a"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 1); }); } @@ -140,11 +259,30 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.blocking.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryBlockingDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -153,11 +291,31 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.client.health-indicator.enabled=true"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryHealthIndicatorEnabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.client.health-indicator.enabled=true", + "spring.cloud.kubernetes.discovery.namespaces=b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 1); }); } @@ -166,11 +324,31 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.client.health-indicator.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryHealthIndicatorDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.client.health-indicator.enabled=false", + "spring.cloud.kubernetes.discovery.namespaces=b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 1); }); } @@ -179,11 +357,31 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setupWithFilteredClassLoader(HealthIndicator.class, "spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.client.health-indicator.enabled=true"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryHealthIndicatorEnabledHealthIndicatorMissingWithSelectiveNamespaces() { + setupWithFilteredClassLoader(HealthIndicator.class, "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.config.enabled=false", "spring.cloud.discovery.client.health-indicator.enabled=true", + "spring.cloud.kubernetes.discovery.namespaces=a,b,c,d,e"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 5); }); } @@ -195,11 +393,34 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.reactive.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + /** + * reactive is disabled and should not impact blocking in any way + */ + @Test + void reactiveDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.reactive.enabled=false", + "spring.cloud.kubernetes.discovery.namespaces=a,b,c,d,e"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 5); }); } @@ -207,7 +428,8 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests applicationContextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(KubernetesInformerDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesClientInformerAutoConfiguration.class)) + KubernetesClientInformerAutoConfiguration.class, + KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class)) .withUserConfiguration(ApiClientConfig.class).withPropertyValues(properties); } @@ -215,7 +437,8 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests applicationContextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(KubernetesInformerDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesClientInformerAutoConfiguration.class)) + KubernetesClientInformerAutoConfiguration.class, + KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class)) .withClassLoader(new FilteredClassLoader(cls)).withUserConfiguration(ApiClientConfig.class) .withPropertyValues(properties); } diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java index 3eab6f51..52484266 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientTests.java @@ -95,9 +95,11 @@ class KubernetesInformerDiscoveryClientTests { @Test void testDiscoveryGetServicesAllNamespaceShouldWork() { Lister serviceLister = setupServiceLister(SERVICE_1, SERVICE_2); + Lister endpointsLister = setupEndpointsLister(ENDPOINTS_NO_UNSET_PORT_NAME); KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( - SHARED_INFORMER_FACTORY, serviceLister, null, null, null, KubernetesDiscoveryProperties.DEFAULT); + SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, + KubernetesDiscoveryProperties.DEFAULT); assertThat(discoveryClient.getServices().toArray()).containsOnly(SERVICE_1.getMetadata().getName(), SERVICE_2.getMetadata().getName()); @@ -107,12 +109,13 @@ class KubernetesInformerDiscoveryClientTests { @Test void testDiscoveryWithServiceLabels() { Lister serviceLister = setupServiceLister(SERVICE_1, SERVICE_2, SERVICE_3); + Lister endpointsLister = setupEndpointsLister(ENDPOINTS_NO_UNSET_PORT_NAME); Map labels = Map.of("k8s", "true", "spring", "true"); KubernetesDiscoveryProperties kubernetesDiscoveryProperties = properties(true, labels); KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( - SHARED_INFORMER_FACTORY, serviceLister, null, null, null, kubernetesDiscoveryProperties); + SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties); assertThat(discoveryClient.getServices().toArray()).containsOnly(SERVICE_3.getMetadata().getName()); @@ -164,9 +167,11 @@ class KubernetesInformerDiscoveryClientTests { @Test void testDiscoveryGetServicesOneNamespaceShouldWork() { Lister serviceLister = setupServiceLister(SERVICE_1, SERVICE_2); + Lister endpointsLister = setupEndpointsLister(ENDPOINTS_NO_UNSET_PORT_NAME); KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( - SHARED_INFORMER_FACTORY, serviceLister, null, null, null, KubernetesDiscoveryProperties.DEFAULT); + SHARED_INFORMER_FACTORY, serviceLister, endpointsLister, null, null, + KubernetesDiscoveryProperties.DEFAULT); assertThat(discoveryClient.getServices().toArray()).containsOnly(SERVICE_1.getMetadata().getName()); diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/TestUtils.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/TestUtils.java new file mode 100644 index 00000000..2cccd1f8 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/TestUtils.java @@ -0,0 +1,161 @@ +/* + * 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 io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1Service; + +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Some common class that test code delegates to. + * + * @author wind57 + */ +public final class TestUtils { + + private TestUtils() { + + } + + public static void assertSelectiveNamespacesBeansMissing(AssertableApplicationContext context) { + String[] sharedInformerFactoriesBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>() { + })); + assertThat(sharedInformerFactoriesBeanName).isEmpty(); + + String[] serviceSharedIndexInformersBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>>() { + })); + assertThat(serviceSharedIndexInformersBeanName).isEmpty(); + + String[] endpointsSharedIndexInformersBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>>() { + })); + assertThat(endpointsSharedIndexInformersBeanName).isEmpty(); + + String[] serviceListersBeanName = context + .getBeanNamesForType(ResolvableType.forType(new ParameterizedTypeReference>>() { + })); + assertThat(serviceListersBeanName).isEmpty(); + + String[] endpointsListersBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>>() { + })); + assertThat(endpointsListersBeanName).isEmpty(); + } + + @SuppressWarnings("unchecked") + public static void assertSelectiveNamespacesBeansPresent(AssertableApplicationContext context, int times) { + String sharedInformerFactoriesBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>() { + }))[0]; + List sharedInformerFactories = (List) context + .getBean(sharedInformerFactoriesBeanName); + assertThat(sharedInformerFactories.size()).isEqualTo(times); + + String serviceSharedIndexInformersBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>>() { + }))[0]; + List> serviceSharedIndexInformers = (List>) context + .getBean(serviceSharedIndexInformersBeanName); + assertThat(serviceSharedIndexInformers.size()).isEqualTo(times); + + String endpointsSharedIndexInformersBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>>() { + }))[0]; + List> endpointsSharedIndexInformers = (List>) context + .getBean(endpointsSharedIndexInformersBeanName); + assertThat(endpointsSharedIndexInformers.size()).isEqualTo(times); + + String serviceListersBeanName = context + .getBeanNamesForType(ResolvableType.forType(new ParameterizedTypeReference>>() { + }))[0]; + List> serviceListers = (List>) context.getBean(serviceListersBeanName); + assertThat(serviceListers.size()).isEqualTo(times); + + String endpointsListersBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>>() { + }))[0]; + List> endpointsListers = (List>) context + .getBean(endpointsListersBeanName); + assertThat(endpointsListers.size()).isEqualTo(times); + } + + @SuppressWarnings("unchecked") + public static void assertNonSelectiveNamespacesBeansPresent(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(SharedInformerFactory.class); + + String serviceSharedIndexInformerBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>() { + }))[0]; + SharedIndexInformer serviceSharedIndexInformer = (SharedIndexInformer) context + .getBean(serviceSharedIndexInformerBeanName); + assertThat(serviceSharedIndexInformer).isNotNull(); + + String endpointSharedIndexInformerBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>() { + }))[0]; + SharedIndexInformer endpointsSharedIndexInformer = (SharedIndexInformer) context + .getBean(endpointSharedIndexInformerBeanName); + assertThat(endpointsSharedIndexInformer).isNotNull(); + + String serviceListerBeanName = context + .getBeanNamesForType(ResolvableType.forType(new ParameterizedTypeReference>() { + }))[0]; + Lister serviceLister = (Lister) context.getBean(serviceListerBeanName); + assertThat(serviceLister).isNotNull(); + + String endpointsListerBeanName = context + .getBeanNamesForType(ResolvableType.forType(new ParameterizedTypeReference>() { + }))[0]; + Lister endpointsLister = (Lister) context.getBean(endpointsListerBeanName); + assertThat(endpointsLister).isNotNull(); + } + + public static void assertNonSelectiveNamespacesBeansMissing(AssertableApplicationContext context) { + String[] serviceSharedIndexInformerBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>() { + })); + assertThat(serviceSharedIndexInformerBeanName).isEmpty(); + + String[] endpointSharedIndexInformerBeanName = context.getBeanNamesForType( + ResolvableType.forType(new ParameterizedTypeReference>() { + })); + assertThat(endpointSharedIndexInformerBeanName).isEmpty(); + + String[] serviceListerBeanName = context + .getBeanNamesForType(ResolvableType.forType(new ParameterizedTypeReference>() { + })); + assertThat(serviceListerBeanName).isEmpty(); + + String[] endpointsListerBeanName = context + .getBeanNamesForType(ResolvableType.forType(new ParameterizedTypeReference>() { + })); + assertThat(endpointsListerBeanName).isEmpty(); + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java index ae7d5311..68d4f215 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java @@ -18,9 +18,6 @@ package org.springframework.cloud.kubernetes.client.discovery.reactive; import java.io.StringReader; -import io.kubernetes.client.informer.SharedIndexInformer; -import io.kubernetes.client.informer.SharedInformerFactory; -import io.kubernetes.client.informer.cache.Lister; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.util.Config; import org.junit.jupiter.api.AfterAll; @@ -35,7 +32,10 @@ import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactive import org.springframework.cloud.commons.util.UtilAutoConfiguration; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerSelectiveNamespacesAutoConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; import org.springframework.cloud.kubernetes.commons.KubernetesCommonsAutoConfiguration; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; import org.springframework.context.annotation.Bean; @@ -43,6 +43,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertNonSelectiveNamespacesBeansMissing; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertNonSelectiveNamespacesBeansPresent; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertSelectiveNamespacesBeansMissing; +import static org.springframework.cloud.kubernetes.client.discovery.TestUtils.assertSelectiveNamespacesBeansPresent; /** * Test various conditionals for @@ -65,12 +69,36 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte void discoveryEnabledDefault() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false"); applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("kubernetesClientInformerDiscoveryClient"); + assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); + // simple from commons and ours assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void discoveryEnabledDefaultWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.namespaces=a,b,c"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("selectiveNamespacesKubernetesClientInformerDiscoveryClient"); assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + // simple from commons and ours + assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 3); }); } @@ -79,12 +107,36 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.enabled=true"); applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("kubernetesClientInformerDiscoveryClient"); + assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); + // simple from commons and ours assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void discoveryEnabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.enabled=true", "spring.cloud.kubernetes.discovery.namespaces=a,b,c"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("selectiveNamespacesKubernetesClientInformerDiscoveryClient"); assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + // simple from commons and ours + assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 3); }); } @@ -93,11 +145,30 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).doesNotHaveBean(SharedInformerFactory.class); - assertThat(context).doesNotHaveBean(SharedIndexInformer.class); - assertThat(context).doesNotHaveBean(Lister.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void discoveryDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.enabled=false", "spring.cloud.kubernetes.discovery.namespaces=a,b,c"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); }); } @@ -106,12 +177,36 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=true"); applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("kubernetesClientInformerDiscoveryClient"); + assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); + // simple from commons and ours assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryEnabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.enabled=true", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("selectiveNamespacesKubernetesClientInformerDiscoveryClient"); assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + // simple from commons and ours + assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -120,12 +215,32 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.enabled=false"); applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); // only "simple" one from commons, as ours is not picked up assertThat(context).hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesDiscoveryDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.kubernetes.discovery.enabled=false", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).doesNotHaveBean(SharedInformerFactory.class); - assertThat(context).doesNotHaveBean(SharedIndexInformer.class); - assertThat(context).doesNotHaveBean(Lister.class); + + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + // only "simple" one from commons, as ours is not picked up + assertThat(context).hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansMissing(context); }); } @@ -134,12 +249,36 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.reactive.enabled=true"); applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("kubernetesClientInformerDiscoveryClient"); + assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); + // simple from commons and ours assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesReactiveDiscoveryEnabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.reactive.enabled=true", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("selectiveNamespacesKubernetesClientInformerDiscoveryClient"); assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + // simple from commons and ours + assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -148,11 +287,30 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.reactive.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void kubernetesReactiveDiscoveryDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.reactive.enabled=false", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).doesNotHaveBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).doesNotHaveBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -164,12 +322,39 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.blocking.enabled=false"); applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("kubernetesClientInformerDiscoveryClient"); + assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); + // simple from commons and ours assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + /** + * blocking is disabled, and it should not impact reactive in any way. + */ + @Test + void blockingDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.blocking.enabled=false", "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("selectiveNamespacesKubernetesClientInformerDiscoveryClient"); assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + // simple from commons and ours + assertThat(context).getBeans(ReactiveDiscoveryClientHealthIndicator.class).size().isEqualTo(2); + assertThat(context).hasSingleBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + assertThat(context).hasBean("reactiveIndicatorInitializer"); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -178,11 +363,33 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.client.health-indicator.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("kubernetesClientInformerDiscoveryClient"); assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void healthDisabledWithSelectiveNamespaces() { + setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.client.health-indicator.enabled=false", + "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("selectiveNamespacesKubernetesClientInformerDiscoveryClient"); + assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -192,11 +399,34 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte "spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", "spring.cloud.discovery.client.health-indicator.enabled=false"); applicationContextRunner.run(context -> { - assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("kubernetesClientInformerDiscoveryClient"); assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); - assertThat(context).hasSingleBean(SharedInformerFactory.class); - assertThat(context).getBeans(SharedIndexInformer.class).hasSize(2); - assertThat(context).getBeans(Lister.class).hasSize(2); + + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + + assertNonSelectiveNamespacesBeansPresent(context); + assertSelectiveNamespacesBeansMissing(context); + }); + } + + @Test + void healthEnabledClassNotPresentWithSelectiveNamespaces() { + setupWithFilteredClassLoader("org.springframework.boot.actuate.health.ReactiveHealthIndicator", + "spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false", + "spring.cloud.discovery.client.health-indicator.enabled=false", + "spring.cloud.kubernetes.discovery.namespaces=a,b"); + applicationContextRunner.run(context -> { + assertThat(context).hasSingleBean(KubernetesInformerDiscoveryClient.class); + assertThat(context).hasBean("selectiveNamespacesKubernetesClientInformerDiscoveryClient"); + assertThat(context).hasSingleBean(KubernetesInformerReactiveDiscoveryClient.class); + + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class); + assertThat(context).doesNotHaveBean(KubernetesDiscoveryClientHealthIndicatorInitializer.class); + + assertNonSelectiveNamespacesBeansMissing(context); + assertSelectiveNamespacesBeansPresent(context, 2); }); } @@ -206,6 +436,7 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte KubernetesInformerReactiveDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class, UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, + KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class, KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class)) .withUserConfiguration(ApiClientConfig.class).withPropertyValues(properties); } @@ -216,6 +447,7 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte KubernetesInformerReactiveDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class, UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, + KubernetesClientInformerSelectiveNamespacesAutoConfiguration.class, KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class)) .withUserConfiguration(ApiClientConfig.class).withClassLoader(new FilteredClassLoader(name)) .withPropertyValues(properties); diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java index 78d705ab..16d98059 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientTests.java @@ -37,14 +37,11 @@ import org.mockito.Mockito; import reactor.test.StepVerifier; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.mock.env.MockEnvironment; import static io.kubernetes.client.util.Namespaces.NAMESPACE_ALL; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; /** * @author Ryan Baxter @@ -88,13 +85,14 @@ class KubernetesInformerReactiveDiscoveryClientTests { void testDiscoveryGetServicesAllNamespaceShouldWork() { Lister serviceLister = setupServiceLister(NAMESPACE_ALL, TEST_SERVICE_1, TEST_SERVICE_2, TEST_SERVICE_3); + Lister endpointsLister = setupEndpointsLister(""); KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false); KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister, null, - null, null, kubernetesDiscoveryProperties); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + kubernetesDiscoveryProperties)); StepVerifier.create(discoveryClient.getServices()) .expectNext(TEST_SERVICE_1.getMetadata().getName(), TEST_SERVICE_2.getMetadata().getName()) @@ -105,12 +103,11 @@ class KubernetesInformerReactiveDiscoveryClientTests { @Test void testDiscoveryGetServicesOneNamespaceShouldWork() { Lister serviceLister = setupServiceLister(NAMESPACE_1, TEST_SERVICE_1, TEST_SERVICE_2); + Lister endpointsLister = setupEndpointsLister(""); - KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn(NAMESPACE_1); KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null, - KubernetesDiscoveryProperties.DEFAULT); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + KubernetesDiscoveryProperties.DEFAULT)); StepVerifier.create(discoveryClient.getServices()).expectNext(TEST_SERVICE_1.getMetadata().getName()) .expectComplete().verify(); @@ -127,8 +124,8 @@ class KubernetesInformerReactiveDiscoveryClientTests { KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister, - endpointsLister, null, null, kubernetesDiscoveryProperties); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + kubernetesDiscoveryProperties)); StepVerifier.create(discoveryClient.getInstances("test-svc-1")) .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, @@ -147,11 +144,9 @@ class KubernetesInformerReactiveDiscoveryClientTests { Set.of(), true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); - KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn(NAMESPACE_1); KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null, - kubernetesDiscoveryProperties); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + kubernetesDiscoveryProperties)); StepVerifier.create(discoveryClient.getInstances("test-svc-1")) .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, @@ -172,6 +167,8 @@ class KubernetesInformerReactiveDiscoveryClientTests { */ @Test void testAllNamespacesTwoServicesPresent() { + Lister endpointsLister = setupEndpointsLister(""); + boolean allNamespaces = true; V1Service serviceA = new V1Service().metadata(new V1ObjectMeta().name("service-a").namespace("namespace-a")); V1Service serviceB = new V1Service().metadata(new V1ObjectMeta().name("service-b").namespace("namespace-b")); @@ -182,12 +179,9 @@ class KubernetesInformerReactiveDiscoveryClientTests { KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false); - KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant"); - KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null, - kubernetesDiscoveryProperties); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + kubernetesDiscoveryProperties)); List result = discoveryClient.getServices().collectList().block(); Assertions.assertEquals(result.size(), 2); @@ -207,6 +201,8 @@ class KubernetesInformerReactiveDiscoveryClientTests { */ @Test void testSingleNamespaceTwoServicesPresent() { + Lister endpointsLister = setupEndpointsLister(""); + boolean allNamespaces = false; V1Service serviceA = new V1Service().metadata(new V1ObjectMeta().name("service-a").namespace("namespace-a")); V1Service serviceB = new V1Service().metadata(new V1ObjectMeta().name("service-b").namespace("namespace-b")); @@ -217,12 +213,9 @@ class KubernetesInformerReactiveDiscoveryClientTests { KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false); - KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant"); - KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null, - kubernetesDiscoveryProperties); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + kubernetesDiscoveryProperties)); List result = discoveryClient.getServices().collectList().block(); Assertions.assertEquals(result.size(), 1); @@ -261,19 +254,16 @@ class KubernetesInformerReactiveDiscoveryClientTests { endpointsCache.add(endpointsXNamespaceA); endpointsCache.add(endpointsXNamespaceB); - Lister endpointsLister = new Lister<>(endpointsCache).namespace(NAMESPACE_ALL); - Lister serviceLister = new Lister<>(serviceCache).namespace(NAMESPACE_ALL); + Lister endpointsLister = new Lister<>(endpointsCache, NAMESPACE_ALL); + Lister serviceLister = new Lister<>(serviceCache, NAMESPACE_ALL); KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); - KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant"); - KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null, - kubernetesDiscoveryProperties); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + kubernetesDiscoveryProperties)); List result = discoveryClient.getInstances("endpoints-x").collectList().block(); Assertions.assertEquals(result.size(), 2); @@ -320,12 +310,9 @@ class KubernetesInformerReactiveDiscoveryClientTests { allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); - KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant"); - KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null, - kubernetesDiscoveryProperties); + new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, null, null, + kubernetesDiscoveryProperties)); List result = discoveryClient.getInstances("endpoints-x").collectList().block(); Assertions.assertEquals(result.size(), 1); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java index e7e964ff..59cd031b 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java @@ -21,8 +21,10 @@ import java.util.List; import reactor.core.publisher.Mono; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; /** @@ -45,4 +47,9 @@ public class ReactiveDiscoveryController { return reactiveDiscoveryClient.getServices().collectList(); } + @GetMapping("reactive/service-instances/{serviceId}") + public Mono> serviceInstances(@PathVariable("serviceId") String serviceId) { + return reactiveDiscoveryClient.getInstances(serviceId).collectList(); + } + } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientIT.java similarity index 91% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientIT.java index 56f4fb01..a37c4467 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryClientIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery; +package org.springframework.cloud.kubernetes.client.discovery.it; import java.time.Duration; import java.util.ArrayList; @@ -31,7 +31,6 @@ import io.kubernetes.client.openapi.models.V1Service; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; import org.testcontainers.k3s.K3sContainer; @@ -92,7 +91,7 @@ class KubernetesClientDiscoveryClientIT { manifests(false, null, Phase.CREATE); util.busybox(NAMESPACE, Phase.CREATE); - assertLogStatement("serviceSharedInformer will use namespace : default"); + Assertions.assertTrue(logs().contains("serviceSharedInformer will use namespace : default")); WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); @@ -107,7 +106,7 @@ class KubernetesClientDiscoveryClientIT { Assertions.assertTrue(servicesResult.contains("busybox-service")); WebClient ourServiceClient = builder() - .baseUrl("http://localhost//service-instances/spring-cloud-kubernetes-client-discovery-it").build(); + .baseUrl("http://localhost/service-instances/spring-cloud-kubernetes-client-discovery-it").build(); List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() .bodyToMono(new ParameterizedTypeReference>() { @@ -126,7 +125,7 @@ class KubernetesClientDiscoveryClientIT { Assertions.assertEquals(serviceInstance.getPort(), 8080); Assertions.assertEquals(serviceInstance.getNamespace(), "default"); - WebClient busyBoxServiceClient = builder().baseUrl("http://localhost//service-instances/busybox-service") + WebClient busyBoxServiceClient = builder().baseUrl("http://localhost/service-instances/busybox-service") .build(); List busyBoxServiceInstances = busyBoxServiceClient.method(HttpMethod.GET) .retrieve().bodyToMono(new ParameterizedTypeReference>() { @@ -169,7 +168,7 @@ class KubernetesClientDiscoveryClientIT { util.busybox(NAMESPACE_B, Phase.CREATE); manifests(true, null, Phase.CREATE); - assertLogStatement("serviceSharedInformer will use all-namespaces"); + Assertions.assertTrue(logs().contains("serviceSharedInformer will use all-namespaces")); WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); List servicesResult = servicesClient.method(HttpMethod.GET).retrieve() @@ -211,20 +210,21 @@ class KubernetesClientDiscoveryClientIT { * */ @Test - @Disabled - // TODO will be fixed by this issue : - // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1289 void testSpecificNamespace() { util.createNamespace(NAMESPACE_A); util.createNamespace(NAMESPACE_B); - util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE_A)); + util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A)); util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE); util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE); manifests(false, NAMESPACE_A, Phase.CREATE); // first check that wiremock service is present in both namespaces a and b assertServicePresentInNamespaces(List.of("a", "b"), "service-wiremock", "service-wiremock"); - assertLogStatement("serviceSharedInformer will use namespace : a"); + String logs = logs(); + Assertions.assertTrue(logs.contains("using selective namespaces : [a]")); + Assertions.assertTrue(logs.contains("reading pod in namespace : default")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : a")); WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); List servicesResult = servicesClient.method(HttpMethod.GET).retrieve() @@ -234,7 +234,7 @@ class KubernetesClientDiscoveryClientIT { Assertions.assertEquals(servicesResult.size(), 1); Assertions.assertTrue(servicesResult.contains("service-wiremock")); - WebClient wiremockInNamespaceAClient = builder().baseUrl("http://localhost//service-instances/service-wiremock") + WebClient wiremockInNamespaceAClient = builder().baseUrl("http://localhost/service-instances/service-wiremock") .build(); List wiremockInNamespaceA = wiremockInNamespaceAClient.method(HttpMethod.GET) @@ -261,7 +261,7 @@ class KubernetesClientDiscoveryClientIT { manifests(false, NAMESPACE_A, Phase.DELETE); util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE); util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE); - util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE_A)); + util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A)); util.deleteNamespace(NAMESPACE_A); util.deleteNamespace(NAMESPACE_B); } @@ -276,6 +276,10 @@ class KubernetesClientDiscoveryClientIT { .orElse(List.of())); V1EnvVar debugLevel = new V1EnvVar().name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY") .value("DEBUG"); + + V1EnvVar debugLevelForClient = new V1EnvVar().name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT") + .value("DEBUG"); + if (allNamespaces) { V1EnvVar allNamespacesVar = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES") .value("TRUE"); @@ -283,11 +287,12 @@ class KubernetesClientDiscoveryClientIT { } if (clientSpecificNamespace != null) { - V1EnvVar clientNamespace = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_CLIENT_NAMESPACE") + V1EnvVar clientNamespace = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0") .value(NAMESPACE_A); envVars.add(clientNamespace); } envVars.add(debugLevel); + envVars.add(debugLevelForClient); deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); if (phase.equals(Phase.CREATE)) { @@ -306,14 +311,13 @@ class KubernetesClientDiscoveryClientIT { return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); } - private void assertLogStatement(String message) { + private String logs() { try { String appPodName = K3S.execInContainer("sh", "-c", "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); - String ok = execResult.getStdout(); - Assertions.assertTrue(ok.contains(message)); + return execResult.getStdout(); } catch (Exception e) { e.printStackTrace(); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryHealthIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryHealthIT.java similarity index 99% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryHealthIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryHealthIT.java index 77c1c328..6346d559 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryHealthIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryHealthIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery; +package org.springframework.cloud.kubernetes.client.discovery.it; import java.time.Duration; import java.util.ArrayList; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryMultipleSelectiveNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryMultipleSelectiveNamespacesIT.java new file mode 100644 index 00000000..76b050d3 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryMultipleSelectiveNamespacesIT.java @@ -0,0 +1,326 @@ +/* + * 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.it; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1EnvVar; +import io.kubernetes.client.openapi.models.V1Ingress; +import io.kubernetes.client.openapi.models.V1Service; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Container; +import org.testcontainers.k3s.K3sContainer; +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author wind57 + */ +class KubernetesClientDiscoveryMultipleSelectiveNamespacesIT { + + private static final String BLOCKING_PUBLISH = "Will publish InstanceRegisteredEvent from blocking implementation"; + + private static final String REACTIVE_PUBLISH = "Will publish InstanceRegisteredEvent from reactive implementation"; + + private static final String NAMESPACE = "default"; + + private static final String NAMESPACE_A = "a"; + + private static final String NAMESPACE_B = "b"; + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it"; + + private static Util util; + + private static final K3sContainer K3S = Commons.container(); + + @BeforeAll + static void beforeAll() throws Exception { + K3S.start(); + Commons.validateImage(IMAGE_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); + + util = new Util(K3S); + + util.createNamespace(NAMESPACE_A); + util.createNamespace(NAMESPACE_B); + util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B)); + util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE); + } + + @AfterAll + static void afterAll() throws Exception { + Commons.cleanUp(IMAGE_NAME, K3S); + + util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE); + util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B)); + util.deleteNamespace(NAMESPACE_A); + util.deleteNamespace(NAMESPACE_B); + } + + /** + * Deploy wiremock in 3 namespaces: default, a, b. Search in selective namespaces 'a' + * and 'b' with blocking enabled and reactive disabled, as such find services and it's + * instances. + */ + @Test + void testTwoNamespacesBlockingOnly() { + + manifests(Phase.CREATE, false, true); + + String logs = logs(); + Assertions.assertTrue(logs.contains("using selective namespaces : [a, b]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a, b]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a, b]")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : b")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : b")); + + // this tiny checks makes sure that blocking is enabled and reactive is disabled. + Assertions.assertTrue(logs.contains(BLOCKING_PUBLISH)); + Assertions.assertFalse(logs.contains(REACTIVE_PUBLISH)); + + blockingCheck(); + + manifests(Phase.DELETE, false, true); + + } + + /** + * Deploy wiremock in 3 namespaces: default, a, b. Search in selective namespaces 'a' + * and 'b' with blocking disabled and reactive enabled, as such find services and it's + * instances. + */ + @Test + void testTwoNamespaceReactiveOnly() { + + manifests(Phase.CREATE, true, false); + + String logs = logs(); + Assertions.assertTrue(logs.contains("using selective namespaces : [a, b]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a, b]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a, b]")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : b")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : b")); + + // this tiny checks makes sure that blocking is disabled and reactive is enabled. + Assertions.assertFalse(logs.contains(BLOCKING_PUBLISH)); + Assertions.assertTrue(logs.contains(REACTIVE_PUBLISH)); + + reactiveCheck(); + + manifests(Phase.DELETE, true, false); + + } + + /** + * Deploy wiremock in 3 namespaces: default, a, b. Search in selective namespaces 'a' + * and 'b' with blocking enabled and reactive enabled, as such find services and its + * service instances. + */ + @Test + void testTwoNamespacesBothBlockingAndReactive() { + + manifests(Phase.CREATE, false, false); + + String logs = logs(); + Assertions.assertTrue(logs.contains("using selective namespaces : [a, b]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a, b]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a, b]")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : b")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : b")); + + // this tiny checks makes sure that blocking is enabled and reactive is enabled. + Assertions.assertTrue(logs.contains(BLOCKING_PUBLISH)); + Assertions.assertTrue(logs.contains(REACTIVE_PUBLISH)); + + blockingCheck(); + reactiveCheck(); + + manifests(Phase.DELETE, false, false); + + } + + private static void manifests(Phase phase, boolean disableBlocking, boolean disableReactive) { + V1Deployment deployment = (V1Deployment) util.yaml("kubernetes-discovery-deployment.yaml"); + V1Service service = (V1Service) util.yaml("kubernetes-discovery-service.yaml"); + V1Ingress ingress = (V1Ingress) util.yaml("kubernetes-discovery-ingress.yaml"); + + List envVars = new ArrayList<>( + Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) + .orElse(List.of())); + V1EnvVar debugLevel = new V1EnvVar().name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY") + .value("DEBUG"); + V1EnvVar selectiveNamespaceA = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0") + .value(NAMESPACE_A); + V1EnvVar selectiveNamespaceB = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1") + .value(NAMESPACE_B); + + if (disableReactive) { + V1EnvVar disableReactiveEnvVar = new V1EnvVar().name("SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED") + .value("FALSE"); + envVars.add(disableReactiveEnvVar); + } + + if (disableBlocking) { + V1EnvVar disableBlockingEnvVar = new V1EnvVar().name("SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED") + .value("FALSE"); + envVars.add(disableBlockingEnvVar); + } + + envVars.add(debugLevel); + envVars.add(selectiveNamespaceA); + envVars.add(selectiveNamespaceB); + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); + + if (phase.equals(Phase.CREATE)) { + util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); + } + else if (phase.equals(Phase.DELETE)) { + util.deleteAndWait(NAMESPACE, deployment, service, ingress); + } + } + + private String logs() { + try { + String appPodName = K3S.execInContainer("sh", "-c", + "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); + + Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); + return execResult.getStdout(); + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + private void reactiveCheck() { + WebClient servicesClient = builder().baseUrl("http://localhost/reactive/services").build(); + + List servicesResult = servicesClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + // we get two here, but since there is 'distinct' call, only 1 will be reported + // but service instances will report 2 nevertheless + Assertions.assertEquals(servicesResult.size(), 1); + Assertions.assertTrue(servicesResult.contains("service-wiremock")); + + WebClient ourServiceClient = builder().baseUrl("http://localhost/reactive/service-instances/service-wiremock") + .build(); + + List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(ourServiceInstances.size(), 2); + ourServiceInstances = ourServiceInstances.stream() + .sorted(Comparator.comparing(DefaultKubernetesServiceInstance::namespace)).toList(); + + DefaultKubernetesServiceInstance serviceInstanceA = ourServiceInstances.get(0); + // we only care about namespace here, as all other fields are tested in various + // other tests. + Assertions.assertEquals(serviceInstanceA.getNamespace(), "a"); + + DefaultKubernetesServiceInstance serviceInstanceB = ourServiceInstances.get(1); + // we only care about namespace here, as all other fields are tested in various + // other tests. + Assertions.assertEquals(serviceInstanceB.getNamespace(), "b"); + } + + private void blockingCheck() { + WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); + + List servicesResult = servicesClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + // we get two here, but since there is 'distinct' call, only 1 will be reported + // but service instances will report 2 nevertheless + Assertions.assertEquals(servicesResult.size(), 1); + Assertions.assertTrue(servicesResult.contains("service-wiremock")); + + WebClient ourServiceClient = builder().baseUrl("http://localhost/service-instances/service-wiremock").build(); + + List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(ourServiceInstances.size(), 2); + ourServiceInstances = ourServiceInstances.stream() + .sorted(Comparator.comparing(DefaultKubernetesServiceInstance::namespace)).toList(); + + DefaultKubernetesServiceInstance serviceInstanceA = ourServiceInstances.get(0); + // we only care about namespace here, as all other fields are tested in various + // other tests. + Assertions.assertEquals(serviceInstanceA.getNamespace(), "a"); + + DefaultKubernetesServiceInstance serviceInstanceB = ourServiceInstances.get(1); + // we only care about namespace here, as all other fields are tested in various + // other tests. + Assertions.assertEquals(serviceInstanceB.getNamespace(), "b"); + } + + private WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + private RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryPodMetadataIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryPodMetadataIT.java similarity index 98% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryPodMetadataIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryPodMetadataIT.java index 170bbc25..b6439c4a 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryPodMetadataIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoveryPodMetadataIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.client.discovery; +package org.springframework.cloud.kubernetes.client.discovery.it; import java.time.Duration; import java.util.ArrayList; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoverySingleSelectiveNamespaceIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoverySingleSelectiveNamespaceIT.java new file mode 100644 index 00000000..a75d702e --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/it/KubernetesClientDiscoverySingleSelectiveNamespaceIT.java @@ -0,0 +1,297 @@ +/* + * 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.it; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1EnvVar; +import io.kubernetes.client.openapi.models.V1Ingress; +import io.kubernetes.client.openapi.models.V1Service; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Container; +import org.testcontainers.k3s.K3sContainer; +import reactor.netty.http.client.HttpClient; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; + +import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.Phase; +import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author wind57 + */ +class KubernetesClientDiscoverySingleSelectiveNamespaceIT { + + private static final String BLOCKING_PUBLISH = "Will publish InstanceRegisteredEvent from blocking implementation"; + + private static final String REACTIVE_PUBLISH = "Will publish InstanceRegisteredEvent from reactive implementation"; + + private static final String NAMESPACE = "default"; + + private static final String NAMESPACE_A = "a"; + + private static final String NAMESPACE_B = "b"; + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it"; + + private static Util util; + + private static final K3sContainer K3S = Commons.container(); + + @BeforeAll + static void beforeAll() throws Exception { + K3S.start(); + Commons.validateImage(IMAGE_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S); + + util = new Util(K3S); + + util.createNamespace(NAMESPACE_A); + util.createNamespace(NAMESPACE_B); + util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B)); + util.wiremock(NAMESPACE, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE); + } + + @AfterAll + static void after() throws Exception { + Commons.cleanUp(IMAGE_NAME, K3S); + + util.wiremock(NAMESPACE, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE); + util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE, NAMESPACE_A, NAMESPACE_B)); + util.deleteNamespace(NAMESPACE_A); + util.deleteNamespace(NAMESPACE_B); + } + + /** + * Deploy wiremock in 3 namespaces: default, a, b. Search only in selective namespace + * 'a' with blocking enabled and reactive disabled, as such find a single service and + * its service instance. + */ + @Test + void testOneNamespaceBlockingOnly() { + + manifests(Phase.CREATE, false, true); + + String logs = logs(); + Assertions.assertTrue(logs.contains("using selective namespaces : [a]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a]")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : a")); + + // this tiny checks makes sure that blocking is enabled and reactive is disabled. + Assertions.assertTrue(logs.contains(BLOCKING_PUBLISH)); + Assertions.assertFalse(logs.contains(REACTIVE_PUBLISH)); + + blockingCheck(); + + manifests(Phase.DELETE, false, true); + + } + + /** + * Deploy wiremock in 3 namespaces: default, a, b. Search only in selective namespace + * 'a' with blocking disabled and reactive enabled, as such find a single service and + * its service instance. + */ + @Test + void testOneNamespaceReactiveOnly() { + + manifests(Phase.CREATE, true, false); + + String logs = logs(); + Assertions.assertTrue(logs.contains("using selective namespaces : [a]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a]")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : a")); + + // this tiny checks makes sure that reactive is enabled and blocking is disabled. + Assertions.assertFalse(logs.contains(BLOCKING_PUBLISH)); + Assertions.assertTrue(logs.contains(REACTIVE_PUBLISH)); + + reactiveCheck(); + + manifests(Phase.DELETE, true, false); + + } + + /** + * Deploy wiremock in 3 namespaces: default, a, b. Search only in selective namespace + * 'a' with blocking enabled and reactive enabled, as such find a single service and + * its service instance. + */ + @Test + void testOneNamespaceBothBlockingAndReactive() { + + manifests(Phase.CREATE, false, false); + + String logs = logs(); + Assertions.assertTrue(logs.contains("using selective namespaces : [a]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesMissing : found selective namespaces : [a]")); + Assertions.assertTrue( + logs.contains("ConditionalOnSelectiveNamespacesPresent : found selective namespaces : [a]")); + Assertions.assertTrue(logs.contains("registering lister (for services) in namespace : a")); + Assertions.assertTrue(logs.contains("registering lister (for endpoints) in namespace : a")); + + // this tiny checks makes sure that blocking and reactive is enabled. + Assertions.assertTrue(logs.contains(BLOCKING_PUBLISH)); + Assertions.assertTrue(logs.contains(REACTIVE_PUBLISH)); + + blockingCheck(); + reactiveCheck(); + + manifests(Phase.DELETE, false, false); + + } + + private static void manifests(Phase phase, boolean disableBlocking, boolean disableReactive) { + V1Deployment deployment = (V1Deployment) util.yaml("kubernetes-discovery-deployment.yaml"); + V1Service service = (V1Service) util.yaml("kubernetes-discovery-service.yaml"); + V1Ingress ingress = (V1Ingress) util.yaml("kubernetes-discovery-ingress.yaml"); + + List envVars = new ArrayList<>( + Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) + .orElse(List.of())); + V1EnvVar debugLevel = new V1EnvVar().name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY") + .value("DEBUG"); + V1EnvVar selectiveNamespaceA = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0") + .value(NAMESPACE_A); + if (disableReactive) { + V1EnvVar disableReactiveEnvVar = new V1EnvVar().name("SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED") + .value("FALSE"); + envVars.add(disableReactiveEnvVar); + } + + if (disableBlocking) { + V1EnvVar disableBlockingEnvVar = new V1EnvVar().name("SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED") + .value("FALSE"); + envVars.add(disableBlockingEnvVar); + } + + envVars.add(debugLevel); + envVars.add(selectiveNamespaceA); + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); + + if (phase.equals(Phase.CREATE)) { + util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); + } + else if (phase.equals(Phase.DELETE)) { + util.deleteAndWait(NAMESPACE, deployment, service, ingress); + } + } + + private void reactiveCheck() { + WebClient servicesClient = builder().baseUrl("http://localhost/reactive/services").build(); + + List servicesResult = servicesClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(servicesResult.size(), 1); + Assertions.assertTrue(servicesResult.contains("service-wiremock")); + + WebClient ourServiceClient = builder().baseUrl("http://localhost/reactive/service-instances/service-wiremock") + .build(); + + List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(ourServiceInstances.size(), 1); + + DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0); + // we only care about namespace here, as all other fields are tested in various + // other tests. + Assertions.assertEquals(serviceInstance.getNamespace(), "a"); + } + + private void blockingCheck() { + WebClient servicesClient = builder().baseUrl("http://localhost/services").build(); + + List servicesResult = servicesClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(servicesResult.size(), 1); + Assertions.assertTrue(servicesResult.contains("service-wiremock")); + + WebClient ourServiceClient = builder().baseUrl("http://localhost/service-instances/service-wiremock").build(); + + List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(ourServiceInstances.size(), 1); + + DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0); + // we only care about namespace here, as all other fields are tested in various + // other tests. + Assertions.assertEquals(serviceInstance.getNamespace(), "a"); + } + + private String logs() { + try { + String appPodName = K3S.execInContainer("sh", "-c", + "kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout(); + + Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim()); + return execResult.getStdout(); + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + private WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + private RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + +}