Use endpoint slices for fabric8 catalog watcher (#1149)

This commit is contained in:
erabii
2022-12-06 23:55:54 +02:00
committed by GitHub
parent 4d48adaaa7
commit 3dcab2bc69
30 changed files with 867 additions and 176 deletions

View File

@@ -134,8 +134,19 @@ should not rely on the details. Instead, they should see if there are difference
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].
In order to enable this functionality you need to add
`@EnableScheduling` on a configuration class in your application.
By default, we use the `Endpoints`(see https://kubernetes.io/docs/concepts/services-networking/service/#endpoints) API to find out the current state of services. There is another way though, via `EndpointSlices` (https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/). Such support can be enabled via a property: `spring.cloud.kubernetes.discovery.use-endpoint-slices=true` (by default it is `false`). Of course, your cluster has to support it also. As a matter of fact, if you enable this property, but your cluster does not support it, we will fail starting the application. If you decide to enable such support, you also need proper Role/ClusterRole set-up. For example:
```
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: namespace-reader
rules:
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
```

View File

@@ -124,7 +124,7 @@ public class KubernetesInformerDiscoveryClientTests {
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), Map.of(), null,
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -157,7 +157,7 @@ public class KubernetesInformerDiscoveryClientTests {
labels.put("spring", "true");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), labels, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), labels, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("",
sharedInformerFactory, serviceLister, null, null, null, kubernetesDiscoveryProperties);
@@ -176,7 +176,7 @@ public class KubernetesInformerDiscoveryClientTests {
labels.put("spring", "true");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), labels, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), labels, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -192,7 +192,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Service> serviceLister = setupServiceLister(testServiceSecuredAnnotation1);
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), new HashMap<>(), null, null, 0);
Set.of(), true, 60, false, null, Set.of(), new HashMap<>(), null, null, 0, false);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
assertThat(discoveryClient.getServices().toArray())
@@ -207,7 +207,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Service> serviceLister = setupServiceLister(testServiceSecuredLabel1);
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), new HashMap<>(), null, null, 0);
Set.of(), true, 60, false, null, Set.of(), new HashMap<>(), null, null, 0, false);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -235,7 +235,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -250,7 +250,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -277,7 +277,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpointWithoutReadyAddresses);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, true, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, true, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -318,7 +318,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpointWithMultiplePorts);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -337,7 +337,7 @@ public class KubernetesInformerDiscoveryClientTests {
testEndpointWithMultiplePortsWithoutSupportedPortNames);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -353,7 +353,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpointWithMultiplePorts);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, "https", null, 0);
Set.of(), true, 60, false, null, Set.of(), null, "https", null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -369,7 +369,7 @@ public class KubernetesInformerDiscoveryClientTests {
testEndpointWithMultiplePortsWithoutSupportedPortNames);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, "oops", null, 0);
Set.of(), true, 60, false, null, Set.of(), null, "oops", null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -384,7 +384,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpointWithMultiplePorts);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -399,7 +399,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpointWithMultiplePortsWithoutHttps);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -415,7 +415,7 @@ public class KubernetesInformerDiscoveryClientTests {
testEndpointWithMultiplePortsWithoutSupportedPortNames);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, true);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient("namespace1",
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);
@@ -430,7 +430,7 @@ public class KubernetesInformerDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1, testEndpoints2);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, false);
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(null,
sharedInformerFactory, serviceLister, endpointsLister, null, null, kubernetesDiscoveryProperties);

View File

@@ -101,7 +101,7 @@ public class KubernetesInformerReactiveDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, false);
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister,
@@ -120,7 +120,7 @@ public class KubernetesInformerReactiveDiscoveryClientTests {
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0);
Set.of(), true, 60, false, null, Set.of(), null, null, null, 0, false);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("namespace1");

View File

@@ -147,7 +147,7 @@ class KubernetesClientServicesListSupplierTests {
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("default");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), Map.of(), null,
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
CoreV1Api coreV1Api = new CoreV1Api();
KubernetesClientServiceInstanceMapper mapper = new KubernetesClientServiceInstanceMapper(
new KubernetesLoadBalancerProperties(), kubernetesDiscoveryProperties);

View File

