From f1ee034d69db7433a5e52a477fafbf48e06fe84b Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Wed, 18 Apr 2018 03:34:43 +0900 Subject: [PATCH] Support DNS based Service Discovery #19 --- ...CloudFoundryAppServiceDiscoveryClient.java | 70 +++++++++++ .../CloudFoundryDiscoveryClient.java | 12 +- ...udFoundryDiscoveryClientConfiguration.java | 25 +++- ...dFoundryAppServiceDiscoveryClientTest.java | 115 ++++++++++++++++++ ...undryDiscoveryClientConfigurationTest.java | 72 +++++++++++ 5 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClient.java create mode 100644 spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClientTest.java create mode 100644 spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfigurationTest.java diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClient.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClient.java new file mode 100644 index 00000000..d2a74cc2 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClient.java @@ -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 CF App Service + * Discovery Release + * @see Polyglot + * Service Discovery for Container Networking in Cloud Foundry + * + * @author Toshiaki Maki + */ +public class CloudFoundryAppServiceDiscoveryClient extends CloudFoundryDiscoveryClient { + + private static final String INTERNAL_DOMAIN = "apps.internal"; + + CloudFoundryAppServiceDiscoveryClient(CloudFoundryOperations cloudFoundryOperations, + CloudFoundryService svc) { + super(cloudFoundryOperations, svc); + } + + @Override + public String description() { + return "CF App Service Discovery Client"; + } + + @Override + public List 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 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); + } +} 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 ad9d3f7a..ace528a4 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 @@ -16,6 +16,10 @@ package org.springframework.cloud.cloudfoundry.discovery; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationSummary; @@ -25,10 +29,6 @@ import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.cloudfoundry.CloudFoundryService; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - /** * Cloud Foundry maintains a registry of running applications which we expose here as CloudFoundryService instances. * @@ -89,4 +89,8 @@ public List getServices() { .blockOptional() .orElse(new ArrayList<>()); } + + CloudFoundryService getCloudFoundryService() { + return this.cloudFoundryService; + } } \ No newline at end of file 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 bf637447..ab67a63e 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 @@ -34,11 +34,26 @@ @EnableConfigurationProperties(CloudFoundryDiscoveryProperties.class) public class CloudFoundryDiscoveryClientConfiguration { - @Bean - @ConditionalOnMissingBean(CloudFoundryDiscoveryClient.class) - public CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient( - CloudFoundryOperations cf, CloudFoundryService svc) { - return new CloudFoundryDiscoveryClient(cf, svc); + @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) { + return new CloudFoundryDiscoveryClient(cf, svc); + } + } + + @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) { + return new CloudFoundryAppServiceDiscoveryClient(cf, svc); + } } @Bean diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClientTest.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClientTest.java new file mode 100644 index 00000000..b2cc8110 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAppServiceDiscoveryClientTest.java @@ -0,0 +1,115 @@ +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); + } + + @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 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() { + { + 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 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() { + { + put("applicationId", "billing-id"); + put("instanceId", "0"); + } + })); + assertThat(instances.get(1)).isEqualTo(new DefaultServiceInstance(serviceId, + "1.billing.apps.internal", 8080, false, new HashMap() { + { + put("applicationId", "billing-id"); + put("instanceId", "1"); + } + })); + assertThat(instances.get(2)).isEqualTo(new DefaultServiceInstance(serviceId, + "2.billing.apps.internal", 8080, false, new HashMap() { + { + 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 instances = this.discoveryClient.getInstances(serviceId); + + assertThat(instances).isEmpty(); + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfigurationTest.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfigurationTest.java new file mode 100644 index 00000000..926b0b79 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfigurationTest.java @@ -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); + } + } +} \ No newline at end of file