This commit is contained in:
erabii
2022-12-12 15:50:42 +02:00
committed by GitHub
parent 93ec00ce32
commit d1e7c63d76
12 changed files with 1140 additions and 524 deletions

View File

@@ -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.

View File

@@ -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) {
}

View File

@@ -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<EndpointNameAndNamespace> apply(Fabric8CatalogWatchContext context) {
// take only pods that have endpoints
List<EndpointSlice> 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<EndpointSlice> 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<ObjectReference> references = endpointSlices.stream().map(EndpointSlice::getEndpoints)
@@ -70,4 +73,10 @@ final class Fabric8EndpointSliceV1CatalogWatch
}
private List<EndpointSlice> endpointSlices(Fabric8CatalogWatchContext context, String namespace,
KubernetesClient client) {
return client.discovery().v1().endpointSlices().inNamespace(namespace)
.withLabels(context.properties().serviceLabels()).list().getItems();
}
}

View File

@@ -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<EndpointNameAndNamespace> apply(Fabric8CatalogWatchContext context) {
// take only pods that have endpoints
List<Endpoints> 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<Endpoints> 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> endpoints(Fabric8CatalogWatchContext context, String namespace, KubernetesClient client) {
return client.endpoints().inNamespace(namespace).withLabels(context.properties().serviceLabels()).list()
.getItems();
}
}

View File

@@ -84,6 +84,12 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
@PostConstruct
void postConstruct() {
stateGenerator = stateGenerator();
}
Function<Fabric8CatalogWatchContext, List<EndpointNameAndNamespace>> stateGenerator() {
Function<Fabric8CatalogWatchContext, List<EndpointNameAndNamespace>> 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;
}
}

View File

@@ -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<HeartbeatEvent> 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();
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
abstract void testInSpecificNamespaceWithServiceLabels();
/**
* <pre>
*
* - 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
*
* </pre>
*/
abstract void testInSpecificNamespaceWithoutServiceLabels();
/**
* <pre>
*
* - 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
*
* </pre>
*/
abstract void testInAllNamespacesWithServiceLabels();
/**
* <pre>
*
* - 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
*
* </pre>
*/
abstract void testInAllNamespacesWithoutServiceLabels();
/**
* <pre>
* - 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
*
* </pre>
*/
abstract void testAllNamespacesTrueOtherBranchesNotCalled();
/**
* <pre>
* - 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)
*
* </pre>
*/
abstract void testAllNamespacesFalseNamespacesPresent();
/**
* <pre>
* - 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)
*
* </pre>
*/
abstract void testAllNamespacesFalseNamespacesNotPresent();
/**
* <pre>
* - 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
*
* </pre>
*/
abstract void testTwoNamespacesOutOfThree();
KubernetesCatalogWatch createWatcherInAllNamespacesWithLabels(Map<String, String> labels, Set<String> 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<String, String> 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<String> namespaces,
Map<String, String> 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<String, String> 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<String, String> 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<EndpointNameAndNamespace> 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();
}
}

View File

@@ -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());
}
/**
* <pre>
* - endpoint slices are enabled, but are not supported by the cluster, as such we will fail
* with an IllegalArgumentException
* - ApiVersions is empty
* </pre>
*/
@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());
}
}

View File

@@ -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<HeartbeatEvent> 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);
}
/**
* <pre>
* - 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
* </pre>
*/
@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<EndpointNameAndNamespace> expectedOutput = List.of(new EndpointNameAndNamespace("podB", "namespaceA"));
assertThat(event.getValue()).isEqualTo(expectedOutput);
invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA")));
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
@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<EndpointNameAndNamespace> 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")));
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
@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<EndpointNameAndNamespace> 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")));
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
@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<EndpointNameAndNamespace> 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")));
}
/**
* <pre>
* - endpoint slices are enabled, but are not supported by the cluster, as such we will fail
* with an IllegalArgumentException
* - ApiGroups is empty
* </pre>
*/
@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")));
}
/**
* <pre>
* - endpoint slices are enabled, but are not supported by the cluster, as such we will fail
* with an IllegalArgumentException
* - ApiVersions is empty
* </pre>
*/
@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<String, String> 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<String, String> 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<String, String> 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;
}
}

View File

@@ -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<HeartbeatEvent> 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();
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
@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<EndpointNameAndNamespace> expectedOutput = List.of(new EndpointNameAndNamespace("podB", "namespaceA"));
assertThat(event.getValue()).isEqualTo(expectedOutput);
invokeAndAssert(watch, List.of(new EndpointNameAndNamespace("podB", "namespaceA")));
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
@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<EndpointNameAndNamespace> 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")));
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
@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<EndpointNameAndNamespace> 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")));
}
/**
* <pre>
*
* - 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
*
* </pre>
*/
@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<EndpointNameAndNamespace> 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<String, String> 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<String, String> 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<String, String> 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;
}
}

View File

@@ -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();
}
/**
* <pre>
* - 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
* </pre>
*/
@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<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) 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<EndpointNameAndNamespace> result = (List<EndpointNameAndNamespace>) 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<EnvVar> 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);
}
}

View File

@@ -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<String> 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,

View File

@@ -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"]