Merge branch 'making-app-sd'
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package org.springframework.cloud.cloudfoundry.discovery;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.cloudfoundry.operations.applications.ApplicationDetail;
|
||||
import org.cloudfoundry.operations.applications.InstanceDetail;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
|
||||
|
||||
/**
|
||||
*
|
||||
* Discovery Client implementation using Cloud Foundry's Native DNS based Service
|
||||
* Discovery
|
||||
*
|
||||
* @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>
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
public class CloudFoundryAppServiceDiscoveryClient extends CloudFoundryDiscoveryClient {
|
||||
|
||||
private static final String INTERNAL_DOMAIN = "apps.internal";
|
||||
|
||||
CloudFoundryAppServiceDiscoveryClient(CloudFoundryOperations cloudFoundryOperations,
|
||||
CloudFoundryService svc, CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties) {
|
||||
super(cloudFoundryOperations, svc, cloudFoundryDiscoveryProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "CF App Service Discovery Client";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> getInstances(String serviceId) {
|
||||
return getCloudFoundryService()
|
||||
.getApplicationInstances(serviceId)
|
||||
.filter(tuple -> tuple.getT1().getUrls().stream()
|
||||
.anyMatch(this::isInternalDomain))
|
||||
.map(tuple -> {
|
||||
ApplicationDetail applicationDetail = tuple.getT1();
|
||||
InstanceDetail instanceDetail = tuple.getT2();
|
||||
String applicationId = applicationDetail.getId();
|
||||
String applicationIndex = instanceDetail.getIndex();
|
||||
String name = applicationDetail.getName();
|
||||
String url = applicationDetail.getUrls().stream()
|
||||
.filter(this::isInternalDomain)
|
||||
.findFirst()
|
||||
.map(x -> instanceDetail.getIndex() + "." + x)
|
||||
.get();
|
||||
HashMap<String, String> metadata = new HashMap<>();
|
||||
metadata.put("applicationId", applicationId);
|
||||
metadata.put("instanceId", applicationIndex);
|
||||
return (ServiceInstance) new DefaultServiceInstance(name, url, 8080,
|
||||
false, metadata);
|
||||
})
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
private boolean isInternalDomain(String url) {
|
||||
return url != null && url.endsWith(INTERNAL_DOMAIN);
|
||||
}
|
||||
}
|
||||
@@ -98,4 +98,7 @@ public class CloudFoundryDiscoveryClient implements DiscoveryClient {
|
||||
return this.properties.getOrder();
|
||||
}
|
||||
|
||||
CloudFoundryService getCloudFoundryService() {
|
||||
return this.cloudFoundryService;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
package org.springframework.cloud.cloudfoundry.discovery;
|
||||
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
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.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
|
||||
import org.springframework.cloud.cloudfoundry.discovery.SimpleDnsBasedDiscoveryClient.ServiceIdToHostnameConverter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -35,15 +38,46 @@ import org.springframework.context.annotation.Configuration;
|
||||
@EnableConfigurationProperties(CloudFoundryDiscoveryProperties.class)
|
||||
public class CloudFoundryDiscoveryClientConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CloudFoundryDiscoveryClient.class)
|
||||
public CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient(
|
||||
CloudFoundryOperations cf, CloudFoundryService svc,
|
||||
CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties) {
|
||||
return new CloudFoundryDiscoveryClient(cf, svc, cloudFoundryDiscoveryProperties);
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.use-dns", havingValue = "false", matchIfMissing = true)
|
||||
public static class CloudFoundryDiscoveryClientConfig {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(DiscoveryClient.class)
|
||||
public DiscoveryClient cloudFoundryDiscoveryClient(CloudFoundryOperations cf,
|
||||
CloudFoundryService svc,
|
||||
CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties) {
|
||||
return new CloudFoundryDiscoveryClient(cf, svc,
|
||||
cloudFoundryDiscoveryProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.use-dns", havingValue = "true")
|
||||
public static class DnsBasedCloudFoundryDiscoveryClientConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.use-container-ip", havingValue = "true")
|
||||
@ConditionalOnMissingBean(DiscoveryClient.class)
|
||||
public DiscoveryClient discoveryClient(
|
||||
ObjectProvider<ServiceIdToHostnameConverter> provider) {
|
||||
ServiceIdToHostnameConverter converter = provider.getIfAvailable();
|
||||
return converter == null ? new SimpleDnsBasedDiscoveryClient()
|
||||
: new SimpleDnsBasedDiscoveryClient(converter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.use-container-ip", havingValue = "false", matchIfMissing = true)
|
||||
@ConditionalOnMissingBean(DiscoveryClient.class)
|
||||
public DiscoveryClient cloudFoundryDiscoveryClient(CloudFoundryOperations cf,
|
||||
CloudFoundryService svc,
|
||||
CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties) {
|
||||
return new CloudFoundryAppServiceDiscoveryClient(cf, svc,
|
||||
cloudFoundryDiscoveryProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(CloudFoundryDiscoveryClient.class)
|
||||
public CloudFoundryHeartbeatSender cloudFoundryHeartbeatSender(
|
||||
CloudFoundryDiscoveryClient client) {
|
||||
return new CloudFoundryHeartbeatSender(client);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
package org.springframework.cloud.cloudfoundry.discovery;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
|
||||
/**
|
||||
* Discovery Client implementation using Cloud Foundry's Native DNS based Service
|
||||
* Discovery
|
||||
*
|
||||
* @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>
|
||||
*
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
public class SimpleDnsBasedDiscoveryClient implements DiscoveryClient {
|
||||
public static final String INTERNAL_DOMAIN = "apps.internal";
|
||||
private final Logger log = LoggerFactory
|
||||
.getLogger(SimpleDnsBasedDiscoveryClient.class);
|
||||
private final ServiceIdToHostnameConverter serviceIdToHostnameConverter;
|
||||
|
||||
private static final ServiceIdToHostnameConverter DEFAULT_CONVERTER = serviceId -> serviceId
|
||||
+ "." + INTERNAL_DOMAIN;
|
||||
|
||||
public SimpleDnsBasedDiscoveryClient(
|
||||
ServiceIdToHostnameConverter serviceIdToHostnameConverter) {
|
||||
this.serviceIdToHostnameConverter = serviceIdToHostnameConverter;
|
||||
}
|
||||
|
||||
public SimpleDnsBasedDiscoveryClient() {
|
||||
this(DEFAULT_CONVERTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "DNS Based CF Service Discovery Client";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> getInstances(String serviceId) {
|
||||
String hostname = this.serviceIdToHostnameConverter.toHostname(serviceId);
|
||||
try {
|
||||
List<ServiceInstance> serviceInstances = new ArrayList<>();
|
||||
InetAddress[] addresses = InetAddress.getAllByName(hostname);
|
||||
if (addresses != null) {
|
||||
for (InetAddress address : addresses) {
|
||||
DefaultServiceInstance serviceInstance = new DefaultServiceInstance(
|
||||
serviceId, address.getHostAddress(), 8080, false);
|
||||
serviceInstances.add(serviceInstance);
|
||||
}
|
||||
}
|
||||
return serviceInstances;
|
||||
}
|
||||
catch (UnknownHostException e) {
|
||||
log.warn("{}", e.getMessage());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getServices() {
|
||||
log.warn("getServices is not supported");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ServiceIdToHostnameConverter {
|
||||
String toHostname(String serviceId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.cloud.cloudfoundry.discovery.use-dns",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to use BOSH DNS for the discovery. In order to use this feature, your Cloud Foundry installation must support Service Discovery.",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.cloudfoundry.discovery.use-container-ip",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Whether to resolve hostname when BOSH DNS is used. In order to use this feature, spring.cloud.cloudfoundry.discovery.use-dns must be true.",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.springframework.cloud.cloudfoundry.discovery;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.cloudfoundry.operations.applications.ApplicationDetail;
|
||||
import org.cloudfoundry.operations.applications.InstanceDetail;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
/**
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
public class CloudFoundryAppServiceDiscoveryClientTest {
|
||||
private CloudFoundryAppServiceDiscoveryClient discoveryClient;
|
||||
private CloudFoundryOperations cloudFoundryOperations;
|
||||
private CloudFoundryService cloudFoundryService;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.cloudFoundryOperations = mock(CloudFoundryOperations.class);
|
||||
this.cloudFoundryService = mock(CloudFoundryService.class);
|
||||
this.discoveryClient = new CloudFoundryAppServiceDiscoveryClient(
|
||||
this.cloudFoundryOperations, this.cloudFoundryService,
|
||||
new CloudFoundryDiscoveryProperties());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getInstancesOneInstance() {
|
||||
String serviceId = "billing";
|
||||
ApplicationDetail applicationDetail = ApplicationDetail.builder().id("billing1")
|
||||
.name("billing").instances(1).memoryLimit(1024).stack("cflinux2")
|
||||
.diskQuota(1024).requestedState("Running").runningInstances(1)
|
||||
.url("billing.apps.example.com", "billing.apps.internal").build();
|
||||
given(this.cloudFoundryService.getApplicationInstances(serviceId))
|
||||
.willReturn(Flux.just(Tuples.of(applicationDetail,
|
||||
InstanceDetail.builder().index("0").build())));
|
||||
List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
|
||||
|
||||
assertThat(instances).hasSize(1);
|
||||
assertThat(instances.get(0)).isEqualTo(new DefaultServiceInstance(serviceId,
|
||||
"0.billing.apps.internal", 8080, false, new HashMap<String, String>() {
|
||||
{
|
||||
put("applicationId", "billing1");
|
||||
put("instanceId", "0");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getInstancesThreeInstance() {
|
||||
String serviceId = "billing";
|
||||
ApplicationDetail applicationDetail = ApplicationDetail.builder().id("billing-id")
|
||||
.name("billing").instances(3).memoryLimit(1024).stack("cflinux2")
|
||||
.diskQuota(1024).requestedState("Running").runningInstances(3)
|
||||
.url("billing.apps.example.com", "billing.apps.internal").build();
|
||||
given(this.cloudFoundryService.getApplicationInstances(serviceId))
|
||||
.willReturn(Flux.just(
|
||||
Tuples.of(applicationDetail,
|
||||
InstanceDetail.builder().index("0").build()),
|
||||
Tuples.of(applicationDetail,
|
||||
InstanceDetail.builder().index("1").build()),
|
||||
Tuples.of(applicationDetail,
|
||||
InstanceDetail.builder().index("2").build())));
|
||||
List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
|
||||
|
||||
assertThat(instances).hasSize(3);
|
||||
assertThat(instances.get(0)).isEqualTo(new DefaultServiceInstance(serviceId,
|
||||
"0.billing.apps.internal", 8080, false, new HashMap<String, String>() {
|
||||
{
|
||||
put("applicationId", "billing-id");
|
||||
put("instanceId", "0");
|
||||
}
|
||||
}));
|
||||
assertThat(instances.get(1)).isEqualTo(new DefaultServiceInstance(serviceId,
|
||||
"1.billing.apps.internal", 8080, false, new HashMap<String, String>() {
|
||||
{
|
||||
put("applicationId", "billing-id");
|
||||
put("instanceId", "1");
|
||||
}
|
||||
}));
|
||||
assertThat(instances.get(2)).isEqualTo(new DefaultServiceInstance(serviceId,
|
||||
"2.billing.apps.internal", 8080, false, new HashMap<String, String>() {
|
||||
{
|
||||
put("applicationId", "billing-id");
|
||||
put("instanceId", "2");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getInstancesEmpty() {
|
||||
String serviceId = "billing";
|
||||
ApplicationDetail applicationDetail = ApplicationDetail.builder().id("billing1")
|
||||
.name("billing").instances(1).memoryLimit(1024).stack("cflinux2")
|
||||
.diskQuota(1024).requestedState("Running").runningInstances(1)
|
||||
.url("billing.apps.example.com").build();
|
||||
given(this.cloudFoundryService.getApplicationInstances(serviceId))
|
||||
.willReturn(Flux.just(Tuples.of(applicationDetail,
|
||||
InstanceDetail.builder().index("0").build())));
|
||||
List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
|
||||
|
||||
assertThat(instances).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.springframework.cloud.cloudfoundry.discovery;
|
||||
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CloudFoundryDiscoveryClientConfiguration}.
|
||||
* @author Toshiaki Maki
|
||||
*/
|
||||
public class CloudFoundryDiscoveryClientConfigurationTest {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations
|
||||
.of(CloudFoundryDiscoveryClientConfiguration.class));
|
||||
|
||||
@Test
|
||||
public void testDefault() {
|
||||
this.contextRunner.withUserConfiguration(CloudFoundryConfig.class)
|
||||
.run((context) -> {
|
||||
DiscoveryClient discoveryClient = context
|
||||
.getBean(DiscoveryClient.class);
|
||||
assertThat(discoveryClient.getClass())
|
||||
.isEqualTo(CloudFoundryDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUseDnsTrue() {
|
||||
this.contextRunner.withUserConfiguration(CloudFoundryConfig.class)
|
||||
.withPropertyValues("spring.cloud.cloudfoundry.discovery.use-dns=true")
|
||||
.run((context) -> {
|
||||
DiscoveryClient discoveryClient = context
|
||||
.getBean(DiscoveryClient.class);
|
||||
assertThat(discoveryClient.getClass())
|
||||
.isEqualTo(CloudFoundryAppServiceDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUseDnsFalse() {
|
||||
this.contextRunner.withUserConfiguration(CloudFoundryConfig.class)
|
||||
.withPropertyValues("spring.cloud.cloudfoundry.discovery.use-dns=false")
|
||||
.run((context) -> {
|
||||
DiscoveryClient discoveryClient = context
|
||||
.getBean(DiscoveryClient.class);
|
||||
assertThat(discoveryClient.getClass())
|
||||
.isEqualTo(CloudFoundryDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUseContainerIpFalse() {
|
||||
this.contextRunner.withUserConfiguration(CloudFoundryConfig.class)
|
||||
.withPropertyValues("spring.cloud.cloudfoundry.discovery.use-dns=true",
|
||||
"spring.cloud.cloudfoundry.discovery.use-container-ip=false")
|
||||
.run((context) -> {
|
||||
DiscoveryClient discoveryClient = context
|
||||
.getBean(DiscoveryClient.class);
|
||||
assertThat(discoveryClient.getClass())
|
||||
.isEqualTo(CloudFoundryAppServiceDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUseContainerIpTrue() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.cloud.cloudfoundry.discovery.use-dns=true",
|
||||
"spring.cloud.cloudfoundry.discovery.use-container-ip=true")
|
||||
.run((context) -> {
|
||||
DiscoveryClient discoveryClient = context
|
||||
.getBean(DiscoveryClient.class);
|
||||
assertThat(discoveryClient.getClass())
|
||||
.isEqualTo(SimpleDnsBasedDiscoveryClient.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class CloudFoundryConfig {
|
||||
@Bean
|
||||
public CloudFoundryOperations cloudFoundryOperations() {
|
||||
return Mockito.mock(CloudFoundryOperations.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CloudFoundryService cloudFoundryService() {
|
||||
return Mockito.mock(CloudFoundryService.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryClient;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.cloudfoundry.discovery.EnableCloudFoundryClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@@ -55,7 +55,7 @@ public class CloudFoundryApplication {
|
||||
}
|
||||
|
||||
@Bean
|
||||
CommandLineRunner demo(CloudFoundryDiscoveryClient discoveryClient) {
|
||||
CommandLineRunner demo(DiscoveryClient discoveryClient) {
|
||||
Log log = LogFactory.getLog(getClass());
|
||||
return args -> discoveryClient.getServices().forEach(svc -> {
|
||||
log.info("service = " + svc);
|
||||
|
||||
Reference in New Issue
Block a user