Skip to content

Commit

Permalink
Support DNS based Service Discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
making committed Oct 15, 2018
1 parent cf5ecce commit 0da3915
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,8 @@ public List<String> getServices() {
public int getOrder() {
return this.properties.getOrder();
}

CloudFoundryService getCloudFoundryService() {
return this.cloudFoundryService;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ public CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient(
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(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 = "true")
public static class DnsBasedCloudFoundryDiscoveryClientConfig {
@Bean
@ConditionalOnMissingBean(CloudFoundryDiscoveryClient.class)
public CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient(
CloudFoundryOperations cf, CloudFoundryService svc,
CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties) {
return new CloudFoundryAppServiceDiscoveryClient(cf, svc, cloudFoundryDiscoveryProperties);
}
}

@Bean
public CloudFoundryHeartbeatSender cloudFoundryHeartbeatSender(CloudFoundryDiscoveryClient client) {
return new CloudFoundryHeartbeatSender(client);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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);
});
}

@Configuration
public static class CloudFoundryConfig {
@Bean
public CloudFoundryOperations cloudFoundryOperations() {
return Mockito.mock(CloudFoundryOperations.class);
}

@Bean
public CloudFoundryService cloudFoundryService() {
return Mockito.mock(CloudFoundryService.class);
}
}
}

0 comments on commit 0da3915

Please sign in to comment.