diff --git a/spring-cloud-kubernetes-fabric8-discovery/pom.xml b/spring-cloud-kubernetes-fabric8-discovery/pom.xml index 215f45ed..d6b8be9d 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/pom.xml +++ b/spring-cloud-kubernetes-fabric8-discovery/pom.xml @@ -48,7 +48,20 @@ org.springframework.boot spring-boot-starter-test test + + + + org.mockito + mockito-core + + + + + org.mockito + mockito-inline + + org.junit.vintage junit-vintage-engine diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryServicesAdapter.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryServicesAdapter.java new file mode 100644 index 00000000..5cc5420b --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryServicesAdapter.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; + +/** + * Adapts a {@link KubernetesClientServicesFunction} to a Function that takes a + * KubernetesClient as input and returns a List of Services(s), plus adds functionality + * not supported by it. + * + * @author wind57 + */ +final class Fabric8DiscoveryServicesAdapter implements Function> { + + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + + private static final SimpleEvaluationContext EVALUATION_CONTEXT = SimpleEvaluationContext.forReadOnlyDataBinding() + .withInstanceMethods().build(); + + private final KubernetesClientServicesFunction function; + + private final KubernetesDiscoveryProperties properties; + + private final Predicate filter; + + Fabric8DiscoveryServicesAdapter(KubernetesClientServicesFunction function, KubernetesDiscoveryProperties properties, + Predicate filter) { + this.function = function; + this.properties = properties; + if (filter == null) { + this.filter = filter(); + } + else { + this.filter = filter; + } + } + + @Override + public List apply(KubernetesClient client) { + if (!properties.namespaces().isEmpty()) { + List services = new ArrayList<>(); + properties.namespaces().forEach(namespace -> services.addAll(client.services().inNamespace(namespace) + .withLabels(properties.serviceLabels()).list().getItems().stream().filter(filter).toList())); + return services; + } + return function.apply(client).list().getItems().stream().filter(filter).toList(); + } + + Predicate filter() { + String spelExpression = properties.filter(); + Predicate predicate; + if (spelExpression == null || spelExpression.isEmpty()) { + predicate = service -> true; + } + else { + Expression filterExpr = PARSER.parseExpression(spelExpression); + predicate = service -> { + Boolean include = filterExpr.getValue(EVALUATION_CONTEXT, service, Boolean.class); + return Optional.ofNullable(include).orElse(false); + }; + } + return predicate; + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java index 340a7820..9f643758 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClient.java @@ -36,9 +36,6 @@ import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -61,14 +58,11 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { private final KubernetesDiscoveryProperties properties; - private final ServicePortSecureResolver servicePortSecureResolver; - private final KubernetesClientServicesFunction kubernetesClientServicesFunction; - private final SpelExpressionParser parser = new SpelExpressionParser(); + private final ServicePortSecureResolver servicePortSecureResolver; - private final SimpleEvaluationContext evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding() - .withInstanceMethods().build(); + private final Fabric8DiscoveryServicesAdapter adapter; private KubernetesClient client; @@ -76,18 +70,20 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { KubernetesDiscoveryProperties kubernetesDiscoveryProperties, KubernetesClientServicesFunction kubernetesClientServicesFunction) { - this(client, kubernetesDiscoveryProperties, kubernetesClientServicesFunction, + this(client, kubernetesDiscoveryProperties, kubernetesClientServicesFunction, null, new ServicePortSecureResolver(kubernetesDiscoveryProperties)); } KubernetesDiscoveryClient(KubernetesClient client, KubernetesDiscoveryProperties kubernetesDiscoveryProperties, - KubernetesClientServicesFunction kubernetesClientServicesFunction, + KubernetesClientServicesFunction kubernetesClientServicesFunction, Predicate filter, ServicePortSecureResolver servicePortSecureResolver) { this.client = client; this.properties = kubernetesDiscoveryProperties; - this.kubernetesClientServicesFunction = kubernetesClientServicesFunction; this.servicePortSecureResolver = servicePortSecureResolver; + this.kubernetesClientServicesFunction = kubernetesClientServicesFunction; + this.adapter = new Fabric8DiscoveryServicesAdapter(kubernetesClientServicesFunction, + kubernetesDiscoveryProperties, filter); } public KubernetesClient getClient() { @@ -291,35 +287,13 @@ public class KubernetesDiscoveryClient implements DiscoveryClient { @Override public List getServices() { - String spelExpression = this.properties.filter(); - Predicate filteredServices; - if (spelExpression == null || spelExpression.isEmpty()) { - filteredServices = (Service instance) -> true; - } - else { - Expression filterExpr = this.parser.parseExpression(spelExpression); - filteredServices = (Service instance) -> { - Boolean include = filterExpr.getValue(this.evalCtxt, instance, Boolean.class); - if (include == null) { - return false; - } - return include; - }; - } - return getServices(filteredServices); + return adapter.apply(client).stream().map(s -> s.getMetadata().getName()).toList(); } + @Deprecated(forRemoval = true) public List getServices(Predicate filter) { - if (properties.namespaces().isEmpty()) { - return this.kubernetesClientServicesFunction.apply(this.client).list().getItems().stream().filter(filter) - .map(s -> s.getMetadata().getName()).collect(Collectors.toList()); - } - List services = new ArrayList<>(); - for (String ns : properties.namespaces()) { - services.addAll(getClient().services().inNamespace(ns).list().getItems().stream().filter(filter) - .map(s -> s.getMetadata().getName()).toList()); - } - return services; + return new Fabric8DiscoveryServicesAdapter(kubernetesClientServicesFunction, properties, filter).apply(client) + .stream().map(s -> s.getMetadata().getName()).toList(); } @Override diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java index a0c443c3..f2c5c124 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -31,14 +31,17 @@ import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.PodUtils; import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration; +import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; /** * Auto configuration for discovery clients. @@ -55,23 +58,18 @@ import org.springframework.context.annotation.Configuration; public class KubernetesDiscoveryClientAutoConfiguration { @Bean - public KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties) { - if (properties.serviceLabels().isEmpty()) { - if (properties.allNamespaces()) { - return (client) -> client.services().inAnyNamespace(); - } - else { - return KubernetesClient::services; - } - } - else { - if (properties.allNamespaces()) { - return (client) -> client.services().inAnyNamespace().withLabels(properties.serviceLabels()); - } - else { - return (client) -> client.services().withLabels(properties.serviceLabels()); - } + public KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties, + Environment environment) { + + if (properties.allNamespaces()) { + return (client) -> client.services().inAnyNamespace().withLabels(properties.serviceLabels()); } + + return client -> { + String namespace = Fabric8Utils.getApplicationNamespace(client, null, "discovery-service", + new KubernetesNamespaceProvider(environment)); + return client.services().inNamespace(namespace).withLabels(properties.serviceLabels()); + }; } @ConditionalOnClass({ HealthIndicator.class }) @@ -82,7 +80,7 @@ public class KubernetesDiscoveryClientAutoConfiguration { @Bean public KubernetesDiscoveryClientHealthIndicatorInitializer indicatorInitializer( - ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { + ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher); } @@ -98,7 +96,7 @@ public class KubernetesDiscoveryClientAutoConfiguration { public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client, KubernetesDiscoveryProperties properties, KubernetesClientServicesFunction kubernetesClientServicesFunction) { - return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction, + return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction, null, new ServicePortSecureResolver(properties)); } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryServicesAdapterTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryServicesAdapterTests.java new file mode 100644 index 00000000..df45b1ed --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8DiscoveryServicesAdapterTests.java @@ -0,0 +1,479 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; +import org.springframework.mock.env.MockEnvironment; + +/** + * @author wind57 + */ +@EnableKubernetesMockClient(crud = true, https = false) +class Fabric8DiscoveryServicesAdapterTests { + + private static KubernetesClient client; + + private static MockedStatic utils; + + @BeforeEach + void beforeEach() { + utils = Mockito.mockStatic(Fabric8Utils.class); + } + + @AfterEach + void afterEach() { + client.services().inAnyNamespace().delete(); + utils.close(); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - labels = {}
+	 *     - filter = null
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=red}
+	 *     - serviceB exists in namespaceB with labels = {color=blue}
+	 *
+	 *     - we get both services as a result.
+	 * 
+ */ + @Test + void testAllNamespacesWithoutLabelsWithoutFilter() { + boolean allNamespaces = true; + Map labels = Map.of(); + String spelFilter = null; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(), + true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + service("namespaceA", "serviceA", Map.of("color", "red")); + service("namespaceB", "serviceB", Map.of("color", "blue")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + Assertions.assertEquals(result.get(1).getMetadata().getName(), "serviceB"); + Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "namespaceB"); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - labels = {color=red}
+	 *     - filter = null
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=red}
+	 *     - serviceB exists in namespaceB with labels = {color=blue}
+	 *
+	 *     - we get only serviceA as a result.
+	 * 
+ */ + @Test + void testAllNamespacesWithLabelsWithoutFilter() { + boolean allNamespaces = true; + Map labels = Map.of("color", "red"); + String spelFilter = null; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(), + true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + service("namespaceA", "serviceA", Map.of("color", "red")); + service("namespaceB", "serviceB", Map.of("color", "blue")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - labels = {}
+	 *     - filter = "#root.metadata.namespace matches '^.+A$'"
+	 *       (ends in A)
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=red}
+	 *     - serviceB exists in namespaceB with labels = {color=blue}
+	 *
+	 *     - we get only serviceA as a result.
+	 * 
+ */ + @Test + void testAllNamespacesWithoutLabelsWithNamespaceFilter() { + boolean allNamespaces = true; + Map labels = Map.of(); + String spelFilter = """ + #root.metadata.namespace matches "^.+A$" + """; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(), + true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + service("namespaceA", "serviceA", Map.of("color", "red")); + service("namespaceB", "serviceB", Map.of("color", "blue")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + } + + /** + *
+	 *     - all-namespaces = true
+	 *     - labels = {}
+	 *     - filter = "#root.metadata.namespace matches '^namespace[A|B]$'"
+	 *       (namespaceA or namespaceB)
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=red}
+	 *     - serviceB exists in namespaceB with labels = {color=blue}
+	 *     - serviceC exists in namespaceC with labels = {color=purple}
+	 *
+	 *     - we get only serviceA and serviceB as a result.
+	 * 
+ */ + @Test + void testAllNamespacesWithoutLabelsWithNamespacesFilter() { + boolean allNamespaces = true; + Map labels = Map.of(); + String spelFilter = """ + #root.metadata.namespace matches "^namespace[A|B]$" + """; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(), + true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + service("namespaceA", "serviceA", Map.of("color", "red")); + service("namespaceB", "serviceB", Map.of("color", "blue")); + service("namespaceC", "serviceC", Map.of("color", "purple")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 2); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + Assertions.assertEquals(result.get(1).getMetadata().getName(), "serviceB"); + Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "namespaceB"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - specific namespace = namespaceA
+	 *     - labels = {}
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=red}
+	 *     - serviceB exists in namespaceB with labels = {color=blue}
+	 *
+	 *     - we get only serviceA as a result.
+	 * 
+ */ + @Test + void testSpecificNamespaceWithoutLabelsWithoutFilter() { + boolean allNamespaces = false; + Map labels = Map.of(); + String spelFilter = null; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(), + true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + utils.when(() -> Fabric8Utils.getApplicationNamespace(Mockito.any(KubernetesClient.class), + Mockito.nullable(String.class), Mockito.anyString(), Mockito.any(KubernetesNamespaceProvider.class))) + .thenReturn("namespaceA"); + + service("namespaceA", "serviceA", Map.of("color", "red")); + service("namespaceB", "serviceB", Map.of("color", "blue")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - specific namespace = namespaceA
+	 *     - labels = {color = purple}
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=red}
+	 *     - serviceB exists in namespaceA with labels = {color=purple}
+	 *     - serviceC exists in namespaceC with labels = {color=purple}
+	 *
+	 *     - we get only serviceB as a result, even if such labels are also
+	 *       present on a different service (but it's in a different namespace).
+	 * 
+ */ + @Test + void testSpecificNamespaceWithLabelsWithoutFilter() { + boolean allNamespaces = false; + Map labels = Map.of("color", "purple"); + String spelFilter = null; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(), + true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + utils.when(() -> Fabric8Utils.getApplicationNamespace(Mockito.any(KubernetesClient.class), + Mockito.nullable(String.class), Mockito.anyString(), Mockito.any(KubernetesNamespaceProvider.class))) + .thenReturn("namespaceA"); + + service("namespaceA", "serviceA", Map.of("color", "red")); + service("namespaceA", "serviceB", Map.of("color", "purple")); + service("namespaceC", "serviceC", Map.of("color", "purple")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceB"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - specific namespace = namespaceA
+	 *     - labels = {}
+	 *     - filter = "#root.metadata.labels.containsKey("number")"
+	 *       (namespaceA or namespaceB)
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=red, number=1}
+	 *     - serviceB exists in namespaceA with labels = {color=purple, cycle=create}
+	 *     - serviceC exists in namespaceC with labels = {color=purple, number=1}
+	 *
+	 *     - we get only serviceB as a result (because of the filter) even if such labels are also
+	 *       present on a different service (but it's in a different namespace).
+	 * 
+ */ + @Test + void testSpecificNamespaceWithoutLabelsWithFilter() { + boolean allNamespaces = false; + Map labels = Map.of(); + String spelFilter = """ + #root.metadata.labels.containsKey("number") + """.stripLeading(); + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(), + true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + utils.when(() -> Fabric8Utils.getApplicationNamespace(Mockito.any(KubernetesClient.class), + Mockito.nullable(String.class), Mockito.anyString(), Mockito.any(KubernetesNamespaceProvider.class))) + .thenReturn("namespaceA"); + + service("namespaceA", "serviceA", Map.of("color", "red", "number", "1")); + service("namespaceA", "serviceB", Map.of("color", "purple", "cycle", "create")); + service("namespaceC", "serviceC", Map.of("color", "purple", "number", "1")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + Assertions.assertEquals(result.get(0).getMetadata().getLabels(), Map.of("color", "red", "number", "1")); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - some namespaces = [namespaceA, namespaceB]
+	 *     - labels = {}
+	 *     - filter = null
+	 *
+	 *     - serviceA exists in namespaceA with labels = {}
+	 *     - serviceB exists in namespaceB with labels = {}
+	 *     - serviceC exists in namespaceC with labels = {}
+	 *
+	 *     - we get serviceA and serviceB as a result, because their namespaces match.
+	 * 
+ */ + @Test + void testSomeNamespacesWithoutLabelsWithoutFilter() { + boolean allNamespaces = false; + Set someNamespaces = Set.of("namespaceA", "namespaceB"); + Map labels = Map.of(); + String spelFilter = null; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, + someNamespaces, true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + service("namespaceA", "serviceA", Map.of()); + service("namespaceB", "serviceB", Map.of()); + service("namespaceC", "serviceC", Map.of()); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 2); + result = result.stream().sorted(Comparator.comparing(x -> x.getMetadata().getName())).toList(); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + Assertions.assertEquals(result.get(1).getMetadata().getName(), "serviceB"); + Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "namespaceB"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - some namespaces = [namespaceA, namespaceB]
+	 *     - labels = {color=purple}
+	 *     - filter = null
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=purple}
+	 *     - serviceB exists in namespaceB with labels = {color=red}
+	 *     - serviceC exists in namespaceC with labels = {color=purple}
+	 *
+	 *     - we get serviceA as a result
+	 * 
+ */ + @Test + void testSomeNamespacesWithLabelsWithoutFilter() { + boolean allNamespaces = false; + Set someNamespaces = Set.of("namespaceA", "namespaceB"); + Map labels = Map.of("color", "purple"); + String spelFilter = null; + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, + someNamespaces, true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + service("namespaceA", "serviceA", Map.of("color", "purple")); + service("namespaceB", "serviceB", Map.of("color", "red")); + service("namespaceC", "serviceC", Map.of("color", "purple")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + } + + /** + *
+	 *     - all-namespaces = false
+	 *     - some namespaces = [namespaceA, namespaceB]
+	 *     - labels = {color=purple}
+	 *     - filter = #root.metadata.labels.containsKey("number")
+	 *
+	 *     - serviceA exists in namespaceA with labels = {color=purple}
+	 *     - serviceB exists in namespaceB with labels = {color=red}
+	 *     - serviceC exists in namespaceC with labels = {color=purple}
+	 *
+	 *     - we get serviceA as a result
+	 * 
+ */ + @Test + void testSomeNamespacesWithLabelsWithFilter() { + boolean allNamespaces = false; + Set someNamespaces = Set.of("namespaceA", "namespaceB"); + Map labels = Map.of("color", "purple"); + String spelFilter = """ + #root.metadata.labels.containsKey("number") + """.stripLeading(); + + MockEnvironment environment = new MockEnvironment(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, + someNamespaces, true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false); + + Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter( + new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties, + null); + + service("namespaceA", "serviceA", Map.of("color", "purple", "number", "1")); + service("namespaceB", "serviceB", Map.of("color", "purple", "cycle", "create")); + service("namespaceC", "serviceC", Map.of("color", "purple", "number", "1")); + + List result = adapter.apply(client); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA"); + } + + private void service(String namespace, String name, Map labels) { + client.services().inNamespace(namespace) + .resource(new ServiceBuilder().withNewMetadata().withName(name).withLabels(labels).and().build()) + .create(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java index 477436ba..4f041eb2 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientTest.java @@ -85,7 +85,7 @@ public class KubernetesDiscoveryClientTest { mockClient.services().inNamespace("test").resource(service).create(); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, - KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, + KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null, new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT)); final List instances = discoveryClient.getInstances("endpoint"); @@ -115,7 +115,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(), labels, "http_tcp", Metadata.DEFAULT, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint"); @@ -137,7 +137,7 @@ public class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint).create(); final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, - KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, + KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null, new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT)); final List result_endpoints = discoveryClient.getEndPointsList("endpoint"); @@ -164,7 +164,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List result_endpoints = discoveryClient.getEndPointsList("endpoint"); @@ -194,7 +194,7 @@ public class KubernetesDiscoveryClientTest { KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List result_endpoints = discoveryClient.getEndPointsList("endpoint"); @@ -226,7 +226,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(443, 8443), labels, null, metadata, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint"); @@ -254,7 +254,7 @@ public class KubernetesDiscoveryClientTest { mockClient.services().inNamespace("test").resource(service3).create(); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, - KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, + KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null, new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT)); final List services = discoveryClient.getServices(); @@ -280,7 +280,7 @@ public class KubernetesDiscoveryClientTest { final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, KubernetesDiscoveryProperties.DEFAULT, - client -> client.services().withLabels(Collections.singletonMap("label", "value")), + client -> client.services().withLabels(Collections.singletonMap("label", "value")), null, new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT)); final List services = discoveryClient.getServices(); @@ -313,7 +313,7 @@ public class KubernetesDiscoveryClientTest { KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List services = discoveryClient.getServices(); @@ -346,7 +346,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(), Map.of(), null, Metadata.DEFAULT, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint"); @@ -369,7 +369,7 @@ public class KubernetesDiscoveryClientTest { mockClient.endpoints().inNamespace("test").resource(endPoint).create(); final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, - KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, + KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null, new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT)); final List instances = discoveryClient.getInstances("endpoint1"); @@ -398,7 +398,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint2"); @@ -429,7 +429,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint3"); @@ -459,7 +459,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(443, 8443), Map.of(), "oops", Metadata.DEFAULT, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint4"); @@ -488,7 +488,7 @@ public class KubernetesDiscoveryClientTest { 60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint5"); @@ -515,7 +515,7 @@ public class KubernetesDiscoveryClientTest { mockClient.services().inNamespace("test").resource(service).create(); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, - KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, + KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null, new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT)); final List instances = discoveryClient.getInstances("endpoint5"); @@ -545,7 +545,7 @@ public class KubernetesDiscoveryClientTest { 60, true, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true); final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties, - KubernetesClient::services, new ServicePortSecureResolver(properties)); + KubernetesClient::services, null, new ServicePortSecureResolver(properties)); final List instances = discoveryClient.getInstances("endpoint5");