diff --git a/spring-cloud-cloudfoundry-discovery/pom.xml b/spring-cloud-cloudfoundry-discovery/pom.xml
index 535e429..445f620 100644
--- a/spring-cloud-cloudfoundry-discovery/pom.xml
+++ b/spring-cloud-cloudfoundry-discovery/pom.xml
@@ -18,11 +18,21 @@
spring-cloud-cloudfoundry-commons
${project.version}
+
+ org.springframework.boot
+ spring-boot-actuator
+ true
+
org.springframework.boot
spring-boot-configuration-processor
true
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+ true
+
org.springframework.cloud
spring-cloud-context
@@ -51,5 +61,10 @@
spring-boot-starter-test
test
+
+ io.projectreactor
+ reactor-test
+ test
+
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClient.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClient.java
index 6160cb7..f673101 100644
--- a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClient.java
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClient.java
@@ -81,8 +81,8 @@ public class CloudFoundryDiscoveryClient implements DiscoveryClient {
metadata.put("applicationId", applicationId);
metadata.put("instanceId", applicationIndex);
- return (ServiceInstance) new DefaultServiceInstance(instanceId, name, url, 80,
- secure, metadata);
+ return (ServiceInstance) new DefaultServiceInstance(instanceId, name, url,
+ secure ? 443 : 80, secure, metadata);
}).collectList().blockOptional().orElse(new ArrayList<>());
}
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfiguration.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfiguration.java
index 1649c21..15e7432 100644
--- a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfiguration.java
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfiguration.java
@@ -24,6 +24,8 @@ 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.cloud.client.ConditionalOnBlockingDiscoveryEnabled;
+import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
import org.springframework.cloud.cloudfoundry.discovery.SimpleDnsBasedDiscoveryClient.ServiceIdToHostnameConverter;
@@ -35,8 +37,9 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@ConditionalOnClass(CloudFoundryOperations.class)
-@ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.enabled",
- matchIfMissing = true)
+@ConditionalOnDiscoveryEnabled
+@ConditionalOnBlockingDiscoveryEnabled
+@ConditionalOnCloudFoundryDiscoveryEnabled
@EnableConfigurationProperties(CloudFoundryDiscoveryProperties.class)
public class CloudFoundryDiscoveryClientConfiguration {
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/ConditionalOnCloudFoundryDiscoveryEnabled.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/ConditionalOnCloudFoundryDiscoveryEnabled.java
new file mode 100644
index 0000000..82afcd1
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/ConditionalOnCloudFoundryDiscoveryEnabled.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.cloudfoundry.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.cloudfoundry.discovery.enabled.
+ *
+ * @author Tim Ysewyn
+ * @since 2.2.0
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.enabled",
+ matchIfMissing = true)
+public @interface ConditionalOnCloudFoundryDiscoveryEnabled {
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryAppServiceReactiveDiscoveryClient.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryAppServiceReactiveDiscoveryClient.java
new file mode 100644
index 0000000..73f76d9
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryAppServiceReactiveDiscoveryClient.java
@@ -0,0 +1,68 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import org.cloudfoundry.operations.CloudFoundryOperations;
+import reactor.core.publisher.Flux;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.cloudfoundry.CloudFoundryService;
+import org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryProperties;
+
+/**
+ * Discovery Client implementation using Cloud Foundry's Native DNS based Service
+ * Discovery.
+ *
+ * @author Tim Ysewyn
+ * @see CF App Service
+ * Discovery Release
+ * @see Polyglot
+ * Service Discovery for Container Networking in Cloud Foundry
+ */
+public class CloudFoundryAppServiceReactiveDiscoveryClient
+ extends CloudFoundryNativeReactiveDiscoveryClient {
+
+ private static final String INTERNAL_DOMAIN = "apps.internal";
+
+ private final CloudFoundryService cloudFoundryService;
+
+ CloudFoundryAppServiceReactiveDiscoveryClient(
+ CloudFoundryOperations cloudFoundryOperations, CloudFoundryService svc,
+ CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties) {
+ super(cloudFoundryOperations, svc, cloudFoundryDiscoveryProperties);
+ this.cloudFoundryService = svc;
+ }
+
+ @Override
+ public String description() {
+ return "CF App Reactive Service Discovery Client";
+ }
+
+ @Override
+ public Flux getInstances(String serviceId) {
+ return cloudFoundryService.getApplicationInstances(serviceId)
+ .filter(tuple -> tuple.getT1().getUrls().stream()
+ .anyMatch(this::isInternalDomain))
+ .map(this::mapApplicationInstanceToServiceInstance);
+ }
+
+ private boolean isInternalDomain(String url) {
+ return url != null && url.endsWith(INTERNAL_DOMAIN);
+ }
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryNativeReactiveDiscoveryClient.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryNativeReactiveDiscoveryClient.java
new file mode 100644
index 0000000..fd49171
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryNativeReactiveDiscoveryClient.java
@@ -0,0 +1,99 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import java.util.HashMap;
+
+import org.cloudfoundry.operations.CloudFoundryOperations;
+import org.cloudfoundry.operations.applications.ApplicationDetail;
+import org.cloudfoundry.operations.applications.ApplicationSummary;
+import org.cloudfoundry.operations.applications.InstanceDetail;
+import reactor.core.publisher.Flux;
+import reactor.util.function.Tuple2;
+
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.cloudfoundry.CloudFoundryService;
+import org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryProperties;
+
+/**
+ * Cloud Foundry maintains a registry of running applications which we expose here as
+ * CloudFoundryService instances.
+ *
+ * @author Tim Ysewyn
+ */
+public class CloudFoundryNativeReactiveDiscoveryClient
+ implements ReactiveDiscoveryClient {
+
+ private final CloudFoundryService cloudFoundryService;
+
+ private final CloudFoundryOperations cloudFoundryOperations;
+
+ private final CloudFoundryDiscoveryProperties properties;
+
+ CloudFoundryNativeReactiveDiscoveryClient(CloudFoundryOperations operations,
+ CloudFoundryService svc, CloudFoundryDiscoveryProperties properties) {
+ this.cloudFoundryService = svc;
+ this.cloudFoundryOperations = operations;
+ this.properties = properties;
+ }
+
+ @Override
+ public String description() {
+ return "CF Reactive Service Discovery Client";
+ }
+
+ @Override
+ public Flux getInstances(String serviceId) {
+ return this.cloudFoundryService.getApplicationInstances(serviceId)
+ .map(this::mapApplicationInstanceToServiceInstance);
+ }
+
+ @Override
+ public Flux getServices() {
+ return this.cloudFoundryOperations.applications().list()
+ .map(ApplicationSummary::getName);
+ }
+
+ @Override
+ public int getOrder() {
+ return this.properties.getOrder();
+ }
+
+ protected ServiceInstance mapApplicationInstanceToServiceInstance(
+ Tuple2 tuple) {
+ ApplicationDetail applicationDetail = tuple.getT1();
+ InstanceDetail instanceDetail = tuple.getT2();
+
+ String applicationId = applicationDetail.getId();
+ String applicationIndex = instanceDetail.getIndex();
+ String instanceId = applicationId + "." + applicationIndex;
+ String name = applicationDetail.getName();
+ String url = applicationDetail.getUrls().size() > 0
+ ? applicationDetail.getUrls().get(0) : null;
+ boolean secure = (url + "").toLowerCase().startsWith("https");
+
+ HashMap metadata = new HashMap<>();
+ metadata.put("applicationId", applicationId);
+ metadata.put("instanceId", applicationIndex);
+
+ return new DefaultServiceInstance(instanceId, name, url, secure ? 443 : 80,
+ secure, metadata);
+ }
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveDiscoveryClientConfiguration.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveDiscoveryClientConfiguration.java
new file mode 100644
index 0000000..b3a3e8c
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveDiscoveryClientConfiguration.java
@@ -0,0 +1,164 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import org.cloudfoundry.operations.CloudFoundryOperations;
+
+import org.springframework.beans.factory.ObjectProvider;
+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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+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.cloudfoundry.CloudFoundryService;
+import org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryProperties;
+import org.springframework.cloud.cloudfoundry.discovery.ConditionalOnCloudFoundryDiscoveryEnabled;
+import org.springframework.cloud.cloudfoundry.discovery.reactive.SimpleDnsBasedReactiveDiscoveryClient.ServiceIdToHostnameConverter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configuration related to service discovery when using Cloud Foundry.
+ *
+ * @author Tim Ysewyn
+ */
+@Configuration
+@ConditionalOnClass(CloudFoundryOperations.class)
+@ConditionalOnDiscoveryEnabled
+@ConditionalOnReactiveDiscoveryEnabled
+@ConditionalOnCloudFoundryDiscoveryEnabled
+@EnableConfigurationProperties(CloudFoundryDiscoveryProperties.class)
+@AutoConfigureAfter(ReactiveCompositeDiscoveryClientAutoConfiguration.class)
+@AutoConfigureBefore(ReactiveCommonsClientAutoConfiguration.class)
+public class CloudFoundryReactiveDiscoveryClientConfiguration {
+
+ @Configuration
+ @ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.use-dns",
+ havingValue = "false", matchIfMissing = true)
+ public static class CloudFoundryNativeReactiveDiscoveryClientConfig {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public CloudFoundryNativeReactiveDiscoveryClient nativeCloudFoundryDiscoveryClient(
+ CloudFoundryOperations cf, CloudFoundryService svc,
+ CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties) {
+ return new CloudFoundryNativeReactiveDiscoveryClient(cf, svc,
+ cloudFoundryDiscoveryProperties);
+ }
+
+ @Bean
+ @ConditionalOnClass(
+ name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator")
+ @ConditionalOnDiscoveryHealthIndicatorEnabled
+ public ReactiveDiscoveryClientHealthIndicator cloudFoundryReactiveDiscoveryClientHealthIndicator(
+ CloudFoundryNativeReactiveDiscoveryClient client,
+ DiscoveryClientHealthIndicatorProperties properties) {
+ return new ReactiveDiscoveryClientHealthIndicator(client, properties);
+ }
+
+ @Bean
+ public CloudFoundryReactiveHeartbeatSender cloudFoundryHeartbeatSender(
+ CloudFoundryNativeReactiveDiscoveryClient client) {
+ return new CloudFoundryReactiveHeartbeatSender(client);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.use-dns",
+ havingValue = "true")
+ public static class DnsBasedCloudFoundryReactiveDiscoveryClientConfig {
+
+ @Configuration
+ @ConditionalOnProperty(
+ value = "spring.cloud.cloudfoundry.discovery.use-container-ip",
+ havingValue = "true")
+ public static class CloudFoundrySimpleDnsBasedReactiveDiscoveryClientConfig {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public SimpleDnsBasedReactiveDiscoveryClient dnsBasedReactiveDiscoveryClient(
+ ObjectProvider provider,
+ CloudFoundryDiscoveryProperties properties) {
+ ServiceIdToHostnameConverter converter = provider.getIfAvailable();
+ return converter == null
+ ? new SimpleDnsBasedReactiveDiscoveryClient(properties)
+ : new SimpleDnsBasedReactiveDiscoveryClient(converter);
+ }
+
+ @Bean
+ @ConditionalOnClass(
+ name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator")
+ @ConditionalOnDiscoveryHealthIndicatorEnabled
+ public ReactiveDiscoveryClientHealthIndicator cloudFoundryReactiveDiscoveryClientHealthIndicator(
+ SimpleDnsBasedReactiveDiscoveryClient client,
+ DiscoveryClientHealthIndicatorProperties properties) {
+ return new ReactiveDiscoveryClientHealthIndicator(client, properties);
+ }
+
+ @Bean
+ public CloudFoundryReactiveHeartbeatSender cloudFoundryHeartbeatSender(
+ SimpleDnsBasedReactiveDiscoveryClient client) {
+ return new CloudFoundryReactiveHeartbeatSender(client);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnProperty(
+ value = "spring.cloud.cloudfoundry.discovery.use-container-ip",
+ havingValue = "false", matchIfMissing = true)
+ public static class CloudFoundryAppServiceReactiveDiscoveryClientConfig {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public CloudFoundryAppServiceReactiveDiscoveryClient appServiceReactiveDiscoveryClient(
+ CloudFoundryOperations cf, CloudFoundryService svc,
+ CloudFoundryDiscoveryProperties properties) {
+ return new CloudFoundryAppServiceReactiveDiscoveryClient(cf, svc,
+ properties);
+ }
+
+ @Bean
+ @ConditionalOnClass(
+ name = "org.springframework.boot.actuate.health.ReactiveHealthIndicator")
+ @ConditionalOnDiscoveryHealthIndicatorEnabled
+ public ReactiveDiscoveryClientHealthIndicator cloudFoundryReactiveDiscoveryClientHealthIndicator(
+ CloudFoundryAppServiceReactiveDiscoveryClient client,
+ DiscoveryClientHealthIndicatorProperties properties) {
+ return new ReactiveDiscoveryClientHealthIndicator(client, properties);
+ }
+
+ @Bean
+ public CloudFoundryReactiveHeartbeatSender cloudFoundryHeartbeatSender(
+ CloudFoundryAppServiceReactiveDiscoveryClient client) {
+ return new CloudFoundryReactiveHeartbeatSender(client);
+ }
+
+ }
+
+ }
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveHeartbeatSender.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveHeartbeatSender.java
new file mode 100644
index 0000000..e633f29
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveHeartbeatSender.java
@@ -0,0 +1,58 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.client.discovery.event.HeartbeatEvent;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationEventPublisherAware;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * Publishes a {@link HeartbeatEvent} with a `Flux` of services as its state indicator. If
+ * consumers detect a change it doesn't necessarily mean there is a change in the catalog.
+ *
+ * @author Tim Ysewyn
+ */
+@Component
+public class CloudFoundryReactiveHeartbeatSender
+ implements ApplicationEventPublisherAware {
+
+ private final ReactiveDiscoveryClient client;
+
+ private ApplicationEventPublisher publisher;
+
+ public CloudFoundryReactiveHeartbeatSender(ReactiveDiscoveryClient client) {
+ this.client = client;
+ }
+
+ @Scheduled(
+ fixedDelayString = "${spring.cloud.cloudfoundry.discovery.heartbeatFrequency:5000}")
+ public void poll() {
+ if (this.publisher != null) {
+ this.publisher.publishEvent(
+ new HeartbeatEvent(this.client, this.client.getServices()));
+ }
+ }
+
+ @Override
+ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
+ this.publisher = publisher;
+ }
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/SimpleDnsBasedReactiveDiscoveryClient.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/SimpleDnsBasedReactiveDiscoveryClient.java
new file mode 100644
index 0000000..6d5d98e
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/reactive/SimpleDnsBasedReactiveDiscoveryClient.java
@@ -0,0 +1,98 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.function.Function;
+
+import org.reactivestreams.Publisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.cloud.client.DefaultServiceInstance;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryProperties;
+
+/**
+ * Reactive Discovery Client implementation using Cloud Foundry's Native DNS based Service
+ * Discovery.
+ *
+ * @author Tim Ysewyn
+ * @see Polyglot
+ * Service Discovery for Container Networking in Cloud Foundry
+ */
+public class SimpleDnsBasedReactiveDiscoveryClient implements ReactiveDiscoveryClient {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(SimpleDnsBasedReactiveDiscoveryClient.class);
+
+ private final ServiceIdToHostnameConverter serviceIdToHostnameConverter;
+
+ public SimpleDnsBasedReactiveDiscoveryClient(
+ ServiceIdToHostnameConverter serviceIdToHostnameConverter) {
+ this.serviceIdToHostnameConverter = serviceIdToHostnameConverter;
+ }
+
+ public SimpleDnsBasedReactiveDiscoveryClient(
+ CloudFoundryDiscoveryProperties properties) {
+ this(serviceId -> serviceId + "." + properties.getInternalDomain());
+ }
+
+ @Override
+ public String description() {
+ return "DNS Based CF Service Reactive Discovery Client";
+ }
+
+ @Override
+ public Flux getInstances(String serviceId) {
+ return Mono.justOrEmpty(serviceIdToHostnameConverter.toHostname(serviceId))
+ .flatMapMany(getInetAddresses())
+ .map(address -> new DefaultServiceInstance(serviceId,
+ address.getHostAddress(), 8080, false));
+ }
+
+ private Function> getInetAddresses() {
+ return hostname -> {
+ try {
+ return Flux.fromArray(InetAddress.getAllByName(hostname));
+ }
+ catch (UnknownHostException e) {
+ log.warn("{}", e.getMessage());
+ return Flux.empty();
+ }
+ };
+ }
+
+ @Override
+ public Flux getServices() {
+ log.warn("getServices is not supported");
+ return Flux.empty();
+ }
+
+ @FunctionalInterface
+ public interface ServiceIdToHostnameConverter {
+
+ String toHostname(String serviceId);
+
+ }
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-cloudfoundry-discovery/src/main/resources/META-INF/spring.factories
index 07b467a..5cebac6 100644
--- a/spring-cloud-cloudfoundry-discovery/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-cloudfoundry-discovery/src/main/resources/META-INF/spring.factories
@@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.cloudfoundry.discovery.RibbonCloudFoundryAutoConfiguration
# Discovery Client Configuration
org.springframework.cloud.client.discovery.EnableDiscoveryClient=\
-org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryClientConfiguration
+org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryClientConfiguration, \
+org.springframework.cloud.cloudfoundry.discovery.reactive.CloudFoundryReactiveDiscoveryClientConfiguration
diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryAppServiceReactiveDiscoveryClientTests.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryAppServiceReactiveDiscoveryClientTests.java
new file mode 100644
index 0000000..443c4ff
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryAppServiceReactiveDiscoveryClientTests.java
@@ -0,0 +1,72 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import java.util.UUID;
+
+import org.cloudfoundry.operations.applications.ApplicationDetail;
+import org.cloudfoundry.operations.applications.InstanceDetail;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.cloudfoundry.CloudFoundryService;
+
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Tim Ysewyn
+ */
+@ExtendWith(MockitoExtension.class)
+class CloudFoundryAppServiceReactiveDiscoveryClientTests {
+
+ @Mock
+ private CloudFoundryService svc;
+
+ @InjectMocks
+ private CloudFoundryAppServiceReactiveDiscoveryClient client;
+
+ @Test
+ public void shouldReturnFluxOfServiceInstances() {
+ ApplicationDetail appDetail1 = ApplicationDetail.builder()
+ .id(UUID.randomUUID().toString()).stack("stack").instances(1)
+ .memoryLimit(1024).requestedState("requestedState").diskQuota(1024)
+ .name("service").runningInstances(1).url("instance.apps.internal")
+ .build();
+ Tuple2 instance1 = Tuples.of(appDetail1,
+ InstanceDetail.builder().index("0").build());
+ ApplicationDetail appDetail2 = ApplicationDetail.builder()
+ .id(UUID.randomUUID().toString()).stack("stack").instances(1)
+ .memoryLimit(1024).requestedState("requestedState").diskQuota(1024)
+ .name("service").runningInstances(1).url("instance.apps.not.internal")
+ .build();
+ Tuple2 instance2 = Tuples.of(appDetail2,
+ InstanceDetail.builder().index("0").build());
+ when(this.svc.getApplicationInstances("service"))
+ .thenReturn(Flux.just(instance1, instance2));
+ Flux instances = this.client.getInstances("service");
+ StepVerifier.create(instances).expectNextCount(1).expectComplete().verify();
+ }
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryNativeReactiveDiscoveryClientTests.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryNativeReactiveDiscoveryClientTests.java
new file mode 100644
index 0000000..b8c8c59
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryNativeReactiveDiscoveryClientTests.java
@@ -0,0 +1,104 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import java.util.UUID;
+
+import org.cloudfoundry.operations.CloudFoundryOperations;
+import org.cloudfoundry.operations.applications.ApplicationDetail;
+import org.cloudfoundry.operations.applications.ApplicationSummary;
+import org.cloudfoundry.operations.applications.Applications;
+import org.cloudfoundry.operations.applications.InstanceDetail;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.cloudfoundry.CloudFoundryService;
+import org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Tim Ysewyn
+ */
+@ExtendWith(MockitoExtension.class)
+class CloudFoundryNativeReactiveDiscoveryClientTests {
+
+ @Mock
+ private CloudFoundryOperations operations;
+
+ @Mock
+ private CloudFoundryService svc;
+
+ @Mock
+ private CloudFoundryDiscoveryProperties properties;
+
+ @InjectMocks
+ private CloudFoundryNativeReactiveDiscoveryClient client;
+
+ @Test
+ public void verifyDefaults() {
+ when(properties.getOrder()).thenReturn(0);
+ assertThat(client.description())
+ .isEqualTo("CF Reactive Service Discovery Client");
+ assertThat(client.getOrder()).isEqualTo(0);
+ }
+
+ @Test
+ public void shouldReturnFluxOfServices() {
+ Applications apps = mock(Applications.class);
+ when(operations.applications()).thenReturn(apps);
+ ApplicationSummary summary = ApplicationSummary.builder()
+ .id(UUID.randomUUID().toString()).instances(1).memoryLimit(1024)
+ .requestedState("requestedState").diskQuota(1024).name("service")
+ .runningInstances(1).build();
+ when(apps.list()).thenReturn(Flux.just(summary));
+ Flux services = this.client.getServices();
+ StepVerifier.create(services).expectNext("service").expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnEmptyFluxForNonExistingService() {
+ when(svc.getApplicationInstances("service")).thenReturn(Flux.empty());
+ Flux instances = this.client.getInstances("service");
+ StepVerifier.create(instances).expectNextCount(0).expectComplete().verify();
+ }
+
+ @Test
+ public void shouldReturnFluxOfServiceInstances() {
+ ApplicationDetail applicationDetail = ApplicationDetail.builder()
+ .id(UUID.randomUUID().toString()).stack("stack").instances(1)
+ .memoryLimit(1024).requestedState("requestedState").diskQuota(1024)
+ .name("service").runningInstances(1).build();
+ InstanceDetail instanceDetail = InstanceDetail.builder().index("0").build();
+ Tuple2 instance = Tuples.of(applicationDetail,
+ instanceDetail);
+ when(this.svc.getApplicationInstances("service")).thenReturn(Flux.just(instance));
+ Flux instances = this.client.getInstances("service");
+ StepVerifier.create(instances).expectNextCount(1).expectComplete().verify();
+ }
+
+}
diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveDiscoveryClientConfigurationTests.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveDiscoveryClientConfigurationTests.java
new file mode 100644
index 0000000..aeca87b
--- /dev/null
+++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/reactive/CloudFoundryReactiveDiscoveryClientConfigurationTests.java
@@ -0,0 +1,216 @@
+/*
+ * 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.cloudfoundry.discovery.reactive;
+
+import org.cloudfoundry.operations.CloudFoundryOperations;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
+import org.springframework.cloud.client.discovery.health.reactive.ReactiveDiscoveryClientHealthIndicator;
+import org.springframework.cloud.cloudfoundry.CloudFoundryService;
+import org.springframework.context.annotation.Bean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * @author Tim Ysewyn
+ */
+class CloudFoundryReactiveDiscoveryClientConfigurationTests {
+
+ private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(
+ AutoConfigurations.of(MockedCloudFoundryConfiguration.class,
+ CloudFoundryReactiveDiscoveryClientConfiguration.class));
+
+ @Test
+ public void shouldNotHaveDiscoveryClientsWhenDiscoveryDisabled() {
+ contextRunner.withPropertyValues("spring.cloud.discovery.enabled=false")
+ .run(context -> {
+ assertThat(context).doesNotHaveBean("cloudFoundryHeartbeatSender");
+ assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
+ assertThat(context).doesNotHaveBean(
+ ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @Test
+ public void shouldNotHaveDiscoveryClientsWhenReactiveDiscoveryDisabled() {
+ contextRunner.withPropertyValues("spring.cloud.discovery.reactive.enabled=false")
+ .run(context -> {
+ assertThat(context).doesNotHaveBean("cloudFoundryHeartbeatSender");
+ assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
+ assertThat(context).doesNotHaveBean(
+ ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @Test
+ public void shouldNotHaveDiscoveryClientsWhenCloudFoundryDiscoveryDisabled() {
+ contextRunner
+ .withPropertyValues("spring.cloud.cloudfoundry.discovery.enabled=false")
+ .run(context -> {
+ assertThat(context).doesNotHaveBean("cloudFoundryHeartbeatSender");
+ assertThat(context).doesNotHaveBean(ReactiveDiscoveryClient.class);
+ assertThat(context).doesNotHaveBean(
+ ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @Test
+ public void shouldUseNativeDiscovery() {
+ contextRunner
+ .withConfiguration(AutoConfigurations
+ .of(ReactiveCommonsClientAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context)
+ .hasSingleBean(CloudFoundryReactiveHeartbeatSender.class);
+ assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);
+ assertThat(context).hasBean("nativeCloudFoundryDiscoveryClient");
+ assertThat(context)
+ .hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @Test
+ public void shouldUseDnsDiscovery() {
+ contextRunner
+ .withConfiguration(AutoConfigurations
+ .of(ReactiveCommonsClientAutoConfiguration.class))
+ .withPropertyValues("spring.cloud.cloudfoundry.discovery.use-dns=true",
+ "spring.cloud.cloudfoundry.discovery.use-container-ip=true")
+ .run(context -> {
+ assertThat(context)
+ .hasSingleBean(CloudFoundryReactiveHeartbeatSender.class);
+ assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);
+ assertThat(context).hasBean("dnsBasedReactiveDiscoveryClient");
+ assertThat(context)
+ .hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @Test
+ public void shouldUseAppServiceDiscovery() {
+ contextRunner
+ .withConfiguration(AutoConfigurations
+ .of(ReactiveCommonsClientAutoConfiguration.class))
+ .withPropertyValues("spring.cloud.cloudfoundry.discovery.use-dns=true",
+ "spring.cloud.cloudfoundry.discovery.use-container-ip=false")
+ .run(context -> {
+ assertThat(context)
+ .hasSingleBean(CloudFoundryReactiveHeartbeatSender.class);
+ assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);
+ assertThat(context).hasBean("appServiceReactiveDiscoveryClient");
+ assertThat(context)
+ .hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @Test
+ public void shouldUseCustomServiceDiscovery() {
+ contextRunner
+ .withConfiguration(AutoConfigurations
+ .of(ReactiveCommonsClientAutoConfiguration.class))
+ .withUserConfiguration(
+ CustomCloudFoundryReactiveDiscoveryClientConfiguration.class)
+ .run(context -> {
+ assertThat(context)
+ .hasSingleBean(CloudFoundryReactiveHeartbeatSender.class);
+ assertThat(context).getBeans(ReactiveDiscoveryClient.class)
+ .hasSize(2);
+ assertThat(context).hasBean("nativeCloudFoundryDiscoveryClient");
+ assertThat(context)
+ .hasSingleBean(ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @Test
+ public void worksWithoutWebflux() {
+ contextRunner
+ .withClassLoader(
+ new FilteredClassLoader("org.springframework.web.reactive"))
+ .run(context -> {
+ assertThat(context)
+ .doesNotHaveBean(CloudFoundryReactiveHeartbeatSender.class);
+ 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(CloudFoundryReactiveHeartbeatSender.class);
+ assertThat(context).hasSingleBean(ReactiveDiscoveryClient.class);
+ assertThat(context).doesNotHaveBean(
+ ReactiveDiscoveryClientHealthIndicator.class);
+ });
+ }
+
+ @TestConfiguration
+ static class MockedCloudFoundryConfiguration {
+
+ @Bean
+ public CloudFoundryOperations mockedOperations() {
+ return mock(CloudFoundryOperations.class);
+ }
+
+ @Bean
+ public CloudFoundryService mockedService() {
+ return mock(CloudFoundryService.class);
+ }
+
+ }
+
+ @TestConfiguration
+ static class CustomCloudFoundryReactiveDiscoveryClientConfiguration {
+
+ @Bean
+ public ReactiveDiscoveryClient customDiscoveryClient() {
+ return new ReactiveDiscoveryClient() {
+ @Override
+ public String description() {
+ return "Custom Reactive Discovery Client";
+ }
+
+ @Override
+ public Flux getInstances(String serviceId) {
+ return Flux.empty();
+ }
+
+ @Override
+ public Flux getServices() {
+ return Flux.empty();
+ }
+ };
+ }
+
+ }
+
+}