Clean fabric8 discovery 1 (#1183)
This commit is contained in:
@@ -48,7 +48,20 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<!-- in favor of mockito-inline -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
|
||||
/**
|
||||
* Adapts a {@link KubernetesClientServicesFunction} to a Function that takes a
|
||||
* KubernetesClient as input and returns a List of Services(s), plus adds functionality
|
||||
* not supported by it.
|
||||
*
|
||||
* @author wind57
|
||||
*/
|
||||
final class Fabric8DiscoveryServicesAdapter implements Function<KubernetesClient, List<Service>> {
|
||||
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
private static final SimpleEvaluationContext EVALUATION_CONTEXT = SimpleEvaluationContext.forReadOnlyDataBinding()
|
||||
.withInstanceMethods().build();
|
||||
|
||||
private final KubernetesClientServicesFunction function;
|
||||
|
||||
private final KubernetesDiscoveryProperties properties;
|
||||
|
||||
private final Predicate<Service> filter;
|
||||
|
||||
Fabric8DiscoveryServicesAdapter(KubernetesClientServicesFunction function, KubernetesDiscoveryProperties properties,
|
||||
Predicate<Service> filter) {
|
||||
this.function = function;
|
||||
this.properties = properties;
|
||||
if (filter == null) {
|
||||
this.filter = filter();
|
||||
}
|
||||
else {
|
||||
this.filter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Service> apply(KubernetesClient client) {
|
||||
if (!properties.namespaces().isEmpty()) {
|
||||
List<Service> services = new ArrayList<>();
|
||||
properties.namespaces().forEach(namespace -> services.addAll(client.services().inNamespace(namespace)
|
||||
.withLabels(properties.serviceLabels()).list().getItems().stream().filter(filter).toList()));
|
||||
return services;
|
||||
}
|
||||
return function.apply(client).list().getItems().stream().filter(filter).toList();
|
||||
}
|
||||
|
||||
Predicate<Service> filter() {
|
||||
String spelExpression = properties.filter();
|
||||
Predicate<Service> predicate;
|
||||
if (spelExpression == null || spelExpression.isEmpty()) {
|
||||
predicate = service -> true;
|
||||
}
|
||||
else {
|
||||
Expression filterExpr = PARSER.parseExpression(spelExpression);
|
||||
predicate = service -> {
|
||||
Boolean include = filterExpr.getValue(EVALUATION_CONTEXT, service, Boolean.class);
|
||||
return Optional.ofNullable(include).orElse(false);
|
||||
};
|
||||
}
|
||||
return predicate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,9 +36,6 @@ import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -61,14 +58,11 @@ public class KubernetesDiscoveryClient implements DiscoveryClient {
|
||||
|
||||
private final KubernetesDiscoveryProperties properties;
|
||||
|
||||
private final ServicePortSecureResolver servicePortSecureResolver;
|
||||
|
||||
private final KubernetesClientServicesFunction kubernetesClientServicesFunction;
|
||||
|
||||
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
private final ServicePortSecureResolver servicePortSecureResolver;
|
||||
|
||||
private final SimpleEvaluationContext evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding()
|
||||
.withInstanceMethods().build();
|
||||
private final Fabric8DiscoveryServicesAdapter adapter;
|
||||
|
||||
private KubernetesClient client;
|
||||
|
||||
@@ -76,18 +70,20 @@ public class KubernetesDiscoveryClient implements DiscoveryClient {
|
||||
KubernetesDiscoveryProperties kubernetesDiscoveryProperties,
|
||||
KubernetesClientServicesFunction kubernetesClientServicesFunction) {
|
||||
|
||||
this(client, kubernetesDiscoveryProperties, kubernetesClientServicesFunction,
|
||||
this(client, kubernetesDiscoveryProperties, kubernetesClientServicesFunction, null,
|
||||
new ServicePortSecureResolver(kubernetesDiscoveryProperties));
|
||||
}
|
||||
|
||||
KubernetesDiscoveryClient(KubernetesClient client, KubernetesDiscoveryProperties kubernetesDiscoveryProperties,
|
||||
KubernetesClientServicesFunction kubernetesClientServicesFunction,
|
||||
KubernetesClientServicesFunction kubernetesClientServicesFunction, Predicate<Service> filter,
|
||||
ServicePortSecureResolver servicePortSecureResolver) {
|
||||
|
||||
this.client = client;
|
||||
this.properties = kubernetesDiscoveryProperties;
|
||||
this.kubernetesClientServicesFunction = kubernetesClientServicesFunction;
|
||||
this.servicePortSecureResolver = servicePortSecureResolver;
|
||||
this.kubernetesClientServicesFunction = kubernetesClientServicesFunction;
|
||||
this.adapter = new Fabric8DiscoveryServicesAdapter(kubernetesClientServicesFunction,
|
||||
kubernetesDiscoveryProperties, filter);
|
||||
}
|
||||
|
||||
public KubernetesClient getClient() {
|
||||
@@ -291,35 +287,13 @@ public class KubernetesDiscoveryClient implements DiscoveryClient {
|
||||
|
||||
@Override
|
||||
public List<String> getServices() {
|
||||
String spelExpression = this.properties.filter();
|
||||
Predicate<Service> filteredServices;
|
||||
if (spelExpression == null || spelExpression.isEmpty()) {
|
||||
filteredServices = (Service instance) -> true;
|
||||
}
|
||||
else {
|
||||
Expression filterExpr = this.parser.parseExpression(spelExpression);
|
||||
filteredServices = (Service instance) -> {
|
||||
Boolean include = filterExpr.getValue(this.evalCtxt, instance, Boolean.class);
|
||||
if (include == null) {
|
||||
return false;
|
||||
}
|
||||
return include;
|
||||
};
|
||||
}
|
||||
return getServices(filteredServices);
|
||||
return adapter.apply(client).stream().map(s -> s.getMetadata().getName()).toList();
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
public List<String> getServices(Predicate<Service> filter) {
|
||||
if (properties.namespaces().isEmpty()) {
|
||||
return this.kubernetesClientServicesFunction.apply(this.client).list().getItems().stream().filter(filter)
|
||||
.map(s -> s.getMetadata().getName()).collect(Collectors.toList());
|
||||
}
|
||||
List<String> services = new ArrayList<>();
|
||||
for (String ns : properties.namespaces()) {
|
||||
services.addAll(getClient().services().inNamespace(ns).list().getItems().stream().filter(filter)
|
||||
.map(s -> s.getMetadata().getName()).toList());
|
||||
}
|
||||
return services;
|
||||
return new Fabric8DiscoveryServicesAdapter(kubernetesClientServicesFunction, properties, filter).apply(client)
|
||||
.stream().map(s -> s.getMetadata().getName()).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,14 +31,17 @@ import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled;
|
||||
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
|
||||
import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled;
|
||||
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.PodUtils;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration;
|
||||
import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Auto configuration for discovery clients.
|
||||
@@ -55,23 +58,18 @@ import org.springframework.context.annotation.Configuration;
|
||||
public class KubernetesDiscoveryClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties) {
|
||||
if (properties.serviceLabels().isEmpty()) {
|
||||
if (properties.allNamespaces()) {
|
||||
return (client) -> client.services().inAnyNamespace();
|
||||
}
|
||||
else {
|
||||
return KubernetesClient::services;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (properties.allNamespaces()) {
|
||||
return (client) -> client.services().inAnyNamespace().withLabels(properties.serviceLabels());
|
||||
}
|
||||
else {
|
||||
return (client) -> client.services().withLabels(properties.serviceLabels());
|
||||
}
|
||||
public KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties,
|
||||
Environment environment) {
|
||||
|
||||
if (properties.allNamespaces()) {
|
||||
return (client) -> client.services().inAnyNamespace().withLabels(properties.serviceLabels());
|
||||
}
|
||||
|
||||
return client -> {
|
||||
String namespace = Fabric8Utils.getApplicationNamespace(client, null, "discovery-service",
|
||||
new KubernetesNamespaceProvider(environment));
|
||||
return client.services().inNamespace(namespace).withLabels(properties.serviceLabels());
|
||||
};
|
||||
}
|
||||
|
||||
@ConditionalOnClass({ HealthIndicator.class })
|
||||
@@ -82,7 +80,7 @@ public class KubernetesDiscoveryClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public KubernetesDiscoveryClientHealthIndicatorInitializer indicatorInitializer(
|
||||
ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) {
|
||||
ApplicationEventPublisher applicationEventPublisher, PodUtils<?> podUtils) {
|
||||
return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher);
|
||||
}
|
||||
|
||||
@@ -98,7 +96,7 @@ public class KubernetesDiscoveryClientAutoConfiguration {
|
||||
public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client,
|
||||
KubernetesDiscoveryProperties properties,
|
||||
KubernetesClientServicesFunction kubernetesClientServicesFunction) {
|
||||
return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction,
|
||||
return new KubernetesDiscoveryClient(client, properties, kubernetesClientServicesFunction, null,
|
||||
new ServicePortSecureResolver(properties));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* 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.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServiceBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
/**
|
||||
* @author wind57
|
||||
*/
|
||||
@EnableKubernetesMockClient(crud = true, https = false)
|
||||
class Fabric8DiscoveryServicesAdapterTests {
|
||||
|
||||
private static KubernetesClient client;
|
||||
|
||||
private static MockedStatic<Fabric8Utils> utils;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
utils = Mockito.mockStatic(Fabric8Utils.class);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
client.services().inAnyNamespace().delete();
|
||||
utils.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = true
|
||||
* - labels = {}
|
||||
* - filter = null
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=red}
|
||||
* - serviceB exists in namespaceB with labels = {color=blue}
|
||||
*
|
||||
* - we get both services as a result.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testAllNamespacesWithoutLabelsWithoutFilter() {
|
||||
boolean allNamespaces = true;
|
||||
Map<String, String> labels = Map.of();
|
||||
String spelFilter = null;
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(),
|
||||
true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "red"));
|
||||
service("namespaceB", "serviceB", Map.of("color", "blue"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 2);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
Assertions.assertEquals(result.get(1).getMetadata().getName(), "serviceB");
|
||||
Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "namespaceB");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = true
|
||||
* - labels = {color=red}
|
||||
* - filter = null
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=red}
|
||||
* - serviceB exists in namespaceB with labels = {color=blue}
|
||||
*
|
||||
* - we get only serviceA as a result.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testAllNamespacesWithLabelsWithoutFilter() {
|
||||
boolean allNamespaces = true;
|
||||
Map<String, String> labels = Map.of("color", "red");
|
||||
String spelFilter = null;
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(),
|
||||
true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "red"));
|
||||
service("namespaceB", "serviceB", Map.of("color", "blue"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = true
|
||||
* - labels = {}
|
||||
* - filter = "#root.metadata.namespace matches '^.+A$'"
|
||||
* (ends in A)
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=red}
|
||||
* - serviceB exists in namespaceB with labels = {color=blue}
|
||||
*
|
||||
* - we get only serviceA as a result.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testAllNamespacesWithoutLabelsWithNamespaceFilter() {
|
||||
boolean allNamespaces = true;
|
||||
Map<String, String> labels = Map.of();
|
||||
String spelFilter = """
|
||||
#root.metadata.namespace matches "^.+A$"
|
||||
""";
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(),
|
||||
true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "red"));
|
||||
service("namespaceB", "serviceB", Map.of("color", "blue"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = true
|
||||
* - labels = {}
|
||||
* - filter = "#root.metadata.namespace matches '^namespace[A|B]$'"
|
||||
* (namespaceA or namespaceB)
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=red}
|
||||
* - serviceB exists in namespaceB with labels = {color=blue}
|
||||
* - serviceC exists in namespaceC with labels = {color=purple}
|
||||
*
|
||||
* - we get only serviceA and serviceB as a result.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testAllNamespacesWithoutLabelsWithNamespacesFilter() {
|
||||
boolean allNamespaces = true;
|
||||
Map<String, String> labels = Map.of();
|
||||
String spelFilter = """
|
||||
#root.metadata.namespace matches "^namespace[A|B]$"
|
||||
""";
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(),
|
||||
true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "red"));
|
||||
service("namespaceB", "serviceB", Map.of("color", "blue"));
|
||||
service("namespaceC", "serviceC", Map.of("color", "purple"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 2);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
Assertions.assertEquals(result.get(1).getMetadata().getName(), "serviceB");
|
||||
Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "namespaceB");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = false
|
||||
* - specific namespace = namespaceA
|
||||
* - labels = {}
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=red}
|
||||
* - serviceB exists in namespaceB with labels = {color=blue}
|
||||
*
|
||||
* - we get only serviceA as a result.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testSpecificNamespaceWithoutLabelsWithoutFilter() {
|
||||
boolean allNamespaces = false;
|
||||
Map<String, String> labels = Map.of();
|
||||
String spelFilter = null;
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(),
|
||||
true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
utils.when(() -> Fabric8Utils.getApplicationNamespace(Mockito.any(KubernetesClient.class),
|
||||
Mockito.nullable(String.class), Mockito.anyString(), Mockito.any(KubernetesNamespaceProvider.class)))
|
||||
.thenReturn("namespaceA");
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "red"));
|
||||
service("namespaceB", "serviceB", Map.of("color", "blue"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = false
|
||||
* - specific namespace = namespaceA
|
||||
* - labels = {color = purple}
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=red}
|
||||
* - serviceB exists in namespaceA with labels = {color=purple}
|
||||
* - serviceC exists in namespaceC with labels = {color=purple}
|
||||
*
|
||||
* - we get only serviceB as a result, even if such labels are also
|
||||
* present on a different service (but it's in a different namespace).
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testSpecificNamespaceWithLabelsWithoutFilter() {
|
||||
boolean allNamespaces = false;
|
||||
Map<String, String> labels = Map.of("color", "purple");
|
||||
String spelFilter = null;
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(),
|
||||
true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
utils.when(() -> Fabric8Utils.getApplicationNamespace(Mockito.any(KubernetesClient.class),
|
||||
Mockito.nullable(String.class), Mockito.anyString(), Mockito.any(KubernetesNamespaceProvider.class)))
|
||||
.thenReturn("namespaceA");
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "red"));
|
||||
service("namespaceA", "serviceB", Map.of("color", "purple"));
|
||||
service("namespaceC", "serviceC", Map.of("color", "purple"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceB");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = false
|
||||
* - specific namespace = namespaceA
|
||||
* - labels = {}
|
||||
* - filter = "#root.metadata.labels.containsKey("number")"
|
||||
* (namespaceA or namespaceB)
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=red, number=1}
|
||||
* - serviceB exists in namespaceA with labels = {color=purple, cycle=create}
|
||||
* - serviceC exists in namespaceC with labels = {color=purple, number=1}
|
||||
*
|
||||
* - we get only serviceB as a result (because of the filter) even if such labels are also
|
||||
* present on a different service (but it's in a different namespace).
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testSpecificNamespaceWithoutLabelsWithFilter() {
|
||||
boolean allNamespaces = false;
|
||||
Map<String, String> labels = Map.of();
|
||||
String spelFilter = """
|
||||
#root.metadata.labels.containsKey("number")
|
||||
""".stripLeading();
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces, Set.of(),
|
||||
true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
utils.when(() -> Fabric8Utils.getApplicationNamespace(Mockito.any(KubernetesClient.class),
|
||||
Mockito.nullable(String.class), Mockito.anyString(), Mockito.any(KubernetesNamespaceProvider.class)))
|
||||
.thenReturn("namespaceA");
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "red", "number", "1"));
|
||||
service("namespaceA", "serviceB", Map.of("color", "purple", "cycle", "create"));
|
||||
service("namespaceC", "serviceC", Map.of("color", "purple", "number", "1"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getLabels(), Map.of("color", "red", "number", "1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = false
|
||||
* - some namespaces = [namespaceA, namespaceB]
|
||||
* - labels = {}
|
||||
* - filter = null
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {}
|
||||
* - serviceB exists in namespaceB with labels = {}
|
||||
* - serviceC exists in namespaceC with labels = {}
|
||||
*
|
||||
* - we get serviceA and serviceB as a result, because their namespaces match.
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testSomeNamespacesWithoutLabelsWithoutFilter() {
|
||||
boolean allNamespaces = false;
|
||||
Set<String> someNamespaces = Set.of("namespaceA", "namespaceB");
|
||||
Map<String, String> labels = Map.of();
|
||||
String spelFilter = null;
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces,
|
||||
someNamespaces, true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
service("namespaceA", "serviceA", Map.of());
|
||||
service("namespaceB", "serviceB", Map.of());
|
||||
service("namespaceC", "serviceC", Map.of());
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 2);
|
||||
result = result.stream().sorted(Comparator.comparing(x -> x.getMetadata().getName())).toList();
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
Assertions.assertEquals(result.get(1).getMetadata().getName(), "serviceB");
|
||||
Assertions.assertEquals(result.get(1).getMetadata().getNamespace(), "namespaceB");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = false
|
||||
* - some namespaces = [namespaceA, namespaceB]
|
||||
* - labels = {color=purple}
|
||||
* - filter = null
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=purple}
|
||||
* - serviceB exists in namespaceB with labels = {color=red}
|
||||
* - serviceC exists in namespaceC with labels = {color=purple}
|
||||
*
|
||||
* - we get serviceA as a result
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testSomeNamespacesWithLabelsWithoutFilter() {
|
||||
boolean allNamespaces = false;
|
||||
Set<String> someNamespaces = Set.of("namespaceA", "namespaceB");
|
||||
Map<String, String> labels = Map.of("color", "purple");
|
||||
String spelFilter = null;
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces,
|
||||
someNamespaces, true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "purple"));
|
||||
service("namespaceB", "serviceB", Map.of("color", "red"));
|
||||
service("namespaceC", "serviceC", Map.of("color", "purple"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* - all-namespaces = false
|
||||
* - some namespaces = [namespaceA, namespaceB]
|
||||
* - labels = {color=purple}
|
||||
* - filter = #root.metadata.labels.containsKey("number")
|
||||
*
|
||||
* - serviceA exists in namespaceA with labels = {color=purple}
|
||||
* - serviceB exists in namespaceB with labels = {color=red}
|
||||
* - serviceC exists in namespaceC with labels = {color=purple}
|
||||
*
|
||||
* - we get serviceA as a result
|
||||
* </pre>
|
||||
*/
|
||||
@Test
|
||||
void testSomeNamespacesWithLabelsWithFilter() {
|
||||
boolean allNamespaces = false;
|
||||
Set<String> someNamespaces = Set.of("namespaceA", "namespaceB");
|
||||
Map<String, String> labels = Map.of("color", "purple");
|
||||
String spelFilter = """
|
||||
#root.metadata.labels.containsKey("number")
|
||||
""".stripLeading();
|
||||
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(false, allNamespaces,
|
||||
someNamespaces, true, 60L, false, spelFilter, Set.of(), labels, null, null, 0, false);
|
||||
|
||||
Fabric8DiscoveryServicesAdapter adapter = new Fabric8DiscoveryServicesAdapter(
|
||||
new KubernetesDiscoveryClientAutoConfiguration().servicesFunction(properties, environment), properties,
|
||||
null);
|
||||
|
||||
service("namespaceA", "serviceA", Map.of("color", "purple", "number", "1"));
|
||||
service("namespaceB", "serviceB", Map.of("color", "purple", "cycle", "create"));
|
||||
service("namespaceC", "serviceC", Map.of("color", "purple", "number", "1"));
|
||||
|
||||
List<Service> result = adapter.apply(client);
|
||||
Assertions.assertEquals(result.size(), 1);
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getName(), "serviceA");
|
||||
Assertions.assertEquals(result.get(0).getMetadata().getNamespace(), "namespaceA");
|
||||
}
|
||||
|
||||
private void service(String namespace, String name, Map<String, String> labels) {
|
||||
client.services().inNamespace(namespace)
|
||||
.resource(new ServiceBuilder().withNewMetadata().withName(name).withLabels(labels).and().build())
|
||||
.create();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -85,7 +85,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
mockClient.services().inNamespace("test").resource(service).create();
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null,
|
||||
new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint");
|
||||
@@ -115,7 +115,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
60, false, null, Set.of(), labels, "http_tcp", Metadata.DEFAULT, 0, true);
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
|
||||
KubernetesClient::services, new ServicePortSecureResolver(properties));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint");
|
||||
|
||||
@@ -137,7 +137,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
mockClient.endpoints().inNamespace("test").resource(endPoint).create();
|
||||
|
||||
final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null,
|
||||
new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT));
|
||||
|
||||
final List<Endpoints> result_endpoints = discoveryClient.getEndPointsList("endpoint");
|
||||
@@ -164,7 +164,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
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));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<Endpoints> result_endpoints = discoveryClient.getEndPointsList("endpoint");
|
||||
|
||||
@@ -194,7 +194,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
|
||||
|
||||
final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
|
||||
KubernetesClient::services, new ServicePortSecureResolver(properties));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<Endpoints> result_endpoints = discoveryClient.getEndPointsList("endpoint");
|
||||
|
||||
@@ -226,7 +226,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
60, false, null, Set.of(443, 8443), labels, null, metadata, 0, true);
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
|
||||
KubernetesClient::services, new ServicePortSecureResolver(properties));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint");
|
||||
|
||||
@@ -254,7 +254,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
mockClient.services().inNamespace("test").resource(service3).create();
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null,
|
||||
new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT));
|
||||
|
||||
final List<String> services = discoveryClient.getServices();
|
||||
@@ -280,7 +280,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient,
|
||||
KubernetesDiscoveryProperties.DEFAULT,
|
||||
client -> client.services().withLabels(Collections.singletonMap("label", "value")),
|
||||
client -> client.services().withLabels(Collections.singletonMap("label", "value")), null,
|
||||
new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT));
|
||||
|
||||
final List<String> services = discoveryClient.getServices();
|
||||
@@ -313,7 +313,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
KubernetesDiscoveryProperties.Metadata.DEFAULT, 0, false);
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
|
||||
KubernetesClient::services, new ServicePortSecureResolver(properties));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<String> services = discoveryClient.getServices();
|
||||
|
||||
@@ -346,7 +346,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
60, false, null, Set.of(), Map.of(), null, Metadata.DEFAULT, 0, true);
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient, properties,
|
||||
KubernetesClient::services, new ServicePortSecureResolver(properties));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint");
|
||||
|
||||
@@ -369,7 +369,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
mockClient.endpoints().inNamespace("test").resource(endPoint).create();
|
||||
|
||||
final KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null,
|
||||
new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint1");
|
||||
@@ -398,7 +398,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
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));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint2");
|
||||
|
||||
@@ -429,7 +429,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
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));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint3");
|
||||
|
||||
@@ -459,7 +459,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
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));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint4");
|
||||
|
||||
@@ -488,7 +488,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
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));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint5");
|
||||
|
||||
@@ -515,7 +515,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
mockClient.services().inNamespace("test").resource(service).create();
|
||||
|
||||
final DiscoveryClient discoveryClient = new KubernetesDiscoveryClient(mockClient,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services,
|
||||
KubernetesDiscoveryProperties.DEFAULT, KubernetesClient::services, null,
|
||||
new ServicePortSecureResolver(KubernetesDiscoveryProperties.DEFAULT));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint5");
|
||||
@@ -545,7 +545,7 @@ public class KubernetesDiscoveryClientTest {
|
||||
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));
|
||||
KubernetesClient::services, null, new ServicePortSecureResolver(properties));
|
||||
|
||||
final List<ServiceInstance> instances = discoveryClient.getInstances("endpoint5");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user