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 index 5cc5420b..cdb079c8 100644 --- 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 @@ -24,8 +24,10 @@ import java.util.function.Predicate; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.client.KubernetesClient; +import org.apache.commons.logging.LogFactory; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.core.log.LogAccessor; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; @@ -39,6 +41,8 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext; */ final class Fabric8DiscoveryServicesAdapter implements Function> { + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8DiscoveryServicesAdapter.class)); + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private static final SimpleEvaluationContext EVALUATION_CONTEXT = SimpleEvaluationContext.forReadOnlyDataBinding() @@ -65,6 +69,8 @@ final class Fabric8DiscoveryServicesAdapter implements Function apply(KubernetesClient client) { if (!properties.namespaces().isEmpty()) { + LOG.debug(() -> "searching in namespaces : " + properties.namespaces() + " with filter : " + + properties.filter()); List services = new ArrayList<>(); properties.namespaces().forEach(namespace -> services.addAll(client.services().inNamespace(namespace) .withLabels(properties.serviceLabels()).list().getItems().stream().filter(filter).toList())); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsCatalogWatch.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsCatalogWatch.java index 55a60b1d..f429b394 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsCatalogWatch.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsCatalogWatch.java @@ -41,7 +41,7 @@ final class Fabric8EndpointsCatalogWatch @Override public List apply(Fabric8CatalogWatchContext context) { List endpoints = endpoints(context.properties(), context.kubernetesClient(), - context.namespaceProvider(), "catalog-watcher", null); + context.namespaceProvider(), "catalog-watcher", null, x -> true); /** *
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java
index af34fadc..e8966882 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtils.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -187,7 +188,8 @@ final class Fabric8KubernetesDiscoveryClientUtils {
 	}
 
 	static List endpoints(KubernetesDiscoveryProperties properties, KubernetesClient client,
-			KubernetesNamespaceProvider namespaceProvider, String target, @Nullable String serviceName) {
+			KubernetesNamespaceProvider namespaceProvider, String target, @Nullable String serviceName,
+			Predicate filter) {
 
 		List endpoints;
 
@@ -209,7 +211,35 @@ final class Fabric8KubernetesDiscoveryClientUtils {
 					serviceName);
 		}
 
-		return endpoints;
+		return withFilter(endpoints, properties, client, filter);
+	}
+
+	// see https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1182 on why this
+	// is needed
+	static List withFilter(List initial, KubernetesDiscoveryProperties properties,
+			KubernetesClient client, Predicate filter) {
+
+		if (properties.filter() == null || properties.filter().isBlank()) {
+			LOG.debug(() -> "filter not present");
+			return initial;
+		}
+
+		List result = new ArrayList<>();
+		// group by namespace in order to make a single API call per namespace when
+		// retrieving services
+		Map> byNamespace = initial.stream()
+				.collect(Collectors.groupingBy(x -> x.getMetadata().getNamespace()));
+
+		for (Map.Entry> entry : byNamespace.entrySet()) {
+			Set withFilter = client.services().inNamespace(entry.getKey()).list().getItems().stream()
+					.filter(filter).map(service -> service.getMetadata().getName()).collect(Collectors.toSet());
+
+			result.addAll(
+					entry.getValue().stream().filter(x -> withFilter.contains(x.getMetadata().getName())).toList());
+
+		}
+
+		return result;
 	}
 
 	/**
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 508374ff..1fb12e94 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
@@ -131,7 +131,7 @@ public class KubernetesDiscoveryClient implements DiscoveryClient, EnvironmentAw
 	}
 
 	public List getEndPointsList(String serviceId) {
-		return endpoints(properties, client, namespaceProvider, "fabric8-discovery", serviceId);
+		return endpoints(properties, client, namespaceProvider, "fabric8-discovery", serviceId, adapter.filter());
 	}
 
 	private List getNamespaceServiceInstances(EndpointSubsetNS es, String serviceId) {
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java
index 52a1aa20..23dd7308 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java
@@ -28,6 +28,8 @@ import io.fabric8.kubernetes.api.model.Endpoints;
 import io.fabric8.kubernetes.api.model.EndpointsBuilder;
 import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
 import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceBuilder;
 import io.fabric8.kubernetes.api.model.discovery.v1.Endpoint;
 import io.fabric8.kubernetes.api.model.discovery.v1.EndpointBuilder;
 import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSlice;
@@ -305,6 +307,14 @@ abstract class Fabric8EndpointsAndEndpointSlicesTests {
 		mockClient().endpoints().inNamespace(namespace).resource(endpoints).create();
 	}
 
+	void service(String namespace, Map labels, String podName) {
+
+		Service service = new ServiceBuilder()
+				.withMetadata(new ObjectMetaBuilder().withLabels(labels).withName("endpoints-" + podName).build())
+				.build();
+		mockClient().services().inNamespace(namespace).resource(service).create();
+	}
+
 	static void endpointSlice(String namespace, Map labels, String podName) {
 
 		Endpoint endpoint = new EndpointBuilder()
@@ -320,7 +330,8 @@ abstract class Fabric8EndpointsAndEndpointSlicesTests {
 	static void invokeAndAssert(KubernetesCatalogWatch watch, List state) {
 		watch.catalogServicesWatch();
 
-		verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture());
+		verify(APPLICATION_EVENT_PUBLISHER, Mockito.atLeastOnce())
+				.publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture());
 
 		HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue();
 		assertThat(event.getValue()).isInstanceOf(List.class);
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java
index de18f413..92eda531 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointsTests.java
@@ -22,6 +22,7 @@ import java.util.Set;
 
 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.Test;
 
 import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
@@ -38,6 +39,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 
 	private static KubernetesClient mockClient;
 
+	@AfterEach
+	void afterEach() {
+		mockClient.endpoints().inAnyNamespace().delete();
+		mockClient.services().inAnyNamespace().delete();
+	}
+
 	@Test
 	@Override
 	void testInSpecificNamespaceWithServiceLabels() {
@@ -51,6 +58,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podD");
 		endpoints("namespaceB", Map.of(), "podE");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+
 		invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA")));
 	}
 
@@ -67,6 +80,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podD");
 		endpoints("namespaceB", Map.of(), "podE");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+
 		invokeAndAssert(watch,
 				List.of(new EndpointNameAndNamespace("podA", "namespaceA"),
 						new EndpointNameAndNamespace("podB", "namespaceA"),
@@ -86,6 +105,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podD");
 		endpoints("namespaceB", Map.of(), "podE");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+
 		invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"),
 				new EndpointNameAndNamespace("podD", "namespaceB")));
 	}
@@ -102,6 +127,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podD");
 		endpoints("namespaceB", Map.of(), "podE");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+
 		invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podA", "namespaceA"),
 				new EndpointNameAndNamespace("podB", "namespaceA"), new EndpointNameAndNamespace("podC", "namespaceA"),
 				new EndpointNameAndNamespace("podD", "namespaceB"),
@@ -121,6 +152,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podD");
 		endpoints("namespaceB", Map.of(), "podE");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+
 		invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"),
 				new EndpointNameAndNamespace("podD", "namespaceB")));
 	}
@@ -138,6 +175,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podD");
 		endpoints("namespaceB", Map.of(), "podE");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+
 		invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA")));
 	}
 
@@ -154,6 +197,12 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podD");
 		endpoints("namespaceB", Map.of(), "podE");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+
 		invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA")));
 	}
 
@@ -172,6 +221,14 @@ class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEnd
 		endpoints("namespaceB", Map.of("color", "blue"), "podF");
 		endpoints("namespaceC", Map.of("color", "blue"), "podO");
 
+		service("namespaceA", Map.of(), "podA");
+		service("namespaceA", Map.of("color", "blue"), "podB");
+		service("namespaceA", Map.of("color", "red"), "podC");
+		service("namespaceB", Map.of("color", "blue"), "podD");
+		service("namespaceB", Map.of(), "podE");
+		service("namespaceB", Map.of("color", "blue"), "podF");
+		service("namespaceC", Map.of("color", "blue"), "podO");
+
 		invokeAndAssert(watch,
 				List.of(new EndpointNameAndNamespace("podB", "namespaceA"),
 						new EndpointNameAndNamespace("podD", "namespaceB"),
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java
index 21ebc4f4..4632237b 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTest.java
@@ -61,7 +61,8 @@ class Fabric8KubernetesDiscoveryClientTest {
 
 	@AfterEach
 	void afterEach() {
-		mockClient.close();
+		mockClient.endpoints().inAnyNamespace().delete();
+		mockClient.services().inAnyNamespace().delete();
 	}
 
 	@Test
@@ -128,8 +129,11 @@ class Fabric8KubernetesDiscoveryClientTest {
 				.withLabels(labels).endMetadata().addNewSubset().addNewAddress().withIp("ip1").withNewTargetRef()
 				.withUid("30").endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP").endSubset()
 				.build();
+		Service service = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace("test")
+				.endMetadata().build();
 
 		mockClient.endpoints().inNamespace("test").resource(endPoint).create();
+		mockClient.services().inNamespace("test").resource(service).create();
 
 		KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient,
 				KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null,
@@ -148,18 +152,24 @@ class Fabric8KubernetesDiscoveryClientTest {
 
 		Endpoints endPoint1 = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace1)
 				.endMetadata().build();
-
 		Endpoints endPoint2 = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace2)
 				.endMetadata().build();
 
+		Service service1 = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace1)
+				.endMetadata().build();
+		Service service2 = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace2)
+				.endMetadata().build();
+
 		mockClient.endpoints().inNamespace(namespace1).resource(endPoint1).create();
 		mockClient.endpoints().inNamespace(namespace2).resource(endPoint2).create();
+		mockClient.services().inNamespace(namespace1).resource(service1).create();
+		mockClient.services().inNamespace(namespace2).resource(service2).create();
 
 		KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
 				false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
 
 		KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
-				KubernetesClient::services, null, new ServicePortSecureResolver(properties));
+				KubernetesClient::services, x -> true, new ServicePortSecureResolver(properties));
 
 		List result_endpoints = discoveryClient.getEndPointsList("endpoint");
 
@@ -180,9 +190,19 @@ class Fabric8KubernetesDiscoveryClientTest {
 		Endpoints endPoint3 = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace3)
 				.endMetadata().build();
 
+		Service service1 = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace1)
+				.endMetadata().build();
+		Service service2 = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace2)
+				.endMetadata().build();
+		Service service3 = new ServiceBuilder().withNewMetadata().withName("endpoint").withNamespace(namespace3)
+				.endMetadata().build();
+
 		mockClient.endpoints().inNamespace(namespace1).resource(endPoint1).create();
 		mockClient.endpoints().inNamespace(namespace2).resource(endPoint2).create();
 		mockClient.endpoints().inNamespace(namespace3).resource(endPoint3).create();
+		mockClient.services().inNamespace(namespace1).resource(service1).create();
+		mockClient.services().inNamespace(namespace2).resource(service2).create();
+		mockClient.services().inNamespace(namespace3).resource(service3).create();
 
 		KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false,
 				Set.of(namespace1, namespace3), true, 60, false, null, Set.of(), Map.of(), null,
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTests.java
index 60ddce49..5a45f50d 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTests.java
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientTests.java
@@ -93,6 +93,7 @@ class Fabric8KubernetesDiscoveryClientTests {
 	void testAllNamespacesSingleEndpointsMatchExactLabels(CapturedOutput output) {
 
 		createEndpoints("default", "blue-service", Map.of("color", "blue"));
+		createService("default", "blue-service", Map.of("color", "blue"));
 
 		boolean allNamespaces = true;
 		Set namespaces = Set.of();
@@ -118,6 +119,7 @@ class Fabric8KubernetesDiscoveryClientTests {
 	void testAllNamespacesSingleEndpointsMatchPartialLabels(CapturedOutput output) {
 
 		createEndpoints("default", "blue-service", Map.of("color", "blue", "shape", "round"));
+		createService("default", "blue-service", Map.of("color", "blue", "shape", "round"));
 
 		boolean allNamespaces = true;
 		Set namespaces = Set.of();
@@ -170,6 +172,9 @@ class Fabric8KubernetesDiscoveryClientTests {
 		createEndpoints("default", "service-one", Map.of("color", "blue", "shape", "round"));
 		createEndpoints("default", "service-two", Map.of("color", "blue", "shape", "round"));
 
+		createService("default", "service-one", Map.of("color", "blue", "shape", "round"));
+		createService("default", "service-two", Map.of("color", "blue", "shape", "round"));
+
 		boolean allNamespaces = true;
 		Set namespaces = Set.of();
 		Map serviceLabels = Map.of("color", "blue");
@@ -196,6 +201,9 @@ class Fabric8KubernetesDiscoveryClientTests {
 		createEndpoints("a", "service-one", Map.of("color", "blue", "shape", "round"));
 		createEndpoints("b", "service-one", Map.of("color", "blue", "shape", "round"));
 
+		createService("a", "service-one", Map.of("color", "blue", "shape", "round"));
+		createService("b", "service-one", Map.of("color", "blue", "shape", "round"));
+
 		boolean allNamespaces = true;
 		Set namespaces = Set.of();
 		Map serviceLabels = Map.of("color", "blue");
@@ -244,6 +252,7 @@ class Fabric8KubernetesDiscoveryClientTests {
 	void testClientNamespaceSingleEndpointsMatchExactLabels(CapturedOutput output) {
 
 		createEndpoints("test", "blue-service", Map.of("color", "blue"));
+		createService("test", "blue-service", Map.of("color", "blue"));
 
 		boolean allNamespaces = false;
 		Set namespaces = Set.of();
@@ -269,6 +278,7 @@ class Fabric8KubernetesDiscoveryClientTests {
 	void testClientNamespaceSingleEndpointsMatchPartialLabels(CapturedOutput output) {
 
 		createEndpoints("test", "blue-service", Map.of("color", "blue", "shape", "round"));
+		createService("test", "blue-service", Map.of("color", "blue", "shape", "round"));
 
 		boolean allNamespaces = false;
 		Set namespaces = Set.of();
@@ -321,6 +331,9 @@ class Fabric8KubernetesDiscoveryClientTests {
 		createEndpoints("test", "service-one", Map.of("color", "blue", "shape", "round"));
 		createEndpoints("test", "service-two", Map.of("color", "blue", "shape", "round"));
 
+		createService("test", "service-one", Map.of("color", "blue", "shape", "round"));
+		createService("test", "service-two", Map.of("color", "blue", "shape", "round"));
+
 		boolean allNamespaces = false;
 		Set namespaces = Set.of();
 		Map serviceLabels = Map.of("color", "blue");
@@ -347,6 +360,9 @@ class Fabric8KubernetesDiscoveryClientTests {
 		createEndpoints("test", "service-one", Map.of("color", "blue", "shape", "round"));
 		createEndpoints("b", "service-one", Map.of("color", "blue", "shape", "round"));
 
+		createService("test", "service-one", Map.of("color", "blue", "shape", "round"));
+		createService("b", "service-one", Map.of("color", "blue", "shape", "round"));
+
 		boolean allNamespaces = false;
 		Set namespaces = Set.of();
 		Map serviceLabels = Map.of("color", "blue");
@@ -395,6 +411,7 @@ class Fabric8KubernetesDiscoveryClientTests {
 	void testSelectiveNamespacesSingleEndpointsMatchExactLabels(CapturedOutput output) {
 
 		createEndpoints("test", "blue-service", Map.of("color", "blue"));
+		createService("test", "blue-service", Map.of("color", "blue"));
 
 		boolean allNamespaces = false;
 		Set namespaces = Set.of("test");
@@ -402,7 +419,8 @@ class Fabric8KubernetesDiscoveryClientTests {
 		KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, namespaces,
 				true, 60L, false, "", Set.of(), serviceLabels, "", null, 0, false);
 
-		KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, null, null);
+		KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(client, properties, null, x -> true,
+				null);
 		List result = discoveryClient.getEndPointsList("blue-service");
 		Assertions.assertEquals(result.size(), 1);
 		Assertions.assertTrue(output.getOut().contains("discovering endpoints in namespaces : [test]"));
@@ -423,6 +441,9 @@ class Fabric8KubernetesDiscoveryClientTests {
 		createEndpoints("a", "blue-service", Map.of("color", "blue", "shape", "round"));
 		createEndpoints("b", "blue-service", Map.of("color", "blue", "shape", "rectangle"));
 
+		createService("a", "blue-service", Map.of("color", "blue", "shape", "round"));
+		createService("b", "blue-service", Map.of("color", "blue", "shape", "rectangle"));
+
 		boolean allNamespaces = false;
 		Set namespaces = Set.of("a");
 		Map serviceLabels = Map.of("color", "blue");
@@ -451,6 +472,9 @@ class Fabric8KubernetesDiscoveryClientTests {
 		createEndpoints("a", "blue-service", Map.of("color", "blue"));
 		createEndpoints("b", "blue-service", Map.of("color", "blue"));
 
+		createService("a", "blue-service", Map.of("color", "blue"));
+		createService("b", "blue-service", Map.of("color", "blue"));
+
 		boolean allNamespaces = false;
 		Set namespaces = Set.of("a", "b");
 		// so that assertion is correct
@@ -564,4 +588,11 @@ class Fabric8KubernetesDiscoveryClientTests {
 				.create();
 	}
 
+	private void createService(String namespace, String name, Map labels) {
+		client.services().inNamespace(namespace)
+				.resource(new ServiceBuilder()
+						.withMetadata(new ObjectMetaBuilder().withName(name).withLabels(labels).build()).build())
+				.create();
+	}
+
 }
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsFilterTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsFilterTests.java
new file mode 100644
index 00000000..d5a4d926
--- /dev/null
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesDiscoveryClientUtilsFilterTests.java
@@ -0,0 +1,230 @@
+/*
+ * 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.fabric8.discovery;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import io.fabric8.kubernetes.api.model.Endpoints;
+import io.fabric8.kubernetes.api.model.EndpointsBuilder;
+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.Test;
+
+import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
+
+/**
+ * @author wind57
+ */
+@EnableKubernetesMockClient(crud = true, https = false)
+class Fabric8KubernetesDiscoveryClientUtilsFilterTests {
+
+	private static KubernetesClient client;
+
+	private static final KubernetesDiscoveryProperties PROPERTIES = new KubernetesDiscoveryProperties(true, true,
+			Set.of(), false, 60L, false, "some", Set.of(), Map.of(), "", null, 0, false);
+
+	@AfterEach
+	void afterEach() {
+		client.endpoints().inAnyNamespace().delete();
+		client.services().inAnyNamespace().delete();
+	}
+
+	@Test
+	void withFilterEmptyInput() {
+		List result = Fabric8KubernetesDiscoveryClientUtils.withFilter(List.of(), PROPERTIES, client,
+				x -> true);
+		Assertions.assertEquals(result.size(), 0);
+	}
+
+	/**
+	 * 
+	 *     - Endpoints with name : "a" and namespace "namespace-a", present
+	 *     - Service with name "a" and namespace "namespace-not-a" present
+	 *
+	 *     As such, there is no match, empty result.
+	 * 
+ */ + @Test + void withFilterOneEndpointsNoMatchInService() { + Endpoints endpoints = createEndpoints("a", "namespace-a"); + createService("a", "namespace-not-a"); + List result = Fabric8KubernetesDiscoveryClientUtils.withFilter(List.of(endpoints), PROPERTIES, + client, x -> true); + Assertions.assertEquals(result.size(), 0); + } + + /** + *
+	 *     - Endpoints with name : "a" and namespace "namespace-a", present
+	 *     - Service with name "a" and namespace "namespace-a" present
+	 *
+	 *     As such, there is a match.
+	 * 
+ */ + @Test + void withFilterOneEndpointsMatchInService() { + Endpoints endpoints = createEndpoints("a", "namespace-a"); + createService("a", "namespace-a"); + List result = Fabric8KubernetesDiscoveryClientUtils.withFilter(List.of(endpoints), PROPERTIES, + client, x -> true); + Assertions.assertEquals(result.size(), 1); + } + + /** + *
+	 *     - Endpoints with name : "a" and namespace "namespace-a", present
+	 *     - Endpoints with name : "b" and namespace "namespace-b", present
+	 *     - Service with name "a" and namespace "namespace-a" present
+	 *
+	 *     As such, there is a match, single endpoints as result.
+	 * 
+ */ + @Test + void withFilterTwoEndpointsOneMatchInService() { + Endpoints endpointsA = createEndpoints("a", "namespace-a"); + Endpoints endpointsB = createEndpoints("b", "namespace-b"); + createService("a", "namespace-a"); + List result = Fabric8KubernetesDiscoveryClientUtils.withFilter(List.of(endpointsA, endpointsB), + PROPERTIES, client, x -> true); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "a"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespace-a"); + } + + /** + *
+	 *     - Endpoints with name : "a" and namespace "namespace-a", present
+	 *     - Endpoints with name : "b" and namespace "namespace-b", present
+	 *     - Service with name "a" and namespace "namespace-a" present
+	 *     - Service with name "b" and namespace "namespace-b" present
+	 *     - Service with name "c" and namespace "namespace-c" present
+	 *
+	 *     As such, there are two matches.
+	 * 
+ */ + @Test + void withFilterTwoEndpointsAndThreeServices() { + Endpoints endpointsA = createEndpoints("a", "namespace-a"); + Endpoints endpointsB = createEndpoints("b", "namespace-b"); + createService("a", "namespace-a"); + createService("b", "namespace-b"); + createService("c", "namespace-c"); + + List result = Fabric8KubernetesDiscoveryClientUtils.withFilter(List.of(endpointsA, endpointsB), + PROPERTIES, client, x -> true); + Assertions.assertEquals(result.size(), 2); + result = result.stream().sorted(Comparator.comparing(x -> x.getMetadata().getName())).toList(); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "a"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespace-a"); + Assertions.assertEquals(result.get(1).getMetadata().getName(), "b"); + Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "namespace-b"); + } + + /** + *
+	 *     - Endpoints with name : "a" and namespace "namespace-a", present
+	 *     - Endpoints with name : "b" and namespace "namespace-b", present
+	 *     - Service with name "a" and namespace "namespace-a" present
+	 *     - Service with name "b" and namespace "namespace-b" present
+	 *     - Service with name "c" and namespace "namespace-c" present
+	 *
+	 *     As such, there are two matches.
+	 * 
+ */ + @Test + void withFilterSingleEndpointsMatchesFilter() { + Endpoints endpointsA = createEndpoints("a", "namespace-a"); + createService("a", "namespace-a"); + + List result = Fabric8KubernetesDiscoveryClientUtils.withFilter(List.of(endpointsA), PROPERTIES, + client, x -> x.getMetadata().getNamespace().equals("namespace-a")); + Assertions.assertEquals(result.size(), 1); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "a"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespace-a"); + } + + /** + *
+	 *     - Endpoints with name : "a-1" and namespace "default", present
+	 *     - Endpoints with name : "b-1" and namespace "default", present
+	 *     - Endpoints with name : "c-2" and namespace "default", present
+	 *     - Service with name "a-1" and namespace "default" present
+	 *     - Service with name "b-1" and namespace "default" present
+	 *
+	 *     As such, there are two matches.
+	 * 
+ */ + @Test + void withFilterTwoEndpointsMatchesFilter() { + Endpoints endpointsA = createEndpoints("a-1", "default"); + Endpoints endpointsB = createEndpoints("b-1", "default"); + Endpoints endpointsC = createEndpoints("c-2", "default"); + createService("a-1", "default"); + createService("b-1", "default"); + + List result = Fabric8KubernetesDiscoveryClientUtils.withFilter( + List.of(endpointsA, endpointsB, endpointsC), PROPERTIES, client, + x -> x.getMetadata().getName().contains("1")); + Assertions.assertEquals(result.size(), 2); + result = result.stream().sorted(Comparator.comparing(x -> x.getMetadata().getName())).toList(); + Assertions.assertEquals(result.get(0).getMetadata().getName(), "a-1"); + Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "default"); + Assertions.assertEquals(result.get(1).getMetadata().getName(), "b-1"); + Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "default"); + } + + /** + *
+	 *     - Endpoints with name : "a" and namespace "default", present
+	 *     - Service with name "a-1" and namespace "default" present
+	 *     - Service with name "b-1" and namespace "default" present
+	 *
+	 *     As such, there are two matches.
+	 * 
+ */ + @Test + void withFilterSingleEndpointsNoPredicateMatch() { + Endpoints endpointsA = createEndpoints("a", "default"); + createService("a-1", "default"); + createService("b-1", "default"); + + List result = Fabric8KubernetesDiscoveryClientUtils.withFilter(List.of(endpointsA), PROPERTIES, + client, x -> !x.getMetadata().getName().contains("1")); + Assertions.assertEquals(result.size(), 0); + } + + private Endpoints createEndpoints(String name, String namespace) { + Endpoints endpoints = new EndpointsBuilder().withNewMetadata().withName(name).withNamespace(namespace) + .endMetadata().build(); + client.endpoints().inNamespace(namespace).resource(endpoints).create(); + return endpoints; + } + + private void createService(String name, String namespace) { + Service service = new ServiceBuilder().withNewMetadata().withName(name).withNamespace(namespace).endMetadata() + .build(); + client.services().inNamespace(namespace).resource(service).create(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchTest.java index 5f3f7b9f..dbefc0e6 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchTest.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatchTest.java @@ -25,14 +25,21 @@ import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.EndpointAddress; import io.fabric8.kubernetes.api.model.EndpointSubset; import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsBuilder; import io.fabric8.kubernetes.api.model.EndpointsList; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.ObjectReference; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServiceList; +import io.fabric8.kubernetes.api.model.ServiceListBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.FilterNested; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -92,8 +99,10 @@ class KubernetesCatalogWatchTest { createInSpecificNamespaceWatcher(); when(FILTER_WATCH_LIST_DELETABLE.list()) - .thenReturn(createSingleEndpointEndpointListByPodName("api-pod", "other-pod")) - .thenReturn(createSingleEndpointEndpointListByPodName("other-pod", "api-pod")); + .thenReturn(createSingleEndpointEndpointListByPodName("test", "api-pod", "other-pod")) + .thenReturn(createSingleEndpointEndpointListByPodName("test", "other-pod", "api-pod")); + mockServicesCall("api-pod", "test"); + mockServicesCall("other-pod", "test"); kubernetesCatalogWatch.catalogServicesWatch(); // second execution on shuffleServices @@ -108,8 +117,11 @@ class KubernetesCatalogWatchTest { createInAllNamespaceWatcher(); when(FILTER_WATCH_LIST_DELETABLE.list()) - .thenReturn(createSingleEndpointEndpointListByPodName("api-pod", "other-pod")) - .thenReturn(createSingleEndpointEndpointListByPodName("other-pod", "api-pod")); + .thenReturn(createSingleEndpointEndpointListByPodName("test", "api-pod", "other-pod")) + .thenReturn(createSingleEndpointEndpointListByPodName("test", "other-pod", "api-pod")); + + mockServicesCall("api-pod", "test"); + mockServicesCall("other-pod", "test"); kubernetesCatalogWatch.catalogServicesWatch(); // second execution on shuffleServices @@ -124,8 +136,10 @@ class KubernetesCatalogWatchTest { createInSpecificNamespaceWatcher(); when(FILTER_WATCH_LIST_DELETABLE.list()) - .thenReturn(createEndpointsListByServiceName("api-service", "other-service")) - .thenReturn(createEndpointsListByServiceName("other-service", "api-service")); + .thenReturn(createEndpointsListByServiceName("test", "api-service", "other-service")) + .thenReturn(createEndpointsListByServiceName("test", "other-service", "api-service")); + mockServicesCall("api-service", "test"); + mockServicesCall("other-service", "test"); kubernetesCatalogWatch.catalogServicesWatch(); // second execution on shuffleServices @@ -140,8 +154,10 @@ class KubernetesCatalogWatchTest { createInAllNamespaceWatcher(); when(FILTER_WATCH_LIST_DELETABLE.list()) - .thenReturn(createEndpointsListByServiceName("api-service", "other-service")) - .thenReturn(createEndpointsListByServiceName("other-service", "api-service")); + .thenReturn(createEndpointsListByServiceName("test", "api-service", "other-service")) + .thenReturn(createEndpointsListByServiceName("test", "other-service", "api-service")); + mockServicesCall("api-service", "test"); + mockServicesCall("other-service", "test"); kubernetesCatalogWatch.catalogServicesWatch(); // second execution on shuffleServices @@ -156,7 +172,9 @@ class KubernetesCatalogWatchTest { createInSpecificNamespaceWatcher(); when(FILTER_WATCH_LIST_DELETABLE.list()) - .thenReturn(createSingleEndpointListWithNamespace("default", "api-pod", "other-pod")); + .thenReturn(createSingleEndpointListWithNamespace("default", "other-pod", "api-pod", "other-pod")); + mockServicesCall("api-pod", "default"); + mockServicesCall("other-pod", "default"); kubernetesCatalogWatch.catalogServicesWatch(); @@ -176,7 +194,9 @@ class KubernetesCatalogWatchTest { createInAllNamespaceWatcher(); when(FILTER_WATCH_LIST_DELETABLE.list()) - .thenReturn(createSingleEndpointListWithNamespace("default", "api-pod", "other-pod")); + .thenReturn(createSingleEndpointListWithNamespace("default", "other-pod", "api-pod", "other-pod")); + mockServicesCall("api-pod", "default"); + mockServicesCall("other-pod", "default"); kubernetesCatalogWatch.catalogServicesWatch(); @@ -195,8 +215,8 @@ class KubernetesCatalogWatchTest { createInSpecificNamespaceWatcher(); - EndpointsList endpoints = createSingleEndpointEndpointListWithoutSubsets(); - + EndpointsList endpoints = createSingleEndpointEndpointListWithoutSubsets("name", "test"); + mockServicesCall("name", "test"); when(FILTER_WATCH_LIST_DELETABLE.list()).thenReturn(endpoints); kubernetesCatalogWatch.catalogServicesWatch(); @@ -211,7 +231,8 @@ class KubernetesCatalogWatchTest { createInAllNamespaceWatcher(); - EndpointsList endpoints = createSingleEndpointEndpointListWithoutSubsets(); + EndpointsList endpoints = createSingleEndpointEndpointListWithoutSubsets("name", "test"); + mockServicesCall("name", "test"); when(FILTER_WATCH_LIST_DELETABLE.list()).thenReturn(endpoints); @@ -227,7 +248,8 @@ class KubernetesCatalogWatchTest { createInSpecificNamespaceWatcher(); - EndpointsList endpoints = createSingleEndpointEndpointListByPodName("api-pod"); + EndpointsList endpoints = createSingleEndpointEndpointListByPodName("test", "api-pod"); + mockServicesCall("api-pod", "test"); endpoints.getItems().get(0).getSubsets().get(0).setAddresses(null); when(FILTER_WATCH_LIST_DELETABLE.list()).thenReturn(endpoints); @@ -244,7 +266,8 @@ class KubernetesCatalogWatchTest { createInAllNamespaceWatcher(); - EndpointsList endpoints = createSingleEndpointEndpointListByPodName("api-pod"); + EndpointsList endpoints = createSingleEndpointEndpointListByPodName("test", "api-pod"); + mockServicesCall("api-pod", "test"); endpoints.getItems().get(0).getSubsets().get(0).setAddresses(null); when(FILTER_WATCH_LIST_DELETABLE.list()).thenReturn(endpoints); @@ -261,7 +284,8 @@ class KubernetesCatalogWatchTest { createInSpecificNamespaceWatcher(); - EndpointsList endpoints = createSingleEndpointEndpointListByPodName("api-pod"); + EndpointsList endpoints = createSingleEndpointEndpointListByPodName("test", "api-pod"); + mockServicesCall("api-pod", "test"); endpoints.getItems().get(0).getSubsets().get(0).getAddresses().get(0).setTargetRef(null); when(FILTER_WATCH_LIST_DELETABLE.list()).thenReturn(endpoints); @@ -278,7 +302,8 @@ class KubernetesCatalogWatchTest { createInAllNamespaceWatcher(); - EndpointsList endpoints = createSingleEndpointEndpointListByPodName("api-pod"); + EndpointsList endpoints = createSingleEndpointEndpointListByPodName("test", "api-pod"); + mockServicesCall("api-pod", "test"); endpoints.getItems().get(0).getSubsets().get(0).getAddresses().get(0).setTargetRef(null); when(FILTER_WATCH_LIST_DELETABLE.list()).thenReturn(endpoints); @@ -290,34 +315,38 @@ class KubernetesCatalogWatchTest { verify(APPLICATION_EVENT_PUBLISHER).publishEvent(any(HeartbeatEvent.class)); } - private EndpointsList createEndpointsListByServiceName(String... serviceNames) { - List endpoints = stream(serviceNames).map(s -> createEndpointsByPodName(s + "-singlePodUniqueId")) - .collect(Collectors.toList()); + private EndpointsList createEndpointsListByServiceName(String namespace, String... serviceNames) { + List endpoints = stream(serviceNames) + .map(s -> createEndpointsByPodName(namespace, s + "-singlePodUniqueId")).collect(Collectors.toList()); EndpointsList endpointsList = new EndpointsList(); endpointsList.setItems(endpoints); return endpointsList; } - private EndpointsList createSingleEndpointEndpointListWithoutSubsets() { - Endpoints endpoints = new Endpoints(); + private EndpointsList createSingleEndpointEndpointListWithoutSubsets(String name, String namespace) { + Endpoints endpoints = new EndpointsBuilder().withNewMetadata().withName(name).withNamespace(namespace) + .endMetadata().build(); EndpointsList endpointsList = new EndpointsList(); endpointsList.setItems(Collections.singletonList(endpoints)); return endpointsList; } - private EndpointsList createSingleEndpointEndpointListByPodName(String... podNames) { + private EndpointsList createSingleEndpointEndpointListByPodName(String namespace, String... podNames) { Endpoints endpoints = new Endpoints(); endpoints.setSubsets(createSubsetsByPodName(podNames)); + endpoints.setMetadata(new ObjectMetaBuilder().withNamespace(namespace).build()); EndpointsList endpointsList = new EndpointsList(); endpointsList.setItems(Collections.singletonList(endpoints)); return endpointsList; } - private EndpointsList createSingleEndpointListWithNamespace(String namespace, String... podNames) { - Endpoints endpoints = new Endpoints(); + private EndpointsList createSingleEndpointListWithNamespace(String namespace, String endpointsName, + String... podNames) { + Endpoints endpoints = new EndpointsBuilder().withNewMetadata().withNamespace(namespace).withName(endpointsName) + .and().build(); endpoints.setSubsets(createSubsetsWithNamespace(namespace, podNames)); EndpointsList endpointsList = new EndpointsList(); @@ -325,8 +354,8 @@ class KubernetesCatalogWatchTest { return endpointsList; } - private Endpoints createEndpointsByPodName(String podName) { - Endpoints endpoints = new Endpoints(); + private Endpoints createEndpointsByPodName(String namespace, String podName) { + Endpoints endpoints = new EndpointsBuilder().withNewMetadata().withNamespace(namespace).and().build(); endpoints.setSubsets(createSubsetsByPodName(podName)); return endpoints; } @@ -401,4 +430,16 @@ class KubernetesCatalogWatchTest { when(FILTER_NESTED.endFilter()).thenReturn(FILTER_WATCH_LIST_DELETABLE); } + private void mockServicesCall(String name, String namespace) { + MixedOperation> mixedOperation = Mockito + .mock(MixedOperation.class); + NonNamespaceOperation> nonNamespaceOperation = Mockito + .mock(NonNamespaceOperation.class); + when(CLIENT.services()).thenReturn(mixedOperation); + when(mixedOperation.inNamespace(namespace)).thenReturn(nonNamespaceOperation); + when(nonNamespaceOperation.list()).thenReturn(new ServiceListBuilder().withItems( + new ServiceBuilder().withNewMetadata().withName(name).withNamespace(namespace).endMetadata().build()) + .build()); + } + } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java index 19908c9f..8d4e00ea 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesDiscoveryClientFilterMetadataTest.java @@ -30,6 +30,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServiceList; +import io.fabric8.kubernetes.api.model.ServiceListBuilder; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.ServicePortBuilder; import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; @@ -245,9 +246,12 @@ class KubernetesDiscoveryClientFilterMetadataTest { when(serviceResource.get()).thenReturn(service); when(CLIENT.services()).thenReturn(serviceOperation); when(CLIENT.services().inNamespace(anyString())).thenReturn(serviceOperation); + when(serviceOperation.list()).thenReturn(new ServiceListBuilder().withItems(new ServiceBuilder() + .withNewMetadata().withName(serviceId).withNamespace(namespace).endMetadata().build()).build()); ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setNamespace(namespace); + objectMeta.setName(serviceId); Endpoints endpoints = new EndpointsBuilder().withMetadata(objectMeta).addNewSubset() .addAllToPorts(getEndpointPorts(ports)).addNewAddress().endAddress().endSubset().build(); diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java index 192db70f..33625931 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java @@ -158,7 +158,8 @@ class KubernetesReactiveDiscoveryClientTests { kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services/existing-service") .andReturn(200, services.getItems().get(0)).once(); - Metadata metadata = new Metadata(false, null, false, null, true, "port."); + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services").andReturn(200, services).once(); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient(kubernetesClient, KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services); Flux instances = client.getInstances("existing-service"); @@ -174,7 +175,7 @@ class KubernetesReactiveDiscoveryClientTests { .withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).endItem().build()) .once(); - Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace("test") + Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("existing-service").withNamespace("test") .endMetadata().addNewSubset().addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP").endSubset().build(); @@ -208,7 +209,7 @@ class KubernetesReactiveDiscoveryClientTests { .withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).endItem().build()) .once(); - Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace("test") + Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("existing-service").withNamespace("test") .endMetadata().addNewSubset().addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") .addNewPort("https", "https_tcp", 443, "TCP").endSubset().build(); @@ -243,9 +244,9 @@ class KubernetesReactiveDiscoveryClientTests { .withSpec(new ServiceSpecBuilder().withType("ExternalName").build()).endItem().build()) .once(); - Endpoints endpoints = new EndpointsBuilder().withNewMetadata().withName("endpoint").withNamespace("test") - .endMetadata().addNewSubset().addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") - .endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") + Endpoints endpoints = new EndpointsBuilder().withNewMetadata().withName("existing-service") + .withNamespace("test").endMetadata().addNewSubset().addNewAddress().withIp("ip1").withNewTargetRef() + .withUid("uid1").endTargetRef().endAddress().addNewPort("http", "http_tcp", 80, "TCP") .addNewPort("https", "https_tcp", 443, "TCP").endSubset().build(); EndpointsList endpointsList = new EndpointsList(); 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/configmap/Fabric8DiscoveryFilterIT.java new file mode 100644 index 00000000..3ce43cd3 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/configmap/Fabric8DiscoveryFilterIT.java @@ -0,0 +1,252 @@ +/* + * 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.fabric8.configmap; + +import java.io.InputStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.networking.v1.Ingress; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.fabric8_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; + +class Fabric8DiscoveryFilterIT { + + private static final String FILTER_BOTH_NAMESPACES = "#root.metadata.namespace matches '^.*uat$'"; + + private static final String FILTER_SINGLE_NAMESPACE = "#root.metadata.namespace matches 'a-uat$'"; + + private static final String NAMESPACE_A_UAT = "a-uat"; + + private static final String NAMESPACE_B_UAT = "b-uat"; + + private static final String NAMESPACE = "default"; + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-fabric8-client-discovery"; + + private static KubernetesClient client; + + 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); + client = util.client(); + util.setUp(NAMESPACE); + } + + @BeforeEach + void beforeEach() { + util.createNamespace(NAMESPACE_A_UAT); + util.createNamespace(NAMESPACE_B_UAT); + util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.CREATE); + util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.CREATE); + } + + @AfterEach + void afterEach() { + util.wiremock(NAMESPACE_A_UAT, "/wiremock", Phase.DELETE); + util.wiremock(NAMESPACE_B_UAT, "/wiremock", Phase.DELETE); + util.deleteNamespace(NAMESPACE_A_UAT); + util.deleteNamespace(NAMESPACE_B_UAT); + } + + @AfterAll + static void after() throws Exception { + Commons.cleanUp(IMAGE_NAME, K3S); + } + + /** + *
+	 *     - service "wiremock" is present in namespace "a-uat"
+	 *     - service "wiremock" is present in namespace "b-uat"
+	 *
+	 *     - we search with a predicate : "#root.metadata.namespace matches '^uat.*$'"
+	 *
+	 *     As such, both services are found via 'getInstances' call.
+	 * 
+ */ + @Test + void filterMatchesBothNamespacesViaThePredicate() { + + manifests(Phase.CREATE, FILTER_BOTH_NAMESPACES); + + WebClient clientServices = builder().baseUrl("http://localhost/services").build(); + + @SuppressWarnings("unchecked") + List services = (List) clientServices.method(HttpMethod.GET).retrieve().bodyToMono(List.class) + .retryWhen(retrySpec()).block(); + + Assertions.assertEquals(services.size(), 1); + Assertions.assertTrue(services.contains("service-wiremock")); + + WebClient client = builder().baseUrl("http://localhost/service-instances/service-wiremock").build(); + List serviceInstances = client.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(serviceInstances.size(), 2); + List sorted = serviceInstances.stream() + .sorted(Comparator.comparing(DefaultKubernetesServiceInstance::getNamespace)).toList(); + + DefaultKubernetesServiceInstance first = sorted.get(0); + Assertions.assertEquals(first.getServiceId(), "service-wiremock"); + Assertions.assertNotNull(first.getInstanceId()); + Assertions.assertEquals(first.getPort(), 8080); + Assertions.assertEquals(first.getNamespace(), "a-uat"); + Assertions.assertEquals(first.getMetadata(), + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); + + DefaultKubernetesServiceInstance second = sorted.get(1); + Assertions.assertEquals(second.getServiceId(), "service-wiremock"); + Assertions.assertNotNull(second.getInstanceId()); + Assertions.assertEquals(second.getPort(), 8080); + Assertions.assertEquals(second.getNamespace(), "b-uat"); + Assertions.assertEquals(second.getMetadata(), + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "b-uat", "type", "ClusterIP")); + + manifests(Phase.DELETE, FILTER_BOTH_NAMESPACES); + } + + /** + *
+	 *     - service "wiremock" is present in namespace "a-uat"
+	 *     - service "wiremock" is present in namespace "b-uat"
+	 *
+	 *     - we search with a predicate : "#root.metadata.namespace matches 'a-uat$'"
+	 *
+	 *     As such, only service from 'a-uat' namespace matches.
+	 * 
+ */ + @Test + void filterMatchesOneNamespaceViaThePredicate() { + manifests(Phase.CREATE, FILTER_SINGLE_NAMESPACE); + + WebClient clientServices = builder().baseUrl("http://localhost/services").build(); + + @SuppressWarnings("unchecked") + List services = (List) clientServices.method(HttpMethod.GET).retrieve().bodyToMono(List.class) + .retryWhen(retrySpec()).block(); + + Assertions.assertEquals(services.size(), 1); + Assertions.assertTrue(services.contains("service-wiremock")); + + WebClient client = builder().baseUrl("http://localhost/service-instances/service-wiremock").build(); + List serviceInstances = client.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + + }).retryWhen(retrySpec()).block(); + + Assertions.assertEquals(serviceInstances.size(), 1); + + DefaultKubernetesServiceInstance first = serviceInstances.get(0); + Assertions.assertEquals(first.getServiceId(), "service-wiremock"); + Assertions.assertNotNull(first.getInstanceId()); + Assertions.assertEquals(first.getPort(), 8080); + Assertions.assertEquals(first.getNamespace(), "a-uat"); + Assertions.assertEquals(first.getMetadata(), + Map.of("app", "service-wiremock", "port.http", "8080", "k8s_namespace", "a-uat", "type", "ClusterIP")); + + manifests(Phase.DELETE, FILTER_SINGLE_NAMESPACE); + } + + private static void manifests(Phase phase, String serviceFilter) { + + InputStream deploymentStream = util.inputStream("fabric8-discovery-deployment.yaml"); + InputStream serviceStream = util.inputStream("fabric8-discovery-service.yaml"); + InputStream ingressStream = util.inputStream("fabric8-discovery-ingress.yaml"); + + Deployment deployment = client.apps().deployments().load(deploymentStream).get(); + List envVars = new ArrayList<>( + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); + EnvVar namespaceAUat = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0") + .withValue(NAMESPACE_A_UAT).build(); + EnvVar namespaceBUat = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1") + .withValue(NAMESPACE_B_UAT).build(); + EnvVar filter = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_FILTER") + .withValue(serviceFilter).build(); + EnvVar debug = new EnvVarBuilder() + .withName("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY").withValue("DEBUG") + .build(); + envVars.add(namespaceAUat); + envVars.add(namespaceBUat); + envVars.add(filter); + envVars.add(debug); + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); + + Service service = client.services().load(serviceStream).get(); + Ingress ingress = client.network().v1().ingresses().load(ingressStream).get(); + + if (phase.equals(Phase.CREATE)) { + client.rbac().clusterRoleBindings().resource(client.rbac().clusterRoleBindings().load(getAdminRole()).get()) + .create(); + util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); + } + else { + client.rbac().clusterRoleBindings().resource(client.rbac().clusterRoleBindings().load(getAdminRole()).get()) + .delete(); + util.deleteAndWait(NAMESPACE, deployment, service, ingress); + } + + } + + private static InputStream getAdminRole() { + return util.inputStream("namespace-filter/fabric8-cluster-admin-serviceaccount-role.yaml"); + } + + private WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + private RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(2)).filter(Objects::nonNull); + } + +} 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/configmap/Fabric8DiscoveryNamespaceFilterIT.java index 104eebb6..1951e776 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/configmap/Fabric8DiscoveryNamespaceFilterIT.java @@ -137,6 +137,8 @@ class Fabric8DiscoveryNamespaceFilterIT { util.createAndWait(NAMESPACE, null, deployment, service, ingress, true); } else { + client.rbac().clusterRoleBindings().resource(client.rbac().clusterRoleBindings().load(getAdminRole()).get()) + .delete(); util.deleteAndWait(NAMESPACE, deployment, service, ingress); }