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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.cloudfoundry.CloudFoundryClientAutoConfiguration
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user