From d9c458347dcf38b1d3cc701cb4968ee0061de69c Mon Sep 17 00:00:00 2001 From: erabii Date: Tue, 4 Apr 2023 17:34:39 +0300 Subject: [PATCH] New Kubernetes Informer Auto Config (#1276) --- .../ApiClientUserAgentDefaultHeader.java | 4 +- .../ApiClientUserAgentNonDefaultHeader.java | 4 +- .../DefaultApiClientNotSameAsApiClient.java | 4 +- ...rnetesClientInformerAutoConfiguration.java | 134 ++++++++ .../KubernetesInformerAutoConfiguration.java | 4 + .../KubernetesInformerDiscoveryClient.java | 32 +- ...ormerDiscoveryClientAutoConfiguration.java | 13 +- ...ctiveDiscoveryClientAutoConfiguration.java | 4 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- ...oConfigurationApplicationContextTests.java | 4 +- ...oConfigurationApplicationContextTests.java | 6 +- ...sInformerReactiveDiscoveryClientTests.java | 302 ++++++++++++++---- .../pom.xml | 1 + ...ernetesClientCatalogWatchNamespacesIT.java | 1 + .../event/reload/ConfigMapEventReloadIT.java | 1 + .../pom.xml | 129 ++++++++ .../client/discovery/it/DiscoveryApp.java | 32 ++ .../discovery/it/DiscoveryController.java | 49 +++ .../KubernetesClientDiscoveryClientIT.java | 301 +++++++++++++++++ .../kubernetes-discovery-deployment.yaml | 28 ++ .../kubernetes-discovery-ingress.yaml | 16 + .../kubernetes-discovery-service.yaml | 14 + .../src/test/resources/logback-test.xml | 14 + .../ActuatorRefreshMultipleNamespacesIT.java | 1 + .../Fabric8DiscoveryApp.java | 2 +- .../Fabric8DiscoveryController.java | 3 +- .../Fabric8DiscoveryFilterIT.java | 2 +- .../Fabric8DiscoveryIT.java | 2 +- .../Fabric8DiscoveryNamespaceFilterIT.java | 2 +- .../Fabric8DiscoveryPodMetadataIT.java | 2 +- .../tests/commons/native_client/Util.java | 70 ++++ .../cluster/cluster-role-binding.yaml | 14 + 32 files changed, 1102 insertions(+), 95 deletions(-) create mode 100644 spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApp.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-deployment.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-ingress.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-service.yaml create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/logback-test.xml rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/{configmap => discovery}/Fabric8DiscoveryApp.java (93%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/{configmap => discovery}/Fabric8DiscoveryController.java (91%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/{configmap => discovery}/Fabric8DiscoveryFilterIT.java (99%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/{configmap => discovery}/Fabric8DiscoveryIT.java (99%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/{configmap => discovery}/Fabric8DiscoveryNamespaceFilterIT.java (98%) rename spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/{configmap => discovery}/Fabric8DiscoveryPodMetadataIT.java (99%) create mode 100644 spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role-binding.yaml diff --git a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentDefaultHeader.java b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentDefaultHeader.java index 788d2180..46ac865e 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentDefaultHeader.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentDefaultHeader.java @@ -36,8 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat; * itself as the default apiClient. This is to avoid overwriting the user's * defaultApiClient if they include this project. * - * kubernetes informer is disabled because KubernetesInformerAutoConfiguration creates a - * defaultApiClient that will be autowired instead of the ApiClient created in + * kubernetes informer is disabled because KubernetesClientInformerAutoConfiguration + * creates a defaultApiClient that will be autowired instead of the ApiClient created in * KubernetesClientAutoConfiguration */ @SpringBootTest(classes = App.class, diff --git a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentNonDefaultHeader.java b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentNonDefaultHeader.java index 24b2a95d..7c08fb6b 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentNonDefaultHeader.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/ApiClientUserAgentNonDefaultHeader.java @@ -37,8 +37,8 @@ import static org.assertj.core.api.Assertions.assertThat; * itself as the default apiClient. This is to avoid overwriting the user's * defaultApiClient if they include this project. * - * kubernetes informer is disabled because KubernetesInformerAutoConfiguration creates a - * defaultApiClient that will be autowired instead of the ApiClient created in + * kubernetes informer is disabled because KubernetesClientInformerAutoConfiguration + * creates a defaultApiClient that will be autowired instead of the ApiClient created in * KubernetesClientAutoConfiguration */ @SpringBootTest(classes = App.class, properties = { "kubernetes.informer.enabled=false", diff --git a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/DefaultApiClientNotSameAsApiClient.java b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/DefaultApiClientNotSameAsApiClient.java index e0f7a008..b5afab49 100644 --- a/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/DefaultApiClientNotSameAsApiClient.java +++ b/spring-cloud-kubernetes-client-autoconfig/src/test/java/org/springframework/cloud/kubernetes/client/default_api/DefaultApiClientNotSameAsApiClient.java @@ -31,8 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat; * itself as the default apiClient. This is to avoid overwriting the user's * defaultApiClient if they include this project. * - * kubernetes informer is disabled because KubernetesInformerAutoConfiguration creates a - * defaultApiClient that will be autowired instead of the ApiClient created in + * kubernetes informer is disabled because KubernetesClientInformerAutoConfiguration + * creates a defaultApiClient that will be autowired instead of the ApiClient created in * KubernetesClientAutoConfiguration */ @SpringBootTest(classes = App.class, 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 new file mode 100644 index 00000000..132c0f88 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientInformerAutoConfiguration.java @@ -0,0 +1,134 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import io.kubernetes.client.informer.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.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException; +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.Configuration; +import org.springframework.core.log.LogAccessor; + +import static io.kubernetes.client.util.Namespaces.NAMESPACE_ALL; +import static io.kubernetes.client.util.Namespaces.NAMESPACE_DEFAULT; +import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.getApplicationNamespace; + +/** + * @author wind57 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnDiscoveryEnabled +@ConditionalOnKubernetesDiscoveryEnabled +@ConditionalOnBlockingOrReactiveEnabled +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) +@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class }) +public class KubernetesClientInformerAutoConfiguration { + + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(KubernetesClientInformerAutoConfiguration.class)); + + @Bean + @ConditionalOnMissingBean + public SharedInformerFactory sharedInformerFactory(ApiClient client) { + return new SharedInformerFactory(client); + } + + @Bean + public String kubernetesClientNamespace(KubernetesDiscoveryProperties properties, + KubernetesNamespaceProvider provider) { + String namespace; + if (properties.allNamespaces()) { + namespace = NAMESPACE_ALL; + LOG.debug(() -> "serviceSharedInformer will use all-namespaces"); + } + else { + try { + namespace = getApplicationNamespace(null, "kubernetes client discovery", provider); + } + catch (NamespaceResolutionFailedException ex) { + LOG.warn(() -> "failed to resolve namespace, defaulting to :" + NAMESPACE_DEFAULT + + ". This will fail in a future release."); + namespace = NAMESPACE_DEFAULT; + } + LOG.debug("serviceSharedInformer will use namespace : " + namespace); + } + + return namespace; + } + + @Bean + @ConditionalOnMissingBean(value = V1Service.class, parameterizedContainer = SharedIndexInformer.class) + public SharedIndexInformer servicesSharedIndexInformer(SharedInformerFactory sharedInformerFactory, + ApiClient apiClient, String kubernetesClientNamespace) { + + GenericKubernetesApi servicesApi = new GenericKubernetesApi<>(V1Service.class, + V1ServiceList.class, "", "v1", "services", apiClient); + + return sharedInformerFactory.sharedIndexInformerFor(servicesApi, V1Service.class, 0L, + kubernetesClientNamespace); + } + + @Bean + @ConditionalOnMissingBean(value = V1Endpoints.class, parameterizedContainer = SharedIndexInformer.class) + public SharedIndexInformer endpointsSharedIndexInformer(SharedInformerFactory sharedInformerFactory, + ApiClient apiClient, String kubernetesClientNamespace) { + + GenericKubernetesApi servicesApi = new GenericKubernetesApi<>(V1Endpoints.class, + V1EndpointsList.class, "", "v1", "endpoints", apiClient); + + return sharedInformerFactory.sharedIndexInformerFor(servicesApi, V1Endpoints.class, 0L, + kubernetesClientNamespace); + } + + @Bean + @ConditionalOnMissingBean(value = V1Service.class, parameterizedContainer = Lister.class) + public Lister servicesLister(SharedIndexInformer servicesSharedIndexInformer, + String kubernetesClientNamespace) { + return new Lister<>(servicesSharedIndexInformer.getIndexer(), kubernetesClientNamespace); + } + + @Bean + @ConditionalOnMissingBean(value = V1Endpoints.class, parameterizedContainer = Lister.class) + public Lister endpointsLister(SharedIndexInformer endpointsSharedIndexInformer, + String kubernetesClientNamespace) { + return new Lister<>(endpointsSharedIndexInformer.getIndexer(), kubernetesClientNamespace); + } + +} 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 00b1bf40..c7792e6e 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 @@ -48,8 +48,12 @@ import static io.kubernetes.client.util.Namespaces.NAMESPACE_ALL; import static io.kubernetes.client.util.Namespaces.NAMESPACE_DEFAULT; /** + * This configuration is not used by us internally and will be removed in a future + * release. Use it at your own risk. + * * @author wind57 */ +@Deprecated(forRemoval = true) @Configuration(proxyBeanMethods = false) @ConditionalOnDiscoveryEnabled @ConditionalOnKubernetesDiscoveryEnabled 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 6ad44034..0bde6760 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 @@ -72,38 +72,40 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { private final KubernetesDiscoveryProperties properties; - private final String namespace; - + @Deprecated(forRemoval = true) public KubernetesInformerDiscoveryClient(String namespace, SharedInformerFactory sharedInformerFactory, Lister serviceLister, Lister endpointsLister, SharedInformer serviceInformer, SharedInformer endpointsInformer, KubernetesDiscoveryProperties properties) { - this.namespace = namespace; this.sharedInformerFactory = sharedInformerFactory; - this.serviceLister = serviceLister; this.endpointsLister = endpointsLister; this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); + this.properties = properties; + } + public KubernetesInformerDiscoveryClient(SharedInformerFactory sharedInformerFactory, + Lister serviceLister, Lister endpointsLister, + SharedInformer serviceInformer, SharedInformer endpointsInformer, + KubernetesDiscoveryProperties properties) { + this.sharedInformerFactory = sharedInformerFactory; + this.serviceLister = serviceLister; + this.endpointsLister = endpointsLister; + this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced(); this.properties = properties; } @Override public String description() { - return "Fabric8 Kubernetes Client Discovery"; + return "Kubernetes Client Discovery"; } @Override public List getInstances(String serviceId) { Objects.requireNonNull(serviceId, "serviceId must be provided"); - if (!StringUtils.hasText(namespace) && !properties.allNamespaces()) { - LOG.warn(() -> "Namespace is null or empty, this may cause issues looking up services"); - } - - List services = properties.allNamespaces() - ? serviceLister.list().stream().filter(svc -> serviceId.equals(svc.getMetadata().getName())).toList() - : List.of(serviceLister.namespace(namespace).get(serviceId)); + 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<>(); @@ -226,10 +228,8 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { @Override public List getServices() { - List services = properties.allNamespaces() ? serviceLister.list() - : serviceLister.namespace(namespace).list(); - return services.stream().filter(service -> matchesServiceLabels(service, properties)) - .map(s -> s.getMetadata().getName()).collect(Collectors.toList()); + return serviceLister.list().stream().filter(service -> matchesServiceLabels(service, properties)) + .map(s -> s.getMetadata().getName()).distinct().toList(); } @PostConstruct 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 7e5abb81..046d3a2c 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 @@ -55,7 +55,7 @@ import org.springframework.context.annotation.Configuration; @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesInformerAutoConfiguration.class }) + KubernetesClientInformerAutoConfiguration.class }) public class KubernetesInformerDiscoveryClientAutoConfiguration { @Bean @@ -66,6 +66,7 @@ public class KubernetesInformerDiscoveryClientAutoConfiguration { return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher); } + @Deprecated(forRemoval = true) @Bean @ConditionalOnMissingBean public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient( @@ -77,4 +78,14 @@ public class KubernetesInformerDiscoveryClientAutoConfiguration { serviceLister, endpointsLister, serviceInformer, endpointsInformer, properties); } + @Bean + @ConditionalOnMissingBean + public KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient( + SharedInformerFactory sharedInformerFactory, Lister serviceLister, + Lister endpointsLister, SharedInformer serviceInformer, + SharedInformer endpointsInformer, KubernetesDiscoveryProperties properties) { + return new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, + serviceInformer, endpointsInformer, properties); + } + } 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 f408a9b2..a526d677 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 @@ -38,7 +38,7 @@ 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.KubernetesInformerAutoConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; @@ -58,7 +58,7 @@ import org.springframework.context.annotation.Configuration; @AutoConfigureBefore({ SimpleReactiveDiscoveryClientAutoConfiguration.class, ReactiveCommonsClientAutoConfiguration.class }) @AutoConfigureAfter({ ReactiveCompositeDiscoveryClientAutoConfiguration.class, - KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesInformerAutoConfiguration.class }) + KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class }) public class KubernetesInformerReactiveDiscoveryClientAutoConfiguration { @Bean 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 33056cf4..d7f1ac9f 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 @@ -1,4 +1,4 @@ org.springframework.cloud.kubernetes.client.discovery.catalog.KubernetesCatalogWatchAutoConfiguration org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClientAutoConfiguration -org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerAutoConfiguration +org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClientAutoConfiguration 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 c215c92d..28de3bbf 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 @@ -207,7 +207,7 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests applicationContextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(KubernetesInformerDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesInformerAutoConfiguration.class)) + KubernetesClientInformerAutoConfiguration.class)) .withUserConfiguration(ApiClientConfig.class).withPropertyValues(properties); } @@ -215,7 +215,7 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests applicationContextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(KubernetesInformerDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesInformerAutoConfiguration.class)) + KubernetesClientInformerAutoConfiguration.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/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java index 64b812d2..cbb862c8 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 @@ -28,7 +28,7 @@ import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscov import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; import org.springframework.cloud.commons.util.UtilAutoConfiguration; import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; -import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerAutoConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesCommonsAutoConfiguration; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; @@ -189,7 +189,7 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte KubernetesInformerReactiveDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class, UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesCommonsAutoConfiguration.class, KubernetesInformerAutoConfiguration.class)) + KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class)) .withPropertyValues(properties); } @@ -199,7 +199,7 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte KubernetesInformerReactiveDiscoveryClientAutoConfiguration.class, KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class, UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, - KubernetesCommonsAutoConfiguration.class, KubernetesInformerAutoConfiguration.class)) + KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.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 4f6a98ef..1f96f8e8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * 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. @@ -16,7 +16,7 @@ package org.springframework.cloud.kubernetes.client.discovery.reactive; -import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -29,77 +29,97 @@ import io.kubernetes.client.openapi.models.V1EndpointSubset; import io.kubernetes.client.openapi.models.V1Endpoints; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.openapi.models.V1Service; -import io.kubernetes.client.openapi.models.V1ServiceSpec; -import io.kubernetes.client.openapi.models.V1ServiceStatus; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +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.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 */ -@RunWith(MockitoJUnitRunner.class) -public class KubernetesInformerReactiveDiscoveryClientTests { +class KubernetesInformerReactiveDiscoveryClientTests { - @Mock - private SharedInformerFactory sharedInformerFactory; + // default constructor partitions by namespace + private Cache serviceCache = new Cache<>(); - private static final V1Service testService1 = new V1Service() - .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1")) - .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus()); + // default constructor partitions by namespace + private Cache endpointsCache = new Cache<>(); - private static final V1Service testService2 = new V1Service() - .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace2")) - .spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus()); + private static final String NAMESPACE_1 = "namespace1"; - private static final V1Endpoints testEndpoints1 = new V1Endpoints() - .metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1")) + private static final String NAMESPACE_2 = "namespace2"; + + private final SharedInformerFactory sharedInformerFactory = Mockito.mock(SharedInformerFactory.class); + + private static final V1Service TEST_SERVICE_1 = new V1Service() + .metadata(new V1ObjectMeta().name("test-svc-1").namespace(NAMESPACE_1)); + + private static final V1Service TEST_SERVICE_2 = new V1Service() + .metadata(new V1ObjectMeta().name("test-svc-2").namespace(NAMESPACE_2)); + + // same name as TEST_SERVICE_1, to test distinct + private static final V1Service TEST_SERVICE_3 = new V1Service() + .metadata(new V1ObjectMeta().name("test-svc-2").namespace(NAMESPACE_2)); + + private static final V1Endpoints TEST_ENDPOINTS_1 = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test-svc-1").namespace(NAMESPACE_1)) .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080)) .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2"))); - @Test - public void testDiscoveryGetServicesAllNamespaceShouldWork() { - Lister serviceLister = setupServiceLister(testService1, testService2); - - KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister, null, - null, null, KubernetesDiscoveryProperties.DEFAULT); - - StepVerifier.create(discoveryClient.getServices()) - .expectNext(testService1.getMetadata().getName(), testService2.getMetadata().getName()).expectComplete() - .verify(); - + @AfterEach + void afterEach() { + serviceCache = new Cache<>(); + endpointsCache = new Cache<>(); } @Test - public void testDiscoveryGetServicesOneNamespaceShouldWork() { - Lister serviceLister = setupServiceLister(testService1, testService2); + void testDiscoveryGetServicesAllNamespaceShouldWork() { + Lister serviceLister = setupServiceLister(NAMESPACE_ALL, TEST_SERVICE_1, TEST_SERVICE_2, + TEST_SERVICE_3); + + KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true, + Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false); - KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn("namespace1"); KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( - kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null, - KubernetesDiscoveryProperties.DEFAULT); + new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister, null, + null, null, kubernetesDiscoveryProperties); - StepVerifier.create(discoveryClient.getServices()).expectNext(testService1.getMetadata().getName()) + StepVerifier.create(discoveryClient.getServices()) + .expectNext(TEST_SERVICE_1.getMetadata().getName(), TEST_SERVICE_2.getMetadata().getName()) .expectComplete().verify(); } @Test - public void testDiscoveryGetInstanceAllNamespaceShouldWork() { - Lister serviceLister = setupServiceLister(testService1, testService2); - Lister endpointsLister = setupEndpointsLister(testEndpoints1); + void testDiscoveryGetServicesOneNamespaceShouldWork() { + Lister serviceLister = setupServiceLister(NAMESPACE_1, TEST_SERVICE_1, TEST_SERVICE_2); + + KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); + when(kubernetesNamespaceProvider.getNamespace()).thenReturn(NAMESPACE_1); + KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( + kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null, + KubernetesDiscoveryProperties.DEFAULT); + + StepVerifier.create(discoveryClient.getServices()).expectNext(TEST_SERVICE_1.getMetadata().getName()) + .expectComplete().verify(); + + } + + @Test + void testDiscoveryGetInstanceAllNamespaceShouldWork() { + Lister serviceLister = setupServiceLister(NAMESPACE_ALL, TEST_SERVICE_1, TEST_SERVICE_2); + Lister endpointsLister = setupEndpointsLister(NAMESPACE_1, TEST_ENDPOINTS_1); KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false); @@ -108,45 +128,213 @@ public class KubernetesInformerReactiveDiscoveryClientTests { new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties); - StepVerifier - .create(discoveryClient.getInstances("test-svc-1")).expectNext(new DefaultKubernetesServiceInstance("", - "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null)) + StepVerifier.create(discoveryClient.getInstances("test-svc-1")) + .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, Map.of(), false, + NAMESPACE_1, null)) .expectComplete().verify(); } @Test - public void testDiscoveryGetInstanceOneNamespaceShouldWork() { - Lister serviceLister = setupServiceLister(testService1, testService2); - Lister endpointsLister = setupEndpointsLister(testEndpoints1); + void testDiscoveryGetInstanceOneNamespaceShouldWork() { + Lister serviceLister = setupServiceLister(NAMESPACE_1, TEST_SERVICE_1, TEST_SERVICE_2); + Lister endpointsLister = setupEndpointsLister(NAMESPACE_1, TEST_ENDPOINTS_1); KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false); KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class); - when(kubernetesNamespaceProvider.getNamespace()).thenReturn("namespace1"); + when(kubernetesNamespaceProvider.getNamespace()).thenReturn(NAMESPACE_1); KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient( kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties); - StepVerifier - .create(discoveryClient.getInstances("test-svc-1")).expectNext(new DefaultKubernetesServiceInstance("", - "test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null)) + StepVerifier.create(discoveryClient.getInstances("test-svc-1")) + .expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, Map.of(), false, + NAMESPACE_1, null)) .expectComplete().verify(); } - private Lister setupServiceLister(V1Service... services) { - Cache serviceCache = new Cache<>(); - Lister serviceLister = new Lister<>(serviceCache); + /** + *
+	 *     - all-namespaces = true
+	 *     - service-a in namespace-a exists
+	 *     - service-b in namespace-b exists
+	 *
+	 *     As such, both services are found.
+	 * 
+ */ + @Test + void testAllNamespacesTwoServicesPresent() { + 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")); + serviceCache.add(serviceA); + serviceCache.add(serviceB); + + Lister serviceLister = new Lister<>(serviceCache).namespace(NAMESPACE_ALL); + 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); + + List result = discoveryClient.getServices().collectList().block(); + Assertions.assertEquals(result.size(), 2); + Assertions.assertTrue(result.contains("service-a")); + Assertions.assertTrue(result.contains("service-b")); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - service-a in namespace-a exists
+	 *     - service-b in namespace-b exists
+	 *     - service lister exists in namespace-a
+	 *
+	 *     As such, one service is found.
+	 * 
+ */ + @Test + void testSingleNamespaceTwoServicesPresent() { + 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")); + serviceCache.add(serviceA); + serviceCache.add(serviceB); + + Lister serviceLister = new Lister<>(serviceCache).namespace("namespace-a"); + 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); + + List result = discoveryClient.getServices().collectList().block(); + Assertions.assertEquals(result.size(), 1); + Assertions.assertTrue(result.contains("service-a")); + Assertions.assertFalse(result.contains("service-b")); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - endpoints-X in namespace-a exists
+	 *     - endpoints-X in namespace-b exists
+	 *
+	 *     As such, both endpoints are found.
+	 * 
+ */ + @Test + void testAllNamespacesTwoEndpointsPresent() { + boolean allNamespaces = true; + + V1Service serviceXNamespaceA = new V1Service() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")); + V1Service serviceXNamespaceB = new V1Service() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")); + serviceCache.add(serviceXNamespaceA); + serviceCache.add(serviceXNamespaceB); + + V1Endpoints endpointsXNamespaceA = new V1Endpoints() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080)) + .addAddressesItem(new V1EndpointAddress().ip("1.1.1.1"))); + V1Endpoints endpointsXNamespaceB = new V1Endpoints() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080)) + .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2"))); + endpointsCache.add(endpointsXNamespaceA); + endpointsCache.add(endpointsXNamespaceB); + + Lister endpointsLister = new Lister<>(endpointsCache).namespace(NAMESPACE_ALL); + Lister serviceLister = new Lister<>(serviceCache).namespace(NAMESPACE_ALL); + + 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, endpointsLister, null, null, + kubernetesDiscoveryProperties); + + List result = discoveryClient.getInstances("endpoints-x").collectList().block(); + Assertions.assertEquals(result.size(), 2); + List byIp = result.stream().map(ServiceInstance::getHost).sorted().toList(); + Assertions.assertTrue(byIp.contains("1.1.1.1")); + Assertions.assertTrue(byIp.contains("2.2.2.2")); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - endpoints-X in namespace-a exists
+	 *     - endpoints-X in namespace-b exists
+	 *
+	 *     We search in namespace-a, only. As such, single endpoints is found.
+	 * 
+ */ + @Test + void testAllSingleTwoEndpointsPresent() { + boolean allNamespaces = true; + + V1Service serviceXNamespaceA = new V1Service() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")); + V1Service serviceXNamespaceB = new V1Service() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")); + serviceCache.add(serviceXNamespaceA); + serviceCache.add(serviceXNamespaceB); + + V1Endpoints endpointsXNamespaceA = new V1Endpoints() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a")) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080)) + .addAddressesItem(new V1EndpointAddress().ip("1.1.1.1"))); + V1Endpoints endpointsXNamespaceB = new V1Endpoints() + .metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b")) + .addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080)) + .addAddressesItem(new V1EndpointAddress().ip("2.2.2.2"))); + endpointsCache.add(endpointsXNamespaceA); + endpointsCache.add(endpointsXNamespaceB); + + Lister endpointsLister = new Lister<>(endpointsCache).namespace("namespace-a"); + Lister serviceLister = new Lister<>(serviceCache).namespace("namespace-a"); + + 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, endpointsLister, null, null, + kubernetesDiscoveryProperties); + + List result = discoveryClient.getInstances("endpoints-x").collectList().block(); + Assertions.assertEquals(result.size(), 1); + List byIp = result.stream().map(ServiceInstance::getHost).sorted().toList(); + Assertions.assertTrue(byIp.contains("1.1.1.1")); + } + + private Lister setupServiceLister(String namespace, V1Service... services) { + Lister serviceLister = new Lister<>(serviceCache, namespace); for (V1Service svc : services) { serviceCache.add(svc); } return serviceLister; } - private Lister setupEndpointsLister(V1Endpoints... endpoints) { - Cache endpointsCache = new Cache<>(); + private Lister setupEndpointsLister(String namespace, V1Endpoints... endpoints) { Lister endpointsLister = new Lister<>(endpointsCache); for (V1Endpoints ep : endpoints) { endpointsCache.add(ep); diff --git a/spring-cloud-kubernetes-integration-tests/pom.xml b/spring-cloud-kubernetes-integration-tests/pom.xml index 99b6a4d5..c4ad2d6c 100644 --- a/spring-cloud-kubernetes-integration-tests/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/pom.xml @@ -92,6 +92,7 @@ spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps spring-cloud-kubernetes-fabric8-client-catalog-watcher spring-cloud-kubernetes-client-catalog-watcher + spring-cloud-kubernetes-client-discovery-it spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchNamespacesIT.java index 79736806..a078f770 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchNamespacesIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/client/catalog/KubernetesClientCatalogWatchNamespacesIT.java @@ -85,6 +85,7 @@ public class KubernetesClientCatalogWatchNamespacesIT { @AfterEach void afterEach() { + util.deleteClusterWide(NAMESPACE_DEFAULT, Set.of(NAMESPACE_A, NAMESPACE_B)); util.deleteNamespace(NAMESPACE_A); util.deleteNamespace(NAMESPACE_B); } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/ConfigMapEventReloadIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/ConfigMapEventReloadIT.java index 077fef03..50673a1d 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/ConfigMapEventReloadIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-configmap-event-reload/src/test/java/org/springframework/cloud/kubernetes/client/configmap/event/reload/ConfigMapEventReloadIT.java @@ -78,6 +78,7 @@ class ConfigMapEventReloadIT { @AfterAll static void afterAll() throws Exception { + util.deleteClusterWide(NAMESPACE, Set.of("left", "right")); util.deleteNamespace("left"); util.deleteNamespace("right"); Commons.cleanUp(IMAGE_NAME, K3S); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml new file mode 100644 index 00000000..0c86b73b --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml @@ -0,0 +1,129 @@ + + + + spring-cloud-kubernetes-integration-tests + org.springframework.cloud + 3.0.3-SNAPSHOT + + 4.0.0 + + spring-cloud-kubernetes-client-discovery-it + + + + org.springframework.cloud + spring-cloud-kubernetes-client-discovery + + + org.springframework.cloud + spring-cloud-kubernetes-test-support + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + com.github.docker-java + docker-java-core + test + + + com.github.docker-java + docker-java-transport-httpclient5 + test + + + + + + + ../src/main/resources + true + + + src/main/resources + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + docker.io/springcloud/${project.artifactId}:${project.version} + paketobuildpacks/builder + + + + build-image + + ${skip.build.image} + + package + + build-image + + + + repackage + package + + repackage + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + + + + + + ${testsToRun} + + + + + + + + + imagename + + + !env.IMAGE + + + + springcloud/${project.artifactId}:${project.version} + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApp.java new file mode 100644 index 00000000..3a430e58 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApp.java @@ -0,0 +1,32 @@ +/* + * 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 org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author wind57 + */ +@SpringBootApplication +public class DiscoveryApp { + + public static void main(String[] args) { + SpringApplication.run(DiscoveryApp.class, args); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java new file mode 100644 index 00000000..7e24e97a --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java @@ -0,0 +1,49 @@ +/* + * 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.util.List; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author wind57 + */ +@RestController +public class DiscoveryController { + + private final KubernetesInformerDiscoveryClient discoveryClient; + + public DiscoveryController(KubernetesInformerDiscoveryClient discoveryClient) { + this.discoveryClient = discoveryClient; + } + + @GetMapping("/services") + public List allServices() { + return discoveryClient.getServices(); + } + + @GetMapping("/service-instances/{serviceId}") + public List serviceInstances(@PathVariable("serviceId") String serviceId) { + return discoveryClient.getInstances(serviceId); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-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/KubernetesClientDiscoveryClientIT.java new file mode 100644 index 00000000..c58552e9 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java @@ -0,0 +1,301 @@ +/* + * 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.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +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 KubernetesClientDiscoveryClientIT { + + 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); + } + + @AfterAll + static void after() throws Exception { + Commons.cleanUp(IMAGE_NAME, K3S); + } + + /** + * Three services are deployed in the default namespace. We do not configure any + * explicit namespace and 'default' must be picked-up. + */ + @Test + void testSimple() { + + // set-up + util.setUp(NAMESPACE); + manifests(false, null, Phase.CREATE); + util.busybox(NAMESPACE, Phase.CREATE); + + assertLogStatement("serviceSharedInformer will use namespace : default"); + + 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(), 3); + Assertions.assertTrue(servicesResult.contains("kubernetes")); + Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-client-discovery-it")); + Assertions.assertTrue(servicesResult.contains("busybox-service")); + + WebClient ourServiceClient = builder() + .baseUrl("http://localhost//service-instances/spring-cloud-kubernetes-client-discovery-it").build(); + + List ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(ourServiceInstances.size(), 1); + + DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0); + Assertions.assertNotNull(serviceInstance.getInstanceId()); + Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-client-discovery-it"); + Assertions.assertNotNull(serviceInstance.getHost()); + Assertions.assertEquals(serviceInstance.getMetadata(), + Map.of("http", "8080", "app", "spring-cloud-kubernetes-client-discovery-it")); + Assertions.assertEquals(serviceInstance.getPort(), 8080); + Assertions.assertEquals(serviceInstance.getNamespace(), "default"); + + WebClient busyBoxServiceClient = builder().baseUrl("http://localhost//service-instances/busybox-service") + .build(); + List busyBoxServiceInstances = busyBoxServiceClient.method(HttpMethod.GET) + .retrieve().bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(busyBoxServiceInstances.size(), 2); + + // clean-up + util.busybox(NAMESPACE, Phase.DELETE); + manifests(false, null, Phase.DELETE); + } + + /** + *
+	 *     - config server is enabled for all namespaces
+	 *     - wiremock service is deployed in namespace-a
+	 *     - busybox service is deployed in namespace-b
+	 *
+	 *     Our discovery searches in all namespaces, thus finds them both.
+	 * 
+ */ + @Test + void testAllNamespaces() { + util.createNamespace(NAMESPACE_A); + util.createNamespace(NAMESPACE_B); + util.setUpClusterWideClusterRoleBinding(NAMESPACE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE); + util.busybox(NAMESPACE_B, Phase.CREATE); + manifests(true, null, Phase.CREATE); + + assertLogStatement("serviceSharedInformer will use all-namespaces"); + + 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(), 7); + Assertions.assertTrue(servicesResult.contains("kubernetes")); + Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-client-discovery-it")); + Assertions.assertTrue(servicesResult.contains("busybox-service")); + Assertions.assertTrue(servicesResult.contains("service-wiremock")); + + manifests(true, null, Phase.DELETE); + util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE); + util.busybox(NAMESPACE_B, Phase.DELETE); + util.deleteClusterWideClusterRoleBinding(NAMESPACE); + util.deleteNamespace(NAMESPACE_A); + util.deleteNamespace(NAMESPACE_B); + } + + /** + *
+	 *     - config server is enabled for namespace-a
+	 *     - wiremock service is deployed in namespace-a
+	 *     - wiremock service is deployed in namespace-b
+	 *
+	 *     Only service in namespace-a is found.
+	 * 
+ */ + @Test + void testSpecificNamespace() { + util.createNamespace(NAMESPACE_A); + util.createNamespace(NAMESPACE_B); + util.setUpClusterWide(NAMESPACE, Set.of(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"); + + 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 wiremockInNamespaceAClient = builder().baseUrl("http://localhost//service-instances/service-wiremock") + .build(); + + List wiremockInNamespaceA = wiremockInNamespaceAClient.method(HttpMethod.GET) + .retrieve().bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(wiremockInNamespaceA.size(), 1); + + DefaultKubernetesServiceInstance serviceInstance = wiremockInNamespaceA.get(0); + Assertions.assertEquals(serviceInstance.getNamespace(), "a"); + + 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.deleteNamespace(NAMESPACE_A); + util.deleteNamespace(NAMESPACE_B); + } + + private static void manifests(boolean allNamespaces, String clientSpecificNamespace, Phase phase) { + 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"); + if (allNamespaces) { + V1EnvVar allNamespacesVar = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES") + .value("TRUE"); + envVars.add(allNamespacesVar); + } + + if (clientSpecificNamespace != null) { + V1EnvVar clientNamespace = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_CLIENT_NAMESPACE") + .value(NAMESPACE_A); + envVars.add(clientNamespace); + } + envVars.add(debugLevel); + 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 WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + private RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + + private void assertLogStatement(String message) { + 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)); + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + } + + private void assertServicePresentInNamespaces(List namespaces, String value, String serviceName) { + namespaces.forEach(x -> { + try { + String service = K3S.execInContainer("sh", "-c", + "kubectl get services -n " + x + " -l app=" + value + " -o=name --no-headers | tr -d '\n'") + .getStdout(); + Assertions.assertEquals(service, "service/" + serviceName); + } + catch (Exception e) { + throw new RuntimeException(e); + } + + }); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-deployment.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-deployment.yaml new file mode 100644 index 00000000..d25dadb8 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-deployment.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spring-cloud-kubernetes-client-discovery-deployment-it +spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-client-discovery-it + template: + metadata: + labels: + app: spring-cloud-kubernetes-client-discovery-it + spec: + serviceAccountName: spring-cloud-kubernetes-serviceaccount + containers: + - name: spring-cloud-kubernetes-client-discovery + image: docker.io/springcloud/spring-cloud-kubernetes-client-discovery-it + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8080 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8080 + path: /actuator/health/liveness + ports: + - containerPort: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-ingress.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-ingress.yaml new file mode 100644 index 00000000..5bb49e97 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-ingress.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: spring-cloud-kubernetes-client-discovery-ingress-it + namespace: default +spec: + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: spring-cloud-kubernetes-client-discovery-it + port: + number: 8080 diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-service.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-service.yaml new file mode 100644 index 00000000..63b551e4 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/kubernetes-discovery-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: spring-cloud-kubernetes-client-discovery-it + name: spring-cloud-kubernetes-client-discovery-it +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: spring-cloud-kubernetes-client-discovery-it + type: ClusterIP diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/logback-test.xml new file mode 100644 index 00000000..dc3dfa74 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java index 808f747e..7f2128dc 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/src/test/java/org/springframework/cloud/kubernetes/configuration/watcher/ActuatorRefreshMultipleNamespacesIT.java @@ -77,6 +77,7 @@ class ActuatorRefreshMultipleNamespacesIT { @AfterAll static void afterAll() throws Exception { + util.deleteClusterWide(DEFAULT_NAMESPACE, Set.of(DEFAULT_NAMESPACE, LEFT_NAMESPACE, RIGHT_NAMESPACE)); util.deleteNamespace(LEFT_NAMESPACE); util.deleteNamespace(RIGHT_NAMESPACE); Commons.cleanUp(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryApp.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryApp.java similarity index 93% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryApp.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryApp.java index e48f0615..88675932 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryApp.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryApp.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.fabric8.discovery; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryController.java similarity index 91% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryController.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryController.java index 430901f1..f0e36e25 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryController.java @@ -14,14 +14,13 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.fabric8.discovery; import java.util.List; import io.fabric8.kubernetes.api.model.Endpoints; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryFilterIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryFilterIT.java similarity index 99% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryFilterIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryFilterIT.java index 3ce43cd3..0c5decb5 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryFilterIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryFilterIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.fabric8.discovery; import java.io.InputStream; import java.time.Duration; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryIT.java similarity index 99% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryIT.java index f1d2f962..894294b2 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.fabric8.discovery; import java.io.InputStream; import java.time.Duration; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryNamespaceFilterIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryNamespaceFilterIT.java similarity index 98% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryNamespaceFilterIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryNamespaceFilterIT.java index 1951e776..0fcb547f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryNamespaceFilterIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryNamespaceFilterIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.fabric8.discovery; import java.io.InputStream; import java.time.Duration; diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryPodMetadataIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryPodMetadataIT.java similarity index 99% rename from spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryPodMetadataIT.java rename to spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryPodMetadataIT.java index fc0c81d8..a05ed37f 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryPodMetadataIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryPodMetadataIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.kubernetes.fabric8.configmap; +package org.springframework.cloud.kubernetes.fabric8.discovery; import java.io.InputStream; import java.time.Duration; diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java index aefe2cd1..102cbd8b 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java @@ -36,6 +36,7 @@ import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.apis.NetworkingV1Api; import io.kubernetes.client.openapi.apis.RbacAuthorizationV1Api; import io.kubernetes.client.openapi.models.V1ClusterRole; +import io.kubernetes.client.openapi.models.V1ClusterRoleBinding; import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1Deployment; import io.kubernetes.client.openapi.models.V1DeploymentList; @@ -131,6 +132,7 @@ public final class Util { } } catch (Exception e) { + e.printStackTrace(); throw new RuntimeException(e); } } @@ -298,6 +300,49 @@ public final class Util { } + public void setUpClusterWideClusterRoleBinding(String serviceAccountNamespace) { + + try { + V1ServiceAccount serviceAccount = (V1ServiceAccount) yaml("cluster/service-account.yaml"); + CheckedSupplier accountSupplier = () -> coreV1Api.readNamespacedServiceAccount( + serviceAccount.getMetadata().getName(), serviceAccountNamespace, null); + CheckedSupplier accountDefaulter = () -> coreV1Api + .createNamespacedServiceAccount(serviceAccountNamespace, serviceAccount, null, null, null, null); + notExistsHandler(accountSupplier, accountDefaulter); + + V1ClusterRole clusterRole = (V1ClusterRole) yaml("cluster/cluster-role.yaml"); + notExistsHandler(() -> rbacApi.readClusterRole(clusterRole.getMetadata().getName(), null), + () -> rbacApi.createClusterRole(clusterRole, null, null, null, null)); + + V1ClusterRoleBinding clusterRoleBinding = (V1ClusterRoleBinding) yaml("cluster/cluster-role-binding.yaml"); + notExistsHandler(() -> rbacApi.readClusterRoleBinding(clusterRoleBinding.getMetadata().getName(), null), + () -> rbacApi.createClusterRoleBinding(clusterRoleBinding, null, null, null, null)); + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + } + + public void deleteClusterWideClusterRoleBinding(String serviceAccountNamespace) { + try { + V1ServiceAccount serviceAccount = (V1ServiceAccount) yaml("cluster/service-account.yaml"); + V1ClusterRole clusterRole = (V1ClusterRole) yaml("cluster/cluster-role.yaml"); + V1ClusterRoleBinding clusterRoleBinding = (V1ClusterRoleBinding) yaml("cluster/cluster-role-binding.yaml"); + + coreV1Api.deleteNamespacedServiceAccount(serviceAccount.getMetadata().getName(), serviceAccountNamespace, + null, null, null, null, null, null); + rbacApi.deleteClusterRole(clusterRole.getMetadata().getName(), null, null, null, null, null, null); + rbacApi.deleteClusterRoleBinding(clusterRoleBinding.getMetadata().getName(), null, null, null, null, null, + null); + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + public void setUpClusterWide(String serviceAccountNamespace, Set namespaces) { try { @@ -332,6 +377,31 @@ public final class Util { } + public void deleteClusterWide(String serviceAccountNamespace, Set namespaces) { + try { + V1ServiceAccount serviceAccount = (V1ServiceAccount) yaml("cluster/service-account.yaml"); + V1ClusterRole clusterRole = (V1ClusterRole) yaml("cluster/cluster-role.yaml"); + V1RoleBinding roleBinding = (V1RoleBinding) yaml("cluster/role-binding.yaml"); + + coreV1Api.deleteNamespacedServiceAccount(serviceAccount.getMetadata().getName(), serviceAccountNamespace, + null, null, null, null, null, null); + rbacApi.deleteClusterRole(clusterRole.getMetadata().getName(), null, null, null, null, null, null); + namespaces.forEach(namespace -> { + roleBinding.getMetadata().setNamespace(namespace); + try { + rbacApi.deleteNamespacedRoleBinding(roleBinding.getMetadata().getName(), namespace, null, null, + null, null, null, null); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + public void deleteNamespace(String name) { try { coreV1Api.deleteNamespace(name, null, null, null, null, null, null); diff --git a/spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role-binding.yaml b/spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role-binding.yaml new file mode 100644 index 00000000..28429698 --- /dev/null +++ b/spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role-binding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app: spring-cloud-kubernetes-core-k8s-client-it + name: spring-cloud-kubernetes-cluster-role-binding +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: cluster-role +subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-serviceaccount + namespace: default