@@ -42,6 +42,7 @@ import static org.springframework.cloud.client.discovery.DiscoveryClient.DEFAULT
* fetched from the Kubernetes API server.
* @param primaryPortName If set then the port with a given name is used as primary when
* multiple ports are defined for a service.
* @param useEndpointSlices use EndpointSlice instead of Endpoints
*/
// @formatter:off
@ConfigurationProperties("spring.cloud.kubernetes.discovery")
@@ -54,14 +55,15 @@ public record KubernetesDiscoveryProperties(
@DefaultValue({"443", "8443"}) Set<Integer> knownSecurePorts,
@DefaultValue Map<String, String> serviceLabels, String primaryPortName,
@DefaultValue Metadata metadata,
@DefaultValue("" + DEFAULT_ORDER) int order) {
@DefaultValue("" + DEFAULT_ORDER) int order,
boolean useEndpointSlices) {
// @formatter:on
/**
* Default instance.
*/
public static final KubernetesDiscoveryProperties DEFAULT = new KubernetesDiscoveryProperties(true, false, Set.of(),
true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
/**
* @param addLabels include labels as metadata

View File

@@ -52,6 +52,7 @@ class KubernetesDiscoveryPropertiesTests {
assertThat(props.serviceLabels()).isEmpty();
assertThat(props.primaryPortName()).isNull();
assertThat(props.order()).isZero();
assertThat(props.useEndpointSlices()).isFalse();
});
}
@@ -61,6 +62,7 @@ class KubernetesDiscoveryPropertiesTests {
.withPropertyValues("spring.cloud.kubernetes.discovery.filter=some-filter",
"spring.cloud.kubernetes.discovery.knownSecurePorts[0]=222",
"spring.cloud.kubernetes.discovery.metadata.labelsPrefix=labelsPrefix",
"spring.cloud.kubernetes.discovery.use-endpoint-slices=true",
"spring.cloud.kubernetes.discovery.namespaces[0]=ns1",
"spring.cloud.kubernetes.discovery.namespaces[1]=ns2")
.run(context -> {
@@ -81,6 +83,7 @@ class KubernetesDiscoveryPropertiesTests {
assertThat(props.serviceLabels()).isEmpty();
assertThat(props.primaryPortName()).isNull();
assertThat(props.order()).isZero();
assertThat(props.useEndpointSlices()).isTrue();
});
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import io.fabric8.kubernetes.api.model.ObjectReference;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
/**
* A simple holder for some instances needed for either Endpoints or EndpointSlice catalog
* implementations.
*
* @author wind57
*/
record Fabric8CatalogWatchContext(KubernetesClient kubernetesClient, KubernetesDiscoveryProperties properties,
KubernetesNamespaceProvider namespaceProvider) {
static List<EndpointNameAndNamespace> state(Stream<ObjectReference> references) {
return references.filter(Objects::nonNull).map(x -> new EndpointNameAndNamespace(x.getName(), x.getNamespace()))
.sorted(Comparator.comparing(EndpointNameAndNamespace::endpointName, String::compareTo)).toList();
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import io.fabric8.kubernetes.api.model.ObjectReference;
import io.fabric8.kubernetes.api.model.discovery.v1.Endpoint;
import io.fabric8.kubernetes.api.model.discovery.v1.EndpointSlice;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils;
import org.springframework.core.log.LogAccessor;
/**
* Implementation that is based on EndpointSlice V1.
*
* @author wind57
*/
final class Fabric8EndpointSliceV1CatalogWatch
implements Function<Fabric8CatalogWatchContext, List<EndpointNameAndNamespace>> {
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8EndpointSliceV1CatalogWatch.class));
@Override
public List<EndpointNameAndNamespace> apply(Fabric8CatalogWatchContext context) {
// take only pods that have endpoints
List<EndpointSlice> endpointSlices;
if (context.properties().allNamespaces()) {
LOG.debug(() -> "discovering endpoints in all namespaces");
try (KubernetesClient client = context.kubernetesClient()) {
endpointSlices = client.discovery().v1().endpointSlices().inAnyNamespace()
.withLabels(context.properties().serviceLabels()).list().getItems();
}
}
else {
String namespace = Fabric8Utils.getApplicationNamespace(context.kubernetesClient(), null, "catalog-watcher",
context.namespaceProvider());
LOG.debug(() -> "fabric8 catalog watcher will use namespace : " + namespace);
try (KubernetesClient client = context.kubernetesClient()) {
endpointSlices = client.discovery().v1().endpointSlices().inNamespace(namespace)
.withLabels(context.properties().serviceLabels()).list().getItems();
}
}
Stream<ObjectReference> references = endpointSlices.stream().map(EndpointSlice::getEndpoints)
.flatMap(List::stream).map(Endpoint::getTargetRef);
return Fabric8CatalogWatchContext.state(references);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import io.fabric8.kubernetes.api.model.EndpointAddress;
import io.fabric8.kubernetes.api.model.EndpointSubset;
import io.fabric8.kubernetes.api.model.Endpoints;
import io.fabric8.kubernetes.api.model.ObjectReference;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.kubernetes.commons.discovery.EndpointNameAndNamespace;
import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils;
import org.springframework.core.log.LogAccessor;
/**
* Implementation that is based on Endpoints.
*
* @author wind57
*/
final class Fabric8EndpointsCatalogWatch
implements Function<Fabric8CatalogWatchContext, List<EndpointNameAndNamespace>> {
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Fabric8EndpointsCatalogWatch.class));
@Override
public List<EndpointNameAndNamespace> apply(Fabric8CatalogWatchContext context) {
// take only pods that have endpoints
List<Endpoints> endpoints;
if (context.properties().allNamespaces()) {
LOG.debug(() -> "discovering endpoints in all namespaces");
try (KubernetesClient client = context.kubernetesClient()) {
endpoints = client.endpoints().inAnyNamespace().withLabels(context.properties().serviceLabels()).list()
.getItems();
}
}
else {
String namespace = Fabric8Utils.getApplicationNamespace(context.kubernetesClient(), null, "catalog-watcher",
context.namespaceProvider());
LOG.debug(() -> "fabric8 catalog watcher will use namespace : " + namespace);
try (KubernetesClient client = context.kubernetesClient()) {
endpoints = client.endpoints().inNamespace(namespace).withLabels(context.properties().serviceLabels())
.list().getItems();
}
}
/**
* <pre>
* - An "Endpoints" holds a List of EndpointSubset.
* - A single EndpointSubset holds a List of EndpointAddress
*
* - (The union of all EndpointSubsets is the Set of all Endpoints)
* - Set of Endpoints is the cartesian product of :
* EndpointSubset::getAddresses and EndpointSubset::getPorts (each is a List)
* </pre>
*/
Stream<ObjectReference> references = endpoints.stream().map(Endpoints::getSubsets).filter(Objects::nonNull)
.flatMap(List::stream).map(EndpointSubset::getAddresses).filter(Objects::nonNull).flatMap(List::stream)
.map(EndpointAddress::getTargetRef);
return Fabric8CatalogWatchContext.state(references);
}
}

View File

@@ -16,21 +16,20 @@
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import io.fabric8.kubernetes.api.model.EndpointAddress;
import io.fabric8.kubernetes.api.model.EndpointSubset;
import io.fabric8.kubernetes.api.model.Endpoints;
import io.fabric8.kubernetes.api.model.APIResource;
import io.fabric8.kubernetes.api.model.APIResourceList;
import io.fabric8.kubernetes.api.model.GroupVersionForDiscovery;
import io.fabric8.kubernetes.client.KubernetesClient;
import jakarta.annotation.PostConstruct;
import org.apache.commons.logging.LogFactory;
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.cloud.kubernetes.fabric8.Fabric8Utils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.log.LogAccessor;
@@ -41,13 +40,15 @@ import org.springframework.scheduling.annotation.Scheduled;
*/
public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
private static final String DISCOVERY_GROUP_VERSION = "discovery.k8s.io/v1";
private static final String ENDPOINT_SLICE = "EndpointSlice";
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(KubernetesCatalogWatch.class));
private final KubernetesClient kubernetesClient;
private final Fabric8CatalogWatchContext context;
private final KubernetesDiscoveryProperties properties;
private final KubernetesNamespaceProvider namespaceProvider;
private Function<Fabric8CatalogWatchContext, List<EndpointNameAndNamespace>> stateGenerator;
private volatile List<EndpointNameAndNamespace> catalogEndpointsState = null;
@@ -55,9 +56,7 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
public KubernetesCatalogWatch(KubernetesClient kubernetesClient, KubernetesDiscoveryProperties properties,
KubernetesNamespaceProvider namespaceProvider) {
this.kubernetesClient = kubernetesClient;
this.properties = properties;
this.namespaceProvider = namespaceProvider;
context = new Fabric8CatalogWatchContext(kubernetesClient, properties, namespaceProvider);
}
@Override
@@ -69,37 +68,7 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
public void catalogServicesWatch() {
try {
// not all pods participate in the service discovery. only those that have
// endpoints.
List<Endpoints> endpoints;
if (properties.allNamespaces()) {
LOG.debug(() -> "discovering endpoints in all namespaces");
endpoints = kubernetesClient.endpoints().inAnyNamespace().withLabels(properties.serviceLabels()).list()
.getItems();
}
else {
String namespace = Fabric8Utils.getApplicationNamespace(kubernetesClient, null, "catalog-watcher",
namespaceProvider);
LOG.debug(() -> "fabric8 catalog watcher will use namespace : " + namespace);
endpoints = kubernetesClient.endpoints().inNamespace(namespace).withLabels(properties.serviceLabels())
.list().getItems();
}
/**
* <pre>
* - An "Endpoints" holds a List of EndpointSubset.
* - A single EndpointSubset holds a List of EndpointAddress
*
* - (The union of all EndpointSubsets is the Set of all Endpoints)
* - Set of Endpoints is the cartesian product of :
* EndpointSubset::getAddresses and EndpointSubset::getPorts (each is a List)
* </pre>
*/
List<EndpointNameAndNamespace> currentState = endpoints.stream().map(Endpoints::getSubsets)
.filter(Objects::nonNull).flatMap(List::stream).map(EndpointSubset::getAddresses)
.filter(Objects::nonNull).flatMap(List::stream).map(EndpointAddress::getTargetRef)
.filter(Objects::nonNull).map(x -> new EndpointNameAndNamespace(x.getName(), x.getNamespace()))
.sorted(Comparator.comparing(EndpointNameAndNamespace::endpointName, String::compareTo)).toList();
List<EndpointNameAndNamespace> currentState = stateGenerator.apply(context);
if (!currentState.equals(catalogEndpointsState)) {
LOG.debug(() -> "Received endpoints update from kubernetesClient: " + currentState);
@@ -113,4 +82,31 @@ public class KubernetesCatalogWatch implements ApplicationEventPublisherAware {
}
}
@PostConstruct
void postConstruct() {
if (context.properties().useEndpointSlices()) {
try (KubernetesClient client = context.kubernetesClient()) {
// this emulates : 'kubectl api-resources | grep -i EndpointSlice'
boolean found = client.getApiGroups().getGroups().stream().flatMap(x -> x.getVersions().stream())
.map(GroupVersionForDiscovery::getGroupVersion).filter(DISCOVERY_GROUP_VERSION::equals)
.findFirst().map(client::getApiResources).map(APIResourceList::getResources)
.map(x -> x.stream().map(APIResource::getKind))
.flatMap(x -> x.filter(y -> y.equals(ENDPOINT_SLICE)).findFirst()).isPresent();
if (!found) {
throw new IllegalArgumentException("EndpointSlices are not supported on the cluster");
}
else {
stateGenerator = new Fabric8EndpointSliceV1CatalogWatch();
}
}
}
else {
stateGenerator = new Fabric8EndpointsCatalogWatch();
}
LOG.debug(() -> "stateGenerator is of type: " + stateGenerator.getClass().getSimpleName());
}
}

View File

@@ -0,0 +1,377 @@
/*
* 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.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 {
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 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() {
KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceAndLabels("namespaceA", Map.of("color", "blue"));
EndpointSlice sliceB = createSingleEndpointWithEndpointSlices("namespaceA", Map.of("color", "blue"), "podB");
EndpointSliceList listInNamespaceA = new EndpointSliceListBuilder().withItems(sliceB).build();
mockServer.expect()
.withPath("/apis/discovery.k8s.io/v1/namespaces/namespaceA/endpointslices?labelSelector=color%3Dblue")
.andReturn(200, listInNamespaceA).once();
// 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);
}
/**
* <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() {
KubernetesCatalogWatch watch = createWatcherInSpecificNamespaceAndLabels("namespaceA", Map.of());
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();
// 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);
}
/**
* <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() {
KubernetesCatalogWatch watch = createWatcherInAllNamespacesAndLabels(Map.of("color", "blue"));
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();
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);
}
/**
* <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() {
KubernetesCatalogWatch watch = createWatcherInAllNamespacesAndLabels(Map.of());
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();
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"),
new EndpointNameAndNamespace("podD", "namespaceB"), new EndpointNameAndNamespace("podE", "namespaceB"));
assertThat(event.getValue()).isEqualTo(expectedOutput);
}
/**
* <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);
APIGroupList groupList = new APIGroupListBuilder().build();
mockServer.expect().withPath("/apis").andReturn(200, groupList).always();
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());
}
/**
* <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, namespaceProvider);
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, watch::postConstruct);
Assertions.assertEquals("EndpointSlices are not supported on the cluster", ex.getMessage());
}
private KubernetesCatalogWatch createWatcherInSpecificNamespaceAndLabels(String namespace,
Map<String, String> labels) {
createEndpointSlicesApiGroup();
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;
}
private KubernetesCatalogWatch createWatcherInAllNamespacesAndLabels(Map<String, String> labels) {
createEndpointSlicesApiGroup();
// 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;
}
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();
}
}

View File

@@ -48,12 +48,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Some tests that use the fabric8 mock client.
* Some tests that use the fabric8 mock client, using Endpoints
*
* @author wind57
*/
@EnableKubernetesMockClient(crud = true, https = false)
class Fabric8KubernetesCatalogWatchTests {
class Fabric8KubernetesCatalogWatchEndpointsTests {
private final KubernetesNamespaceProvider namespaceProvider = Mockito.mock(KubernetesNamespaceProvider.class);
@@ -246,10 +246,10 @@ class Fabric8KubernetesCatalogWatchTests {
// all-namespaces = false
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, "", Set.of(), labels, "", null, 0);
false, "", Set.of(), labels, "", null, 0, false);
KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider);
watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER);
watch.postConstruct();
return watch;
}
@@ -258,10 +258,10 @@ class Fabric8KubernetesCatalogWatchTests {
// all-namespaces = true
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
false, "", Set.of(), labels, "", null, 0);
false, "", Set.of(), labels, "", null, 0, false);
KubernetesCatalogWatch watch = new KubernetesCatalogWatch(mockClient, properties, namespaceProvider);
watch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER);
watch.postConstruct();
return watch;
}

