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

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