New Kubernetes Informer Auto Config (#1276)

This commit is contained in:
erabii
2023-04-04 17:34:39 +03:00
committed by GitHub
parent 9c0856aa07
commit d9c458347d
32 changed files with 1102 additions and 95 deletions

View File

@@ -36,8 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* itself as the default apiClient. This is to avoid overwriting the user's
* defaultApiClient if they include this project.
*
* kubernetes informer is disabled because KubernetesInformerAutoConfiguration creates a
* defaultApiClient that will be autowired instead of the ApiClient created in
* kubernetes informer is disabled because KubernetesClientInformerAutoConfiguration
* creates a defaultApiClient that will be autowired instead of the ApiClient created in
* KubernetesClientAutoConfiguration
*/
@SpringBootTest(classes = App.class,

View File

@@ -37,8 +37,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* itself as the default apiClient. This is to avoid overwriting the user's
* defaultApiClient if they include this project.
*
* kubernetes informer is disabled because KubernetesInformerAutoConfiguration creates a
* defaultApiClient that will be autowired instead of the ApiClient created in
* kubernetes informer is disabled because KubernetesClientInformerAutoConfiguration
* creates a defaultApiClient that will be autowired instead of the ApiClient created in
* KubernetesClientAutoConfiguration
*/
@SpringBootTest(classes = App.class, properties = { "kubernetes.informer.enabled=false",

View File

@@ -31,8 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* itself as the default apiClient. This is to avoid overwriting the user's
* defaultApiClient if they include this project.
*
* kubernetes informer is disabled because KubernetesInformerAutoConfiguration creates a
* defaultApiClient that will be autowired instead of the ApiClient created in
* kubernetes informer is disabled because KubernetesClientInformerAutoConfiguration
* creates a defaultApiClient that will be autowired instead of the ApiClient created in
* KubernetesClientAutoConfiguration
*/
@SpringBootTest(classes = App.class,

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2013-2023 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.client.discovery;
import io.kubernetes.client.informer.SharedIndexInformer;
import io.kubernetes.client.informer.SharedInformerFactory;
import io.kubernetes.client.informer.cache.Lister;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.openapi.models.V1EndpointsList;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.cloud.client.CommonsClientAutoConfiguration;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration;
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.NamespaceResolutionFailedException;
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.log.LogAccessor;
import static io.kubernetes.client.util.Namespaces.NAMESPACE_ALL;
import static io.kubernetes.client.util.Namespaces.NAMESPACE_DEFAULT;
import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.getApplicationNamespace;
/**
* @author wind57
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnKubernetesDiscoveryEnabled
@ConditionalOnBlockingOrReactiveEnabled
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class })
@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class })
public class KubernetesClientInformerAutoConfiguration {
private static final LogAccessor LOG = new LogAccessor(
LogFactory.getLog(KubernetesClientInformerAutoConfiguration.class));
@Bean
@ConditionalOnMissingBean
public SharedInformerFactory sharedInformerFactory(ApiClient client) {
return new SharedInformerFactory(client);
}
@Bean
public String kubernetesClientNamespace(KubernetesDiscoveryProperties properties,
KubernetesNamespaceProvider provider) {
String namespace;
if (properties.allNamespaces()) {
namespace = NAMESPACE_ALL;
LOG.debug(() -> "serviceSharedInformer will use all-namespaces");
}
else {
try {
namespace = getApplicationNamespace(null, "kubernetes client discovery", provider);
}
catch (NamespaceResolutionFailedException ex) {
LOG.warn(() -> "failed to resolve namespace, defaulting to :" + NAMESPACE_DEFAULT
+ ". This will fail in a future release.");
namespace = NAMESPACE_DEFAULT;
}
LOG.debug("serviceSharedInformer will use namespace : " + namespace);
}
return namespace;
}
@Bean
@ConditionalOnMissingBean(value = V1Service.class, parameterizedContainer = SharedIndexInformer.class)
public SharedIndexInformer<V1Service> servicesSharedIndexInformer(SharedInformerFactory sharedInformerFactory,
ApiClient apiClient, String kubernetesClientNamespace) {
GenericKubernetesApi<V1Service, V1ServiceList> servicesApi = new GenericKubernetesApi<>(V1Service.class,
V1ServiceList.class, "", "v1", "services", apiClient);
return sharedInformerFactory.sharedIndexInformerFor(servicesApi, V1Service.class, 0L,
kubernetesClientNamespace);
}
@Bean
@ConditionalOnMissingBean(value = V1Endpoints.class, parameterizedContainer = SharedIndexInformer.class)
public SharedIndexInformer<V1Endpoints> endpointsSharedIndexInformer(SharedInformerFactory sharedInformerFactory,
ApiClient apiClient, String kubernetesClientNamespace) {
GenericKubernetesApi<V1Endpoints, V1EndpointsList> servicesApi = new GenericKubernetesApi<>(V1Endpoints.class,
V1EndpointsList.class, "", "v1", "endpoints", apiClient);
return sharedInformerFactory.sharedIndexInformerFor(servicesApi, V1Endpoints.class, 0L,
kubernetesClientNamespace);
}
@Bean
@ConditionalOnMissingBean(value = V1Service.class, parameterizedContainer = Lister.class)
public Lister<V1Service> servicesLister(SharedIndexInformer<V1Service> servicesSharedIndexInformer,
String kubernetesClientNamespace) {
return new Lister<>(servicesSharedIndexInformer.getIndexer(), kubernetesClientNamespace);
}
@Bean
@ConditionalOnMissingBean(value = V1Endpoints.class, parameterizedContainer = Lister.class)
public Lister<V1Endpoints> endpointsLister(SharedIndexInformer<V1Endpoints> endpointsSharedIndexInformer,
String kubernetesClientNamespace) {
return new Lister<>(endpointsSharedIndexInformer.getIndexer(), kubernetesClientNamespace);
}
}

View File

@@ -48,8 +48,12 @@ import static io.kubernetes.client.util.Namespaces.NAMESPACE_ALL;
import static io.kubernetes.client.util.Namespaces.NAMESPACE_DEFAULT;
/**
* This configuration is not used by us internally and will be removed in a future
* release. Use it at your own risk.
*
* @author wind57
*/
@Deprecated(forRemoval = true)
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnKubernetesDiscoveryEnabled

View File

@@ -72,38 +72,40 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient {
private final KubernetesDiscoveryProperties properties;
private final String namespace;
@Deprecated(forRemoval = true)
public KubernetesInformerDiscoveryClient(String namespace, SharedInformerFactory sharedInformerFactory,
Lister<V1Service> serviceLister, Lister<V1Endpoints> endpointsLister,
SharedInformer<V1Service> serviceInformer, SharedInformer<V1Endpoints> endpointsInformer,
KubernetesDiscoveryProperties properties) {
this.namespace = namespace;
this.sharedInformerFactory = sharedInformerFactory;
this.serviceLister = serviceLister;
this.endpointsLister = endpointsLister;
this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced();
this.properties = properties;
}
public KubernetesInformerDiscoveryClient(SharedInformerFactory sharedInformerFactory,
Lister<V1Service> serviceLister, Lister<V1Endpoints> endpointsLister,
SharedInformer<V1Service> serviceInformer, SharedInformer<V1Endpoints> endpointsInformer,
KubernetesDiscoveryProperties properties) {
this.sharedInformerFactory = sharedInformerFactory;
this.serviceLister = serviceLister;
this.endpointsLister = endpointsLister;
this.informersReadyFunc = () -> serviceInformer.hasSynced() && endpointsInformer.hasSynced();
this.properties = properties;
}
@Override
public String description() {
return "Fabric8 Kubernetes Client Discovery";
return "Kubernetes Client Discovery";
}
@Override
public List<ServiceInstance> getInstances(String serviceId) {
Objects.requireNonNull(serviceId, "serviceId must be provided");
if (!StringUtils.hasText(namespace) && !properties.allNamespaces()) {
LOG.warn(() -> "Namespace is null or empty, this may cause issues looking up services");
}
List<V1Service> services = properties.allNamespaces()
? serviceLister.list().stream().filter(svc -> serviceId.equals(svc.getMetadata().getName())).toList()
: List.of(serviceLister.namespace(namespace).get(serviceId));
List<V1Service> services = serviceLister.list().stream()
.filter(svc -> serviceId.equals(svc.getMetadata().getName())).toList();
if (services.size() == 0 || !services.stream().anyMatch(service -> matchesServiceLabels(service, properties))) {
// no such service present in the cluster
return new ArrayList<>();
@@ -226,10 +228,8 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient {
@Override
public List<String> getServices() {
List<V1Service> services = properties.allNamespaces() ? serviceLister.list()
: serviceLister.namespace(namespace).list();
return services.stream().filter(service -> matchesServiceLabels(service, properties))
.map(s -> s.getMetadata().getName()).collect(Collectors.toList());
return serviceLister.list().stream().filter(service -> matchesServiceLabels(service, properties))
.map(s -> s.getMetadata().getName()).distinct().toList();
}
@PostConstruct

View File

@@ -55,7 +55,7 @@ import org.springframework.context.annotation.Configuration;
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class })
@AutoConfigureAfter({ KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class,
KubernetesInformerAutoConfiguration.class })
KubernetesClientInformerAutoConfiguration.class })
public class KubernetesInformerDiscoveryClientAutoConfiguration {
@Bean
@@ -66,6 +66,7 @@ public class KubernetesInformerDiscoveryClientAutoConfiguration {
return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher);
}
@Deprecated(forRemoval = true)
@Bean
@ConditionalOnMissingBean
public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient(
@@ -77,4 +78,14 @@ public class KubernetesInformerDiscoveryClientAutoConfiguration {
serviceLister, endpointsLister, serviceInformer, endpointsInformer, properties);
}
@Bean
@ConditionalOnMissingBean
public KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient(
SharedInformerFactory sharedInformerFactory, Lister<V1Service> serviceLister,
Lister<V1Endpoints> endpointsLister, SharedInformer<V1Service> serviceInformer,
SharedInformer<V1Endpoints> endpointsInformer, KubernetesDiscoveryProperties properties) {
return new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister,
serviceInformer, endpointsInformer, properties);
}
}

View File

@@ -38,7 +38,7 @@ import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIn
import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator;
import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration;
import org.springframework.cloud.kubernetes.client.KubernetesClientPodUtils;
import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerAutoConfiguration;
import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.ConditionalOnKubernetesDiscoveryEnabled;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
@@ -58,7 +58,7 @@ import org.springframework.context.annotation.Configuration;
@AutoConfigureBefore({ SimpleReactiveDiscoveryClientAutoConfiguration.class,
ReactiveCommonsClientAutoConfiguration.class })
@AutoConfigureAfter({ ReactiveCompositeDiscoveryClientAutoConfiguration.class,
KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesInformerAutoConfiguration.class })
KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class })
public class KubernetesInformerReactiveDiscoveryClientAutoConfiguration {
@Bean

View File

@@ -1,4 +1,4 @@
org.springframework.cloud.kubernetes.client.discovery.catalog.KubernetesCatalogWatchAutoConfiguration
org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClientAutoConfiguration
org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerAutoConfiguration
org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration
org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClientAutoConfiguration

View File

@@ -207,7 +207,7 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests
applicationContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(KubernetesInformerDiscoveryClientAutoConfiguration.class,
KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class,
KubernetesInformerAutoConfiguration.class))
KubernetesClientInformerAutoConfiguration.class))
.withUserConfiguration(ApiClientConfig.class).withPropertyValues(properties);
}
@@ -215,7 +215,7 @@ class KubernetesInformerDiscoveryClientAutoConfigurationApplicationContextTests
applicationContextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(KubernetesInformerDiscoveryClientAutoConfiguration.class,
KubernetesClientAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class,
KubernetesInformerAutoConfiguration.class))
KubernetesClientInformerAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(cls)).withUserConfiguration(ApiClientConfig.class)
.withPropertyValues(properties);
}

