Added support for reactive service discovery
Fixes gh-57
This commit is contained in:
@@ -18,11 +18,21 @@
|
||||
<artifactId>spring-cloud-cloudfoundry-commons</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-actuator</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-context</artifactId>
|
||||
@@ -51,5 +61,10 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -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<>());
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
* <code>spring.cloud.cloudfoundry.discovery.enabled</code>.
|
||||
*
|
||||
* @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 {
|
||||
|
||||
}
|
||||
@@ -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 <a href="https://github.com/cloudfoundry/cf-app-sd-release">CF App Service
|
||||
* Discovery Release</a>
|
||||
* @see <a href=
|
||||
* "https://www.cloudfoundry.org/blog/polyglot-service-discovery-container-networking-cloud-foundry/">Polyglot
|
||||
* Service Discovery for Container Networking in Cloud Foundry</a>
|
||||
*/
|
||||
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<ServiceInstance> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ServiceInstance> getInstances(String serviceId) {
|
||||
return this.cloudFoundryService.getApplicationInstances(serviceId)
|
||||
.map(this::mapApplicationInstanceToServiceInstance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> getServices() {
|
||||
return this.cloudFoundryOperations.applications().list()
|
||||
.map(ApplicationSummary::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.properties.getOrder();
|
||||
}
|
||||
|
||||
protected ServiceInstance mapApplicationInstanceToServiceInstance(
|
||||
Tuple2<ApplicationDetail, InstanceDetail> 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<String, String> metadata = new HashMap<>();
|
||||
metadata.put("applicationId", applicationId);
|
||||
metadata.put("instanceId", applicationIndex);
|
||||
|
||||
return new DefaultServiceInstance(instanceId, name, url, secure ? 443 : 80,
|
||||
secure, metadata);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ServiceIdToHostnameConverter> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <a href=
|
||||
* "https://www.cloudfoundry.org/blog/polyglot-service-discovery-container-networking-cloud-foundry/">Polyglot
|
||||
* Service Discovery for Container Networking in Cloud Foundry</a>
|
||||
*/
|
||||
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<ServiceInstance> getInstances(String serviceId) {
|
||||
return Mono.justOrEmpty(serviceIdToHostnameConverter.toHostname(serviceId))
|
||||
.flatMapMany(getInetAddresses())
|
||||
.map(address -> new DefaultServiceInstance(serviceId,
|
||||
address.getHostAddress(), 8080, false));
|
||||
}
|
||||
|
||||
private Function<String, Publisher<? extends InetAddress>> getInetAddresses() {
|
||||
return hostname -> {
|
||||
try {
|
||||
return Flux.fromArray(InetAddress.getAllByName(hostname));
|
||||
}
|
||||
catch (UnknownHostException e) {
|
||||
log.warn("{}", e.getMessage());
|
||||
return Flux.empty();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> getServices() {
|
||||
log.warn("getServices is not supported");
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ServiceIdToHostnameConverter {
|
||||
|
||||
String toHostname(String serviceId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<ApplicationDetail, InstanceDetail> 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<ApplicationDetail, InstanceDetail> instance2 = Tuples.of(appDetail2,
|
||||
InstanceDetail.builder().index("0").build());
|
||||
when(this.svc.getApplicationInstances("service"))
|
||||
.thenReturn(Flux.just(instance1, instance2));
|
||||
Flux<ServiceInstance> instances = this.client.getInstances("service");
|
||||
StepVerifier.create(instances).expectNextCount(1).expectComplete().verify();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> services = this.client.getServices();
|
||||
StepVerifier.create(services).expectNext("service").expectComplete().verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnEmptyFluxForNonExistingService() {
|
||||
when(svc.getApplicationInstances("service")).thenReturn(Flux.empty());
|
||||
Flux<ServiceInstance> 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<ApplicationDetail, InstanceDetail> instance = Tuples.of(applicationDetail,
|
||||
instanceDetail);
|
||||
when(this.svc.getApplicationInstances("service")).thenReturn(Flux.just(instance));
|
||||
Flux<ServiceInstance> instances = this.client.getInstances("service");
|
||||
StepVerifier.create(instances).expectNextCount(1).expectComplete().verify();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ServiceInstance> getInstances(String serviceId) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> getServices() {
|
||||
return Flux.empty();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user