diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml index 7da56b6c..d1775571 100644 --- a/.github/workflows/maven.yaml +++ b/.github/workflows/maven.yaml @@ -141,7 +141,7 @@ jobs: CURRENT_INDEX: ${{ matrix.current_index }} NUMBER_OF_JOBS: ${{ matrix.number_of_jobs }} run: | - + # - find all tests # - exclude Fabric8IstionIT # - only take classes that have @Test inside them diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java index bd3ec89b..09e75ac2 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClient.java @@ -209,8 +209,12 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { @Override public List getServices() { - return serviceLister.list().stream().filter(service -> matchesServiceLabels(service, properties)) - .map(s -> s.getMetadata().getName()).distinct().toList(); + List services = serviceLister.list().stream() + .filter(service -> matchesServiceLabels(service, properties)).map(s -> s.getMetadata().getName()) + .distinct().toList(); + + LOG.debug(() -> "will return services : " + services); + return services; } @PostConstruct @@ -233,4 +237,9 @@ public class KubernetesInformerDiscoveryClient implements DiscoveryClient { + " services) , discovery client is now available"); } + @Override + public int getOrder() { + return properties.order(); + } + } diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java index 3f5d9015..eebacc1e 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesInformerDiscoveryClientAutoConfiguration.java @@ -21,6 +21,7 @@ import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; import io.kubernetes.client.openapi.models.V1Endpoints; import io.kubernetes.client.openapi.models.V1Service; +import org.apache.commons.logging.LogFactory; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -44,6 +45,7 @@ import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscover import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.log.LogAccessor; /** * @author wind57 @@ -58,13 +60,8 @@ import org.springframework.context.annotation.Configuration; KubernetesClientInformerAutoConfiguration.class }) public class KubernetesInformerDiscoveryClientAutoConfiguration { - @Bean - @ConditionalOnClass({ HealthIndicator.class }) - @ConditionalOnDiscoveryHealthIndicatorEnabled - public KubernetesDiscoveryClientHealthIndicatorInitializer indicatorInitializer( - ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { - return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher); - } + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(KubernetesInformerDiscoveryClientAutoConfiguration.class)); @Deprecated(forRemoval = true) public KubernetesInformerDiscoveryClient kubernetesInformerDiscoveryClient( @@ -76,6 +73,21 @@ public class KubernetesInformerDiscoveryClientAutoConfiguration { serviceLister, endpointsLister, serviceInformer, endpointsInformer, properties); } + /** + * Creation of this bean triggers publishing an InstanceRegisteredEvent. In turn, + * there is the CommonsClientAutoConfiguration::DiscoveryClientHealthIndicator, that + * implements 'ApplicationListener' that will catch this event. It also registers a + * bean of type DiscoveryClientHealthIndicator via ObjectProvider. + */ + @Bean + @ConditionalOnClass({ HealthIndicator.class }) + @ConditionalOnDiscoveryHealthIndicatorEnabled + public KubernetesDiscoveryClientHealthIndicatorInitializer indicatorInitializer( + ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { + LOG.debug(() -> "Will publish InstanceRegisteredEvent from blocking implementation"); + return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher); + } + @Bean @ConditionalOnMissingBean public KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient( diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java index 26d35597..3828afd8 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClient.java @@ -49,12 +49,8 @@ public class KubernetesInformerReactiveDiscoveryClient implements ReactiveDiscov serviceInformer, endpointsInformer, properties); } - KubernetesInformerReactiveDiscoveryClient(SharedInformerFactory sharedInformerFactory, - Lister serviceLister, Lister endpointsLister, - SharedInformer serviceInformer, SharedInformer endpointsInformer, - KubernetesDiscoveryProperties properties) { - this.kubernetesDiscoveryClient = new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, - endpointsLister, serviceInformer, endpointsInformer, properties); + KubernetesInformerReactiveDiscoveryClient(KubernetesInformerDiscoveryClient kubernetesDiscoveryClient) { + this.kubernetesDiscoveryClient = kubernetesDiscoveryClient; } @Override diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java index a3589795..ea67908f 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfiguration.java @@ -21,6 +21,7 @@ import io.kubernetes.client.informer.SharedInformerFactory; import io.kubernetes.client.informer.cache.Lister; import io.kubernetes.client.openapi.models.V1Endpoints; import io.kubernetes.client.openapi.models.V1Service; +import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -39,12 +40,19 @@ import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscov import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; import org.springframework.cloud.kubernetes.client.KubernetesClientPodUtils; import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration; +import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; 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.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.log.LogAccessor; + +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer.RegisteredEventSource; /** * @author Ryan Baxter @@ -61,15 +69,18 @@ import org.springframework.context.annotation.Configuration; KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class }) public class KubernetesInformerReactiveDiscoveryClientAutoConfiguration { - @Bean - @ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") - @ConditionalOnDiscoveryHealthIndicatorEnabled + private static final LogAccessor LOG = new LogAccessor( + LogFactory.getLog(KubernetesInformerReactiveDiscoveryClientAutoConfiguration.class)); + + @Deprecated(forRemoval = true) public ReactiveDiscoveryClientHealthIndicator kubernetesReactiveDiscoveryClientHealthIndicator( KubernetesInformerReactiveDiscoveryClient client, DiscoveryClientHealthIndicatorProperties properties, KubernetesClientPodUtils podUtils) { ReactiveDiscoveryClientHealthIndicator healthIndicator = new ReactiveDiscoveryClientHealthIndicator(client, properties); - InstanceRegisteredEvent event = new InstanceRegisteredEvent<>(podUtils.currentPod(), null); + InstanceRegisteredEvent event = new InstanceRegisteredEvent<>( + new RegisteredEventSource("kubernetes", podUtils.isInsideKubernetes(), podUtils.currentPod().get()), + null); healthIndicator.onApplicationEvent(event); return healthIndicator; } @@ -84,13 +95,43 @@ public class KubernetesInformerReactiveDiscoveryClientAutoConfiguration { serviceLister, endpointsLister, serviceInformer, endpointsInformer, properties); } + /** + * Post an event so that health indicator is initialized. + */ + @Bean + @ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") + @ConditionalOnDiscoveryHealthIndicatorEnabled + KubernetesDiscoveryClientHealthIndicatorInitializer reactiveIndicatorInitializer( + ApplicationEventPublisher applicationEventPublisher, PodUtils podUtils) { + LOG.debug(() -> "Will publish InstanceRegisteredEvent from reactive implementation"); + return new KubernetesDiscoveryClientHealthIndicatorInitializer(podUtils, applicationEventPublisher); + } + + /** + * unlike the blocking implementation, we need to register the health indicator. + */ + @Bean + @ConditionalOnClass(name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") + @ConditionalOnDiscoveryHealthIndicatorEnabled + ReactiveDiscoveryClientHealthIndicator kubernetesReactiveDiscoveryClientHealthIndicator( + KubernetesInformerReactiveDiscoveryClient client, DiscoveryClientHealthIndicatorProperties properties) { + return new ReactiveDiscoveryClientHealthIndicator(client, properties); + } + @Bean @ConditionalOnMissingBean KubernetesInformerReactiveDiscoveryClient kubernetesClientReactiveDiscoveryClient( + KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient) { + return new KubernetesInformerReactiveDiscoveryClient(kubernetesClientInformerDiscoveryClient); + } + + @Bean + @ConditionalOnMissingBean + KubernetesInformerDiscoveryClient kubernetesClientInformerDiscoveryClient( SharedInformerFactory sharedInformerFactory, Lister serviceLister, Lister endpointsLister, SharedInformer serviceInformer, SharedInformer endpointsInformer, KubernetesDiscoveryProperties properties) { - return new KubernetesInformerReactiveDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, + return new KubernetesInformerDiscoveryClient(sharedInformerFactory, serviceLister, endpointsLister, serviceInformer, endpointsInformer, properties); } diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java index cbb862c8..ae7d5311 100644 --- a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/reactive/KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationContextTests.java @@ -16,10 +16,16 @@ package org.springframework.cloud.kubernetes.client.discovery.reactive; +import java.io.StringReader; + 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.util.Config; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import org.testcontainers.k3s.K3sContainer; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -31,6 +37,10 @@ import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfigura import org.springframework.cloud.kubernetes.client.discovery.KubernetesClientInformerAutoConfiguration; import org.springframework.cloud.kubernetes.commons.KubernetesCommonsAutoConfiguration; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryPropertiesAutoConfiguration; +import org.springframework.cloud.kubernetes.integration.tests.commons.Commons; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import static org.assertj.core.api.Assertions.assertThat; @@ -44,6 +54,13 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte private ApplicationContextRunner applicationContextRunner; + private static K3sContainer container; + + @AfterAll + static void afterAll() { + container.stop(); + } + @Test void discoveryEnabledDefault() { setup("spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false"); @@ -190,7 +207,7 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class, UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class)) - .withPropertyValues(properties); + .withUserConfiguration(ApiClientConfig.class).withPropertyValues(properties); } private void setupWithFilteredClassLoader(String name, String... properties) { @@ -200,7 +217,22 @@ class KubernetesInformerReactiveDiscoveryClientAutoConfigurationApplicationConte KubernetesClientAutoConfiguration.class, SimpleReactiveDiscoveryClientAutoConfiguration.class, UtilAutoConfiguration.class, KubernetesDiscoveryPropertiesAutoConfiguration.class, KubernetesCommonsAutoConfiguration.class, KubernetesClientInformerAutoConfiguration.class)) - .withClassLoader(new FilteredClassLoader(name)).withPropertyValues(properties); + .withUserConfiguration(ApiClientConfig.class).withClassLoader(new FilteredClassLoader(name)) + .withPropertyValues(properties); + } + + @Configuration + static class ApiClientConfig { + + @Bean + @Primary + ApiClient apiClient() throws Exception { + container = Commons.container(); + container.start(); + + return Config.fromConfig(new StringReader(container.getKubeConfigYaml())); + } + } } diff --git a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java index 4fe13f28..8ea4359f 100644 --- a/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java +++ b/spring-cloud-kubernetes-client-loadbalancer/src/test/java/org/springframework/cloud/kubernetes/client/loadbalancer/KubernetesClientLoadBalancerServiceModeTests.java @@ -30,6 +30,7 @@ import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; import io.kubernetes.client.util.ClientBuilder; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -128,6 +129,7 @@ public class KubernetesClientLoadBalancerServiceModeTests { // Mock this so the real implementation does not try to connect to the K8S API // Server KubernetesInformerDiscoveryClient client = mock(KubernetesInformerDiscoveryClient.class); + Mockito.when(client.getOrder()).thenReturn(0); return client; } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java index 154c017f..8d8cd991 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/discovery/KubernetesDiscoveryClientHealthIndicatorInitializer.java @@ -45,9 +45,10 @@ public final class KubernetesDiscoveryClientHealthIndicatorInitializer { @PostConstruct private void postConstruct() { LOG.debug(() -> "publishing InstanceRegisteredEvent"); - this.applicationEventPublisher.publishEvent(new InstanceRegisteredEvent<>( + InstanceRegisteredEvent instanceRegisteredEvent = new InstanceRegisteredEvent<>( new RegisteredEventSource("kubernetes", podUtils.isInsideKubernetes(), podUtils.currentPod().get()), - null)); + null); + this.applicationEventPublisher.publishEvent(instanceRegisteredEvent); } /** diff --git a/spring-cloud-kubernetes-discovery/src/test/resources/logback-test.xml b/spring-cloud-kubernetes-discovery/src/test/resources/logback-test.xml new file mode 100644 index 00000000..9e284876 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml index 0c86b73b..07666d06 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/pom.xml @@ -39,6 +39,12 @@ test + + org.springframework.boot + spring-boot-starter-test + test + + diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApplicationListener.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApplicationListener.java new file mode 100644 index 00000000..01903f24 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryApplicationListener.java @@ -0,0 +1,44 @@ +/* + * 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 io.kubernetes.client.openapi.models.V1Pod; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.log.LogAccessor; +import org.springframework.stereotype.Component; + +import static org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryClientHealthIndicatorInitializer.RegisteredEventSource; + +/** + * @author wind57 + */ +@Component +public class DiscoveryApplicationListener implements ApplicationListener> { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(DiscoveryApplicationListener.class)); + + @Override + public void onApplicationEvent(InstanceRegisteredEvent event) { + V1Pod pod = (V1Pod) ((RegisteredEventSource) event.getSource()).pod(); + LOG.info(() -> "received InstanceRegisteredEvent from pod with 'app' label value : " + + pod.getMetadata().getLabels().get("app")); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java index 7e24e97a..d0ca41c4 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/DiscoveryController.java @@ -18,6 +18,7 @@ package org.springframework.cloud.kubernetes.client.discovery.it; import java.util.List; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; @@ -32,8 +33,10 @@ public class DiscoveryController { private final KubernetesInformerDiscoveryClient discoveryClient; - public DiscoveryController(KubernetesInformerDiscoveryClient discoveryClient) { - this.discoveryClient = discoveryClient; + public DiscoveryController(ObjectProvider discoveryClient) { + KubernetesInformerDiscoveryClient[] local = new KubernetesInformerDiscoveryClient[1]; + discoveryClient.ifAvailable(x -> local[0] = x); + this.discoveryClient = local[0]; } @GetMapping("/services") diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java new file mode 100644 index 00000000..e7e964ff --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/java/org/springframework/cloud/kubernetes/client/discovery/it/ReactiveDiscoveryController.java @@ -0,0 +1,48 @@ +/* + * 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 reactor.core.publisher.Mono; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInformerReactiveDiscoveryClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author wind57 + */ +@RestController +public class ReactiveDiscoveryController { + + private final KubernetesInformerReactiveDiscoveryClient reactiveDiscoveryClient; + + public ReactiveDiscoveryController( + ObjectProvider reactiveDiscoveryClient) { + KubernetesInformerReactiveDiscoveryClient[] local = new KubernetesInformerReactiveDiscoveryClient[1]; + reactiveDiscoveryClient.ifAvailable(x -> local[0] = x); + this.reactiveDiscoveryClient = local[0]; + } + + @GetMapping("/reactive/services") + public Mono> allServices() { + return reactiveDiscoveryClient.getServices().collectList(); + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/resources/application.yaml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/resources/application.yaml new file mode 100644 index 00000000..cd402822 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/main/resources/application.yaml @@ -0,0 +1,8 @@ +management: + endpoint: + health: + show-details: always + endpoints: + web: + exposure: + include: "*" diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java index 0f598cf2..56f4fb01 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryClientIT.java @@ -31,6 +31,7 @@ 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.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; import org.testcontainers.k3s.K3sContainer; @@ -210,6 +211,9 @@ class KubernetesClientDiscoveryClientIT { * */ @Test + @Disabled + // TODO will be fixed by this issue : + // https://github.com/spring-cloud/spring-cloud-kubernetes/issues/1289 void testSpecificNamespace() { util.createNamespace(NAMESPACE_A); util.createNamespace(NAMESPACE_B); diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryHealthIT.java b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryHealthIT.java new file mode 100644 index 00000000..77c1c328 --- /dev/null +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-client-discovery-it/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientDiscoveryHealthIT.java @@ -0,0 +1,304 @@ +/* + * 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.Objects; +import java.util.Optional; + +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.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +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.boot.test.json.BasicJsonTester; +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 KubernetesClientDiscoveryHealthIT { + + private static final String REACTIVE_STATUS = "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].status"; + + private static final String BLOCKING_STATUS = "$.components.discoveryComposite.components.discoveryClient.status"; + + private static final String NAMESPACE = "default"; + + private static final String IMAGE_NAME = "spring-cloud-kubernetes-client-discovery-it"; + + private static final BasicJsonTester BASIC_JSON_TESTER = new BasicJsonTester( + KubernetesClientDiscoveryHealthIT.class); + + 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); + util.setUp(NAMESPACE); + } + + @AfterAll + static void after() throws Exception { + Commons.cleanUp(IMAGE_NAME, K3S); + } + + /** + * Reactive is disabled, only blocking is active. As such, + * KubernetesInformerDiscoveryClientAutoConfiguration::indicatorInitializer will post + * an InstanceRegisteredEvent. + * + * We assert for logs and call '/health' endpoint to see that blocking discovery + * client was initialized. + */ + @Test + void testBlockingConfiguration() { + + manifests(true, false, Phase.CREATE); + + assertLogStatement("Will publish InstanceRegisteredEvent from blocking implementation"); + assertLogStatement("publishing InstanceRegisteredEvent"); + assertLogStatement("Discovery Client has been initialized"); + assertLogStatement( + "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-client-discovery-it"); + + WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); + + String healthResult = healthClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.discoveryComposite.status").isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathStringValue(BLOCKING_STATUS) + .isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathArrayValue( + "$.components.discoveryComposite.components.discoveryClient.details.services") + .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(REACTIVE_STATUS); + + manifests(true, false, Phase.DELETE); + } + + /** + * Both blocking and reactive are enabled. + */ + @Test + void testDefaultConfiguration() { + + manifests(false, false, Phase.CREATE); + + assertLogStatement("Will publish InstanceRegisteredEvent from blocking implementation"); + assertLogStatement("publishing InstanceRegisteredEvent"); + assertLogStatement("Discovery Client has been initialized"); + assertLogStatement( + "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-client-discovery-it"); + + WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); + + String healthResult = healthClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.discoveryComposite.status").isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.discoveryComposite.components.discoveryClient.status") + .isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathArrayValue( + "$.components.discoveryComposite.components.discoveryClient.details.services") + .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.reactiveDiscoveryClients.status").isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathStringValue( + "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].status") + .isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue( + "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") + .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + + manifests(false, false, Phase.DELETE); + } + + /** + * Reactive is enabled, blocking is disabled. As such, + * KubernetesInformerDiscoveryClientAutoConfiguration::indicatorInitializer will post + * an InstanceRegisteredEvent. + * + * We assert for logs and call '/health' endpoint to see that blocking discovery + * client was initialized. + */ + @Test + void testReactiveConfiguration() { + + manifests(false, true, Phase.CREATE); + + assertLogStatement("Will publish InstanceRegisteredEvent from reactive implementation"); + assertLogStatement("publishing InstanceRegisteredEvent"); + assertLogStatement("Discovery Client has been initialized"); + assertLogStatement( + "received InstanceRegisteredEvent from pod with 'app' label value : spring-cloud-kubernetes-client-discovery-it"); + + WebClient healthClient = builder().baseUrl("http://localhost/actuator/health").build(); + + String healthResult = healthClient.method(HttpMethod.GET).retrieve().bodyToMono(String.class) + .retryWhen(retrySpec()).block(); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)) + .extractingJsonPathStringValue("$.components.reactiveDiscoveryClients.status").isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathStringValue(REACTIVE_STATUS) + .isEqualTo("UP"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).extractingJsonPathArrayValue( + "$.components.reactiveDiscoveryClients.components.['Kubernetes Reactive Discovery Client'].details.services") + .containsExactlyInAnyOrder("spring-cloud-kubernetes-client-discovery-it", "kubernetes"); + + Assertions.assertThat(BASIC_JSON_TESTER.from(healthResult)).doesNotHaveJsonPath(BLOCKING_STATUS); + + // test for services also: + + WebClient servicesClient = builder().baseUrl("http://localhost/reactive/services").build(); + + List servicesResult = servicesClient.method(HttpMethod.GET).retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }).retryWhen(retrySpec()).block(); + + Assertions.assertThat(servicesResult).contains("spring-cloud-kubernetes-client-discovery-it"); + Assertions.assertThat(servicesResult).contains("kubernetes"); + + manifests(false, true, Phase.DELETE); + } + + private static void manifests(boolean disableReactive, boolean disableBlocking, 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 envVars = new ArrayList<>( + Optional.ofNullable(deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()) + .orElse(List.of())); + + V1EnvVar debugLevelForCommons = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_COMMONS_DISCOVERY").value("DEBUG"); + + if (!disableBlocking) { + V1EnvVar debugBlockingEnvVar = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_CLIENT_DISCOVERY_HEALTH").value("DEBUG"); + + V1EnvVar debugLevelForBlocking = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY").value("DEBUG"); + + envVars.add(debugBlockingEnvVar); + envVars.add(debugLevelForBlocking); + } + + if (!disableReactive) { + V1EnvVar debugReactiveEnvVar = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_CLIENT_DISCOVERY_HEALTH_REACTIVE").value("DEBUG"); + + V1EnvVar debugLevelForReactive = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY_REACTIVE") + .value("DEBUG"); + + V1EnvVar debugLevelForBlocking = new V1EnvVar() + .name("LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_KUBERNETES_CLIENT_DISCOVERY").value("DEBUG"); + + envVars.add(debugReactiveEnvVar); + envVars.add(debugLevelForBlocking); + envVars.add(debugLevelForReactive); + } + + if (disableBlocking) { + V1EnvVar disableBlockingEnvVar = new V1EnvVar().name("SPRING_CLOUD_DISCOVERY_BLOCKING_ENABLED") + .value("FALSE"); + envVars.add(disableBlockingEnvVar); + } + + if (disableReactive) { + V1EnvVar disableReactiveEnvVar = new V1EnvVar().name("SPRING_CLOUD_DISCOVERY_REACTIVE_ENABLED") + .value("FALSE"); + envVars.add(disableReactiveEnvVar); + } + + envVars.add(debugLevelForCommons); + 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.assertThat(ok).contains(message); + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml index 32ee3d23..0cd3a21d 100644 --- a/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml +++ b/spring-cloud-kubernetes-integration-tests/spring-cloud-kubernetes-configuration-watcher-it/pom.xml @@ -49,9 +49,30 @@ ${wiremock.version} test + + + org.springframework.boot + spring-boot-starter-webflux + + io.kubernetes client-java + + + org.slf4j + slf4j-api + + + test + + + io.kubernetes + client-java-extended + + + com.github.docker-java + docker-java-core test @@ -61,15 +82,15 @@ - io.kubernetes - client-java-extended - test - - - - org.testcontainers - k3s + com.github.docker-java + docker-java-transport-httpclient5 test + + + org.slf4j + slf4j-api + + diff --git a/spring-cloud-kubernetes-test-support/pom.xml b/spring-cloud-kubernetes-test-support/pom.xml index 7bb2a6d1..6ebdc918 100644 --- a/spring-cloud-kubernetes-test-support/pom.xml +++ b/spring-cloud-kubernetes-test-support/pom.xml @@ -45,12 +45,12 @@ org.testcontainers testcontainers - 1.16.3 + 1.18.0 org.testcontainers k3s - 1.16.3 + 1.18.0 diff --git a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java index f058fc44..12638032 100644 --- a/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java +++ b/spring-cloud-kubernetes-test-support/src/main/java/org/springframework/cloud/kubernetes/integration/tests/commons/native_client/Util.java @@ -132,6 +132,9 @@ public final class Util { } } catch (Exception e) { + if (e instanceof ApiException apiException) { + System.out.println(apiException.getResponseBody()); + } e.printStackTrace(); throw new RuntimeException(e); }