support Reactor-based CF Java Client (#22)

* reactive Cloud Foundry Java Client

- new support for auto-configuring the reactor-based CF java client v3.3.
- reworkd the `DiscoveryClient` to depend on the standalone support.
- made the tests run conditionally based on presence of certain env vars.

fixes gh-21
This commit is contained in:
Josh Long
2018-01-08 13:45:02 -08:00
committed by Spencer Gibb
parent 2a310c2eec
commit fef7d2c9a7
23 changed files with 870 additions and 851 deletions

View File

@@ -12,6 +12,9 @@ middlware that you "bind" to an app, essentially providing it with an
environment variable containing credentials (e.g. the location and
username to use for the service).
The `spring-cloud-cloudfoundry-commons` module configures the
Reactor-based Cloud Foundry Java client, v 3.0, and can be used standalone.
The `spring-cloud-cloudfoundry-web` project provides basic support for
some enhanced features of webapps in Cloud Foundry: binding
automatically to single-sign-on services and optionally enabling
@@ -20,16 +23,14 @@ sticky routing for discovery.
The `spring-cloud-cloudfoundry-discovery` project provides an
implementation of Spring Cloud Commons `DiscoveryClient` so you can
`@EnableDiscoveryClient` and provide your credentials as
`spring.cloud.cloudfoundry.discovery.[email,password]` and then you
can use the `DiscoveryClient` directly or via a `LoadBalancerClient`
(also `*.url` if you are not connecting to
https://run.pivotal.io[Pivotal Web Services]).
`spring.cloud.cloudfoundry.discovery.[username,password]` (also `*.url` if you are not connecting to https://run.pivotal.io[Pivotal Web Services]) and then you
can use the `DiscoveryClient` directly or via a `LoadBalancerClient`.
> NOTE: if you are looking for a way to bind to services then this is the wrong library. Check out the [Spring Cloud Connectors](https://github.com/spring-cloud/spring-cloud-connectors) instead.
== Building
:jdkversion: 1.7
:jdkversion: 1.8
=== Basic Compile and Test
@@ -70,7 +71,7 @@ https://github.com/spring-cloud-samples/scripts[scripts demo
repository] for specific instructions about the common cases of mongo,
rabbit and redis.
NOTE: If all else fails, build with the command from `.travis.yml` (usually
NOTE: If all else fails, build with the command from `.circle.yml` (usually
`./mvnw install`).
=== Documentation

View File

@@ -6,6 +6,9 @@ middlware that you "bind" to an app, essentially providing it with an
environment variable containing credentials (e.g. the location and
username to use for the service).
The `spring-cloud-cloudfoundry-commons` module configures the
Reactor-based Cloud Foundry Java client, v 3.0, and can be used standalone.
The `spring-cloud-cloudfoundry-web` project provides basic support for
some enhanced features of webapps in Cloud Foundry: binding
automatically to single-sign-on services and optionally enabling
@@ -14,7 +17,5 @@ sticky routing for discovery.
The `spring-cloud-cloudfoundry-discovery` project provides an
implementation of Spring Cloud Commons `DiscoveryClient` so you can
`@EnableDiscoveryClient` and provide your credentials as
`spring.cloud.cloudfoundry.discovery.[email,password]` and then you
can use the `DiscoveryClient` directly or via a `LoadBalancerClient`
(also `*.url` if you are not connecting to
https://run.pivotal.io[Pivotal Web Services]).
`spring.cloud.cloudfoundry.discovery.[username,password]` (also `*.url` if you are not connecting to https://run.pivotal.io[Pivotal Web Services]) and then you
can use the `DiscoveryClient` directly or via a `LoadBalancerClient`.

158
pom.xml
View File

@@ -12,14 +12,15 @@
<artifactId>spring-cloud-build</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>
</parent>
<properties>
<bintray.package>cloudfoundry</bintray.package>
<main.basedir>${basedir}</main.basedir>
<spring-cloud-netflix.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-netflix.version>
<spring-cloud-commons.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
</properties>
<properties>
<cf-java-client.version>3.3.0.RELEASE</cf-java-client.version>
<bintray.package>cloudfoundry</bintray.package>
<main.basedir>${basedir}</main.basedir>
<spring-cloud-netflix.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-netflix.version>
<spring-cloud-commons.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
</properties>
<scm>
<url>https://github.com/spring-cloud/spring-cloud-consul</url>
@@ -34,6 +35,7 @@
<modules>
<module>spring-cloud-cloudfoundry-dependencies</module>
<module>spring-cloud-cloudfoundry-commons</module>
<module>spring-cloud-cloudfoundry-discovery</module>
<module>spring-cloud-cloudfoundry-web</module>
<module>spring-cloud-cloudfoundry-sample</module>
@@ -70,80 +72,80 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-dependencies</artifactId>
<version>${spring-cloud-commons.version}</version>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-dependencies</artifactId>
<version>${spring-cloud-commons.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>spring</id>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<profiles>
<profile>
<id>spring</id>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-cloudfoundry-commons</artifactId>
<packaging>jar</packaging>
<description>Spring Cloud CloudFoundry Commons</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-client-reactor</artifactId>
<version>${cf-java-client.version}</version>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-operations</artifactId>
<version>${cf-java-client.version}</version>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-util</artifactId>
<version>${cf-java-client.version}</version>
</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</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,138 @@
package org.springframework.cloud.cloudfoundry;
import org.cloudfoundry.client.CloudFoundryClient;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
import org.cloudfoundry.reactor.ConnectionContext;
import org.cloudfoundry.reactor.DefaultConnectionContext;
import org.cloudfoundry.reactor.TokenProvider;
import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient;
import org.cloudfoundry.reactor.doppler.ReactorDopplerClient;
import org.cloudfoundry.reactor.routing.ReactorRoutingClient;
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
import org.cloudfoundry.routing.RoutingClient;
import org.cloudfoundry.uaa.UaaClient;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* Provides auto-configuration for the Reactor-based Cloud Foundry client v3.x.
*
* @author <a href="mailto:josh@joshlong.com">Josh Long</a>
* @author Ben Hale
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.cloud.cloudfoundry", name = {"username", "password", "org", "space"})
@ConditionalOnClass(name = {"reactor.core.publisher.Flux", "org.cloudfoundry.operations.DefaultCloudFoundryOperations",
"org.cloudfoundry.reactor.client.ReactorCloudFoundryClient", "org.reactivestreams.Publisher"})
@EnableConfigurationProperties(CloudFoundryProperties.class)
public class CloudFoundryClientAutoConfiguration {
private final CloudFoundryProperties cloudFoundryProperties;
public CloudFoundryClientAutoConfiguration(CloudFoundryProperties cfp) {
this.cloudFoundryProperties = cfp;
}
@Bean
@Lazy
@ConditionalOnMissingBean
public CloudFoundryService cloudFoundryService(CloudFoundryOperations cloudFoundryOperations) {
return new CloudFoundryService(cloudFoundryOperations);
}
@Bean
@Lazy
@ConditionalOnMissingBean
public ReactorCloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorCloudFoundryClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public DefaultCloudFoundryOperations cloudFoundryOperations(CloudFoundryClient cloudFoundryClient,
DopplerClient dopplerClient,
RoutingClient routingClient,
UaaClient uaaClient) {
String organization = this.cloudFoundryProperties.getOrg();
String space = this.cloudFoundryProperties.getSpace();
return DefaultCloudFoundryOperations
.builder()
.cloudFoundryClient(cloudFoundryClient)
.dopplerClient(dopplerClient)
.routingClient(routingClient)
.uaaClient(uaaClient)
.organization(organization)
.space(space)
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public DefaultConnectionContext connectionContext() {
String apiHost = this.cloudFoundryProperties.getUrl();
Boolean skipSslValidation = this.cloudFoundryProperties.isSkipSslValidation();
return DefaultConnectionContext.builder()
.apiHost(apiHost)
.skipSslValidation(skipSslValidation)
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public DopplerClient dopplerClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorDopplerClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public RoutingClient routingClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorRoutingClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public PasswordGrantTokenProvider tokenProvider() {
String username = this.cloudFoundryProperties.getUsername();
String password = this.cloudFoundryProperties.getPassword();
return PasswordGrantTokenProvider.builder()
.password(password)
.username(username)
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public ReactorUaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorUaaClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
}

View File

@@ -0,0 +1,129 @@
package org.springframework.cloud.cloudfoundry;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:josh@joshlong.com">Josh Long</a>
*/
@ConfigurationProperties(prefix = "spring.cloud.cloudfoundry")
public class CloudFoundryProperties implements InitializingBean {
/**
* URL of Cloud Foundry API (Cloud Controller).
*/
private String url = "api.run.pivotal.io";
/**
* Username to authenticate (usually an email address).
*/
private String username;
/**
* Password for user to authenticate and obtain token.
*/
private String password;
/**
* Organization name to authenticate with (default to user's default).
*/
private String org;
/**
* Space name to authenticate with (default to user's default).
*/
@Value("${vcap.application.space_name:}")
private String space;
private boolean skipSslValidation;
public String getUrl() {
return this.url;
}
private String safeUrl(String t) {
String input = t.trim().toLowerCase();
Pattern p = Pattern.compile("(http(s)?://)(.*)");
Matcher matcher = p.matcher(input);
if (matcher.matches()) {
String group = matcher.group(1);
if (StringUtils.hasText(group)) {
return t.substring(group.length());
}
}
return t;
}
public void setUrl(String cloudControllerUrl) {
this.url = cloudControllerUrl;
}
public String getUsername() {
return this.username;
}
public void setUsername(String email) {
this.username = email;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public String getOrg() {
return this.org;
}
public void setOrg(String org) {
this.org = org;
}
public String getSpace() {
return this.space;
}
public void setSpace(String space) {
this.space = space;
}
public boolean isSkipSslValidation() {
return skipSslValidation;
}
public boolean getSkipSslValidation() {
return this.skipSslValidation;
}
public void setSkipSslValidation(boolean skipSslValidation) {
this.skipSslValidation = skipSslValidation;
}
@Override
public void afterPropertiesSet() throws Exception {
this.url = safeUrl(this.url);
this.password = this.password.trim();
this.username = this.username.trim();
this.org = this.org.trim();
this.space = this.space.trim();
Map<String, String> vals = new HashMap<>();
vals.put("org", getOrg());
vals.put("url", getUrl());
vals.put("username", getUsername());
vals.put("password", getPassword());
vals.put("space", getSpace());
vals.forEach((key, value) -> Assert.hasText(value, String.format("'%s' must be provided", key)));
}
}

View File

@@ -0,0 +1,38 @@
package org.springframework.cloud.cloudfoundry;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.GetApplicationRequest;
import org.cloudfoundry.operations.applications.InstanceDetail;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
/**
* Supports the discovery of a combination of an application instance's URI, port,
* application ID, and application index.
*
* @author <a href="mailto:josh@joshlong.com">Josh Long</a>
*/
public class CloudFoundryService {
private final CloudFoundryOperations cloudFoundryOperations;
public CloudFoundryService(CloudFoundryOperations cloudFoundryOperations) {
this.cloudFoundryOperations = cloudFoundryOperations;
}
public Flux<Tuple2<ApplicationDetail, InstanceDetail>> getApplicationInstances(String serviceId) {
GetApplicationRequest applicationRequest = GetApplicationRequest.builder().name(serviceId).build();
return this.cloudFoundryOperations
.applications()
.get(applicationRequest)
.flatMapMany(applicationDetail -> {
Flux<InstanceDetail> ids = Flux.fromStream(applicationDetail.getInstanceDetails().stream())
.filter(id -> id.getState().equalsIgnoreCase("RUNNING"));
Flux<ApplicationDetail> generate = Flux.generate(sink -> sink.next(applicationDetail));
return generate.zipWith(ids);
});
}
}

View File

@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.cloudfoundry.CloudFoundryClientAutoConfiguration

View File

@@ -0,0 +1,129 @@
package org.springframework.cloud.cloudfoundry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.assertj.core.api.Assertions;
import org.cloudfoundry.doppler.DopplerClient;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.GetApplicationRequest;
import org.cloudfoundry.operations.applications.InstanceDetail;
import org.cloudfoundry.operations.organizations.OrganizationSummary;
import org.cloudfoundry.reactor.DefaultConnectionContext;
import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient;
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
import org.cloudfoundry.routing.RoutingClient;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CloudFoundryClientAutoConfigurationTest {
@SpringBootApplication
public static class MyConfig {
}
private static String envVarFromProperty(String propertyName) {
return propertyName
.replaceAll("\\.", "_")
.toUpperCase();
}
private static String CONFIG_KEYS[] = {"spring.cloud.cloudfoundry.username",
"spring.cloud.cloudfoundry.password", "spring.cloud.cloudfoundry.space", "spring.cloud.cloudfoundry.org"};
private static Map<String, Object> defaultConfig() {
Map<String, Object> kvs = new HashMap<>();
for (String k : CONFIG_KEYS)
kvs.put(k, System.getenv(envVarFromProperty(k)));
return kvs;
}
private static boolean configExists() {
for (String k : CONFIG_KEYS)
if (System.getenv(envVarFromProperty(k)) == null)
return false;
return true;
}
@Test
public void producesAllBeans() {
Assume.assumeTrue(configExists());
ApplicationContext ctx = new SpringApplicationBuilder()
.sources(MyConfig.class)
.properties(defaultConfig())
.run();
Class[] tags = {ReactorCloudFoundryClient.class, DefaultCloudFoundryOperations.class,
DefaultConnectionContext.class, DopplerClient.class, RoutingClient.class, PasswordGrantTokenProvider.class,
ReactorUaaClient.class};
for (Class<?> c : tags) {
Assertions.assertThat(ctx.getBean(c)).isNotNull();
}
}
@Test
public void testConnectivity() throws Exception {
Assume.assumeTrue(configExists());
ApplicationContext ctx = new SpringApplicationBuilder()
.sources(MyConfig.class)
.properties(defaultConfig())
.run();
CloudFoundryOperations operations = ctx.getBean(CloudFoundryOperations.class);
Assert.assertNotNull(operations);
OrganizationSummary summary = operations
.organizations()
.list()
.blockFirst();
Assertions.assertThat(summary).isNotNull();
Assertions.assertThat(summary.getId()).isNotEmpty();
Assertions.assertThat(summary.getName()).isNotEmpty();
}
@Ignore
@Test
public void instances() throws Exception {
Assume.assumeTrue(configExists());
Log log = LogFactory.getLog(getClass());
ApplicationContext ctx = new SpringApplicationBuilder()
.sources(MyConfig.class)
.properties(defaultConfig())
.run();
CloudFoundryOperations cf = ctx.getBean(CloudFoundryOperations.class);
Flux<Tuple2<ApplicationDetail, InstanceDetail>> mapMany = cf
.applications()
.get(GetApplicationRequest.builder().name("lo-test").build())
.flatMapMany(applicationDetail -> {
List<InstanceDetail> instanceDetails = applicationDetail.getInstanceDetails();
Flux<InstanceDetail> ids = Flux.fromStream(instanceDetails.stream());
Flux<ApplicationDetail> generate = Flux.generate(sink -> sink.next(applicationDetail));
return generate.zipWith(ids);
});
mapMany
.subscribe(p ->
log.info(p.getT1().getName() + ':' + p.getT1().getId() + ':' + p.getT2().getIndex() + ':' +
p.getT2().getState()));
Thread.sleep(5 * 1000);
}
}

View File

@@ -1,98 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.0.0.BUILD-SNAPSHOT</version>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-cloudfoundry-dependencies</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-cloudfoundry-dependencies</name>
<description>Spring Cloud Cloudfoundry Dependencies</description>
<properties>
<cloudfoundry-client-lib.version>1.1.3</cloudfoundry-client-lib.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-discovery</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-web</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-cloudfoundry</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-client-lib</artifactId>
<version>${cloudfoundry-client-lib.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>spring</id>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
</parent>
<artifactId>spring-cloud-cloudfoundry-dependencies</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-cloudfoundry-dependencies</name>
<description>Spring Cloud Cloudfoundry Dependencies</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-discovery</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-web</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-cloudfoundry</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>spring</id>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
</project>

View File

@@ -2,19 +2,21 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-cloudfoundry-discovery</artifactId>
<packaging>jar</packaging>
<description>Spring Cloud CloudFoundry Discovery</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
@@ -28,10 +30,6 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-client-lib</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
@@ -48,5 +46,4 @@
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>

View File

@@ -16,58 +16,22 @@
package org.springframework.cloud.cloudfoundry.discovery;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.client.lib.CloudCredentials;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import org.cloudfoundry.client.lib.domain.InstanceInfo;
import org.cloudfoundry.client.lib.domain.InstanceState;
import org.cloudfoundry.client.lib.domain.InstancesInfo;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.ApplicationSummary;
import org.cloudfoundry.operations.applications.InstanceDetail;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.env.Environment;
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* A Cloud Foundry v2 API-aware implementation of the {@link DiscoveryClient discovery
* client} SPI. Cloud Foundry already retains a registry of running applications which we
* expose here as services. Newer versions of Cloud Foundry support instance-specific
* networking, but as the Cloud Foundry client API doesn't yet support that, this
* {@link DiscoveryClient implementation doesn't either}.
* <p/>
* You need to provide an instance of the {@link CloudFoundryClient}. A workable
* configuration looks like this:
* <p/>
*
* <pre class="code">
* &#064;Bean
* CloudFoundryClient cloudFoundryClient(
* &#064;Value(&quot;${MY_CUSTOM_CF_API:https://api.run.pivotal.io}&quot;) String api,
* CloudCredentials cc) throws MalformedURLException {
* CloudFoundryClient cloudFoundryClient = new CloudFoundryClient(cc,
* URI.create(api).toURL());
* cloudFoundryClient.login();
* return cloudFoundryClient;
* }
* </pre>
* <p/>
* You can configure all sorts of other things including which Cloud Foundry cloud
* controller URI to use, how and whether to use an HTTP proxy, and more using alternative
* constructors. As configured above, the client will talk to all services and
* applications deployed in <EM>all</EM> spaces and organizations. Use one of the
* {@link CloudFoundryClient#CloudFoundryClient(CloudCredentials, URL, String, String)}
* variants to specify which space and organization to use.
* <p/>
* Cloud Foundry maintains a registry of running applications which we expose here as CloudFoundryService instances.
*
* @author Josh Long
* @author Spencer Gibb
@@ -75,119 +39,60 @@ import org.springframework.core.env.Environment;
*/
public class CloudFoundryDiscoveryClient implements DiscoveryClient {
private static final String DESCRIPTION = "Cloud Foundry "
+ DiscoveryClient.class.getName() + " implementation";
private final CloudFoundryService cloudFoundryService;
private final CloudFoundryOperations cloudFoundryOperations;
private static final Log log = LogFactory.getLog(CloudFoundryDiscoveryClient.class);
private final CloudFoundryClient cloudFoundryClient;
private final String description = "Cloud Foundry " + DiscoveryClient.class.getName() + " implementation";
@Value("${vcap.application.name:${spring.application.name:application}}")
private String vcapApplicationName = "application";
public CloudFoundryDiscoveryClient(CloudFoundryClient cloudFoundryClient,
Environment environment) {
this.cloudFoundryClient = cloudFoundryClient;
CloudFoundryDiscoveryClient(CloudFoundryOperations cloudFoundryOperations,
CloudFoundryService svc) {
this.cloudFoundryService = svc;
this.cloudFoundryOperations = cloudFoundryOperations;
}
@Override
public String description() {
return DESCRIPTION;
}
public ServiceInstance getLocalServiceInstance() {
List<ServiceInstance> serviceInstances = null;
try {
CloudApplication application = this.cloudFoundryClient
.getApplication(this.vcapApplicationName);
serviceInstances = this.createServiceInstancesFromCloudApplications(
Collections.singletonList(application));
}
catch (Exception e) {
log.warn("Could not determine local service instance: " + e.getClass() + " ("
+ e.getMessage() + ")");
}
return serviceInstances != null && serviceInstances.size() > 0
? serviceInstances.iterator().next() : null;
return this.description;
}
@Override
public List<ServiceInstance> getInstances(String s) {
try {
CloudApplication applications = this.cloudFoundryClient.getApplication(s);
return this.createServiceInstancesFromCloudApplications(
Collections.singletonList(applications));
}
catch (Exception e) {
log.warn("Could not get service instances: " + e.getClass() + " ("
+ e.getMessage() + ")");
return Collections.emptyList();
}
}
public List<ServiceInstance> getInstances(String serviceId) {
return cloudFoundryService
.getApplicationInstances(serviceId)
.map(tuple -> {
ApplicationDetail applicationDetail = tuple.getT1();
InstanceDetail instanceDetail = tuple.getT2();
private boolean isRunning(CloudApplication ca) {
InstancesInfo ii = this.cloudFoundryClient.getApplicationInstances(ca);
List<InstanceInfo> instances;
if (ii != null && (instances = ii.getInstances()) != null) {
for (InstanceInfo resolved : instances) {
InstanceState state = resolved.getState();
if (state != null && state.equals(InstanceState.RUNNING)) {
return true;
}
}
}
return false;
String applicationId = applicationDetail.getId();
String applicationIndex = instanceDetail.getIndex();
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 (ServiceInstance) new DefaultServiceInstance(name, url, 80, secure, metadata);
})
.collectList()
.blockOptional()
.orElse(new ArrayList<>());
}
@Override
public List<String> getServices() {
List<String> services = new ArrayList<>();
List<CloudApplication> applications;
try {
applications = this.cloudFoundryClient.getApplications();
}
catch (Exception e) {
log.warn("Could not get applications: " + e.getClass() + " ("
+ e.getMessage() + ")");
applications = Collections.emptyList();
}
Set<String> serviceIds = new HashSet<>();
for (CloudApplication ca : applications) {
if (isRunning(ca)) {
serviceIds.add(ca.getName());
}
}
services.addAll(serviceIds);
return services;
}
protected List<ServiceInstance> createServiceInstancesFromCloudApplications(
Collection<CloudApplication> cloudApplications) {
Set<ServiceInstance> serviceInstances = new HashSet<>();
for (CloudApplication ca : cloudApplications) {
if (isRunning(ca)) {
serviceInstances.add(new CloudFoundryServiceInstance(ca));
}
}
List<ServiceInstance> instances = new ArrayList<>();
instances.addAll(serviceInstances);
return instances;
}
public static class CloudFoundryServiceInstance extends DefaultServiceInstance {
private final CloudApplication cloudApplication;
public CloudApplication getCloudApplication() {
return this.cloudApplication;
}
public CloudFoundryServiceInstance(CloudApplication ca) {
super(ca.getName(),
ca.getUris().isEmpty() ? "localhost" : ca.getUris().iterator().next(),
80, false);
this.cloudApplication = ca;
}
return this
.cloudFoundryOperations
.applications()
.list()
.map(ApplicationSummary::getName)
.collectList()
.blockOptional()
.orElse(new ArrayList<>());
}
}

View File

@@ -16,26 +16,22 @@
package org.springframework.cloud.cloudfoundry.discovery;
import java.net.MalformedURLException;
import java.net.URI;
import org.cloudfoundry.client.lib.CloudCredentials;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.springframework.beans.factory.annotation.Autowired;
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.cloudfoundry.CloudFoundryService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* @author Josh Long
*/
@Configuration
@ConditionalOnClass(CloudFoundryClient.class)
@ConditionalOnClass(CloudFoundryOperations.class)
@ConditionalOnProperty(value = "spring.cloud.cloudfoundry.discovery.enabled", matchIfMissing = true)
@EnableConfigurationProperties(CloudFoundryDiscoveryProperties.class)
public class CloudFoundryDiscoveryClientConfiguration {
@@ -43,41 +39,15 @@ public class CloudFoundryDiscoveryClientConfiguration {
@Autowired
private CloudFoundryDiscoveryProperties discovery;
@Bean
@ConditionalOnMissingBean(CloudCredentials.class)
public CloudCredentials cloudCredentials() {
return new CloudCredentials(this.discovery.getUsername(),
this.discovery.getPassword());
}
@Bean
@ConditionalOnMissingBean(CloudFoundryClient.class)
public CloudFoundryClient cloudFoundryClient(CloudCredentials cc)
throws MalformedURLException {
CloudFoundryClient cloudFoundryClient;
if (StringUtils.hasText(this.discovery.getOrg()) && StringUtils.hasText(this.discovery.getSpace())) {
cloudFoundryClient = new CloudFoundryClient(cc,
URI.create(this.discovery.getUrl()).toURL(), this.discovery.getOrg(),
this.discovery.getSpace());
}
else {
cloudFoundryClient = new CloudFoundryClient(cc,
URI.create(this.discovery.getUrl()).toURL());
}
cloudFoundryClient.login();
return cloudFoundryClient;
}
@Bean
@ConditionalOnMissingBean(CloudFoundryDiscoveryClient.class)
public CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient(
CloudFoundryClient cloudFoundryClient, Environment environment) {
return new CloudFoundryDiscoveryClient(cloudFoundryClient, environment);
CloudFoundryOperations cf, CloudFoundryService svc) {
return new CloudFoundryDiscoveryClient(cf, svc);
}
@Bean
public CloudFoundryHeartbeatSender cloudFoundryHeartbeatSender(CloudFoundryDiscoveryClient client) {
return new CloudFoundryHeartbeatSender(client);
}
}

View File

@@ -25,32 +25,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.cloud.cloudfoundry.discovery")
public class CloudFoundryDiscoveryProperties {
/**
* URL of Cloud Foundry API (Cloud Controller).
*/
private String url = "https://api.run.pivotal.io";
/**
* Username to authenticate (usually an email address).
*/
private String username;
/**
* Password for user to authenticate and obtain token.
*/
private String password;
/**
* Organization name to authenticate with (default to user's default).
*/
private String org;
/**
* Space name to authenticate with (default to user's default).
*/
@Value("${vcap.application.space_name:}")
private String space;
/**
* Flag to indicate that discovery is enabled.
*/
@@ -63,53 +37,13 @@ public class CloudFoundryDiscoveryProperties {
private long heartbeatFrequency = 5000;
public boolean isEnabled() {
return this.enabled;
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getUrl() {
return this.url;
}
public void setUrl(String cloudControllerUrl) {
this.url = cloudControllerUrl;
}
public String getUsername() {
return this.username;
}
public void setUsername(String email) {
this.username = email;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public String getOrg() {
return this.org;
}
public void setOrg(String org) {
this.org = org;
}
public String getSpace() {
return this.space;
}
public void setSpace(String space) {
this.space = space;
}
public long getHeartbeatFrequency() {
return this.heartbeatFrequency;
}

View File

@@ -16,20 +16,19 @@
package org.springframework.cloud.cloudfoundry.discovery;
import javax.annotation.PostConstruct;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.IClientConfig;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.config.DynamicStringProperty;
import com.netflix.loadbalancer.ServerList;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @author Josh Long
@@ -52,10 +51,9 @@ public class CloudFoundryRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(CloudFoundryClient cloudFoundryClient,
IClientConfig config) {
CloudFoundryServerList cloudFoundryServerList = new CloudFoundryServerList(
cloudFoundryClient);
public ServerList<?> ribbonServerList(CloudFoundryService svc,
IClientConfig config) {
CloudFoundryServerList cloudFoundryServerList = new CloudFoundryServerList(svc);
cloudFoundryServerList.initWithNiwsConfig(config);
return cloudFoundryServerList;
}

View File

@@ -16,8 +16,6 @@
package org.springframework.cloud.cloudfoundry.discovery;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import com.netflix.loadbalancer.Server;
/**
@@ -27,14 +25,12 @@ public class CloudFoundryServer extends Server {
private final MetaInfo metaInfo;
public CloudFoundryServer(final CloudApplication cloudApplication) {
super(cloudApplication.getUris().iterator().next(), 80);
public CloudFoundryServer(String appName, String uri, int port) {
super(uri, port);
this.metaInfo = new MetaInfo() {
@Override
public String getAppName() {
return cloudApplication.getName();
return appName;
}
@Override
@@ -44,12 +40,12 @@ public class CloudFoundryServer extends Server {
@Override
public String getServiceIdForDiscovery() {
return cloudApplication.getName();
return appName;
}
@Override
public String getInstanceId() {
return cloudApplication.getName();
return appName;
}
};
}

View File

@@ -16,30 +16,24 @@
package org.springframework.cloud.cloudfoundry.discovery;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractServerList;
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
import java.util.ArrayList;
import java.util.List;
/**
* @author Josh Long
*/
public class CloudFoundryServerList extends AbstractServerList<CloudFoundryServer> {
private static final Log log = LogFactory.getLog(CloudFoundryServerList.class);
private String serviceId;
protected String serviceId;
private final CloudFoundryService cloudFoundryService;
private final CloudFoundryClient cloudFoundryClient;
public CloudFoundryServerList(CloudFoundryClient cloudFoundryClient) {
this.cloudFoundryClient = cloudFoundryClient;
CloudFoundryServerList(CloudFoundryService svc) {
this.cloudFoundryService = svc;
}
@Override
@@ -57,15 +51,12 @@ public class CloudFoundryServerList extends AbstractServerList<CloudFoundryServe
return this.cloudFoundryServers();
}
protected List<CloudFoundryServer> cloudFoundryServers() {
try {
CloudApplication cloudApplications = this.cloudFoundryClient
.getApplication(this.serviceId);
return Collections.singletonList(new CloudFoundryServer(cloudApplications));
}
catch (Exception e) {
log.warn("Cannot determine server list for " + this.serviceId + ": " + e.getClass() + "(" + e.getMessage() + ")");
return Collections.emptyList();
}
private List<CloudFoundryServer> cloudFoundryServers() {
return cloudFoundryService
.getApplicationInstances(this.serviceId)
.map(tpl -> new CloudFoundryServer(tpl.getT1().getName(), tpl.getT1().getUrls().get(0), 80))
.collectList()
.blockOptional()
.orElse(new ArrayList<>());
}
}

View File

@@ -27,8 +27,7 @@ import org.junit.runners.Suite.SuiteClasses;
* @author Dave Syer
*/
@RunWith(Suite.class)
@SuiteClasses({ CloudFoundryServerListTest.class, CloudFoundryAutoConfigurationTest.class,
CloudFoundryServerTest.class, CloudFoundryDiscoveryClientTest.class })
@SuiteClasses({CloudFoundryServerListTest.class})
@Ignore
public class AdhocTestSuite {

View File

@@ -1,101 +0,0 @@
/*
* Copyright 2013-2015 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
*
* http://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 org.apache.commons.logging.LogFactory;
import org.cloudfoundry.client.lib.CloudCredentials;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author <A href= "josh@joshlong.com">Josh Long</A>
*/
public class CloudFoundryAutoConfigurationTest {
private ConfigurableApplicationContext context;
@Before
public void setUp() {
String hiServiceServiceId = "foo-service";
Object vcapAppl = "{\"limits\":{\"mem\":1024,\"disk\":1024,\"fds\":16384},\"application_version\":"
+ "\"36eff082-96d6-498f-8214-508fda72ba65\",\"application_name\":\""
+ hiServiceServiceId
+ "\",\"application_uris\""
+ ":[\""
+ hiServiceServiceId
+ ".cfapps.io\"],\"version\":\"36eff082-96d6-498f-8214-508fda72ba65\",\"name\":"
+ "\"hi-service\",\"space_name\":\"joshlong\",\"space_id\":\"e0cd969c-3461-41ae-abde-4e11bb5acbd1\","
+ "\"uris\":[\"hi-service.cfapps.io\"],\"users\":null,\"application_id\":\"af350f7c-88c4-4e35-a04e-698a1dbc7354\","
+ "\"instance_id\":\"e4843ca23bd947b28e6d4cb3f9b92cbb\",\"instance_index\":0,\"host\":\"0.0.0.0\",\"port\":61590,"
+ "\"started_at\":\"2015-05-07 20:00:10 +0000\",\"started_at_timestamp\":1431028810,\"start\":\"2015-05-07 20:00:10 +0000\","
+ "\"state_timestamp\":1431028810}";
this.context = new SpringApplicationBuilder()
.properties("VCAP_APPLICATION:"+vcapAppl, "server.port=0")
.sources(SimpleConfiguration.class).run();
}
@After
public void after() throws Throwable {
synchronized (this) {
if (null != this.context)
this.context.close();
}
}
@Configuration
@EnableDiscoveryClient
@EnableFeignClients
@EnableAutoConfiguration
public static class SimpleConfiguration {
@Bean
CloudCredentials cloudCredentials() {
return Mockito.mock(CloudCredentials.class);
}
@Bean
CloudFoundryClient cloudFoundryClient() {
return Mockito.mock(CloudFoundryClient.class);
}
}
@Test
public void contextLoaded() {
LogFactory.getLog(getClass()).debug("contextLoad()");
Assert.assertTrue(this.context.getBeansOfType(CloudFoundryDiscoveryClient.class)
.size() > 0);
Assert.assertTrue(this.context.getBeansOfType(
CloudFoundryDiscoveryProperties.class).size() > 0);
Assert.assertTrue(this.context.getBeansOfType(CloudFoundryClient.class).size() > 0);
}
}

View File

@@ -16,31 +16,28 @@
package org.springframework.cloud.cloudfoundry.discovery;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.cloudfoundry.client.lib.CloudFoundryException;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import org.cloudfoundry.client.lib.domain.InstanceInfo;
import org.cloudfoundry.client.lib.domain.InstanceState;
import org.cloudfoundry.client.lib.domain.InstancesInfo;
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.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
/**
* @author <A href="mailto:josh@Joshlong.com">Josh Long</A>
@@ -48,144 +45,59 @@ import org.springframework.http.HttpStatus;
public class CloudFoundryDiscoveryClientTest {
private final Log log = LogFactory.getLog(getClass());
private CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient;
private CloudApplication cloudApplication;
private String hiServiceServiceId = "hi-service";
private CloudFoundryClient cloudFoundryClient;
private CloudApplication fakeCloudApplication(String name, String... uri) {
CloudApplication cloudApplication = mock(CloudApplication.class);
given(cloudApplication.getName()).willReturn(name);
given(cloudApplication.getUris()).willReturn(Arrays.asList(uri));
return cloudApplication;
}
private CloudFoundryOperations ops;
private CloudFoundryService svc;
@Before
public void setUp() {
this.cloudFoundryClient = mock(CloudFoundryClient.class);
Environment environment = mock(Environment.class);
given(environment.getProperty("VCAP_APPLICATION"))
.willReturn(
"{\"limits\":{\"mem\":1024,\"disk\":1024,\"fds\":16384},\"application_version\":"
+ "\"36eff082-96d6-498f-8214-508fda72ba65\",\"application_name\":\""
+ this.hiServiceServiceId
+ "\",\"application_uris\""
+ ":[\""
+ this.hiServiceServiceId
+ ".cfapps.io\"],\"version\":\"36eff082-96d6-498f-8214-508fda72ba65\",\"name\":"
+ "\"hi-service\",\"space_name\":\"joshlong\",\"space_id\":\"e0cd969c-3461-41ae-abde-4e11bb5acbd1\","
+ "\"uris\":[\"hi-service.cfapps.io\"],\"users\":null,\"application_id\":\"af350f7c-88c4-4e35-a04e-698a1dbc7354\","
+ "\"instance_id\":\"e4843ca23bd947b28e6d4cb3f9b92cbb\",\"instance_index\":0,\"host\":\"0.0.0.0\",\"port\":61590,"
+ "\"started_at\":\"2015-05-07 20:00:10 +0000\",\"started_at_timestamp\":1431028810,\"start\":\"2015-05-07 20:00:10 +0000\","
+ "\"state_timestamp\":1431028810}");
List<CloudApplication> cloudApplications = new ArrayList<>();
cloudApplications.add(fakeCloudApplication(this.hiServiceServiceId,
"hi-service.cfapps.io", "hi-service-1.cfapps.io"));
cloudApplications.add(fakeCloudApplication("config-service",
"conf-service.cfapps.io", "conf-service-1.cfapps.io"));
given(this.cloudFoundryClient.getApplications()).willReturn(cloudApplications);
this.cloudApplication = cloudApplications.get(0);
given(this.cloudFoundryClient.getApplication(this.hiServiceServiceId))
.willReturn(this.cloudApplication);
given(this.cloudFoundryClient.getApplication(this.hiServiceServiceId))
.willReturn(this.cloudApplication);
InstanceInfo instanceInfo = mock(InstanceInfo.class);
InstancesInfo instancesInfo = mock(InstancesInfo.class);
given(instancesInfo.getInstances()).willReturn(
Collections.singletonList(instanceInfo));
given(instanceInfo.getState()).willReturn(InstanceState.RUNNING);
given(this.cloudFoundryClient.getApplicationInstances(this.cloudApplication))
.willReturn(instancesInfo);
this.cloudFoundryDiscoveryClient = new CloudFoundryDiscoveryClient(
this.cloudFoundryClient, environment);
this.ops = mock(CloudFoundryOperations.class);
this.svc = mock(CloudFoundryService.class);
this.cloudFoundryDiscoveryClient = new CloudFoundryDiscoveryClient(this.ops, this.svc);
}
@Test
public void testServiceResolution() {
Applications apps = mock(Applications.class);
ApplicationSummary s = ApplicationSummary.builder()
.id(UUID.randomUUID().toString())
.instances(2)
.memoryLimit(1024)
.requestedState("requestedState")
.diskQuota(1024)
.name(this.hiServiceServiceId)
.runningInstances(2)
.build();
Mockito.when(apps.list()).thenReturn(Flux.just(s));
Mockito.when(this.ops.applications()).thenReturn(apps);
List<String> serviceNames = this.cloudFoundryDiscoveryClient.getServices();
Assert.assertTrue("there should be one registered service.",
serviceNames.contains(this.hiServiceServiceId));
for (String serviceName : serviceNames) {
this.log.debug("\t discovered serviceName: " + serviceName);
}
Assert.assertTrue("there should be one registered service.", serviceNames.contains(this.hiServiceServiceId));
serviceNames.forEach(serviceName -> this.log.debug("\t discovered serviceName: " + serviceName));
}
@Test
public void testInstances() {
ApplicationDetail applicationDetail = ApplicationDetail
.builder()
.instances(2)
.name("my-app")
.stack("stack")
.memoryLimit(1024)
.id("id")
.requestedState("requestedState")
.runningInstances(2)
.url("http://my-app.cfapps.io")
.diskQuota(20)
.build();
InstanceDetail instanceDetail = InstanceDetail
.builder()
.index("0")
.build();
Tuple2<ApplicationDetail, InstanceDetail> tuple2 = Tuples.of(applicationDetail, instanceDetail);
Mockito.when(svc.getApplicationInstances(this.hiServiceServiceId)).thenReturn(Flux.just(tuple2));
List<ServiceInstance> instances = this.cloudFoundryDiscoveryClient
.getInstances(this.hiServiceServiceId);
assertEquals("Wrong instances: " + instances, 1, instances.size());
}
@Test
public void testInstancesNotAvailable() {
given(this.cloudFoundryClient.getApplication(this.hiServiceServiceId)).willThrow(new RuntimeException("Planned"));
List<ServiceInstance> instances = this.cloudFoundryDiscoveryClient
.getInstances(this.hiServiceServiceId);
assertEquals("Wrong instances: " + instances, 0, instances.size());
}
@Test
public void testLocalServiceInstanceRunning() {
given(this.cloudFoundryClient.getApplication("application"))
.willReturn(this.cloudApplication);
InstanceInfo instanceInfo = mock(InstanceInfo.class);
InstancesInfo instancesInfo = mock(InstancesInfo.class);
given(instancesInfo.getInstances()).willReturn(
Collections.singletonList(instanceInfo));
given(instanceInfo.getState()).willReturn(InstanceState.RUNNING);
given(this.cloudFoundryClient.getApplicationInstances(this.cloudApplication))
.willReturn(instancesInfo);
ServiceInstance localServiceInstance = this.cloudFoundryDiscoveryClient
.getLocalServiceInstance();
assertTrue(localServiceInstance.getHost().contains("hi-service.cfapps.io"));
assertTrue(localServiceInstance.getServiceId().equals(this.hiServiceServiceId));
assertEquals(localServiceInstance.getPort(), 80);
}
@Test
public void testLocalServiceInstanceNotRunning() {
InstanceInfo instanceInfo = mock(InstanceInfo.class);
InstancesInfo instancesInfo = mock(InstancesInfo.class);
given(instancesInfo.getInstances()).willReturn(
Collections.singletonList(instanceInfo));
given(instanceInfo.getState()).willReturn(InstanceState.CRASHED);
given(this.cloudFoundryClient.getApplicationInstances(this.cloudApplication))
.willReturn(instancesInfo);
ServiceInstance localServiceInstance = this.cloudFoundryDiscoveryClient
.getLocalServiceInstance();
assertNull(localServiceInstance);
}
@Test
public void testLocalServiceInstanceNotFound() {
given(this.cloudFoundryClient.getApplicationInstances(this.cloudApplication))
.willThrow(new CloudFoundryException(HttpStatus.NOT_FOUND));
ServiceInstance localServiceInstance = this.cloudFoundryDiscoveryClient
.getLocalServiceInstance();
assertNull(localServiceInstance);
}
}

View File

@@ -17,22 +17,24 @@
package org.springframework.cloud.cloudfoundry.discovery;
import com.netflix.client.config.IClientConfig;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.cloudfoundry.client.lib.CloudFoundryException;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import org.cloudfoundry.operations.applications.ApplicationDetail;
import org.cloudfoundry.operations.applications.InstanceDetail;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.http.HttpStatus;
import org.mockito.Mockito;
import org.springframework.cloud.cloudfoundry.CloudFoundryService;
import org.springframework.util.ReflectionUtils;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.util.Arrays;
import java.lang.reflect.Field;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author <A href="mailto:josh@Joshlong.com">Josh Long</A>
@@ -41,52 +43,53 @@ public class CloudFoundryServerListTest {
private CloudFoundryServerList cloudFoundryServerList;
private String serviceId = "foo-service";
private CloudFoundryClient cloudFoundryClient;
@Before
public void setUp() {
CloudApplication cloudApplication = mock(CloudApplication.class);
given(cloudApplication.getUris()).will(new Answer<List<String>>() {
@Override
public List<String> answer(InvocationOnMock invocationOnMock)
throws Throwable {
return Arrays.asList("a-url.com", "b-url.com");
}
});
cloudFoundryClient = mock(CloudFoundryClient.class);
given(cloudFoundryClient.getApplication(this.serviceId)).willReturn(
cloudApplication);
IClientConfig iClientConfig = mock(IClientConfig.class);
given(iClientConfig.getClientName()).willReturn(this.serviceId);
when(iClientConfig.getClientName()).thenReturn(this.serviceId);
this.cloudFoundryServerList = new CloudFoundryServerList(cloudFoundryClient);
CloudFoundryService cfs = mock(CloudFoundryService.class);
ApplicationDetail applicationDetail = ApplicationDetail
.builder()
.instances(2)
.name("my-app")
.stack("stack")
.memoryLimit(1024)
.id("id")
.requestedState("requestedState")
.runningInstances(2)
.url("http://my-app.cfapps.io")
.diskQuota(20)
.build();
InstanceDetail instanceDetail = InstanceDetail
.builder()
.index("0")
.build();
Tuple2<ApplicationDetail, InstanceDetail> tuple2 = Tuples.of(applicationDetail, instanceDetail);
Mockito.when(cfs.getApplicationInstances(this.serviceId)).thenReturn(Flux.just(tuple2));
this.cloudFoundryServerList = new CloudFoundryServerList(cfs);
this.cloudFoundryServerList.initWithNiwsConfig(iClientConfig);
}
@Test
public void testListOfServers() {
List<CloudFoundryServer> initialListOfServers = this.cloudFoundryServerList
.getInitialListOfServers();
List<CloudFoundryServer> updatedListOfServers = this.cloudFoundryServerList
.getUpdatedListOfServers();
List<CloudFoundryServer> initialListOfServers = this.cloudFoundryServerList.getInitialListOfServers();
List<CloudFoundryServer> updatedListOfServers = this.cloudFoundryServerList.getUpdatedListOfServers();
Assert.assertEquals(updatedListOfServers, initialListOfServers);
Assert.assertTrue(initialListOfServers.size() == 1);
}
@Test
public void testListOfServersFails() {
given(cloudFoundryClient.getApplication(this.serviceId)).willThrow(
new CloudFoundryException(HttpStatus.NOT_FOUND));
List<CloudFoundryServer> initialListOfServers = this.cloudFoundryServerList
.getInitialListOfServers();
Assert.assertTrue(initialListOfServers.size() == 0);
public void testInit() throws Exception {
Field field = ReflectionUtils.findField(this.cloudFoundryServerList.getClass(), "serviceId");
assert field != null;
ReflectionUtils.makeAccessible(field);
Assert.assertEquals(String.class.cast(field.get(this.cloudFoundryServerList)), this.serviceId);
}
@Test
public void testInit() {
Assert.assertEquals(this.cloudFoundryServerList.serviceId, this.serviceId);
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2013-2015 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
*
* http://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 com.netflix.loadbalancer.Server;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* @author <A href="mailto:josh@Joshlong.com">Josh Long</A>
*/
public class CloudFoundryServerTest {
private CloudFoundryServer cloudFoundryServer;
private List<String> urls = Arrays.asList("a-url.com", "b-url.com");
private String serverName = "server-name";
@Before
public void setUp() {
CloudApplication cloudApplication = mock(CloudApplication.class);
given(cloudApplication.getUris()).willReturn(this.urls);
given(cloudApplication.getName()).willReturn(this.serverName);
given(cloudApplication.getRunningInstances()).willReturn(1);
this.cloudFoundryServer = new CloudFoundryServer(cloudApplication);
}
@Test
public void testProperConstruction() {
Server.MetaInfo metaInfo = this.cloudFoundryServer.getMetaInfo();
Assert.assertEquals(metaInfo.getAppName(), this.serverName);
Assert.assertEquals(metaInfo.getServiceIdForDiscovery(), this.serverName);
Assert.assertEquals(metaInfo.getInstanceId(), this.serverName);
Assert.assertEquals(this.cloudFoundryServer.getHost(), this.urls.get(0));
}
}

View File

@@ -16,14 +16,11 @@
package org.springframework.cloud.cloudfoundry.sample;
import java.util.List;
import org.apache.commons.logging.Log;
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.client.ServiceInstance;
import org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryClient;
import org.springframework.cloud.cloudfoundry.discovery.EnableCloudFoundryClient;
import org.springframework.context.annotation.Bean;
@@ -47,31 +44,13 @@ public class CloudFoundryApplication {
SpringApplication.run(CloudFoundryApplication.class, args);
}
private Log log = LogFactory.getLog(getClass());
@Bean
CommandLineRunner consume(final CloudFoundryDiscoveryClient discoveryClient) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
// this demonstrates using the Spring Cloud Commons DiscoveryClient
// abstraction
log.info("=====================================");
for (String svc : discoveryClient.getServices()) {
CommandLineRunner demo(CloudFoundryDiscoveryClient discoveryClient) {
Log log = LogFactory.getLog(getClass());
return args ->
discoveryClient.getServices().forEach(svc -> {
log.info("service = " + svc);
List<ServiceInstance> instances = discoveryClient.getInstances(svc);
for (ServiceInstance si : instances) {
log.info("\t" + si);
}
}
log.info("=====================================");
log.info("local: ");
log.info("\t" + discoveryClient.getLocalServiceInstance());
}
};
discoveryClient.getInstances(svc).forEach(si -> log.info("\t" + si));
});
}
}