From d1e7c63d7628738b8cafb6c878fa75aa0ccfac0d Mon Sep 17 00:00:00 2001 From: erabii Date: Mon, 12 Dec 2022 15:50:42 +0200 Subject: [PATCH] Fix 1158 (#1160) --- docs/src/main/asciidoc/discovery-client.adoc | 19 +- .../fabric8/config/Fabric8ConfigContext.java | 2 +- .../Fabric8EndpointSliceV1CatalogWatch.java | 27 +- .../Fabric8EndpointsCatalogWatch.java | 26 +- .../discovery/KubernetesCatalogWatch.java | 14 +- ...abric8EndpointsAndEndpointSlicesTests.java | 338 +++++++++++++++ ...atalogWatchEndpointSlicesSupportTests.java | 139 +++++++ ...rnetesCatalogWatchEndpointSlicesTests.java | 392 +++++------------- ...8KubernetesCatalogWatchEndpointsTests.java | 314 +++++--------- .../watch/CatalogWatchWithNamespacesIT.java | 342 +++++++++++++++ .../tests/commons/Fabric8Utils.java | 47 +++ .../main/resources/cluster/cluster-role.yaml | 4 +- 12 files changed, 1140 insertions(+), 524 deletions(-) create mode 100644 spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java create mode 100644 spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesSupportTests.java create mode 100644 spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/CatalogWatchWithNamespacesIT.java diff --git a/docs/src/main/asciidoc/discovery-client.adoc b/docs/src/main/asciidoc/discovery-client.adoc index f81533f4..441866ac 100644 --- a/docs/src/main/asciidoc/discovery-client.adoc +++ b/docs/src/main/asciidoc/discovery-client.adoc @@ -131,8 +131,23 @@ Spring Cloud Kubernetes can also watch the Kubernetes service catalog for change milliseconds (by default it is `30000`). The heartbeat event will contain the target references (and their namespaces of the addresses of all endpoints (for the exact details of what will get returned you can take a look inside `KubernetesCatalogWatch`). This is an implementation detail, and listeners of the heartbeat event should not rely on the details. Instead, they should see if there are differences between two subsequent heartbeats via `equals` method. We will take care to return a correct implementation that adheres to the equals contract. -The endpoints will be queried in either all namespaces (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`), or -we will use: xref:property-source-config.adoc#namespace-resolution[Namespace Resolution]. +The endpoints will be queried in either : + + - all namespaces (enabled via `spring.cloud.kubernetes.discovery.all-namespaces=true`) + + - specific namespaces (enabled via `spring.cloud.kubernetes.discovery.namespaces`), for example: + +``` +spring: + cloud: + kubernetes: + discovery: + namespaces: + - namespace-a + - namespace-b +``` + +- we will use: xref:property-source-config.adoc#namespace-resolution[Namespace Resolution] if the above two paths are not taken. In order to enable this functionality you need to add `@EnableScheduling` on a configuration class in your application. diff --git a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java index c8f032df..0e51ac0c 100644 --- a/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java +++ b/spring-cloud-kubernetes-fabric8-config/src/main/java/org/springframework/cloud/kubernetes/fabric8/config/Fabric8ConfigContext.java @@ -27,6 +27,6 @@ import org.springframework.core.env.Environment; * * @author wind57 */ -final record Fabric8ConfigContext(KubernetesClient client, NormalizedSource normalizedSource, String namespace, +record Fabric8ConfigContext(KubernetesClient client, NormalizedSource normalizedSource, String namespace, Environment environment) { } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointSliceV1CatalogWatch.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointSliceV1CatalogWatch.java index e79f084f..f121eebb 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointSliceV1CatalogWatch.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointSliceV1CatalogWatch.java @@ -16,6 +16,7 @@ package org.springframework.cloud.kubernetes.fabric8.discovery; +import java.util.ArrayList; import java.util.List; import java.util.function.Function; import java.util.stream.Stream; @@ -44,23 +45,25 @@ final class Fabric8EndpointSliceV1CatalogWatch public List apply(Fabric8CatalogWatchContext context) { // take only pods that have endpoints List endpointSlices; + KubernetesClient client = context.kubernetesClient(); + if (context.properties().allNamespaces()) { LOG.debug(() -> "discovering endpoints in all namespaces"); - - // can't use try with resources here as it will close the client - KubernetesClient client = context.kubernetesClient(); endpointSlices = client.discovery().v1().endpointSlices().inAnyNamespace() .withLabels(context.properties().serviceLabels()).list().getItems(); } + else if (!context.properties().namespaces().isEmpty()) { + LOG.debug(() -> "discovering endpoints in " + context.properties().namespaces()); + List inner = new ArrayList<>(context.properties().namespaces().size()); + context.properties().namespaces() + .forEach(namespace -> inner.addAll(endpointSlices(context, namespace, client))); + endpointSlices = inner; + } else { String namespace = Fabric8Utils.getApplicationNamespace(context.kubernetesClient(), null, "catalog-watcher", context.namespaceProvider()); - LOG.debug(() -> "fabric8 catalog watcher will use namespace : " + namespace); - - // can't use try with resources here as it will close the client - KubernetesClient client = context.kubernetesClient(); - endpointSlices = client.discovery().v1().endpointSlices().inNamespace(namespace) - .withLabels(context.properties().serviceLabels()).list().getItems(); + LOG.debug(() -> "discovering endpoints in namespace : " + namespace); + endpointSlices = endpointSlices(context, namespace, client); } Stream references = endpointSlices.stream().map(EndpointSlice::getEndpoints) @@ -70,4 +73,10 @@ final class Fabric8EndpointSliceV1CatalogWatch } + private List endpointSlices(Fabric8CatalogWatchContext context, String namespace, + KubernetesClient client) { + return client.discovery().v1().endpointSlices().inNamespace(namespace) + .withLabels(context.properties().serviceLabels()).list().getItems(); + } + } 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 68c426d7..15849ea7 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 @@ -16,6 +16,7 @@ package org.springframework.cloud.kubernetes.fabric8.discovery; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Function; @@ -44,25 +45,25 @@ final class Fabric8EndpointsCatalogWatch @Override public List apply(Fabric8CatalogWatchContext context) { - // take only pods that have endpoints List endpoints; + KubernetesClient client = context.kubernetesClient(); + if (context.properties().allNamespaces()) { LOG.debug(() -> "discovering endpoints in all namespaces"); - - // can't use try with resources here as it will close the client - KubernetesClient client = context.kubernetesClient(); endpoints = client.endpoints().inAnyNamespace().withLabels(context.properties().serviceLabels()).list() .getItems(); } + else if (!context.properties().namespaces().isEmpty()) { + LOG.debug(() -> "discovering endpoints in " + context.properties().namespaces()); + List inner = new ArrayList<>(context.properties().namespaces().size()); + context.properties().namespaces().forEach(namespace -> inner.addAll(endpoints(context, namespace, client))); + endpoints = inner; + } else { String namespace = Fabric8Utils.getApplicationNamespace(context.kubernetesClient(), null, "catalog-watcher", context.namespaceProvider()); - LOG.debug(() -> "fabric8 catalog watcher will use namespace : " + namespace); - - // can't use try with resources here as it will close the client - KubernetesClient client = context.kubernetesClient(); - endpoints = client.endpoints().inNamespace(namespace).withLabels(context.properties().serviceLabels()) - .list().getItems(); + LOG.debug(() -> "discovering endpoints in namespace : " + namespace); + endpoints = endpoints(context, namespace, client); } /** @@ -82,4 +83,9 @@ final class Fabric8EndpointsCatalogWatch return Fabric8CatalogWatchContext.state(references); } + private List endpoints(Fabric8CatalogWatchContext context, String namespace, KubernetesClient client) { + return client.endpoints().inNamespace(namespace).withLabels(context.properties().serviceLabels()).list() + .getItems(); + } + } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java index 180998b4..69155bcf 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesCatalogWatch.java @@ -84,6 +84,12 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware { @PostConstruct void postConstruct() { + stateGenerator = stateGenerator(); + } + + Function> stateGenerator() { + + Function> localStateGenerator; if (context.properties().useEndpointSlices()) { // can't use try with resources here as it will close the client @@ -99,14 +105,16 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware { throw new IllegalArgumentException("EndpointSlices are not supported on the cluster"); } else { - stateGenerator = new Fabric8EndpointSliceV1CatalogWatch(); + localStateGenerator = new Fabric8EndpointSliceV1CatalogWatch(); } } else { - stateGenerator = new Fabric8EndpointsCatalogWatch(); + localStateGenerator = new Fabric8EndpointsCatalogWatch(); } - LOG.debug(() -> "stateGenerator is of type: " + stateGenerator.getClass().getSimpleName()); + LOG.debug(() -> "stateGenerator is of type: " + localStateGenerator.getClass().getSimpleName()); + + return localStateGenerator; } } 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 new file mode 100644 index 00000000..a289b42c --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8EndpointsAndEndpointSlicesTests.java @@ -0,0 +1,338 @@ +/* + * Copyright 2013-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.List; +import java.util.Map; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.EndpointAddress; +import io.fabric8.kubernetes.api.model.EndpointAddressBuilder; +import io.fabric8.kubernetes.api.model.EndpointSubset; +import io.fabric8.kubernetes.api.model.EndpointSubsetBuilder; +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.discovery.v1.Endpoint; +import io.fabric8.kubernetes.api.model.discovery.v1.EndpointBuilder; +import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSlice; +import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSliceBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.ApplicationEventPublisher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * make sure that all the tests for endpoints are also handled by endpoint slices + * + * @author wind57 + */ +abstract class Fabric8EndpointsAndEndpointSlicesTests { + + static final KubernetesNamespaceProvider NAMESPACE_PROVIDER = Mockito.mock(KubernetesNamespaceProvider.class); + + static final ArgumentCaptor HEARTBEAT_EVENT_ARGUMENT_CAPTOR = ArgumentCaptor + .forClass(HeartbeatEvent.class); + + static final ApplicationEventPublisher APPLICATION_EVENT_PUBLISHER = Mockito.mock(ApplicationEventPublisher.class); + + @BeforeAll + static void setUp() { + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient().getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @AfterEach + void afterEach() { + Mockito.reset(APPLICATION_EVENT_PUBLISHER); + mockClient().discovery().v1().endpointSlices().inAnyNamespace().delete(); + mockClient().endpoints().inAnyNamespace().delete(); + } + + /** + *
+	 *
+	 *     - we have 5 pods involved in this test
+	 *     - podA in namespaceA with no labels
+	 *     - podB in namespaceA with labels {color=blue}
+	 *     - podC in namespaceA with labels {color=red}
+	 *     - podD in namespaceB with labels {color=blue}
+	 *     - podE in namespaceB with no labels
+	 *
+	 *     We set the namespace to be "namespaceA" and search for labels {color=blue}
+	 *     As a result only one pod is taken: podB
+	 *
+	 * 
+ */ + abstract void testInSpecificNamespaceWithServiceLabels(); + + /** + *
+	 *
+	 *     - we have 5 pods involved in this test
+	 *     - podA in namespaceA with no labels
+	 *     - podB in namespaceA with labels {color=blue}
+	 *     - podC in namespaceA with labels {color=red}
+	 *     - podD in namespaceB with labels {color=blue}
+	 *     - podE in namespaceB with no labels
+	 *
+	 *     We set the namespace to be "namespaceA" and search without labels
+	 *     As a result we get three pods:
+	 *       - podA in namespaceA
+	 *       - podB in namespaceA
+	 *       - pocC in namespaceA
+	 *
+	 * 
+ */ + abstract void testInSpecificNamespaceWithoutServiceLabels(); + + /** + *
+	 *
+	 *     - we have 5 pods involved in this test
+	 *     - podA in namespaceA with no labels
+	 *     - podB in namespaceA with labels {color=blue}
+	 *     - podC in namespaceA with labels {color=red}
+	 *     - podD in namespaceB with labels {color=blue}
+	 *     - podE in namespaceB with no labels
+	 *
+	 *     We search in all namespaces with labels {color=blue}
+	 *     As a result two pods are taken:
+	 *       - podB in namespaceA
+	 *       - podD in namespaceB
+	 *
+	 * 
+ */ + abstract void testInAllNamespacesWithServiceLabels(); + + /** + *
+	 *
+	 *     - we have 5 pods involved in this test
+	 *     - podA in namespaceA with no labels
+	 *     - podB in namespaceA with labels {color=blue}
+	 *     - podC in namespaceA with labels {color=red}
+	 *     - podD in namespaceB with labels {color=blue}
+	 *     - podE in namespaceB with no labels
+	 *
+	 *     We search in all namespaces without labels
+	 *     As a result we get all 5 pods
+	 *
+	 * 
+ */ + abstract void testInAllNamespacesWithoutServiceLabels(); + + /** + *
+	 *     - all-namespaces = true
+	 *     - namespaces = [namespaceB]
+	 *
+	 *     - we have 5 pods involved in this test
+	 * 	   - podA in namespaceA with no labels
+	 * 	   - podB in namespaceA with labels {color=blue}
+	 * 	   - podC in namespaceA with labels {color=red}
+	 * 	   - podD in namespaceB with labels {color=blue}
+	 * 	   - podE in namespaceB with no labels
+	 *
+	 *     We search with labels = {color = blue}
+	 *     Even if namespaces = [namespaceB], we still take podB and podD, because all-namespace=true
+	 *
+	 * 
+ */ + abstract void testAllNamespacesTrueOtherBranchesNotCalled(); + + /** + *
+	 *     - all-namespaces = false
+	 *     - namespaces = [namespaceA]
+	 *
+	 *     - we have 5 pods involved in this test
+	 * 	   - podA in namespaceA with no labels
+	 * 	   - podB in namespaceA with labels {color=blue}
+	 * 	   - podC in namespaceA with labels {color=red}
+	 * 	   - podD in namespaceB with labels {color=blue}
+	 * 	   - podE in namespaceB with no labels
+	 *
+	 *     We search with labels = {color = blue}
+	 *     Since namespaces = [namespaceA], we wil take podB, because all-namespace=false (podD is not part of the response)
+	 *
+	 * 
+ */ + abstract void testAllNamespacesFalseNamespacesPresent(); + + /** + *
+	 *     - all-namespaces = false
+	 *     - namespaces = []
+	 *
+	 *     - we have 5 pods involved in this test
+	 * 	   - podA in namespaceA with no labels
+	 * 	   - podB in namespaceA with labels {color=blue}
+	 * 	   - podC in namespaceA with labels {color=red}
+	 * 	   - podD in namespaceB with labels {color=blue}
+	 * 	   - podE in namespaceB with no labels
+	 *
+	 *     We search with labels = {color = blue}
+	 *     Since namespaces = [], we wil take podB, because all-namespace=false (podD is not part of the response)
+	 *
+	 * 
+ */ + abstract void testAllNamespacesFalseNamespacesNotPresent(); + + /** + *
+	 *     - all-namespaces = false
+	 *     - namespaces = [namespaceA, namespaceB]
+	 *
+	 *     - we have 7 pods involved in this test
+	 * 	   - podA in namespaceA with no labels
+	 * 	   - podB in namespaceA with labels {color=blue}
+	 * 	   - podC in namespaceA with labels {color=red}
+	 * 	   - podD in namespaceB with labels {color=blue}
+	 * 	   - podE in namespaceB with no labels
+	 * 	   - podF in namespaceB with labels {color=blue}
+	 * 	   - podO in namespaceC with labels {color=blue}
+	 *
+	 *     We search with labels = {color = blue}
+	 *     Since namespaces = [namespaceA, namespaceB], we wil take podB, podD and podF,
+	 *     but will not take podO
+	 *
+	 * 
+ */ + abstract void testTwoNamespacesOutOfThree(); + + KubernetesCatalogWatch createWatcherInAllNamespacesWithLabels(Map labels, Set namespaces, + boolean endpointSlices) { + + boolean allNamespaces = true; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, namespaces, + true, 60, false, "", Set.of(), labels, "", null, 0, endpointSlices); + KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient(), properties, NAMESPACE_PROVIDER); + + if (endpointSlices) { + watch = Mockito.spy(watch); + Mockito.doReturn(new Fabric8EndpointSliceV1CatalogWatch()).when(watch).stateGenerator(); + } + + watch.postConstruct(); + watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); + return watch; + + } + + KubernetesCatalogWatch createWatcherInSpecificNamespaceWithLabels(String namespace, Map labels, + boolean endpointSlices) { + + when(NAMESPACE_PROVIDER.getNamespace()).thenReturn(namespace); + + boolean allNamespaces = false; + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, allNamespaces, + Set.of(namespace), true, 60, false, "", Set.of(), labels, "", null, 0, endpointSlices); + KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient(), properties, NAMESPACE_PROVIDER); + + if (endpointSlices) { + watch = Mockito.spy(watch); + Mockito.doReturn(new Fabric8EndpointSliceV1CatalogWatch()).when(watch).stateGenerator(); + } + + watch.postConstruct(); + watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); + return watch; + + } + + KubernetesCatalogWatch createWatcherInSpecificNamespacesWithLabels(Set namespaces, + Map labels, boolean endpointSlices) { + + // all-namespaces = false + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, namespaces, true, 60, + false, "", Set.of(), labels, "", null, 0, false); + KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient(), properties, NAMESPACE_PROVIDER); + + if (endpointSlices) { + watch = Mockito.spy(watch); + Mockito.doReturn(new Fabric8EndpointSliceV1CatalogWatch()).when(watch).stateGenerator(); + } + + watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); + watch.postConstruct(); + return watch; + + } + + void endpoints(String namespace, Map labels, String podName) { + + EndpointAddress endpointAddress = new EndpointAddressBuilder() + .withTargetRef(new ObjectReferenceBuilder().withName(podName).withNamespace(namespace).build()).build(); + + EndpointSubset endpointSubset = new EndpointSubsetBuilder().withAddresses(List.of(endpointAddress)).build(); + + Endpoints endpoints = new EndpointsBuilder() + .withMetadata(new ObjectMetaBuilder().withLabels(labels).withName("endpoints-" + podName).build()) + .withSubsets(List.of(endpointSubset)).build(); + mockClient().endpoints().inNamespace(namespace).create(endpoints); + } + + static void endpointSlice(String namespace, Map labels, String podName) { + + Endpoint endpoint = new EndpointBuilder() + .withTargetRef(new ObjectReferenceBuilder().withName(podName).withNamespace(namespace).build()).build(); + + EndpointSlice slice = new EndpointSliceBuilder().withMetadata(new ObjectMetaBuilder().withNamespace(namespace) + .withName("slice-" + podName).withLabels(labels).build()).withEndpoints(endpoint).build(); + + mockClient().discovery().v1().endpointSlices().inNamespace(namespace).resource(slice).create(); + + } + + static void invokeAndAssert(KubernetesCatalogWatch watch, List state) { + watch.catalogServicesWatch(); + + verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); + + HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); + assertThat(event.getValue()).isInstanceOf(List.class); + + assertThat(event.getValue()).isEqualTo(state); + } + + // work-around for : https://github.com/fabric8io/kubernetes-client/issues/4649 + private static KubernetesClient mockClient() { + return Fabric8KubernetesCatalogWatchEndpointsTests.endpointsMockClient() != null + ? Fabric8KubernetesCatalogWatchEndpointsTests.endpointsMockClient() + : Fabric8KubernetesCatalogWatchEndpointSlicesTests.endpointSlicesMockClient(); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesSupportTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesSupportTests.java new file mode 100644 index 00000000..54cfbc8d --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesSupportTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2013-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.Map; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.APIGroup; +import io.fabric8.kubernetes.api.model.APIGroupBuilder; +import io.fabric8.kubernetes.api.model.APIGroupList; +import io.fabric8.kubernetes.api.model.APIGroupListBuilder; +import io.fabric8.kubernetes.api.model.APIResource; +import io.fabric8.kubernetes.api.model.APIResourceBuilder; +import io.fabric8.kubernetes.api.model.APIResourceList; +import io.fabric8.kubernetes.api.model.APIResourceListBuilder; +import io.fabric8.kubernetes.api.model.GroupVersionForDiscovery; +import io.fabric8.kubernetes.api.model.GroupVersionForDiscoveryBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; + +/** + * Tests that only assert the needed support for EndpointSlices in the cluster. + * + * @author wind57 + */ +@EnableKubernetesMockClient +class Fabric8KubernetesCatalogWatchEndpointSlicesSupportTests { + + private static final KubernetesNamespaceProvider NAMESPACE_PROVIDER = Mockito + .mock(KubernetesNamespaceProvider.class); + + private static KubernetesMockServer mockServer; + + private static KubernetesClient mockClient; + + @BeforeEach + void beforeEach() { + mockServer.clearExpectations(); + } + + @Test + void testEndpointSlicesEnabledButNotSupportedViaApiGroups() { + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, "", Set.of(), Map.of(), "", null, 0, true); + + APIGroupList groupList = new APIGroupListBuilder().build(); + mockServer.expect().withPath("/apis").andReturn(200, groupList).always(); + + KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, NAMESPACE_PROVIDER); + IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, watch::postConstruct); + Assertions.assertEquals("EndpointSlices are not supported on the cluster", ex.getMessage()); + } + + /** + *
+	 *     - endpoint slices are enabled, but are not supported by the cluster, as such we will fail
+	 *       with an IllegalArgumentException
+	 *     - ApiVersions is empty
+	 * 
+ */ + @Test + void testEndpointSlicesEnabledButNotSupportedViaApiVersions() { + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, "", Set.of(), Map.of(), "", null, 0, true); + + GroupVersionForDiscovery forDiscovery = new GroupVersionForDiscoveryBuilder() + .withGroupVersion("discovery.k8s.io/v1").build(); + APIGroup apiGroup = new APIGroupBuilder().withApiVersion("v1").withVersions(forDiscovery).build(); + APIGroupList groupList = new APIGroupListBuilder().withGroups(apiGroup).build(); + mockServer.expect().withPath("/apis").andReturn(200, groupList).always(); + + APIResourceList apiResourceList = new APIResourceListBuilder().build(); + mockServer.expect().withPath("/apis/discovery.k8s.io/v1").andReturn(200, apiResourceList).always(); + + KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, NAMESPACE_PROVIDER); + IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, watch::postConstruct); + Assertions.assertEquals("EndpointSlices are not supported on the cluster", ex.getMessage()); + } + + /** + * endpoint slices are disabled via properties, as such we will use a catalog watch + * based on Endpoints + */ + @Test + void testEndpointsSupport() { + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, "", Set.of(), Map.of(), "", null, 0, false); + KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, NAMESPACE_PROVIDER); + + Assertions.assertEquals(Fabric8EndpointsCatalogWatch.class, watch.stateGenerator().getClass()); + } + + /** + * endpoint slices are enabled via properties and supported by the cluster, as such we + * will use a catalog watch based on Endpoint Slices + */ + @Test + void testEndpointSlicesSupport() { + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, + false, "", Set.of(), Map.of(), "", null, 0, true); + KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, NAMESPACE_PROVIDER); + + GroupVersionForDiscovery forDiscovery = new GroupVersionForDiscoveryBuilder() + .withGroupVersion("discovery.k8s.io/v1").build(); + APIGroup apiGroup = new APIGroupBuilder().withApiVersion("v1").withVersions(forDiscovery).build(); + APIGroupList groupList = new APIGroupListBuilder().withGroups(apiGroup).build(); + mockServer.expect().withPath("/apis").andReturn(200, groupList).always(); + + APIResource apiResource = new APIResourceBuilder().withGroup("discovery.k8s.io/v1").withKind("EndpointSlice") + .build(); + APIResourceList apiResourceList = new APIResourceListBuilder().withResources(apiResource).build(); + mockServer.expect().withPath("/apis/discovery.k8s.io/v1").andReturn(200, apiResourceList).always(); + + Assertions.assertEquals(Fabric8EndpointSliceV1CatalogWatch.class, watch.stateGenerator().getClass()); + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesTests.java index f1cbf6f0..62b213fb 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesTests.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8KubernetesCatalogWatchEndpointSlicesTests.java @@ -20,359 +20,167 @@ import java.util.List; import java.util.Map; import java.util.Set; -import io.fabric8.kubernetes.api.model.APIGroup; -import io.fabric8.kubernetes.api.model.APIGroupBuilder; -import io.fabric8.kubernetes.api.model.APIGroupList; -import io.fabric8.kubernetes.api.model.APIGroupListBuilder; -import io.fabric8.kubernetes.api.model.APIResource; -import io.fabric8.kubernetes.api.model.APIResourceBuilder; -import io.fabric8.kubernetes.api.model.APIResourceList; -import io.fabric8.kubernetes.api.model.APIResourceListBuilder; -import io.fabric8.kubernetes.api.model.GroupVersionForDiscovery; -import io.fabric8.kubernetes.api.model.GroupVersionForDiscoveryBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder; -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; -import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSliceBuilder; -import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSliceList; -import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSliceListBuilder; -import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; -import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.springframework.cloud.client.discovery.event.HeartbeatEvent; -import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; -import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.context.ApplicationEventPublisher; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; /** * Some tests that use the fabric8 mock client, using EndpointSlices * * @author wind57 */ -@EnableKubernetesMockClient -class Fabric8KubernetesCatalogWatchEndpointSlicesTests { +@EnableKubernetesMockClient(crud = true, https = false) +class Fabric8KubernetesCatalogWatchEndpointSlicesTests extends Fabric8EndpointsAndEndpointSlicesTests { - private final KubernetesNamespaceProvider namespaceProvider = Mockito.mock(KubernetesNamespaceProvider.class); - - private static final ArgumentCaptor HEARTBEAT_EVENT_ARGUMENT_CAPTOR = ArgumentCaptor - .forClass(HeartbeatEvent.class); - - private static final ApplicationEventPublisher APPLICATION_EVENT_PUBLISHER = Mockito - .mock(ApplicationEventPublisher.class); + private static final Boolean ENDPOINT_SLICES = true; private static KubernetesClient mockClient; - private static KubernetesMockServer mockServer; - - @BeforeAll - static void setUp() { - // Configure the kubernetes master url to point to the mock server - System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); - System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); - System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); - System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); - System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); - System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); - - } - - @AfterEach - void beforeEach() { - mockServer.clearExpectations(); - } - - @AfterEach - void afterEach() { - Mockito.reset(APPLICATION_EVENT_PUBLISHER); - } - - /** - *
-	 *     - we have 2 pods involved in this test
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *
-	 *     We set the namespace to be "namespaceA" and search for labels {color=blue}
-	 *     As a result only one pod is taken: podB
-	 * 
- */ @Test - void testEndpointSlicesInSpecificNamespaceWithServiceLabels() { + @Override + void testInSpecificNamespaceWithServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceAndLabels("namespaceA", Map.of("color", "blue")); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceWithLabels("namespaceA", Map.of("color", "blue"), + ENDPOINT_SLICES); - EndpointSlice sliceB = createSingleEndpointWithEndpointSlices("namespaceA", Map.of("color", "blue"), "podB"); - EndpointSliceList listInNamespaceA = new EndpointSliceListBuilder().withItems(sliceB).build(); + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); - mockServer.expect() - .withPath("/apis/discovery.k8s.io/v1/namespaces/namespaceA/endpointslices?labelSelector=color%3Dblue") - .andReturn(200, listInNamespaceA).always(); - - // this is mocked, but never supposed to be called - EndpointSlice sliceD = createSingleEndpointWithEndpointSlices("namespaceB", Map.of("color", "blue"), "podD"); - EndpointSliceList listInNamespaceB = new EndpointSliceListBuilder().withItems(sliceD).build(); - mockServer.expect() - .withPath("/apis/discovery.k8s.io/v1/namespaces/namespaceB/endpointslices?labelSelector=color%3Dblue") - .andReturn(200, listInNamespaceB); - - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podB", "namespaceA")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"))); } - /** - *
-	 *
-	 *     - we have 5 pods involved in this test
-	 *     - podA in namespaceA with no labels
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podC in namespaceA with labels {color=red}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *     - podE in namespaceB with no labels
-	 *
-	 *     We set the namespace to be "namespaceA" and search without labels
-	 *     As a result we get three pods:
-	 *       - podA in namespaceA
-	 *       - podB in namespaceA
-	 *       - pocC in namespaceA
-	 *
-	 * 
- */ @Test - void testEndpointsInSpecificNamespaceWithoutServiceLabels() { + @Override + void testInSpecificNamespaceWithoutServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceAndLabels("namespaceA", Map.of()); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceWithLabels("namespaceA", Map.of(), + ENDPOINT_SLICES); - EndpointSlice sliceA = createSingleEndpointWithEndpointSlices("namespaceA", Map.of(), "podA"); - EndpointSlice sliceB = createSingleEndpointWithEndpointSlices("namespaceA", Map.of("color", "blue"), "podB"); - EndpointSlice sliceC = createSingleEndpointWithEndpointSlices("namespaceA", Map.of("color", "red"), "podC"); - EndpointSliceList listInNamespaceA = new EndpointSliceListBuilder().withItems(sliceA, sliceB, sliceC).build(); - mockServer.expect().withPath("/apis/discovery.k8s.io/v1/namespaces/namespaceA/endpointslices") - .andReturn(200, listInNamespaceA).once(); + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); - // this is mocked, but never supposed to be called - EndpointSlice sliceD = createSingleEndpointWithEndpointSlices("namespaceB", Map.of("color", "blue"), "podD"); - EndpointSlice sliceE = createSingleEndpointWithEndpointSlices("namespaceB", Map.of(), "podE"); - EndpointSliceList listInNamespaceB = new EndpointSliceListBuilder().withItems(sliceD, sliceE).build(); - mockServer.expect().withPath("/apis/discovery.k8s.io/v1/namespaces/namespaceB/endpointslices") - .andReturn(200, listInNamespaceB).once(); - - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podA", "namespaceA"), - new EndpointNameAndNamespace("podB", "namespaceA"), new EndpointNameAndNamespace("podC", "namespaceA")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + invokeAndAssert(watch, + List.of(new EndpointNameAndNamespace("podA", "namespaceA"), + new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podC", "namespaceA"))); } - /** - *
-	 *
-	 *     - we have 2 pods involved in this test
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *
-	 *     We search in all namespaces with labels {color=blue}
-	 *     As a result two pods are taken:
-	 *       - podB in namespaceA
-	 *       - podD in namespaceB
-	 *
-	 * 
- */ @Test - void testEndpointsInAllNamespacesWithServiceLabels() { + @Override + void testInAllNamespacesWithServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInAllNamespacesAndLabels(Map.of("color", "blue")); + KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of("color", "blue"), Set.of(), + ENDPOINT_SLICES); - EndpointSlice sliceB = createSingleEndpointWithEndpointSlices("namespaceA", Map.of("color", "blue"), "podB"); - EndpointSlice sliceD = createSingleEndpointWithEndpointSlices("namespaceB", Map.of("color", "blue"), "podD"); - EndpointSliceList listInAllNamespaces = new EndpointSliceListBuilder().withItems(sliceB, sliceD).build(); - mockServer.expect().withPath("/apis/discovery.k8s.io/v1/endpointslices?labelSelector=color%3Dblue") - .andReturn(200, listInAllNamespaces).once(); + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podB", "namespaceA"), - new EndpointNameAndNamespace("podD", "namespaceB")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podD", "namespaceB"))); } - /** - *
-	 *
-	 *     - we have 5 pods involved in this test
-	 *     - podA in namespaceA with no labels
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podC in namespaceA with labels {color=red}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *     - podE in namespaceB with no labels
-	 *
-	 *     We search in all namespaces without labels
-	 *     As a result we get all 5 pods
-	 *
-	 * 
- */ @Test - void testEndpointsInAllNamespacesWithoutServiceLabels() { + @Override + void testInAllNamespacesWithoutServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInAllNamespacesAndLabels(Map.of()); + KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of(), Set.of(), ENDPOINT_SLICES); - EndpointSlice sliceA = createSingleEndpointWithEndpointSlices("namespaceA", Map.of(), "podA"); - EndpointSlice sliceB = createSingleEndpointWithEndpointSlices("namespaceA", Map.of("color", "blue"), "podB"); - EndpointSlice sliceC = createSingleEndpointWithEndpointSlices("namespaceA", Map.of("color", "red"), "podC"); - EndpointSlice sliceD = createSingleEndpointWithEndpointSlices("namespaceB", Map.of("color", "blue"), "podD"); - EndpointSlice sliceE = createSingleEndpointWithEndpointSlices("namespaceB", Map.of(), "podE"); - EndpointSliceList listInAllNamespaces = new EndpointSliceListBuilder() - .withItems(sliceA, sliceB, sliceC, sliceD, sliceE).build(); - mockServer.expect().withPath("/apis/discovery.k8s.io/v1/endpointslices").andReturn(200, listInAllNamespaces) - .once(); + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podA", "namespaceA"), + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podA", "namespaceA"), new EndpointNameAndNamespace("podB", "namespaceA"), new EndpointNameAndNamespace("podC", "namespaceA"), - new EndpointNameAndNamespace("podD", "namespaceB"), new EndpointNameAndNamespace("podE", "namespaceB")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + new EndpointNameAndNamespace("podD", "namespaceB"), + new EndpointNameAndNamespace("podE", "namespaceB"))); } - /** - *
-	 *     - endpoint slices are enabled, but are not supported by the cluster, as such we will fail
-	 *       with an IllegalArgumentException
-	 *     - ApiGroups is empty
-	 * 
- */ @Test - void testEndpointSlicesEnabledButNotSupportedViaApiGroups() { - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, - false, "", Set.of(), Map.of(), "", null, 0, true); + @Override + void testAllNamespacesTrueOtherBranchesNotCalled() { - APIGroupList groupList = new APIGroupListBuilder().build(); - mockServer.expect().withPath("/apis").andReturn(200, groupList).always(); + KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of("color", "blue"), Set.of("B"), + ENDPOINT_SLICES); - KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider); - IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, watch::postConstruct); - Assertions.assertEquals("EndpointSlices are not supported on the cluster", ex.getMessage()); + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); + + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podD", "namespaceB"))); } - /** - *
-	 *     - endpoint slices are enabled, but are not supported by the cluster, as such we will fail
-	 *       with an IllegalArgumentException
-	 *     - ApiVersions is empty
-	 * 
- */ @Test - void testEndpointSlicesEnabledButNotSupportedViaApiVersions() { - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, - false, "", Set.of(), Map.of(), "", null, 0, true); + @Override + void testAllNamespacesFalseNamespacesPresent() { - GroupVersionForDiscovery forDiscovery = new GroupVersionForDiscoveryBuilder() - .withGroupVersion("discovery.k8s.io/v1").build(); - APIGroup apiGroup = new APIGroupBuilder().withApiVersion("v1").withVersions(forDiscovery).build(); - APIGroupList groupList = new APIGroupListBuilder().withGroups(apiGroup).build(); - mockServer.expect().withPath("/apis").andReturn(200, groupList).always(); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespacesWithLabels(Set.of("namespaceA"), + Map.of("color", "blue"), ENDPOINT_SLICES); - APIResourceList apiResourceList = new APIResourceListBuilder().build(); - mockServer.expect().withPath("/apis/discovery.k8s.io/v1").andReturn(200, apiResourceList).always(); + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); - KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider); - IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, watch::postConstruct); - Assertions.assertEquals("EndpointSlices are not supported on the cluster", ex.getMessage()); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"))); } - private KubernetesCatalogWatch createWatcherInSpecificNamespaceAndLabels(String namespace, - Map labels) { + @Test + @Override + void testAllNamespacesFalseNamespacesNotPresent() { - createEndpointSlicesApiGroup(); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceWithLabels("namespaceA", Map.of("color", "blue"), + ENDPOINT_SLICES); - when(namespaceProvider.getNamespace()).thenReturn(namespace); - - // all-namespaces = false - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, - false, "", Set.of(), labels, "", null, 0, true); - - KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider); - watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); - watch.postConstruct(); - return watch; + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"))); } - private KubernetesCatalogWatch createWatcherInAllNamespacesAndLabels(Map labels) { + @Test + @Override + void testTwoNamespacesOutOfThree() { - createEndpointSlicesApiGroup(); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespacesWithLabels(Set.of("namespaceA", "namespaceB"), + Map.of("color", "blue"), ENDPOINT_SLICES); - // all-namespaces = true - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, - false, "", Set.of(), labels, "", null, 0, true); - - KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider); - watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); - watch.postConstruct(); - return watch; + endpointSlice("namespaceA", Map.of(), "podA"); + endpointSlice("namespaceA", Map.of("color", "blue"), "podB"); + endpointSlice("namespaceA", Map.of("color", "red"), "podC"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podD"); + endpointSlice("namespaceB", Map.of(), "podE"); + endpointSlice("namespaceB", Map.of("color", "blue"), "podF"); + endpointSlice("namespaceC", Map.of("color", "blue"), "podO"); + invokeAndAssert(watch, + List.of(new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podD", "namespaceB"), + new EndpointNameAndNamespace("podF", "namespaceB"))); } - private static EndpointSlice createSingleEndpointWithEndpointSlices(String namespace, Map labels, - String podName) { - - Endpoint endpoint = new EndpointBuilder() - .withTargetRef(new ObjectReferenceBuilder().withName(podName).withNamespace(namespace).build()).build(); - - return new EndpointSliceBuilder().withMetadata(new ObjectMetaBuilder().withLabels(labels).build()) - .withEndpoints(endpoint).build(); - - } - - // mock KubernetesCatalogWatch::postConstruct - private static void createEndpointSlicesApiGroup() { - - GroupVersionForDiscovery forDiscovery = new GroupVersionForDiscoveryBuilder() - .withGroupVersion("discovery.k8s.io/v1").build(); - APIGroup apiGroup = new APIGroupBuilder().withApiVersion("v1").withVersions(forDiscovery).build(); - APIGroupList groupList = new APIGroupListBuilder().withGroups(apiGroup).build(); - mockServer.expect().withPath("/apis").andReturn(200, groupList).always(); - - APIResource apiResource = new APIResourceBuilder().withKind("EndpointSlice").build(); - APIResourceList apiResourceList = new APIResourceListBuilder().withResources(apiResource).build(); - mockServer.expect().withPath("/apis/discovery.k8s.io/v1").andReturn(200, apiResourceList).always(); - + // work-around for : https://github.com/fabric8io/kubernetes-client/issues/4649 + static KubernetesClient endpointSlicesMockClient() { + return mockClient; } } 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 66c6597a..de18f413 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 @@ -20,263 +20,167 @@ import java.util.List; import java.util.Map; import java.util.Set; -import io.fabric8.kubernetes.api.model.EndpointAddress; -import io.fabric8.kubernetes.api.model.EndpointAddressBuilder; -import io.fabric8.kubernetes.api.model.EndpointSubset; -import io.fabric8.kubernetes.api.model.EndpointSubsetBuilder; -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.client.Config; 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.BeforeAll; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.springframework.cloud.client.discovery.event.HeartbeatEvent; -import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace; -import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.context.ApplicationEventPublisher; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; /** - * Some tests that use the fabric8 mock client, using Endpoints + * Tests for endpoints based catalog watch * * @author wind57 */ @EnableKubernetesMockClient(crud = true, https = false) -class Fabric8KubernetesCatalogWatchEndpointsTests { +class Fabric8KubernetesCatalogWatchEndpointsTests extends Fabric8EndpointsAndEndpointSlicesTests { - private final KubernetesNamespaceProvider namespaceProvider = Mockito.mock(KubernetesNamespaceProvider.class); - - private static final ArgumentCaptor HEARTBEAT_EVENT_ARGUMENT_CAPTOR = ArgumentCaptor - .forClass(HeartbeatEvent.class); - - private static final ApplicationEventPublisher APPLICATION_EVENT_PUBLISHER = Mockito - .mock(ApplicationEventPublisher.class); + private static final Boolean ENDPOINT_SLICES = false; private static KubernetesClient mockClient; - @BeforeAll - static void setUp() { - // Configure the kubernetes master url to point to the mock server - System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); - System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); - System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); - System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); - System.setProperty(Config.KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, "test"); - System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); - } - - @AfterEach - void afterEach() { - Mockito.reset(APPLICATION_EVENT_PUBLISHER); - mockClient.endpoints().inAnyNamespace().delete(); - } - - /** - *
-	 *
-	 *     - we have 5 pods involved in this test
-	 *     - podA in namespaceA with no labels
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podC in namespaceA with labels {color=red}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *     - podE in namespaceB with no labels
-	 *
-	 *     We set the namespace to be "namespaceA" and search for labels {color=blue}
-	 *     As a result only one pod is taken: podB
-	 *
-	 * 
- */ @Test - void testEndpointsInSpecificNamespaceWithServiceLabels() { + @Override + void testInSpecificNamespaceWithServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceAndLabels("namespaceA", Map.of("color", "blue")); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceWithLabels("namespaceA", Map.of("color", "blue"), + ENDPOINT_SLICES); - createSingleEndpoints("namespaceA", Map.of(), "podA"); - createSingleEndpoints("namespaceA", Map.of("color", "blue"), "podB"); - createSingleEndpoints("namespaceA", Map.of("color", "red"), "podC"); - createSingleEndpoints("namespaceB", Map.of("color", "blue"), "podD"); - createSingleEndpoints("namespaceB", Map.of(), "podE"); + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podB", "namespaceA")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"))); } - /** - *
-	 *
-	 *     - we have 5 pods involved in this test
-	 *     - podA in namespaceA with no labels
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podC in namespaceA with labels {color=red}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *     - podE in namespaceB with no labels
-	 *
-	 *     We set the namespace to be "namespaceA" and search without labels
-	 *     As a result we get three pods:
-	 *       - podA in namespaceA
-	 *       - podB in namespaceA
-	 *       - pocC in namespaceA
-	 *
-	 * 
- */ @Test - void testEndpointsInSpecificNamespaceWithoutServiceLabels() { + @Override + void testInSpecificNamespaceWithoutServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceAndLabels("namespaceA", Map.of()); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceWithLabels("namespaceA", Map.of(), + ENDPOINT_SLICES); - createSingleEndpoints("namespaceA", Map.of(), "podA"); - createSingleEndpoints("namespaceA", Map.of("color", "blue"), "podB"); - createSingleEndpoints("namespaceA", Map.of("color", "red"), "podC"); - createSingleEndpoints("namespaceB", Map.of("color", "blue"), "podD"); - createSingleEndpoints("namespaceB", Map.of(), "podE"); + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podA", "namespaceA"), - new EndpointNameAndNamespace("podB", "namespaceA"), new EndpointNameAndNamespace("podC", "namespaceA")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + invokeAndAssert(watch, + List.of(new EndpointNameAndNamespace("podA", "namespaceA"), + new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podC", "namespaceA"))); } - /** - *
-	 *
-	 *     - we have 5 pods involved in this test
-	 *     - podA in namespaceA with no labels
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podC in namespaceA with labels {color=red}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *     - podE in namespaceB with no labels
-	 *
-	 *     We search in all namespaces with labels {color=blue}
-	 *     As a result two pods are taken:
-	 *       - podB in namespaceA
-	 *       - podD in namespaceB
-	 *
-	 * 
- */ @Test - void testEndpointsInAllNamespacesWithServiceLabels() { + @Override + void testInAllNamespacesWithServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInAllNamespacesAndLabels(Map.of("color", "blue")); + KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of("color", "blue"), Set.of(), + ENDPOINT_SLICES); - createSingleEndpoints("namespaceA", Map.of(), "podA"); - createSingleEndpoints("namespaceA", Map.of("color", "blue"), "podB"); - createSingleEndpoints("namespaceA", Map.of("color", "red"), "podC"); - createSingleEndpoints("namespaceB", Map.of("color", "blue"), "podD"); - createSingleEndpoints("namespaceB", Map.of(), "podE"); + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podB", "namespaceA"), - new EndpointNameAndNamespace("podD", "namespaceB")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podD", "namespaceB"))); } - /** - *
-	 *
-	 *     - we have 5 pods involved in this test
-	 *     - podA in namespaceA with no labels
-	 *     - podB in namespaceA with labels {color=blue}
-	 *     - podC in namespaceA with labels {color=red}
-	 *     - podD in namespaceB with labels {color=blue}
-	 *     - podE in namespaceB with no labels
-	 *
-	 *     We search in all namespaces without labels
-	 *     As a result we get all 5 pods
-	 *
-	 * 
- */ @Test - void testEndpointsInAllNamespacesWithoutServiceLabels() { + @Override + void testInAllNamespacesWithoutServiceLabels() { - KubernetesCatalogWatch watch = createWatcherInAllNamespacesAndLabels(Map.of()); + KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of(), Set.of(), ENDPOINT_SLICES); - createSingleEndpoints("namespaceA", Map.of(), "podA"); - createSingleEndpoints("namespaceA", Map.of("color", "blue"), "podB"); - createSingleEndpoints("namespaceA", Map.of("color", "red"), "podC"); - createSingleEndpoints("namespaceB", Map.of("color", "blue"), "podD"); - createSingleEndpoints("namespaceB", Map.of(), "podE"); + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); - watch.catalogServicesWatch(); - - verify(APPLICATION_EVENT_PUBLISHER).publishEvent(HEARTBEAT_EVENT_ARGUMENT_CAPTOR.capture()); - - HeartbeatEvent event = HEARTBEAT_EVENT_ARGUMENT_CAPTOR.getValue(); - assertThat(event.getValue()).isInstanceOf(List.class); - - List expectedOutput = List.of(new EndpointNameAndNamespace("podA", "namespaceA"), + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podA", "namespaceA"), new EndpointNameAndNamespace("podB", "namespaceA"), new EndpointNameAndNamespace("podC", "namespaceA"), - new EndpointNameAndNamespace("podD", "namespaceB"), new EndpointNameAndNamespace("podE", "namespaceB")); - assertThat(event.getValue()).isEqualTo(expectedOutput); + new EndpointNameAndNamespace("podD", "namespaceB"), + new EndpointNameAndNamespace("podE", "namespaceB"))); } - private KubernetesCatalogWatch createWatcherInSpecificNamespaceAndLabels(String namespace, - Map labels) { + @Test + @Override + void testAllNamespacesTrueOtherBranchesNotCalled() { - when(namespaceProvider.getNamespace()).thenReturn(namespace); + KubernetesCatalogWatch watch = createWatcherInAllNamespacesWithLabels(Map.of("color", "blue"), Set.of("B"), + ENDPOINT_SLICES); - // all-namespaces = false - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60, - false, "", Set.of(), labels, "", null, 0, false); - KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider); - watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); - watch.postConstruct(); - return watch; + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podD", "namespaceB"))); } - private KubernetesCatalogWatch createWatcherInAllNamespacesAndLabels(Map labels) { + @Test + @Override + void testAllNamespacesFalseNamespacesPresent() { - // all-namespaces = true - KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60, - false, "", Set.of(), labels, "", null, 0, false); - KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider); - watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER); - watch.postConstruct(); - return watch; + KubernetesCatalogWatch watch = createWatcherInSpecificNamespacesWithLabels(Set.of("namespaceA"), + Map.of("color", "blue"), ENDPOINT_SLICES); + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); + + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"))); } - private static void createSingleEndpoints(String namespace, Map labels, String podName) { + @Test + @Override + void testAllNamespacesFalseNamespacesNotPresent() { - EndpointAddress endpointAddress = new EndpointAddressBuilder() - .withTargetRef(new ObjectReferenceBuilder().withName(podName).withNamespace(namespace).build()).build(); + KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceWithLabels("namespaceA", Map.of("color", "blue"), + ENDPOINT_SLICES); - EndpointSubset endpointSubset = new EndpointSubsetBuilder().withAddresses(List.of(endpointAddress)).build(); + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); - Endpoints endpoints = new EndpointsBuilder() - .withMetadata(new ObjectMetaBuilder().withLabels(labels).withName("endpoints-" + podName).build()) - .withSubsets(List.of(endpointSubset)).build(); - mockClient.endpoints().inNamespace(namespace).resource(endpoints).create(); + invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA"))); + } + + @Test + @Override + void testTwoNamespacesOutOfThree() { + + KubernetesCatalogWatch watch = createWatcherInSpecificNamespacesWithLabels(Set.of("namespaceA", "namespaceB"), + Map.of("color", "blue"), ENDPOINT_SLICES); + + endpoints("namespaceA", Map.of(), "podA"); + endpoints("namespaceA", Map.of("color", "blue"), "podB"); + endpoints("namespaceA", Map.of("color", "red"), "podC"); + endpoints("namespaceB", Map.of("color", "blue"), "podD"); + endpoints("namespaceB", Map.of(), "podE"); + endpoints("namespaceB", Map.of("color", "blue"), "podF"); + endpoints("namespaceC", Map.of("color", "blue"), "podO"); + + invokeAndAssert(watch, + List.of(new EndpointNameAndNamespace("podB", "namespaceA"), + new EndpointNameAndNamespace("podD", "namespaceB"), + new EndpointNameAndNamespace("podF", "namespaceB"))); + } + + // work-around for : https://github.com/fabric8io/kubernetes-client/issues/4649 + static KubernetesClient endpointsMockClient() { + return mockClient; } } diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/CatalogWatchWithNamespacesIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/CatalogWatchWithNamespacesIT.java new file mode 100644 index 00000000..997b5005 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-fabric8-client-catalog-watcher/src/test/java/org/springframework/cloud/kubernetes/fabric8/catalog/watch/CatalogWatchWithNamespacesIT.java @@ -0,0 +1,342 @@ +/* + * Copyright 2013-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.catalog.watch; + +import java.io.InputStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +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.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +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.EndpointNameAndNamespace; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.cloud.kubernetes.integration.tests.commons.Fabric8Utils; +import org.springframework.cloud.kubernetes.integration.tests.commons.K8SUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.awaitility.Awaitility.await; + +/** + * @author wind57 + */ +class CatalogWatchWithNamespacesIT { + + private static final String APP_NAME = "spring-cloud-kubernetes-fabric8-client-catalog-watcher"; + + private static final String NAMESPACE_A = "namespacea"; + + private static final String NAMESPACE_B = "namespaceb"; + + private static final String NAMESPACE_DEFAULT = "default"; + + private static final K3sContainer K3S = Commons.container(); + + private static KubernetesClient client; + + private static String busyboxServiceNameA; + + private static String busyboxServiceNameB; + + private static String busyboxDeploymentNameA; + + private static String busyboxDeploymentNameB; + + private static String appDeploymentName; + + private static String appServiceName; + + private static String appIngressName; + + @BeforeAll + static void beforeAll() throws Exception { + K3S.start(); + Config config = Config.fromKubeconfig(K3S.getKubeConfigYaml()); + client = new KubernetesClientBuilder().withConfig(config).build(); + + Commons.validateImage(APP_NAME, K3S); + Commons.loadSpringCloudKubernetesImage(APP_NAME, K3S); + } + + @BeforeEach + void beforeEach() throws Exception { + client.namespaces().resource(new NamespaceBuilder().withNewMetadata().withName(NAMESPACE_A).and().build()).create(); + client.namespaces().resource(new NamespaceBuilder().withNewMetadata().withName(NAMESPACE_B).and().build()).create(); + Fabric8Utils.setUpClusterWide(client, NAMESPACE_DEFAULT, Set.of(NAMESPACE_DEFAULT, NAMESPACE_A, NAMESPACE_B)); + deployBusyboxManifests(); + } + + @AfterEach + void afterEach() { + Fabric8Utils.cleanUpClusterWide(client, NAMESPACE_DEFAULT, Set.of(NAMESPACE_DEFAULT, NAMESPACE_A, NAMESPACE_B)); + Fabric8Utils.deleteNamespace(client, NAMESPACE_A); + Fabric8Utils.deleteNamespace(client, NAMESPACE_B); + deleteApp(); + } + + /** + *
+	 *     - we deploy one busybox service with 2 replica pods in namespace namespacea
+	 *     - we deploy one busybox service with 2 replica pods in namespace namespaceb
+	 *     - we enable the search to be made in namespacea and default ones
+	 *     - we receive an event from KubernetesCatalogWatcher, assert what is inside it
+	 *     - delete both busybox services in namespacea and namespaceb
+	 *     - assert that we receive only spring-cloud-kubernetes-fabric8-client-catalog-watcher pod
+	 * 
+ */ + @Test + void testCatalogWatchWithEndpoints() throws Exception { + deployApp(false); + assertLogStatement("stateGenerator is of type: Fabric8EndpointsCatalogWatch"); + test(); + } + + @Test + void testCatalogWatchWithEndpointSlices() throws Exception { + deployApp(true); + assertLogStatement("stateGenerator is of type: Fabric8EndpointSliceV1CatalogWatch"); + test(); + } + + /** + * we log in debug mode the type of the StateGenerator we use, be that Endpoints or + * EndpointSlices. Here we make sure that in the test we actually use the correct + * type. + */ + private void assertLogStatement(String log) throws Exception { + String appPodName = K3S + .execInContainer("kubectl", "get", "pods", "-l", + "app=spring-cloud-kubernetes-fabric8-client-catalog-watcher", "-o=name", "--no-headers") + .getStdout(); + String allLogs = K3S.execInContainer("kubectl", "logs", appPodName.trim()).getStdout(); + Assertions.assertTrue(allLogs.contains(log)); + } + + /** + * the test is the same for both endpoints and endpoint slices, the set-up for them is + * different. + */ + @SuppressWarnings("unchecked") + private void test() { + + WebClient client = builder().baseUrl("localhost/result").build(); + EndpointNameAndNamespace[] holder = new EndpointNameAndNamespace[2]; + ResolvableType resolvableType = ResolvableType.forClassWithGenerics(List.class, EndpointNameAndNamespace.class); + + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> { + List result = (List) client.method(HttpMethod.GET) + .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())) + .retryWhen(retrySpec()).block(); + + // we get 3 pods as input, but because they are sorted by name in the catalog + // watcher implementation + // we will get the first busybox instances here. + if (result != null) { + holder[0] = result.get(0); + holder[1] = result.get(1); + return true; + } + + return false; + }); + + EndpointNameAndNamespace resultOne = holder[0]; + EndpointNameAndNamespace resultTwo = holder[1]; + + Assertions.assertNotNull(resultOne); + Assertions.assertNotNull(resultTwo); + + Assertions.assertTrue(resultOne.endpointName().contains("busybox")); + Assertions.assertTrue(resultTwo.endpointName().contains("busybox")); + Assertions.assertEquals(NAMESPACE_A, resultOne.namespace()); + Assertions.assertEquals(NAMESPACE_A, resultTwo.namespace()); + + deleteBusyboxApp(); + + // what we get after delete + EndpointNameAndNamespace[] afterDelete = new EndpointNameAndNamespace[1]; + + await().pollInterval(Duration.ofSeconds(1)).atMost(Duration.ofSeconds(240)).until(() -> { + List result = (List) client.method(HttpMethod.GET) + .retrieve().bodyToMono(ParameterizedTypeReference.forType(resolvableType.getType())) + .retryWhen(retrySpec()).block(); + + // we need to get the event from KubernetesCatalogWatch, but that happens + // on periodic bases. So in order to be sure we got the event we care about + // we wait until the result has a single entry, which means busybox was + // deleted + // + KubernetesCatalogWatch received the new update. + if (result != null && result.size() != 1) { + return false; + } + + // we will only receive one pod here, our own + if (result != null) { + afterDelete[0] = result.get(0); + return true; + } + + return false; + }); + + Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME)); + Assertions.assertEquals("default", afterDelete[0].namespace()); + + } + + private void deployBusyboxManifests() throws Exception { + + Deployment deployment = client.apps().deployments().load(getBusyboxDeployment()).get(); + + String[] image = K8SUtils.getImageFromDeployment(deployment).split(":"); + Commons.pullImage(image[0], image[1], K3S); + Commons.loadImage(image[0], image[1], "busybox", K3S); + + // namespace_a + client.apps().deployments().inNamespace(NAMESPACE_A).resource(deployment).create(); + busyboxDeploymentNameA = deployment.getMetadata().getName(); + + Service busyboxServiceA = client.services().load(getBusyboxService()).get(); + busyboxServiceNameA = busyboxServiceA.getMetadata().getName(); + client.services().inNamespace(NAMESPACE_A).resource(busyboxServiceA).create(); + + Fabric8Utils.waitForDeployment(client, busyboxDeploymentNameA, NAMESPACE_A, 2, 600); + + // namespace_b + client.apps().deployments().inNamespace(NAMESPACE_B).resource(deployment).create(); + busyboxDeploymentNameB = deployment.getMetadata().getName(); + + Service busyboxServiceB = client.services().load(getBusyboxService()).get(); + busyboxServiceNameB = busyboxServiceB.getMetadata().getName(); + client.services().inNamespace(NAMESPACE_B).resource(busyboxServiceB).create(); + + Fabric8Utils.waitForDeployment(client, busyboxDeploymentNameB, NAMESPACE_B, 2, 600); + + } + + private static void deployApp(boolean useEndpointSlices) { + + InputStream deployment = useEndpointSlices ? getEndpointSlicesAppDeployment() : getEndpointsAppDeployment(); + Deployment appDeployment = client.apps().deployments().load(deployment).get(); + + List envVars = new ArrayList<>( + appDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()); + EnvVar namespaceAEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_0") + .withValue(NAMESPACE_A).build(); + EnvVar namespaceDefaultEnvVar = new EnvVarBuilder().withName("SPRING_CLOUD_KUBERNETES_DISCOVERY_NAMESPACES_1") + .withValue(NAMESPACE_DEFAULT).build(); + envVars.add(namespaceAEnvVar); + envVars.add(namespaceDefaultEnvVar); + + appDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars); + + String version = K8SUtils.getPomVersion(); + String currentImage = appDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage(); + appDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).setImage(currentImage + ":" + version); + + client.apps().deployments().inNamespace(NAMESPACE_DEFAULT).resource(appDeployment).create(); + appDeploymentName = appDeployment.getMetadata().getName(); + + Service appService = client.services().load(getAppService()).get(); + appServiceName = appService.getMetadata().getName(); + client.services().inNamespace(NAMESPACE_DEFAULT).resource(appService).create(); + + Fabric8Utils.waitForDeployment(client, appDeploymentName, NAMESPACE_DEFAULT, 2, 600); + + Ingress appIngress = client.network().v1().ingresses().load(getAppIngress()).get(); + appIngressName = appIngress.getMetadata().getName(); + client.network().v1().ingresses().inNamespace(NAMESPACE_DEFAULT).resource(appIngress).create(); + + Fabric8Utils.waitForIngress(client, appIngressName, NAMESPACE_DEFAULT); + + } + + private void deleteBusyboxApp() { + // namespacea + Fabric8Utils.deleteDeployment(client, NAMESPACE_A, busyboxDeploymentNameA); + Fabric8Utils.deleteService(client, NAMESPACE_A, busyboxServiceNameA); + + // namespaceb + Fabric8Utils.deleteDeployment(client, NAMESPACE_B, busyboxDeploymentNameB); + Fabric8Utils.deleteService(client, NAMESPACE_B, busyboxServiceNameB); + } + + private void deleteApp() { + Fabric8Utils.deleteDeployment(client, NAMESPACE_DEFAULT, appDeploymentName); + Fabric8Utils.deleteService(client, NAMESPACE_DEFAULT, appServiceName); + Fabric8Utils.deleteIngress(client, NAMESPACE_DEFAULT, appIngressName); + } + + private static InputStream getBusyboxService() { + return Fabric8Utils.inputStream("busybox/service.yaml"); + } + + private static InputStream getBusyboxDeployment() { + return Fabric8Utils.inputStream("busybox/deployment.yaml"); + } + + /** + * deployment where support for endpoint slices is equal to false + */ + private static InputStream getEndpointsAppDeployment() { + return Fabric8Utils.inputStream("app/watcher-endpoints-deployment.yaml"); + } + + private static InputStream getEndpointSlicesAppDeployment() { + return Fabric8Utils.inputStream("app/watcher-endpoint-slices-deployment.yaml"); + } + + private static InputStream getAppIngress() { + return Fabric8Utils.inputStream("app/watcher-ingress.yaml"); + } + + private static InputStream getAppService() { + return Fabric8Utils.inputStream("app/watcher-service.yaml"); + } + + private WebClient.Builder builder() { + return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())); + } + + private RetryBackoffSpec retrySpec() { + return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull); + } + +} diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java index 3d2cefed..e1afee53 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/Fabric8Utils.java @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Endpoints; import io.fabric8.kubernetes.api.model.LoadBalancerIngress; +import io.fabric8.kubernetes.api.model.Namespace; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceAccount; @@ -126,6 +127,44 @@ public final class Fabric8Utils { } + public static void cleanUpClusterWide(KubernetesClient client, String serviceAccountNamespace, + Set namespaces) { + + InputStream clusterRoleBindingAsStream = inputStream("cluster/cluster-role.yaml"); + InputStream serviceAccountAsStream = inputStream("cluster/service-account.yaml"); + InputStream roleBindingAsStream = inputStream("cluster/role-binding.yaml"); + + ClusterRole clusterRole = client.rbac().clusterRoles().load(clusterRoleBindingAsStream).get(); + client.rbac().clusterRoles().withName(clusterRole.getMetadata().getName()).delete(); + + await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> { + ClusterRole innerClusterRole = client.rbac().clusterRoles().withName(clusterRole.getMetadata().getName()) + .get(); + return innerClusterRole == null; + }); + + ServiceAccount serviceAccount = client.serviceAccounts().load(serviceAccountAsStream).get(); + client.serviceAccounts().inNamespace(serviceAccountNamespace).withName(serviceAccount.getMetadata().getName()) + .delete(); + await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> { + ServiceAccount innerServiceAccount = client.serviceAccounts().inNamespace(serviceAccountNamespace) + .withName(serviceAccount.getMetadata().getName()).get(); + return innerServiceAccount == null; + }); + + RoleBinding roleBinding = client.rbac().roleBindings().load(roleBindingAsStream).get(); + namespaces.forEach(namespace -> { + client.rbac().roleBindings().inNamespace(namespace).withName(roleBinding.getMetadata().getName()).delete(); + + await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> { + RoleBinding innerRoleBinding = client.rbac().roleBindings().inNamespace(namespace) + .withName(roleBinding.getMetadata().getName()).get(); + return innerRoleBinding == null; + }); + }); + + } + public static void setUpIstio(KubernetesClient client, String namespace) { InputStream serviceAccountAsStream = inputStream("istio/service-account.yaml"); InputStream roleBindingAsStream = inputStream("istio/role-binding.yaml"); @@ -223,7 +262,15 @@ public final class Fabric8Utils { Ingress ingress = client.network().v1().ingresses().inNamespace(namespace).withName(name).get(); return ingress == null; }); + } + public static void deleteNamespace(KubernetesClient client, String name) { + client.namespaces().withName(name).delete(); + + await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> { + Namespace namespace = client.namespaces().withName(name).get(); + return namespace == null; + }); } private static void innerSetup(KubernetesClient client, String namespace, InputStream serviceAccountAsStream, diff --git a/spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role.yaml b/spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role.yaml index d5c3c334..6a51f31f 100644 --- a/spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role.yaml +++ b/spring-cloud-kubernetes-test-support/src/main/resources/cluster/cluster-role.yaml @@ -3,6 +3,6 @@ kind: ClusterRole metadata: name: cluster-role rules: - - apiGroups: ["", "extensions", "apps"] - resources: ["configmaps", "pods", "services", "endpoints", "secrets"] + - apiGroups: ["", "extensions", "apps", "discovery.k8s.io"] + resources: ["configmaps", "pods", "services", "endpoints", "secrets", "endpointslices"] verbs: ["get", "list", "watch"]