View File

@@ -28,7 +28,7 @@ import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscov
import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration;
import org.springframework.cloud.commons.util.UtilAutoConfiguration;
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerAutoConfiguration;
import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration;
import org.springframework.cloud.kubernetes.commons.KubernetesCommonsAutoConfiguration;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration;
@@ -189,7 +189,7 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte
KubernetesInformerReactiveDiscoveryClientAutoConfiguration.class,
KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class,
UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class,
KubernetesCommonsAutoConfiguration.class, KubernetesInformerAutoConfiguration.class))
KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class))
.withPropertyValues(properties);
}
@@ -199,7 +199,7 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte
KubernetesInformerReactiveDiscoveryClientAutoConfiguration.class,
KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class,
UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class,
KubernetesCommonsAutoConfiguration.class, KubernetesInformerAutoConfiguration.class))
KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(name)).withPropertyValues(properties);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* Copyright 2013-2023 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.
@@ -16,7 +16,7 @@
package org.springframework.cloud.kubernetes.client.discovery.reactive;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -29,77 +29,97 @@ import io.kubernetes.client.openapi.models.V1EndpointSubset;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServiceSpec;
import io.kubernetes.client.openapi.models.V1ServiceStatus;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.test.StepVerifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.mock.env.MockEnvironment;
import static io.kubernetes.client.util.Namespaces.NAMESPACE_ALL;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Ryan Baxter
*/
@RunWith(MockitoJUnitRunner.class)
public class KubernetesInformerReactiveDiscoveryClientTests {
class KubernetesInformerReactiveDiscoveryClientTests {
@Mock
private SharedInformerFactory sharedInformerFactory;
// default constructor partitions by namespace
private Cache<V1Service> serviceCache = new Cache<>();
private static final V1Service testService1 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1"))
.spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus());
// default constructor partitions by namespace
private Cache<V1Endpoints> endpointsCache = new Cache<>();
private static final V1Service testService2 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace2"))
.spec(new V1ServiceSpec().loadBalancerIP("1.1.1.1")).status(new V1ServiceStatus());
private static final String NAMESPACE_1 = "namespace1";
private static final V1Endpoints testEndpoints1 = new V1Endpoints()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace("namespace1"))
private static final String NAMESPACE_2 = "namespace2";
private final SharedInformerFactory sharedInformerFactory = Mockito.mock(SharedInformerFactory.class);
private static final V1Service TEST_SERVICE_1 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace(NAMESPACE_1));
private static final V1Service TEST_SERVICE_2 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-2").namespace(NAMESPACE_2));
// same name as TEST_SERVICE_1, to test distinct
private static final V1Service TEST_SERVICE_3 = new V1Service()
.metadata(new V1ObjectMeta().name("test-svc-2").namespace(NAMESPACE_2));
private static final V1Endpoints TEST_ENDPOINTS_1 = new V1Endpoints()
.metadata(new V1ObjectMeta().name("test-svc-1").namespace(NAMESPACE_1))
.addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080))
.addAddressesItem(new V1EndpointAddress().ip("2.2.2.2")));
@Test
public void testDiscoveryGetServicesAllNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(testService1, testService2);
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister, null,
null, null, KubernetesDiscoveryProperties.DEFAULT);
StepVerifier.create(discoveryClient.getServices())
.expectNext(testService1.getMetadata().getName(), testService2.getMetadata().getName()).expectComplete()
.verify();
@AfterEach
void afterEach() {
serviceCache = new Cache<>();
endpointsCache = new Cache<>();
}
@Test
public void testDiscoveryGetServicesOneNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(testService1, testService2);
void testDiscoveryGetServicesAllNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(NAMESPACE_ALL, TEST_SERVICE_1, TEST_SERVICE_2,
TEST_SERVICE_3);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("namespace1");
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null,
KubernetesDiscoveryProperties.DEFAULT);
new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister, null,
null, null, kubernetesDiscoveryProperties);
StepVerifier.create(discoveryClient.getServices()).expectNext(testService1.getMetadata().getName())
StepVerifier.create(discoveryClient.getServices())
.expectNext(TEST_SERVICE_1.getMetadata().getName(), TEST_SERVICE_2.getMetadata().getName())
.expectComplete().verify();
}
@Test
public void testDiscoveryGetInstanceAllNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(testService1, testService2);
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
void testDiscoveryGetServicesOneNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(NAMESPACE_1, TEST_SERVICE_1, TEST_SERVICE_2);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn(NAMESPACE_1);
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null,
KubernetesDiscoveryProperties.DEFAULT);
StepVerifier.create(discoveryClient.getServices()).expectNext(TEST_SERVICE_1.getMetadata().getName())
.expectComplete().verify();
}
@Test
void testDiscoveryGetInstanceAllNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(NAMESPACE_ALL, TEST_SERVICE_1, TEST_SERVICE_2);
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(NAMESPACE_1, TEST_ENDPOINTS_1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, true,
Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false);
@@ -108,45 +128,213 @@ public class KubernetesInformerReactiveDiscoveryClientTests {
new KubernetesNamespaceProvider(new MockEnvironment()), sharedInformerFactory, serviceLister,
endpointsLister, null, null, kubernetesDiscoveryProperties);
StepVerifier
.create(discoveryClient.getInstances("test-svc-1")).expectNext(new DefaultKubernetesServiceInstance("",
"test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null))
StepVerifier.create(discoveryClient.getInstances("test-svc-1"))
.expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, Map.of(), false,
NAMESPACE_1, null))
.expectComplete().verify();
}
@Test
public void testDiscoveryGetInstanceOneNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(testService1, testService2);
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(testEndpoints1);
void testDiscoveryGetInstanceOneNamespaceShouldWork() {
Lister<V1Service> serviceLister = setupServiceLister(NAMESPACE_1, TEST_SERVICE_1, TEST_SERVICE_2);
Lister<V1Endpoints> endpointsLister = setupEndpointsLister(NAMESPACE_1, TEST_ENDPOINTS_1);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true, false,
Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("namespace1");
when(kubernetesNamespaceProvider.getNamespace()).thenReturn(NAMESPACE_1);
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null,
kubernetesDiscoveryProperties);
StepVerifier
.create(discoveryClient.getInstances("test-svc-1")).expectNext(new DefaultKubernetesServiceInstance("",
"test-svc-1", "2.2.2.2", 8080, new HashMap<>(), false, "namespace1", null))
StepVerifier.create(discoveryClient.getInstances("test-svc-1"))
.expectNext(new DefaultKubernetesServiceInstance("", "test-svc-1", "2.2.2.2", 8080, Map.of(), false,
NAMESPACE_1, null))
.expectComplete().verify();
}
private Lister<V1Service> setupServiceLister(V1Service... services) {
Cache<V1Service> serviceCache = new Cache<>();
Lister<V1Service> serviceLister = new Lister<>(serviceCache);
/**
* <pre>
* - all-namespaces = true
* - service-a in namespace-a exists
* - service-b in namespace-b exists
*
* As such, both services are found.
* </pre>
*/
@Test
void testAllNamespacesTwoServicesPresent() {
boolean allNamespaces = true;
V1Service serviceA = new V1Service().metadata(new V1ObjectMeta().name("service-a").namespace("namespace-a"));
V1Service serviceB = new V1Service().metadata(new V1ObjectMeta().name("service-b").namespace("namespace-b"));
serviceCache.add(serviceA);
serviceCache.add(serviceB);
Lister<V1Service> serviceLister = new Lister<>(serviceCache).namespace(NAMESPACE_ALL);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true,
allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant");
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null,
kubernetesDiscoveryProperties);
List<String> result = discoveryClient.getServices().collectList().block();
Assertions.assertEquals(result.size(), 2);
Assertions.assertTrue(result.contains("service-a"));
Assertions.assertTrue(result.contains("service-b"));
}
/**
* <pre>
* - all-namespaces = false
* - service-a in namespace-a exists
* - service-b in namespace-b exists
* - service lister exists in namespace-a
*
* As such, one service is found.
* </pre>
*/
@Test
void testSingleNamespaceTwoServicesPresent() {
boolean allNamespaces = false;
V1Service serviceA = new V1Service().metadata(new V1ObjectMeta().name("service-a").namespace("namespace-a"));
V1Service serviceB = new V1Service().metadata(new V1ObjectMeta().name("service-b").namespace("namespace-b"));
serviceCache.add(serviceA);
serviceCache.add(serviceB);
Lister<V1Service> serviceLister = new Lister<>(serviceCache).namespace("namespace-a");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true,
allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant");
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, null, null, null,
kubernetesDiscoveryProperties);
List<String> result = discoveryClient.getServices().collectList().block();
Assertions.assertEquals(result.size(), 1);
Assertions.assertTrue(result.contains("service-a"));
Assertions.assertFalse(result.contains("service-b"));
}
/**
* <pre>
* - all-namespaces = true
* - endpoints-X in namespace-a exists
* - endpoints-X in namespace-b exists
*
* As such, both endpoints are found.
* </pre>
*/
@Test
void testAllNamespacesTwoEndpointsPresent() {
boolean allNamespaces = true;
V1Service serviceXNamespaceA = new V1Service()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a"));
V1Service serviceXNamespaceB = new V1Service()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b"));
serviceCache.add(serviceXNamespaceA);
serviceCache.add(serviceXNamespaceB);
V1Endpoints endpointsXNamespaceA = new V1Endpoints()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a"))
.addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080))
.addAddressesItem(new V1EndpointAddress().ip("1.1.1.1")));
V1Endpoints endpointsXNamespaceB = new V1Endpoints()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b"))
.addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080))
.addAddressesItem(new V1EndpointAddress().ip("2.2.2.2")));
endpointsCache.add(endpointsXNamespaceA);
endpointsCache.add(endpointsXNamespaceB);
Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsCache).namespace(NAMESPACE_ALL);
Lister<V1Service> serviceLister = new Lister<>(serviceCache).namespace(NAMESPACE_ALL);
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true,
allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant");
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null,
kubernetesDiscoveryProperties);
List<ServiceInstance> result = discoveryClient.getInstances("endpoints-x").collectList().block();
Assertions.assertEquals(result.size(), 2);
List<String> byIp = result.stream().map(ServiceInstance::getHost).sorted().toList();
Assertions.assertTrue(byIp.contains("1.1.1.1"));
Assertions.assertTrue(byIp.contains("2.2.2.2"));
}
/**
* <pre>
* - all-namespaces = true
* - endpoints-X in namespace-a exists
* - endpoints-X in namespace-b exists
*
* We search in namespace-a, only. As such, single endpoints is found.
* </pre>
*/
@Test
void testAllSingleTwoEndpointsPresent() {
boolean allNamespaces = true;
V1Service serviceXNamespaceA = new V1Service()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a"));
V1Service serviceXNamespaceB = new V1Service()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b"));
serviceCache.add(serviceXNamespaceA);
serviceCache.add(serviceXNamespaceB);
V1Endpoints endpointsXNamespaceA = new V1Endpoints()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-a"))
.addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080))
.addAddressesItem(new V1EndpointAddress().ip("1.1.1.1")));
V1Endpoints endpointsXNamespaceB = new V1Endpoints()
.metadata(new V1ObjectMeta().name("endpoints-x").namespace("namespace-b"))
.addSubsetsItem(new V1EndpointSubset().addPortsItem(new CoreV1EndpointPort().port(8080))
.addAddressesItem(new V1EndpointAddress().ip("2.2.2.2")));
endpointsCache.add(endpointsXNamespaceA);
endpointsCache.add(endpointsXNamespaceB);
Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsCache).namespace("namespace-a");
Lister<V1Service> serviceLister = new Lister<>(serviceCache).namespace("namespace-a");
KubernetesDiscoveryProperties kubernetesDiscoveryProperties = new KubernetesDiscoveryProperties(true,
allNamespaces, Set.of(), true, 60, false, null, Set.of(), Map.of(), null, null, 0, false);
KubernetesNamespaceProvider kubernetesNamespaceProvider = mock(KubernetesNamespaceProvider.class);
when(kubernetesNamespaceProvider.getNamespace()).thenReturn("irrelevant");
KubernetesInformerReactiveDiscoveryClient discoveryClient = new KubernetesInformerReactiveDiscoveryClient(
kubernetesNamespaceProvider, sharedInformerFactory, serviceLister, endpointsLister, null, null,
kubernetesDiscoveryProperties);
List<ServiceInstance> result = discoveryClient.getInstances("endpoints-x").collectList().block();
Assertions.assertEquals(result.size(), 1);
List<String> byIp = result.stream().map(ServiceInstance::getHost).sorted().toList();
Assertions.assertTrue(byIp.contains("1.1.1.1"));
}
private Lister<V1Service> setupServiceLister(String namespace, V1Service... services) {
Lister<V1Service> serviceLister = new Lister<>(serviceCache, namespace);
for (V1Service svc : services) {
serviceCache.add(svc);
}
return serviceLister;
}
private Lister<V1Endpoints> setupEndpointsLister(V1Endpoints... endpoints) {
Cache<V1Endpoints> endpointsCache = new Cache<>();
private Lister<V1Endpoints> setupEndpointsLister(String namespace, V1Endpoints... endpoints) {
Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsCache);
for (V1Endpoints ep : endpoints) {
endpointsCache.add(ep);

View File

@@ -92,6 +92,7 @@
<module>spring-cloud-kubernetes-client-secrets-event-reload-multiple-apps</module>
<module>spring-cloud-kubernetes-fabric8-client-catalog-watcher</module>
<module>spring-cloud-kubernetes-client-catalog-watcher</module>
<module>spring-cloud-kubernetes-client-discovery-it</module>
<module>spring-cloud-kubernetes-fabric8-client-discovery-with-bootstrap</module>
</modules>
</project>

View File

@@ -85,6 +85,7 @@ public class KubernetesClientCatalogWatchNamespacesIT {
@AfterEach
void afterEach() {
util.deleteClusterWide(NAMESPACE_DEFAULT, Set.of(NAMESPACE_A, NAMESPACE_B));
util.deleteNamespace(NAMESPACE_A);
util.deleteNamespace(NAMESPACE_B);
}

View File

@@ -78,6 +78,7 @@ class ConfigMapEventReloadIT {
@AfterAll
static void afterAll() throws Exception {
util.deleteClusterWide(NAMESPACE, Set.of("left", "right"));
util.deleteNamespace("left");
util.deleteNamespace("right");
Commons.cleanUp(IMAGE_NAME, K3S);

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-kubernetes-integration-tests</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.3-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-kubernetes-client-discovery-it</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-client-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-test-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>../src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!-- build image in the 'package' phase, and ignore plain tests -->
<!-- via maven-surefire-plugin::skipTests -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<imageName>docker.io/springcloud/${project.artifactId}:${project.version}</imageName>
<imageBuilder>paketobuildpacks/builder</imageBuilder>
</configuration>
<executions>
<execution>
<id>build-image</id>
<configuration>
<skip>${skip.build.image}</skip>
</configuration>
<phase>package</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
<execution>
<id>repackage</id>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- ignore plain tests (in the 'test' phase), so that we could build the image first, see above -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<!-- run tests in the 'integration-tests' phase, one that is after 'package' (where we build the image) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>${testsToRun}</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>imagename</id>
<activation>
<property>
<name>!env.IMAGE</name>
</property>
</activation>
<properties>
<env.IMAGE>springcloud/${project.artifactId}:${project.version}</env.IMAGE>
</properties>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2023 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.client.discovery.it;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author wind57
*/
@SpringBootApplication
public class DiscoveryApp {
public static void main(String[] args) {
SpringApplication.run(DiscoveryApp.class, args);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013-2023 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.client.discovery.it;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wind57
*/
@RestController
public class DiscoveryController {
private final KubernetesInformerDiscoveryClient discoveryClient;
public DiscoveryController(KubernetesInformerDiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
@GetMapping("/services")
public List<String> allServices() {
return discoveryClient.getServices();
}
@GetMapping("/service-instances/{serviceId}")
public List<ServiceInstance> serviceInstances(@PathVariable("serviceId") String serviceId) {
return discoveryClient.getInstances(serviceId);
}
}

View File

@@ -0,0 +1,301 @@
/*
* Copyright 2013-2023 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.client.discovery;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1EnvVar;
import io.kubernetes.client.openapi.models.V1Ingress;
import io.kubernetes.client.openapi.models.V1Service;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Container;
import org.testcontainers.k3s.K3sContainer;
import reactor.netty.http.client.HttpClient;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import org.springframework.cloud.kubernetes.commons.discovery.DefaultKubernetesServiceInstance;
import org.springframework.cloud.kubernetes.integration.tests.commons.Commons;
import org.springframework.cloud.kubernetes.integration.tests.commons.Phase;
import org.springframework.cloud.kubernetes.integration.tests.commons.native_client.Util;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author wind57
*/
class KubernetesClientDiscoveryClientIT {
private static final String NAMESPACE = "default";
private static final String NAMESPACE_A = "a";
private static final String NAMESPACE_B = "b";
private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it";
private static Util util;
private static final K3sContainer K3S = Commons.container();
@BeforeAll
static void beforeAll() throws Exception {
K3S.start();
Commons.validateImage(IMAGE_NAME, K3S);
Commons.loadSpringCloudKubernetesImage(IMAGE_NAME, K3S);
util = new Util(K3S);
}
@AfterAll
static void after() throws Exception {
Commons.cleanUp(IMAGE_NAME, K3S);
}
/**
* Three services are deployed in the default namespace. We do not configure any
* explicit namespace and 'default' must be picked-up.
*/
@Test
void testSimple() {
// set-up
util.setUp(NAMESPACE);
manifests(false, null, Phase.CREATE);
util.busybox(NAMESPACE, Phase.CREATE);
assertLogStatement("serviceSharedInformer will use namespace : default");
WebClient servicesClient = builder().baseUrl("http://localhost/services").build();
List<String> servicesResult = servicesClient.method(HttpMethod.GET).retrieve()
.bodyToMono(new ParameterizedTypeReference<List<String>>() {
}).retryWhen(retrySpec()).block();
Assertions.assertEquals(servicesResult.size(), 3);
Assertions.assertTrue(servicesResult.contains("kubernetes"));
Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-client-discovery-it"));
Assertions.assertTrue(servicesResult.contains("busybox-service"));
WebClient ourServiceClient = builder()
.baseUrl("http://localhost//service-instances/spring-cloud-kubernetes-client-discovery-it").build();
List<DefaultKubernetesServiceInstance> ourServiceInstances = ourServiceClient.method(HttpMethod.GET).retrieve()
.bodyToMono(new ParameterizedTypeReference<List<DefaultKubernetesServiceInstance>>() {
}).retryWhen(retrySpec()).block();
Assertions.assertEquals(ourServiceInstances.size(), 1);
DefaultKubernetesServiceInstance serviceInstance = ourServiceInstances.get(0);
Assertions.assertNotNull(serviceInstance.getInstanceId());
Assertions.assertEquals(serviceInstance.getServiceId(), "spring-cloud-kubernetes-client-discovery-it");
Assertions.assertNotNull(serviceInstance.getHost());
Assertions.assertEquals(serviceInstance.getMetadata(),
Map.of("http", "8080", "app", "spring-cloud-kubernetes-client-discovery-it"));
Assertions.assertEquals(serviceInstance.getPort(), 8080);
Assertions.assertEquals(serviceInstance.getNamespace(), "default");
WebClient busyBoxServiceClient = builder().baseUrl("http://localhost//service-instances/busybox-service")
.build();
List<DefaultKubernetesServiceInstance> busyBoxServiceInstances = busyBoxServiceClient.method(HttpMethod.GET)
.retrieve().bodyToMono(new ParameterizedTypeReference<List<DefaultKubernetesServiceInstance>>() {
}).retryWhen(retrySpec()).block();
Assertions.assertEquals(busyBoxServiceInstances.size(), 2);
// clean-up
util.busybox(NAMESPACE, Phase.DELETE);
manifests(false, null, Phase.DELETE);
}
/**
* <pre>
* - config server is enabled for all namespaces
* - wiremock service is deployed in namespace-a
* - busybox service is deployed in namespace-b
*
* Our discovery searches in all namespaces, thus finds them both.
* </pre>
*/
@Test
void testAllNamespaces() {
util.createNamespace(NAMESPACE_A);
util.createNamespace(NAMESPACE_B);
util.setUpClusterWideClusterRoleBinding(NAMESPACE);
util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE);
util.busybox(NAMESPACE_B, Phase.CREATE);
manifests(true, null, Phase.CREATE);
assertLogStatement("serviceSharedInformer will use all-namespaces");
WebClient servicesClient = builder().baseUrl("http://localhost/services").build();
List<String> servicesResult = servicesClient.method(HttpMethod.GET).retrieve()
.bodyToMono(new ParameterizedTypeReference<List<String>>() {
}).retryWhen(retrySpec()).block();
Assertions.assertEquals(servicesResult.size(), 7);
Assertions.assertTrue(servicesResult.contains("kubernetes"));
Assertions.assertTrue(servicesResult.contains("spring-cloud-kubernetes-client-discovery-it"));
Assertions.assertTrue(servicesResult.contains("busybox-service"));
Assertions.assertTrue(servicesResult.contains("service-wiremock"));
manifests(true, null, Phase.DELETE);
util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE);
util.busybox(NAMESPACE_B, Phase.DELETE);
util.deleteClusterWideClusterRoleBinding(NAMESPACE);
util.deleteNamespace(NAMESPACE_A);
util.deleteNamespace(NAMESPACE_B);
}
/**
* <pre>
* - config server is enabled for namespace-a
* - wiremock service is deployed in namespace-a
* - wiremock service is deployed in namespace-b
*
* Only service in namespace-a is found.
* </pre>
*/
@Test
void testSpecificNamespace() {
util.createNamespace(NAMESPACE_A);
util.createNamespace(NAMESPACE_B);
util.setUpClusterWide(NAMESPACE, Set.of(NAMESPACE_A));
util.wiremock(NAMESPACE_A, "/wiremock", Phase.CREATE);
util.wiremock(NAMESPACE_B, "/wiremock", Phase.CREATE);
manifests(false, NAMESPACE_A, Phase.CREATE);
// first check that wiremock service is present in both namespaces a and b
assertServicePresentInNamespaces(List.of("a", "b"), "service-wiremock", "service-wiremock");
assertLogStatement("serviceSharedInformer will use namespace : a");
WebClient servicesClient = builder().baseUrl("http://localhost/services").build();
List<String> servicesResult = servicesClient.method(HttpMethod.GET).retrieve()
.bodyToMono(new ParameterizedTypeReference<List<String>>() {
}).retryWhen(retrySpec()).block();
Assertions.assertEquals(servicesResult.size(), 1);
Assertions.assertTrue(servicesResult.contains("service-wiremock"));
WebClient wiremockInNamespaceAClient = builder().baseUrl("http://localhost//service-instances/service-wiremock")
.build();
List<DefaultKubernetesServiceInstance> wiremockInNamespaceA = wiremockInNamespaceAClient.method(HttpMethod.GET)
.retrieve().bodyToMono(new ParameterizedTypeReference<List<DefaultKubernetesServiceInstance>>() {
}).retryWhen(retrySpec()).block();
Assertions.assertEquals(wiremockInNamespaceA.size(), 1);
DefaultKubernetesServiceInstance serviceInstance = wiremockInNamespaceA.get(0);
Assertions.assertEquals(serviceInstance.getNamespace(), "a");
manifests(false, NAMESPACE_A, Phase.DELETE);
util.wiremock(NAMESPACE_A, "/wiremock", Phase.DELETE);
util.wiremock(NAMESPACE_B, "/wiremock", Phase.DELETE);
util.deleteClusterWide(NAMESPACE, Set.of(NAMESPACE_A));
util.deleteNamespace(NAMESPACE_A);
util.deleteNamespace(NAMESPACE_B);
}
private static void manifests(boolean allNamespaces, String clientSpecificNamespace, Phase phase) {
V1Deployment deployment = (V1Deployment) util.yaml("kubernetes-discovery-deployment.yaml");
V1Service service = (V1Service) util.yaml("kubernetes-discovery-service.yaml");
V1Ingress ingress = (V1Ingress) util.yaml("kubernetes-discovery-ingress.yaml");
List<V1EnvVar> envVars = new ArrayList<>(
Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv())
.orElse(List.of()));
V1EnvVar debugLevel = new V1EnvVar().name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY")
.value("DEBUG");
if (allNamespaces) {
V1EnvVar allNamespacesVar = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_DISCOVERY_ALL_NAMESPACES")
.value("TRUE");
envVars.add(allNamespacesVar);
}
if (clientSpecificNamespace != null) {
V1EnvVar clientNamespace = new V1EnvVar().name("SPRING_CLOUD_KUBERNETES_CLIENT_NAMESPACE")
.value(NAMESPACE_A);
envVars.add(clientNamespace);
}
envVars.add(debugLevel);
deployment.getSpec().getTemplate().getSpec().getContainers().get(0).setEnv(envVars);
if (phase.equals(Phase.CREATE)) {
util.createAndWait(NAMESPACE, null, deployment, service, ingress, true);
}
else if (phase.equals(Phase.DELETE)) {
util.deleteAndWait(NAMESPACE, deployment, service, ingress);
}
}
private WebClient.Builder builder() {
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create()));
}
private RetryBackoffSpec retrySpec() {
return Retry.fixedDelay(15, Duration.ofSeconds(1)).filter(Objects::nonNull);
}
private void assertLogStatement(String message) {
try {
String appPodName = K3S.execInContainer("sh", "-c",
"kubectl get pods -l app=" + IMAGE_NAME + " -o=name --no-headers | tr -d '\n'").getStdout();
Container.ExecResult execResult = K3S.execInContainer("sh", "-c", "kubectl logs " + appPodName.trim());
String ok = execResult.getStdout();
Assertions.assertTrue(ok.contains(message));
}
catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private void assertServicePresentInNamespaces(List<String> namespaces, String value, String serviceName) {
namespaces.forEach(x -> {
try {
String service = K3S.execInContainer("sh", "-c",
"kubectl get services -n " + x + " -l app=" + value + " -o=name --no-headers | tr -d '\n'")
.getStdout();
Assertions.assertEquals(service, "service/" + serviceName);
}
catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}

View File

@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cloud-kubernetes-client-discovery-deployment-it
spec:
selector:
matchLabels:
app: spring-cloud-kubernetes-client-discovery-it
template:
metadata:
labels:
app: spring-cloud-kubernetes-client-discovery-it
spec:
serviceAccountName: spring-cloud-kubernetes-serviceaccount
containers:
- name: spring-cloud-kubernetes-client-discovery
image: docker.io/springcloud/spring-cloud-kubernetes-client-discovery-it
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
ports:
- containerPort: 8080

View File

@@ -0,0 +1,16 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spring-cloud-kubernetes-client-discovery-ingress-it
namespace: default
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spring-cloud-kubernetes-client-discovery-it
port:
number: 8080

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: spring-cloud-kubernetes-client-discovery-it
name: spring-cloud-kubernetes-client-discovery-it
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: spring-cloud-kubernetes-client-discovery-it
type: ClusterIP

View File

@@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.testcontainers" level="INFO"/>
<logger name="com.github.dockerjava" level="INFO"/>
</configuration>

View File

@@ -77,6 +77,7 @@ class ActuatorRefreshMultipleNamespacesIT {
@AfterAll
static void afterAll() throws Exception {
util.deleteClusterWide(DEFAULT_NAMESPACE, Set.of(DEFAULT_NAMESPACE, LEFT_NAMESPACE, RIGHT_NAMESPACE));
util.deleteNamespace(LEFT_NAMESPACE);
util.deleteNamespace(RIGHT_NAMESPACE);
Commons.cleanUp(SPRING_CLOUD_K8S_CONFIG_WATCHER_APP_NAME, K3S);

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.configmap;
package org.springframework.cloud.kubernetes.fabric8.discovery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

View File

@@ -14,14 +14,13 @@
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.configmap;
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.List;
import io.fabric8.kubernetes.api.model.Endpoints;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.configmap;
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.io.InputStream;
import java.time.Duration;

View File

@@ -36,6 +36,7 @@ import io.kubernetes.client.openapi.apis.CoreV1Api;
import io.kubernetes.client.openapi.apis.NetworkingV1Api;
import io.kubernetes.client.openapi.apis.RbacAuthorizationV1Api;
import io.kubernetes.client.openapi.models.V1ClusterRole;
import io.kubernetes.client.openapi.models.V1ClusterRoleBinding;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1DeploymentList;
@@ -131,6 +132,7 @@ public final class Util {
}
}
catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
@@ -298,6 +300,49 @@ public final class Util {
}
public void setUpClusterWideClusterRoleBinding(String serviceAccountNamespace) {
try {
V1ServiceAccount serviceAccount = (V1ServiceAccount) yaml("cluster/service-account.yaml");
CheckedSupplier<V1ServiceAccount> accountSupplier = () -> coreV1Api.readNamespacedServiceAccount(
serviceAccount.getMetadata().getName(), serviceAccountNamespace, null);
CheckedSupplier<V1ServiceAccount> accountDefaulter = () -> coreV1Api
.createNamespacedServiceAccount(serviceAccountNamespace, serviceAccount, null, null, null, null);
notExistsHandler(accountSupplier, accountDefaulter);
V1ClusterRole clusterRole = (V1ClusterRole) yaml("cluster/cluster-role.yaml");
notExistsHandler(() -> rbacApi.readClusterRole(clusterRole.getMetadata().getName(), null),
() -> rbacApi.createClusterRole(clusterRole, null, null, null, null));
V1ClusterRoleBinding clusterRoleBinding = (V1ClusterRoleBinding) yaml("cluster/cluster-role-binding.yaml");
notExistsHandler(() -> rbacApi.readClusterRoleBinding(clusterRoleBinding.getMetadata().getName(), null),
() -> rbacApi.createClusterRoleBinding(clusterRoleBinding, null, null, null, null));
}
catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public void deleteClusterWideClusterRoleBinding(String serviceAccountNamespace) {
try {
V1ServiceAccount serviceAccount = (V1ServiceAccount) yaml("cluster/service-account.yaml");
V1ClusterRole clusterRole = (V1ClusterRole) yaml("cluster/cluster-role.yaml");
V1ClusterRoleBinding clusterRoleBinding = (V1ClusterRoleBinding) yaml("cluster/cluster-role-binding.yaml");
coreV1Api.deleteNamespacedServiceAccount(serviceAccount.getMetadata().getName(), serviceAccountNamespace,
null, null, null, null, null, null);
rbacApi.deleteClusterRole(clusterRole.getMetadata().getName(), null, null, null, null, null, null);
rbacApi.deleteClusterRoleBinding(clusterRoleBinding.getMetadata().getName(), null, null, null, null, null,
null);
}
catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public void setUpClusterWide(String serviceAccountNamespace, Set<String> namespaces) {
try {
@@ -332,6 +377,31 @@ public final class Util {
}
public void deleteClusterWide(String serviceAccountNamespace, Set<String> namespaces) {
try {
V1ServiceAccount serviceAccount = (V1ServiceAccount) yaml("cluster/service-account.yaml");
V1ClusterRole clusterRole = (V1ClusterRole) yaml("cluster/cluster-role.yaml");
V1RoleBinding roleBinding = (V1RoleBinding) yaml("cluster/role-binding.yaml");
coreV1Api.deleteNamespacedServiceAccount(serviceAccount.getMetadata().getName(), serviceAccountNamespace,
null, null, null, null, null, null);
rbacApi.deleteClusterRole(clusterRole.getMetadata().getName(), null, null, null, null, null, null);
namespaces.forEach(namespace -> {
roleBinding.getMetadata().setNamespace(namespace);
try {
rbacApi.deleteNamespacedRoleBinding(roleBinding.getMetadata().getName(), namespace, null, null,
null, null, null, null);
}
catch (Exception e) {
throw new RuntimeException(e);
}
});
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public void deleteNamespace(String name) {
try {
coreV1Api.deleteNamespace(name, null, null, null, null, null, null);

View File

@@ -0,0 +1,14 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app: spring-cloud-kubernetes-core-k8s-client-it
name: spring-cloud-kubernetes-cluster-role-binding
roleRef:
kind: ClusterRole
apiGroup: rbac.authorization.k8s.io
name: cluster-role
subjects:
- kind: ServiceAccount
name: spring-cloud-kubernetes-serviceaccount
namespace: default