View File

@@ -22,8 +22,8 @@ import java.util.List;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.After;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
@@ -45,34 +45,34 @@ public class KubernetesCatalogServicesWatchConfigurationTest {
private ConfigurableApplicationContext context;
@After
public void close() {
@AfterEach
void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void kubernetesCatalogWatchDisabled() throws Exception {
void kubernetesCatalogWatchDisabled() {
setup("spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false");
assertThat(this.context.containsBean("kubernetesCatalogWatch")).isFalse();
}
@Test
public void kubernetesCatalogWatchWhenKubernetesDisabled() throws Exception {
void kubernetesCatalogWatchWhenKubernetesDisabled() {
setup();
assertThat(this.context.containsBean("kubernetesCatalogWatch")).isFalse();
}
@Test
public void kubernetesCatalogWatchWhenServiceDiscoveryDisabled() throws Exception {
void kubernetesCatalogWatchWhenServiceDiscoveryDisabled() {
setup("spring.cloud.discovery.enabled=false");
assertThat(this.context.containsBean("kubernetesCatalogWatch")).isFalse();
}
@Test
public void kubernetesCatalogWatchDefaultEnabled() throws Exception {
setup("spring.main.cloud-platform=KUBERNETES");
void kubernetesCatalogWatchDefaultEnabled() {
setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.discovery.use-endpoint-slices=false");
assertThat(this.context.containsBean("kubernetesCatalogWatch")).isTrue();
}
@@ -93,8 +93,9 @@ public class KubernetesCatalogServicesWatchConfigurationTest {
return mock(KubernetesClient.class);
}
@SuppressWarnings("unchecked")
@Bean
PodUtils podUtils() {
PodUtils<?> podUtils() {
PodUtils<Pod> podPodUtils = mock(PodUtils.class);
when(podPodUtils.currentPod()).thenReturn(() -> mock(Pod.class));
return podPodUtils;

View File

@@ -366,20 +366,22 @@ class KubernetesCatalogWatchTest {
// all-namespaces = true
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
false, "", Set.of(), Map.of(), "", null, 0);
false, "", Set.of(), Map.of(), "", null, 0, false);
kubernetesCatalogWatch = new KubernetesCatalogWatch(CLIENT, properties, namespaceProvider);
kubernetesCatalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER);
kubernetesCatalogWatch.postConstruct();
}
private void createInSpecificNamespaceWatcher() {
// all-namespaces = false
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, "", Set.of(), Map.of(), "", null, 0);
false, "", Set.of(), Map.of(), "", null, 0, false);
kubernetesCatalogWatch = new KubernetesCatalogWatch(CLIENT, properties, namespaceProvider);
kubernetesCatalogWatch.setApplicationEventPublisher(APPLICATION_EVENT_PUBLISHER);
kubernetesCatalogWatch.postConstruct();
when(namespaceProvider.getNamespace()).thenReturn("catalog-watcher-namespace");
when(CLIENT.endpoints()).thenReturn(MIXED_OPERATION);

View File

@@ -22,8 +22,8 @@ import java.util.List;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.After;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
@@ -44,34 +44,34 @@ public class KubernetesDiscoveryClientAutoConfigurationPropertiesTests {
private ConfigurableApplicationContext context;
@After
public void close() {
@AfterEach
void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void kubernetesDiscoveryDisabled() throws Exception {
void kubernetesDiscoveryDisabled() {
setup("spring.cloud.kubernetes.discovery.enabled=false",
"spring.cloud.kubernetes.discovery.catalog-services-watch.enabled=false");
assertThat(this.context.getBeanNamesForType(KubernetesDiscoveryClient.class)).isEmpty();
}
@Test
public void kubernetesDiscoveryWhenKubernetesDisabled() throws Exception {
void kubernetesDiscoveryWhenKubernetesDisabled() {
setup();
assertThat(this.context.getBeanNamesForType(KubernetesDiscoveryClient.class)).isEmpty();
}
@Test
public void kubernetesDiscoveryWhenDiscoveryDisabled() throws Exception {
void kubernetesDiscoveryWhenDiscoveryDisabled() {
setup("spring.cloud.discovery.enabled=false");
assertThat(this.context.getBeanNamesForType(KubernetesDiscoveryClient.class)).isEmpty();
}
@Test
public void kubernetesDiscoveryDefaultEnabled() throws Exception {
void kubernetesDiscoveryDefaultEnabled() {
setup("spring.main.cloud-platform=KUBERNETES");
assertThat(this.context.getBeanNamesForType(KubernetesDiscoveryClient.class)).hasSize(1);
}
@@ -93,8 +93,9 @@ public class KubernetesDiscoveryClientAutoConfigurationPropertiesTests {
return mock(KubernetesClient.class);
}
@SuppressWarnings("unchecked")
@Bean
PodUtils podUtils() {
PodUtils<?> podUtils() {
PodUtils<Pod> podPodUtils = mock(PodUtils.class);
when(podPodUtils.currentPod()).thenReturn(() -> mock(Pod.class));
return podPodUtils;

View File

@@ -16,8 +16,7 @@
package org.springframework.cloud.kubernetes.fabric8.discovery;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -25,20 +24,18 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false" })
public class KubernetesDiscoveryClientAutoConfigurationTests {
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.main.cloud-platform=KUBERNETES",
"spring.cloud.config.enabled=false", "spring.cloud.kubernetes.discovery.use-endpoint-slices=false" })
class KubernetesDiscoveryClientAutoConfigurationTests {
@Autowired(required = false)
private DiscoveryClient discoveryClient;
@Test
public void kubernetesDiscoveryClientCreated() {
void kubernetesDiscoveryClientCreated() {
assertThat(this.discoveryClient).isNotNull().isInstanceOf(CompositeDiscoveryClient.class);
CompositeDiscoveryClient composite = (CompositeDiscoveryClient) this.discoveryClient;

View File

@@ -18,8 +18,9 @@ package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.Collections;
import org.junit.After;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
@@ -34,7 +35,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static junit.framework.TestCase.assertEquals;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
@@ -43,11 +43,11 @@ import static org.mockito.Mockito.verify;
/**
* @author Zhanwei Wang
*/
public class KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests {
class KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests {
private AnnotationConfigApplicationContext context;
@After
@AfterEach
public void close() {
if (this.context != null) {
if (this.context.getParent() != null) {
@@ -58,15 +58,15 @@ public class KubernetesDiscoveryClientConfigClientBootstrapConfigurationTests {
}
@Test
public void onWhenRequested() throws Exception {
void onWhenRequested() {
setup("server.port=7000", "spring.cloud.config.discovery.enabled=true",
"spring.cloud.kubernetes.discovery.enabled:true", "spring.application.name:test",
"spring.cloud.config.discovery.service-id:configserver");
assertEquals(1, this.context.getParent().getBeanNamesForType(DiscoveryClient.class).length);
Assertions.assertEquals(1, this.context.getParent().getBeanNamesForType(DiscoveryClient.class).length);
DiscoveryClient client = this.context.getParent().getBean(DiscoveryClient.class);
verify(client, atLeast(2)).getInstances("configserver");
ConfigClientProperties locator = this.context.getBean(ConfigClientProperties.class);
assertEquals("http://fake:8888/", locator.getUri()[0]);
Assertions.assertEquals("http://fake:8888/", locator.getUri()[0]);
}
private void setup(String... env) {

View File

@@ -80,7 +80,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(false, null, false, null, false, null);
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);
@@ -110,7 +110,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(true, null, false, null, false, null);
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);
@@ -141,7 +141,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(true, "l_", false, null, false, null);
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);
@@ -172,7 +172,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(false, null, true, null, false, null);
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);
@@ -203,7 +203,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(false, null, true, "a_", false, null);
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);
@@ -234,7 +234,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(false, null, false, null, true, null);
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);
@@ -265,7 +265,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(false, null, false, null, true, "p_");
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);
@@ -296,7 +296,7 @@ public class KubernetesDiscoveryClientFilterMetadataTest {
Metadata metadata = new Metadata(true, "l_", true, "a_", true, "p_");
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, null, Set.of(), Map.of(), null, metadata, 0);
false, null, Set.of(), Map.of(), null, metadata, 0, true);
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(CLIENT, properties, a -> null);

View File

@@ -68,7 +68,7 @@ public class KubernetesDiscoveryClientFilterTest {
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, "metadata.additionalProperties['spring-boot']", Set.of(), Map.of(), null,
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true);
KubernetesDiscoveryClient client = new KubernetesDiscoveryClient(this.kubernetesClient, properties,
this.kubernetesClientServicesFunction);
@@ -96,7 +96,7 @@ public class KubernetesDiscoveryClientFilterTest {
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, "metadata.name.startsWith('service')", Set.of(), Map.of(), null,
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true);
KubernetesDiscoveryClient client = new KubernetesDiscoveryClient(this.kubernetesClient, properties,
this.kubernetesClientServicesFunction);
@@ -116,7 +116,7 @@ public class KubernetesDiscoveryClientFilterTest {
when(this.kubernetesClient.services()).thenReturn(this.serviceOperation);
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true, 60,
false, "", Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
false, "", Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, true);
KubernetesDiscoveryClient client = new KubernetesDiscoveryClient(this.kubernetesClient, properties,
this.kubernetesClientServicesFunction);

View File

@@ -112,7 +112,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.services().inNamespace("test").create(service);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true,
60, false, null, Set.of(), labels, "http_tcp", Metadata.DEFAULT, 0);
60, false, null, Set.of(), labels, "http_tcp", Metadata.DEFAULT, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -161,7 +161,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.endpoints().inNamespace(namespace2).create(endPoint2);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true,
60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -191,7 +191,7 @@ public class KubernetesDiscoveryClientTest {
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false,
Set.of(namespace1, namespace3), true, 60, false, null, Set.of(), Map.of(), null,
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -223,7 +223,7 @@ public class KubernetesDiscoveryClientTest {
Metadata metadata = new Metadata(false, null, false, null, true, "port.");
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true,
60, false, null, Set.of(443, 8443), labels, null, metadata, 0);
60, false, null, Set.of(443, 8443), labels, null, metadata, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -310,7 +310,7 @@ public class KubernetesDiscoveryClientTest {
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false,
Set.of(nameSpace1, nameSpace2), true, 60, false, null, Set.of(), Map.of(), null,
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -343,7 +343,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.services().inNamespace("test2").create(service2);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true,
60, false, null, Set.of(), Map.of(), null, Metadata.DEFAULT, 0);
60, false, null, Set.of(), Map.of(), null, Metadata.DEFAULT, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -395,7 +395,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.services().inNamespace("test").create(service);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true,
60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0);
60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -426,7 +426,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.services().inNamespace("test").create(service);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true,
60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0);
60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -456,7 +456,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.services().inNamespace("test").create(service);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true,
60, false, null, Set.of(443, 8443), Map.of(), "oops", Metadata.DEFAULT, 0);
60, false, null, Set.of(443, 8443), Map.of(), "oops", Metadata.DEFAULT, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -485,7 +485,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.services().inNamespace("test").create(service);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, false, Set.of(), true,
60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0);
60, false, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));
@@ -542,7 +542,7 @@ public class KubernetesDiscoveryClientTest {
mockClient.services().inNamespace("test").create(service);
final KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true,
60, true, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0);
60, true, null, Set.of(443, 8443), Map.of(), null, Metadata.DEFAULT, 0, true);
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
KubernetesClient::services, new ServicePortSecureResolver(properties));

View File

@@ -52,7 +52,7 @@ class ServicePortSecureResolverTest {
void testPortNumbersOnly() {
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
false, null, Set.of(443, 8443, 12345), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT,
0);
0, true);
ServicePortSecureResolver secureResolver = new ServicePortSecureResolver(properties);

View File

@@ -36,14 +36,14 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class,
ReactiveCommonsClientAutoConfiguration.class, KubernetesCommonsAutoConfiguration.class,
Fabric8AutoConfiguration.class, KubernetesDiscoveryClientAutoConfiguration.class,
KubernetesReactiveDiscoveryClientAutoConfiguration.class));
@Test
public void shouldWorkWithDefaults() {
void shouldWorkWithDefaults() {
contextRunner.withPropertyValues("spring.main.cloud-platform=KUBERNETES").run(context -> {
assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);
assertThat(context).hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class);
@@ -51,7 +51,7 @@ class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
}
@Test
public void shouldNotHaveDiscoveryClientWhenDiscoveryDisabled() {
void shouldNotHaveDiscoveryClientWhenDiscoveryDisabled() {
contextRunner.withPropertyValues("spring.cloud.discovery.enabled=false").run(context -> {
assertThat(context).doesNotHaveBean("kubernetesReactiveDiscoveryClient");
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
@@ -60,7 +60,7 @@ class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
}
@Test
public void shouldNotHaveDiscoveryClientWhenReactiveDiscoveryDisabled() {
void shouldNotHaveDiscoveryClientWhenReactiveDiscoveryDisabled() {
contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=false").run(context -> {
assertThat(context).doesNotHaveBean("kubernetesReactiveDiscoveryClient");
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
@@ -69,7 +69,7 @@ class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
}
@Test
public void shouldNotHaveDiscoveryClientWhenKubernetesDisabled() {
void shouldNotHaveDiscoveryClientWhenKubernetesDisabled() {
contextRunner.run(context -> {
assertThat(context).doesNotHaveBean("kubernetesReactiveDiscoveryClient");
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
@@ -78,7 +78,7 @@ class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
}
@Test
public void shouldNotHaveDiscoveryClientWhenKubernetesDiscoveryDisabled() {
void shouldNotHaveDiscoveryClientWhenKubernetesDiscoveryDisabled() {
contextRunner.withPropertyValues("spring.cloud.kubernetes.discovery.enabled=false").run(context -> {
assertThat(context).doesNotHaveBean("kubernetesReactiveDiscoveryClient");
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
@@ -87,7 +87,7 @@ class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
}
@Test
public void worksWithoutWebflux() {
void worksWithoutWebflux() {
contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.web.reactive")).run(context -> {
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
assertThat(context).doesNotHaveBean(ReactiveDiscoveryClientHealthIndicator.class);
@@ -95,7 +95,7 @@ class KubernetesReactiveDiscoveryClientAutoConfigurationTests {
}
@Test
public void worksWithoutActuator() {
void worksWithoutActuator() {
contextRunner.withPropertyValues("spring.main.cloud-platform=KUBERNETES")
.withClassLoader(new FilteredClassLoader("org.springframework.boot.actuate")).run(context -> {
assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);

View File

@@ -53,7 +53,7 @@ import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesD
class KubernetesReactiveDiscoveryClientTests {
@BeforeEach
public void setup(@KubernetesExtension.Client KubernetesClient kubernetesClient) {
void setup(@KubernetesExtension.Client KubernetesClient kubernetesClient) {
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY,
kubernetesClient.getConfiguration().getMasterUrl());
@@ -64,7 +64,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void verifyDefaults(@KubernetesExtension.Client KubernetesClient kubernetesClient) {
void verifyDefaults(@KubernetesExtension.Client KubernetesClient kubernetesClient) {
ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient(kubernetesClient,
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services);
assertThat(client.description()).isEqualTo("Kubernetes Reactive Discovery Client");
@@ -72,7 +72,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnFluxOfServices(@KubernetesExtension.Client KubernetesClient kubernetesClient,
void shouldReturnFluxOfServices(@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services")
.andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata().withName("s1")
@@ -96,7 +96,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnEmptyFluxOfServicesWhenNoInstancesFound(
void shouldReturnEmptyFluxOfServicesWhenNoInstancesFound(
@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services")
@@ -109,8 +109,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnEmptyFluxForNonExistingService(
@KubernetesExtension.Client KubernetesClient kubernetesClient,
void shouldReturnEmptyFluxForNonExistingService(@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
kubernetesServer.expect().get()
.withPath("/api/v1/namespaces/test/endpoints?fieldSelector=metadata.name%3Dnonexistent-service")
@@ -123,8 +122,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnEmptyFluxWhenServiceHasNoSubsets(
@KubernetesExtension.Client KubernetesClient kubernetesClient,
void shouldReturnEmptyFluxWhenServiceHasNoSubsets(@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services")
.andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service")
@@ -146,7 +144,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnFlux(@KubernetesExtension.Client KubernetesClient kubernetesClient,
void shouldReturnFlux(@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
ServiceList services = new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service")
.withNamespace("test").withLabels(new HashMap<String, String>() {
@@ -184,7 +182,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnFluxWithPrefixedMetadata(@KubernetesExtension.Client KubernetesClient kubernetesClient,
void shouldReturnFluxWithPrefixedMetadata(@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services")
.andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service")
@@ -226,7 +224,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnFluxWhenServiceHasMultiplePortsAndPrimaryPortNameIsSet(
void shouldReturnFluxWhenServiceHasMultiplePortsAndPrimaryPortNameIsSet(
@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services")
@@ -269,8 +267,7 @@ class KubernetesReactiveDiscoveryClientTests {
}
@Test
public void shouldReturnFluxOfServicesAcrossAllNamespaces(
@KubernetesExtension.Client KubernetesClient kubernetesClient,
void shouldReturnFluxOfServicesAcrossAllNamespaces(@KubernetesExtension.Client KubernetesClient kubernetesClient,
@KubernetesExtension.Server KubernetesServer kubernetesServer) {
kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services")
.andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata().withName("existing-service")
@@ -302,7 +299,7 @@ class KubernetesReactiveDiscoveryClientTests {
.once();
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(true, true, Set.of(), true, 60,
false, null, Set.of(), Map.of(), "https_tcp", Metadata.DEFAULT, 0);
false, null, Set.of(), Map.of(), "https_tcp", Metadata.DEFAULT, 0, true);
ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient(kubernetesClient, properties,
KubernetesClient::services);
Flux<ServiceInstance> instances = client.getInstances("existing-service");

View File

@@ -79,7 +79,8 @@ class Fabric8ServiceInstanceMapperTests {
void testMapperSecureNullLabelsAndAnnotations() {
KubernetesLoadBalancerProperties properties = new KubernetesLoadBalancerProperties();
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(true, true, Set.of(),
true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0,
false);
List<ServicePort> ports = new ArrayList<>();
ports.add(new ServicePortBuilder().withPort(443).build());
Service service = buildService("test", "abc", ports, null, null);

View File

@@ -96,7 +96,8 @@ class KubernetesServiceListSupplierTests {
serviceList.getItems().add(buildService("test-service", 8080));
when(this.multiDeletable.list()).thenReturn(serviceList);
KubernetesDiscoveryProperties discoveryProperties = new KubernetesDiscoveryProperties(true, true, Set.of(),
true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0);
true, 60, false, null, Set.of(), Map.of(), null, KubernetesDiscoveryProperties.Metadata.DEFAULT, 0,
false);
KubernetesServicesListSupplier supplier = new Fabric8ServicesListSupplier(environment, client, mapper,
discoveryProperties);
List<ServiceInstance> instances = supplier.get().blockFirst();

View File

@@ -27,11 +27,11 @@ import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
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.slf4j.LoggerFactory;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.k3s.K3sContainer;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
@@ -58,8 +58,7 @@ class CatalogWatchIT {
private static final String NAMESPACE = "default";
private static final K3sContainer K3S = Commons.container()
.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(CatalogWatchIT.class)));
private static final K3sContainer K3S = Commons.container();
private static KubernetesClient client;
@@ -83,9 +82,16 @@ class CatalogWatchIT {
Commons.loadSpringCloudKubernetesImage(APP_NAME, K3S);
Fabric8Utils.setUp(client, "default");
}
@BeforeEach
void beforeEach() throws Exception {
deployBusyboxManifests();
deployApp();
}
@AfterEach
void afterEach() {
deleteApp();
}
/**
@@ -96,9 +102,40 @@ class CatalogWatchIT {
* - assert that we receive only spring-cloud-kubernetes-fabric8-client-catalog-watcher pod
* </pre>
*/
@SuppressWarnings("unchecked")
@Test
void testCatalogWatch() {
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];
@@ -142,9 +179,17 @@ class CatalogWatchIT {
.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) {
Assertions.assertEquals(1, result.size());
afterDelete[0] = result.get(0);
return true;
}
@@ -155,11 +200,9 @@ class CatalogWatchIT {
Assertions.assertTrue(afterDelete[0].endpointName().contains(APP_NAME));
Assertions.assertEquals("default", afterDelete[0].namespace());
deleteApp();
}
private static void deployBusyboxManifests() throws Exception {
private void deployBusyboxManifests() throws Exception {
Deployment deployment = client.apps().deployments().load(getBusyboxDeployment()).get();
@@ -178,9 +221,10 @@ class CatalogWatchIT {
}
private static void deployApp() {
private static void deployApp(boolean useEndpointSlices) {
Deployment appDeployment = client.apps().deployments().load(getAppDeployment()).get();
InputStream deployment = useEndpointSlices ? getEndpointSlicesAppDeployment() : getEndpointsAppDeployment();
Deployment appDeployment = client.apps().deployments().load(deployment).get();
String version = K8SUtils.getPomVersion();
String currentImage = appDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage();
@@ -211,7 +255,7 @@ class CatalogWatchIT {
private void deleteApp() {
Fabric8Utils.deleteDeployment(client, NAMESPACE, appDeploymentName);
Fabric8Utils.deleteService(client, NAMESPACE, appServiceName);
client.network().v1().ingresses().withName(appIngressName).delete();
Fabric8Utils.deleteIngress(client, NAMESPACE, appIngressName);
}
private static InputStream getBusyboxService() {
@@ -222,8 +266,15 @@ class CatalogWatchIT {
return Fabric8Utils.inputStream("busybox/deployment.yaml");
}
private static InputStream getAppDeployment() {
return Fabric8Utils.inputStream("app/watcher-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() {

View File

@@ -29,3 +29,5 @@ spec:
env:
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY
value: DEBUG
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES
value: true

View File

@@ -0,0 +1,33 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-fabric8-client-catalog-watcher
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
template:
metadata:
labels:
app: spring-cloud-kubernetes-fabric8-client-catalog-watcher
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-fabric8-client-catalog-watcher
image: docker.io/springcloud/spring-cloud-kubernetes-fabric8-client-catalog-watcher
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080
env:
- name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_FABRIC8_DISCOVERY
value: DEBUG
- name: SPRING_CLOUD_KUBERNETES_DISCOVERY_USE_ENDPOINT_SLICES
value: false

View File

@@ -213,6 +213,19 @@ public final class Fabric8Utils {
});
}
/**
* delete ingress and wait for it to be deleted.
*/
public static void deleteIngress(KubernetesClient client, String namespace, String name) {
client.network().v1().ingresses().inNamespace(namespace).withName(name).delete();
await().pollInterval(Duration.ofSeconds(1)).atMost(30, TimeUnit.SECONDS).until(() -> {
Ingress ingress = client.network().v1().ingresses().inNamespace(namespace).withName(name).get();
return ingress == null;
});
}
private static void innerSetup(KubernetesClient client, String namespace, InputStream serviceAccountAsStream,
InputStream roleBindingAsStream, InputStream roleAsStream) {
ServiceAccount serviceAccountFromStream = client.serviceAccounts().load(serviceAccountAsStream).get();

View File

@@ -4,6 +4,6 @@ metadata:
namespace: default
name: namespace-reader
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"]