Fix 1158 (#1160)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user