diff --git a/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/ConditionalOnKubernetesEnabled.java b/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/ConditionalOnKubernetesEnabled.java new file mode 100644 index 0000000000..a9bbcfc228 --- /dev/null +++ b/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/ConditionalOnKubernetesEnabled.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2019 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Provides a more succinct conditional spring.cloud.kubernetes.enabled. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(value = "spring.cloud.kubernetes.enabled", matchIfMissing = true) +public @interface ConditionalOnKubernetesEnabled { + +} diff --git a/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java b/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java index f235c9cd0f..077db0dc3a 100644 --- a/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java +++ b/spring-cloud-kubernetes-core/src/main/java/org/springframework/cloud/kubernetes/KubernetesAutoConfiguration.java @@ -29,7 +29,6 @@ import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,9 +38,10 @@ * * @author Ioannis Canellos * @author EddĂș MelĂ©ndez + * @author Tim Ysewyn */ @Configuration -@ConditionalOnProperty(value = "spring.cloud.kubernetes.enabled", matchIfMissing = true) +@ConditionalOnKubernetesEnabled @EnableConfigurationProperties(KubernetesClientProperties.class) public class KubernetesAutoConfiguration { diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml index d641e21049..c2a028dd1e 100644 --- a/spring-cloud-kubernetes-discovery/pom.xml +++ b/spring-cloud-kubernetes-discovery/pom.xml @@ -42,6 +42,21 @@ spring-boot-autoconfigure true + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-starter-webflux + true + + + org.springframework.boot + spring-boot-starter-webflux + true + org.springframework.cloud spring-cloud-commons @@ -125,6 +140,11 @@ ${spring-cloud-config.version} test + + io.projectreactor + reactor-test + test + diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConditionalOnKubernetesDiscoveryEnabled.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConditionalOnKubernetesDiscoveryEnabled.java new file mode 100644 index 0000000000..0bbdcbbc9f --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConditionalOnKubernetesDiscoveryEnabled.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2019 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.discovery; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Provides a more succinct conditional + * spring.cloud.kubernetes.discovery.enabled. + * + * @author Tim Ysewyn + * @since 2.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ConditionalOnProperty(value = "spring.cloud.kubernetes.discovery.enabled", + matchIfMissing = true) +public @interface ConditionalOnKubernetesDiscoveryEnabled { + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java index 97060f651f..ede5d7697f 100644 --- a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/KubernetesDiscoveryClientAutoConfiguration.java @@ -18,12 +18,15 @@ import io.fabric8.kubernetes.client.KubernetesClient; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.client.CommonsClientAutoConfiguration; +import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; +import org.springframework.cloud.kubernetes.ConditionalOnKubernetesEnabled; +import org.springframework.cloud.kubernetes.KubernetesAutoConfiguration; import org.springframework.cloud.kubernetes.registry.KubernetesRegistration; import org.springframework.cloud.kubernetes.registry.KubernetesServiceRegistry; import org.springframework.context.annotation.Bean; @@ -37,9 +40,10 @@ */ @Configuration @ConditionalOnDiscoveryEnabled -@ConditionalOnProperty(name = "spring.cloud.kubernetes.enabled", matchIfMissing = true) +@ConditionalOnKubernetesEnabled @AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class }) +@AutoConfigureAfter({ KubernetesAutoConfiguration.class }) public class KubernetesDiscoveryClientAutoConfiguration { @Bean @@ -72,18 +76,6 @@ public KubernetesClientServicesFunction servicesFunction( } } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = "spring.cloud.kubernetes.discovery.enabled", - matchIfMissing = true) - public KubernetesDiscoveryClient kubernetesDiscoveryClient(KubernetesClient client, - KubernetesDiscoveryProperties properties, - KubernetesClientServicesFunction kubernetesClientServicesFunction, - DefaultIsServicePortSecureResolver isServicePortSecureResolver) { - return new KubernetesDiscoveryClient(client, properties, - kubernetesClientServicesFunction, isServicePortSecureResolver); - } - @Bean public KubernetesServiceRegistry getServiceRegistry() { return new KubernetesServiceRegistry(); @@ -100,4 +92,21 @@ public KubernetesDiscoveryProperties getKubernetesDiscoveryProperties() { return new KubernetesDiscoveryProperties(); } + @Configuration + @ConditionalOnBlockingDiscoveryEnabled + @ConditionalOnKubernetesDiscoveryEnabled + public static class KubernetesDiscoveryClientConfiguration { + + @Bean + @ConditionalOnMissingBean + public KubernetesDiscoveryClient kubernetesDiscoveryClient( + KubernetesClient client, KubernetesDiscoveryProperties properties, + KubernetesClientServicesFunction kubernetesClientServicesFunction, + DefaultIsServicePortSecureResolver isServicePortSecureResolver) { + return new KubernetesDiscoveryClient(client, properties, + kubernetesClientServicesFunction, isServicePortSecureResolver); + } + + } + } diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClient.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClient.java new file mode 100644 index 0000000000..039ca1ee98 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClient.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2019 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.discovery.reactive; + +import io.fabric8.kubernetes.client.KubernetesClient; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.discovery.KubernetesClientServicesFunction; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClient; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.util.Assert; + +/** + * Kubernetes implementation of {@link ReactiveDiscoveryClient}. Currently relies on the + * {@link KubernetesDiscoveryClient} for feature parity. + * + * @author Tim Ysewyn + */ +public class KubernetesReactiveDiscoveryClient implements ReactiveDiscoveryClient { + + private final KubernetesDiscoveryClient kubernetesDiscoveryClient; + + public KubernetesReactiveDiscoveryClient(KubernetesClient client, + KubernetesDiscoveryProperties properties, + KubernetesClientServicesFunction kubernetesClientServicesFunction) { + this.kubernetesDiscoveryClient = new KubernetesDiscoveryClient(client, properties, + kubernetesClientServicesFunction); + } + + @Override + public String description() { + return "Kubernetes Reactive Discovery Client"; + } + + @Override + public Flux getInstances(String serviceId) { + Assert.notNull(serviceId, + "[Assertion failed] - the object argument must not be null"); + return Flux + .defer(() -> Flux + .fromIterable(kubernetesDiscoveryClient.getInstances(serviceId))) + .subscribeOn(Schedulers.boundedElastic()); + } + + @Override + public Flux getServices() { + return Flux + .defer(() -> Flux.fromIterable(kubernetesDiscoveryClient.getServices())) + .subscribeOn(Schedulers.boundedElastic()); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfiguration.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfiguration.java new file mode 100644 index 0000000000..2e38c3ca37 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2019 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.discovery.reactive; + +import io.fabric8.kubernetes.client.KubernetesClient; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnDiscoveryHealthIndicatorEnabled; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration; +import org.springframework.cloud.client.discovery.composite.reactive.ReactiveCompositeDiscoveryClientAutoConfiguration; +import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicatorProperties; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; +import org.springframework.cloud.client.discovery.simple.reactive.SimpleReactiveDiscoveryClientAutoConfiguration; +import org.springframework.cloud.kubernetes.ConditionalOnKubernetesEnabled; +import org.springframework.cloud.kubernetes.discovery.ConditionalOnKubernetesDiscoveryEnabled; +import org.springframework.cloud.kubernetes.discovery.KubernetesClientServicesFunction; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Auto configuration for reactive discovery client. + * + * @author Tim Ysewyn + */ +@Configuration +@ConditionalOnDiscoveryEnabled +@ConditionalOnReactiveDiscoveryEnabled +@ConditionalOnKubernetesEnabled +@ConditionalOnKubernetesDiscoveryEnabled +@AutoConfigureBefore({ SimpleReactiveDiscoveryClientAutoConfiguration.class, + ReactiveCommonsClientAutoConfiguration.class }) +@AutoConfigureAfter({ ReactiveCompositeDiscoveryClientAutoConfiguration.class, + KubernetesDiscoveryClientAutoConfiguration.class }) +public class KubernetesReactiveDiscoveryClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public KubernetesReactiveDiscoveryClient kubernetesReactiveDiscoveryClient( + KubernetesClient client, KubernetesDiscoveryProperties properties, + KubernetesClientServicesFunction kubernetesClientServicesFunction) { + return new KubernetesReactiveDiscoveryClient(client, properties, + kubernetesClientServicesFunction); + } + + @Bean + @ConditionalOnClass( + name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator") + @ConditionalOnDiscoveryHealthIndicatorEnabled + public ReactiveDiscoveryClientHealthIndicator kubernetesReactiveDiscoveryClientHealthIndicator( + KubernetesReactiveDiscoveryClient client, + DiscoveryClientHealthIndicatorProperties properties) { + return new ReactiveDiscoveryClientHealthIndicator(client, properties); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories index 37f0dbc8f4..dea3148dd9 100644 --- a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.kubernetes.discovery.KubernetesCatalogWatchAutoConfiguration, \ -org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration +org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration, \ +org.springframework.cloud.kubernetes.discovery.reactive.KubernetesReactiveDiscoveryClientAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfigurationTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfigurationTests.java new file mode 100644 index 0000000000..9a2c7e6239 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientAutoConfigurationTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-2019 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.discovery.reactive; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator; +import org.springframework.cloud.commons.util.UtilAutoConfiguration; +import org.springframework.cloud.kubernetes.KubernetesAutoConfiguration; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Tim Ysewyn + */ +class KubernetesReactiveDiscoveryClientAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class, + ReactiveCommonsClientAutoConfiguration.class, + KubernetesAutoConfiguration.class, + KubernetesDiscoveryClientAutoConfiguration.class, + KubernetesReactiveDiscoveryClientAutoConfiguration.class)); + + @Test + public void shouldWorkWithDefaults() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class); + assertThat(context) + .hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class); + }); + } + + @Test + public void shouldNotHaveDiscoveryClientWhenDiscoveryDisabled() { + contextRunner.withPropertyValues("spring.cloud.discovery.enabled=false") + .run(context -> { + assertThat(context) + .doesNotHaveBean("kubernetesReactiveDiscoveryClient"); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean( + ReactiveDiscoveryClientHealthIndicator.class); + }); + } + + @Test + public void shouldNotHaveDiscoveryClientWhenReactiveDiscoveryDisabled() { + contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=false") + .run(context -> { + assertThat(context) + .doesNotHaveBean("kubernetesReactiveDiscoveryClient"); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean( + ReactiveDiscoveryClientHealthIndicator.class); + }); + } + + @Test + public void shouldNotHaveDiscoveryClientWhenKubernetesDisabled() { + contextRunner.withPropertyValues("spring.cloud.kubernetes.enabled=false") + .run(context -> { + assertThat(context) + .doesNotHaveBean("kubernetesReactiveDiscoveryClient"); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean( + ReactiveDiscoveryClientHealthIndicator.class); + }); + } + + @Test + public void shouldNotHaveDiscoveryClientWhenKubernetesDiscoveryDisabled() { + contextRunner + .withPropertyValues("spring.cloud.kubernetes.discovery.enabled=false") + .run(context -> { + assertThat(context) + .doesNotHaveBean("kubernetesReactiveDiscoveryClient"); + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean( + ReactiveDiscoveryClientHealthIndicator.class); + }); + } + + @Test + public void worksWithoutWebflux() { + contextRunner + .withClassLoader( + new FilteredClassLoader("org.springframework.web.reactive")) + .run(context -> { + assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean( + ReactiveDiscoveryClientHealthIndicator.class); + }); + } + + @Test + public void worksWithoutActuator() { + contextRunner + .withClassLoader( + new FilteredClassLoader("org.springframework.boot.actuate")) + .run(context -> { + assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class); + assertThat(context).doesNotHaveBean( + ReactiveDiscoveryClientHealthIndicator.class); + }); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java new file mode 100644 index 0000000000..fa35fa2349 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/reactive/KubernetesReactiveDiscoveryClientTests.java @@ -0,0 +1,329 @@ +/* + * Copyright 2019-2019 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.discovery.reactive; + +import java.util.HashMap; + +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsBuilder; +import io.fabric8.kubernetes.api.model.EndpointsList; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServiceListBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.discovery.support.KubernetesExtension; +import org.springframework.cloud.kubernetes.discovery.support.KubernetesExtension.Client; +import org.springframework.cloud.kubernetes.discovery.support.KubernetesExtension.Server; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Tim Ysewyn + */ +@ExtendWith(KubernetesExtension.class) +class KubernetesReactiveDiscoveryClientTests { + + @BeforeEach + public void setup(@Client KubernetesClient kubernetesClient) { + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, + kubernetesClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, + "false"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + } + + @Test + public void verifyDefaults(@Client KubernetesClient kubernetesClient) { + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + assertThat(client.description()) + .isEqualTo("Kubernetes Reactive Discovery Client"); + assertThat(client.getOrder()).isEqualTo(ReactiveDiscoveryClient.DEFAULT_ORDER); + } + + @Test + public void shouldReturnFluxOfServices(@Client KubernetesClient kubernetesClient, + @Server KubernetesServer kubernetesServer) { + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") + .andReturn(200, new ServiceListBuilder().addNewItem().withNewMetadata() + .withName("s1").withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().endItem().addNewItem().withNewMetadata() + .withName("s2").withLabels(new HashMap() { + { + put("label", "value"); + put("label2", "value2"); + } + }).endMetadata().endItem().addNewItem().withNewMetadata() + .withName("s3").endMetadata().endItem().build()) + .once(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux services = client.getServices(); + StepVerifier.create(services).expectNext("s1", "s2", "s3").expectComplete() + .verify(); + } + + @Test + public void shouldReturnEmptyFluxOfServicesWhenNoInstancesFound( + @Client KubernetesClient kubernetesClient, + @Server KubernetesServer kubernetesServer) { + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") + .andReturn(200, new ServiceListBuilder().build()).once(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux services = client.getServices(); + StepVerifier.create(services).expectNextCount(0).expectComplete().verify(); + } + + @Test + public void shouldReturnEmptyFluxForNonExistingService( + @Client KubernetesClient kubernetesClient) { + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux instances = client.getInstances("nonexistent-service"); + StepVerifier.create(instances).expectNextCount(0).expectComplete().verify(); + } + + @Test + public void shouldReturnEmptyFluxWhenServiceHasNoSubsets( + @Client KubernetesClient kubernetesClient, + @Server KubernetesServer kubernetesServer) { + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().endItem().build()) + .once(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux instances = client.getInstances("existing-service"); + StepVerifier.create(instances).expectNextCount(0).expectComplete().verify(); + } + + @Test + public void shouldReturnFlux(@Client KubernetesClient kubernetesClient, + @Server KubernetesServer kubernetesServer) { + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().endItem().build()) + .once(); + + Endpoints endPoints = new EndpointsBuilder().withNewMetadata() + .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") + .endTargetRef().endAddress().addNewPort("http", 80, "TCP").endSubset() + .build(); + + kubernetesServer.expect().get() + .withPath("/api/v1/namespaces/test/endpoints/existing-service") + .andReturn(200, endPoints).once(); + + kubernetesServer.expect().get() + .withPath("/api/v1/namespaces/test/services/existing-service") + .andReturn(200, + new ServiceBuilder().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().build()) + .once(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux instances = client.getInstances("existing-service"); + StepVerifier.create(instances).expectNextCount(1).expectComplete().verify(); + } + + @Test + public void shouldReturnFluxWithPrefixedMetadata( + @Client KubernetesClient kubernetesClient, + @Server KubernetesServer kubernetesServer) { + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().endItem().build()) + .once(); + + Endpoints endPoints = new EndpointsBuilder().withNewMetadata() + .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") + .endTargetRef().endAddress().addNewPort("http", 80, "TCP").endSubset() + .build(); + + kubernetesServer.expect().get() + .withPath("/api/v1/namespaces/test/endpoints/existing-service") + .andReturn(200, endPoints).once(); + + kubernetesServer.expect().get() + .withPath("/api/v1/namespaces/test/services/existing-service") + .andReturn(200, + new ServiceBuilder().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().build()) + .once(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + properties.getMetadata().setAnnotationsPrefix("annotation."); + properties.getMetadata().setLabelsPrefix("label."); + properties.getMetadata().setPortsPrefix("port."); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux instances = client.getInstances("existing-service"); + StepVerifier.create(instances).expectNextCount(1).expectComplete().verify(); + } + + @Test + public void shouldReturnFluxWhenServiceHasMultiplePortsAndPrimaryPortNameIsSet( + @Client KubernetesClient kubernetesClient, + @Server KubernetesServer kubernetesServer) { + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().endItem().build()) + .once(); + + Endpoints endPoints = new EndpointsBuilder().withNewMetadata() + .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") + .endTargetRef().endAddress().addNewPort("http", 80, "TCP") + .addNewPort("https", 443, "TCP").endSubset().build(); + + kubernetesServer.expect().get() + .withPath("/api/v1/namespaces/test/endpoints/existing-service") + .andReturn(200, endPoints).once(); + + kubernetesServer.expect().get() + .withPath("/api/v1/namespaces/test/services/existing-service") + .andReturn(200, + new ServiceBuilder().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().build()) + .once(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + properties.setPrimaryPortName("https"); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux instances = client.getInstances("existing-service"); + StepVerifier.create(instances).expectNextCount(1).expectComplete().verify(); + } + + @Test + public void shouldReturnFluxOfServicesAcrossAllNamespaces( + @Client KubernetesClient kubernetesClient, + @Server KubernetesServer kubernetesServer) { + kubernetesServer.expect().get().withPath("/api/v1/namespaces/test/services") + .andReturn(200, + new ServiceListBuilder().addNewItem().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().endItem().build()) + .once(); + + Endpoints endpoints = new EndpointsBuilder().withNewMetadata() + .withName("endpoint").withNamespace("test").endMetadata().addNewSubset() + .addNewAddress().withIp("ip1").withNewTargetRef().withUid("uid1") + .endTargetRef().endAddress().addNewPort("http", 80, "TCP") + .addNewPort("https", 443, "TCP").endSubset().build(); + + EndpointsList endpointsList = new EndpointsList(); + endpointsList.setItems(singletonList(endpoints)); + + kubernetesServer.expect().get().withPath( + "/api/v1/endpoints?fieldSelector=metadata.name%3Dexisting-service") + .andReturn(200, endpointsList).once(); + + kubernetesServer.expect().get() + .withPath("/api/v1/namespaces/test/services/existing-service") + .andReturn(200, + new ServiceBuilder().withNewMetadata() + .withName("existing-service") + .withLabels(new HashMap() { + { + put("label", "value"); + } + }).endMetadata().build()) + .once(); + + KubernetesDiscoveryProperties properties = new KubernetesDiscoveryProperties(); + properties.setAllNamespaces(true); + ReactiveDiscoveryClient client = new KubernetesReactiveDiscoveryClient( + kubernetesClient, properties, KubernetesClient::services); + Flux instances = client.getInstances("existing-service"); + StepVerifier.create(instances).expectNextCount(1).expectComplete().verify(); + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/support/KubernetesExtension.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/support/KubernetesExtension.java new file mode 100644 index 0000000000..4ef954be02 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/support/KubernetesExtension.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019-2019 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.discovery.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * @author Tim Ysewyn + */ +public class KubernetesExtension + implements ParameterResolver, BeforeEachCallback, AfterEachCallback { + + private final KubernetesServer mockServer = new KubernetesServer(); + + @Override + public boolean supportsParameter(ParameterContext parameterContext, + ExtensionContext context) { + return (parameterContext.getParameter().isAnnotationPresent(Server.class) + && KubernetesServer.class + .isAssignableFrom(parameterContext.getParameter().getType())) + || (parameterContext.getParameter().isAnnotationPresent(Client.class) + && KubernetesClient.class.isAssignableFrom( + parameterContext.getParameter().getType())); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + mockServer.before(); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + mockServer.after(); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, + ExtensionContext extensionContext) throws ParameterResolutionException { + if (parameterContext.getParameter().isAnnotationPresent(Client.class)) { + return mockServer.getClient(); + } + else { + return mockServer; + } + } + + /** + * Enables injection of kubernetes server to test. + */ + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + public @interface Server { + + } + + /** + * Enables injection of kubernetes client to test. + */ + @Target({ ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + public @interface Client { + + } + +} diff --git a/spring-cloud-kubernetes-integration-tests/simple-core/src/test/java/org/springframework/cloud/kubernetes/it/GreetingAndHealthIT.java b/spring-cloud-kubernetes-integration-tests/simple-core/src/test/java/org/springframework/cloud/kubernetes/it/GreetingAndHealthIT.java index dae83c382d..c21a13776f 100644 --- a/spring-cloud-kubernetes-integration-tests/simple-core/src/test/java/org/springframework/cloud/kubernetes/it/GreetingAndHealthIT.java +++ b/spring-cloud-kubernetes-integration-tests/simple-core/src/test/java/org/springframework/cloud/kubernetes/it/GreetingAndHealthIT.java @@ -46,7 +46,7 @@ public void testGreetingEndpoint() { public void testHealthEndpoint() { given().baseUri(String.format("%s://%s:%d", PROTOCOL, HOST, PORT)) .contentType("application/json").get("actuator/health").then() - .statusCode(200).body("details.kubernetes.details.inside", is(true)); + .statusCode(200).body("components.kubernetes.details.inside", is(true)); } }