Merge branch 'making-app-sd'

This commit is contained in:
Spencer Gibb
2019-04-29 15:06:36 -04:00
8 changed files with 426 additions and 9 deletions

View File

@@ -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);
}
}

View File

@@ -98,4 +98,7 @@ public class CloudFoundryDiscoveryClient implements DiscoveryClient {
return this.properties.getOrder();
}
CloudFoundryService getCloudFoundryService() {
return this.cloudFoundryService;
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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
}
]
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);