Improve Cloud Foundry Java Client auto-configuration.

An organization and space are not required by all CF Java Client operations. Those properties should be optional to the client-autoconfiguration.

The CF target URL should default to the CF an application is running on when it is deployed to CF.
This commit is contained in:
Scott Frederick
2018-04-09 17:11:33 -05:00
parent fa3922b641
commit 510b6ebcfc
7 changed files with 128 additions and 157 deletions

View File

@@ -23,12 +23,12 @@
</properties>
<scm>
<url>https://github.com/spring-cloud/spring-cloud-consul</url>
<url>https://github.com/spring-cloud/spring-cloud-cloudfoundry</url>
<connection>
scm:git:git://github.com/spring-cloud/spring-cloud-consul.git
scm:git:git://github.com/spring-cloud/spring-cloud-cloudfoundry.git
</connection>
<developerConnection>
scm:git:ssh://git@github.com/spring-cloud/spring-cloud-consul.git
scm:git:ssh://git@github.com/spring-cloud/spring-cloud-cloudfoundry.git
</developerConnection>
<tag>HEAD</tag>
</scm>

View File

@@ -43,9 +43,10 @@ import org.springframework.context.annotation.Lazy;
*
* @author Josh Long
* @author Ben Hale
* @author Scott Frederick
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.cloud.cloudfoundry", name = {"username", "password", "org", "space"})
@ConditionalOnProperty(prefix = "spring.cloud.cloudfoundry", name = {"username", "password"})
@ConditionalOnClass(name = {"reactor.core.publisher.Flux", "org.cloudfoundry.operations.DefaultCloudFoundryOperations",
"org.cloudfoundry.reactor.client.ReactorCloudFoundryClient", "org.reactivestreams.Publisher"})
@EnableConfigurationProperties(CloudFoundryProperties.class)
@@ -64,16 +65,6 @@ public class CloudFoundryClientAutoConfiguration {
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
@@ -93,18 +84,13 @@ public class CloudFoundryClientAutoConfiguration {
.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)
public ReactorCloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorCloudFoundryClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
@@ -128,6 +114,29 @@ public class CloudFoundryClientAutoConfiguration {
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public ReactorUaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorUaaClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.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
@@ -139,16 +148,6 @@ public class CloudFoundryClientAutoConfiguration {
.username(username)
.build();
}
@Bean
@Lazy
@ConditionalOnMissingBean
public ReactorUaaClient uaaClient(ConnectionContext connectionContext, TokenProvider tokenProvider) {
return ReactorUaaClient.builder()
.connectionContext(connectionContext)
.tokenProvider(tokenProvider)
.build();
}
}

View File

@@ -28,7 +28,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Configuration properties for a connection to a Cloud Foundry platform.
*
* @author Josh Long
* @author Scott Frederick
*/
@ConfigurationProperties(prefix = "spring.cloud.cloudfoundry")
public class CloudFoundryProperties implements InitializingBean {
@@ -36,7 +39,8 @@ public class CloudFoundryProperties implements InitializingBean {
/**
* URL of Cloud Foundry API (Cloud Controller).
*/
private String url = "api.run.pivotal.io";
@Value("${vcap.application.cf_api:api.run.pivotal.io}")
private String url;
/**
* Username to authenticate (usually an email address).
@@ -49,12 +53,12 @@ public class CloudFoundryProperties implements InitializingBean {
private String password;
/**
* Organization name to authenticate with (default to user's default).
* Organization name to initially target.
*/
private String org;
/**
* Space name to authenticate with (default to user's default).
* Space name to initially target.
*/
@Value("${vcap.application.space_name:}")
private String space;
@@ -127,19 +131,21 @@ public class CloudFoundryProperties implements InitializingBean {
}
@Override
public void afterPropertiesSet() throws Exception {
public void afterPropertiesSet() {
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();
if (this.org != null) {
this.org = this.org.trim();
}
if (this.space != null) {
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

@@ -16,39 +16,40 @@
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 org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class CloudFoundryClientAutoConfigurationTest {
@SpringBootApplication
public static class MyConfig {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CloudFoundryClientAutoConfiguration.class));
private final static String SPRING_CLOUD_PROPERTIES[] = {
"spring.cloud.cloudfoundry.username",
"spring.cloud.cloudfoundry.password",
};
private static boolean requiredPropertiesSet() {
for (String k : SPRING_CLOUD_PROPERTIES) {
if (System.getenv(envVarFromProperty(k)) == null) {
return false;
}
}
return true;
}
private static String envVarFromProperty(String propertyName) {
@@ -57,89 +58,65 @@ public class CloudFoundryClientAutoConfigurationTest {
.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() {
public void autoConfiguresBeansWithAllProperties() {
this.contextRunner
.withPropertyValues(
"spring.cloud.cloudfoundry.username=user",
"spring.cloud.cloudfoundry.password=secret",
"spring.cloud.cloudfoundry.org=myorg",
"spring.cloud.cloudfoundry.space=myspace")
.run((context) -> {
assertCloudFoundryClientBeansPresent(context);
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);
DefaultCloudFoundryOperations operations = context.getBean(DefaultCloudFoundryOperations.class);
assertThat(operations.getOrganization()).isEqualTo("myorg");
assertThat(operations.getSpace()).isEqualTo("myspace");
});
mapMany
.subscribe(p ->
log.info(p.getT1().getName() + ':' + p.getT1().getId() + ':' + p.getT2().getIndex() + ':' +
p.getT2().getState()));
Thread.sleep(5 * 1000);
}
@Test
public void autoConfiguresBeansWithMinimalProperties() {
this.contextRunner
.withPropertyValues(
"spring.cloud.cloudfoundry.username=user",
"spring.cloud.cloudfoundry.password=secret")
.run((context) -> {
assertCloudFoundryClientBeansPresent(context);
DefaultCloudFoundryOperations operations = context.getBean(DefaultCloudFoundryOperations.class);
assertThat(operations.getOrganization()).isNullOrEmpty();
assertThat(operations.getSpace()).isNullOrEmpty();
});
}
@Test
public void organizationsRetrievedWithUserProvidedProperties() {
Assume.assumeTrue(requiredPropertiesSet());
this.contextRunner
.run((context) -> {
assertCloudFoundryClientBeansPresent(context);
CloudFoundryOperations operations = context.getBean(CloudFoundryOperations.class);
OrganizationSummary summary = operations
.organizations()
.list()
.blockFirst();
assertThat(summary).isNotNull();
assertThat(summary.getId()).isNotEmpty();
assertThat(summary.getName()).isNotEmpty();
});
}
private void assertCloudFoundryClientBeansPresent(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(ReactorCloudFoundryClient.class);
assertThat(context).hasSingleBean(DefaultCloudFoundryOperations.class);
assertThat(context).hasSingleBean(DefaultConnectionContext.class);
assertThat(context).hasSingleBean(DopplerClient.class);
assertThat(context).hasSingleBean(RoutingClient.class);
assertThat(context).hasSingleBean(PasswordGrantTokenProvider.class);
assertThat(context).hasSingleBean(ReactorUaaClient.class);
}
}

View File

@@ -20,7 +20,6 @@ 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;
@@ -44,10 +43,6 @@ public class CloudFoundryDiscoveryClient implements DiscoveryClient {
private final String description = "Cloud Foundry " + DiscoveryClient.class.getName() + " implementation";
@Value("${vcap.application.name:${spring.application.name:application}}")
private String vcapApplicationName = "application";
CloudFoundryDiscoveryClient(CloudFoundryOperations cloudFoundryOperations,
CloudFoundryService svc) {
this.cloudFoundryService = svc;
@@ -86,8 +81,7 @@ public class CloudFoundryDiscoveryClient implements DiscoveryClient {
@Override
public List<String> getServices() {
return this
.cloudFoundryOperations
return this.cloudFoundryOperations
.applications()
.list()
.map(ApplicationSummary::getName)

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.cloudfoundry.discovery;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -35,9 +34,6 @@ import org.springframework.context.annotation.Configuration;
@EnableConfigurationProperties(CloudFoundryDiscoveryProperties.class)
public class CloudFoundryDiscoveryClientConfiguration {
@Autowired
private CloudFoundryDiscoveryProperties discovery;
@Bean
@ConditionalOnMissingBean(CloudFoundryDiscoveryClient.class)
public CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient(

View File

@@ -30,12 +30,11 @@ import com.netflix.loadbalancer.AbstractServerList;
* @author Josh Long
*/
public class CloudFoundryServerList extends AbstractServerList<CloudFoundryServer> {
private String serviceId;
private final CloudFoundryService cloudFoundryService;
private final CloudFoundryDiscoveryProperties properties;
private IClientConfig clientConfig;
private String serviceId;
CloudFoundryServerList(CloudFoundryService svc, CloudFoundryDiscoveryProperties properties) {
this.cloudFoundryService = svc;
@@ -45,17 +44,17 @@ public class CloudFoundryServerList extends AbstractServerList<CloudFoundryServe
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
this.clientConfig = clientConfig;
this.serviceId = this.clientConfig.getClientName();
this.serviceId = clientConfig.getClientName();
}
@Override
public List<CloudFoundryServer> getInitialListOfServers() {
return this.cloudFoundryServers();
return cloudFoundryServers();
}
@Override
public List<CloudFoundryServer> getUpdatedListOfServers() {
return this.cloudFoundryServers();
return cloudFoundryServers();
}
private List<CloudFoundryServer> cloudFoundryServers() {
@@ -69,7 +68,7 @@ public class CloudFoundryServerList extends AbstractServerList<CloudFoundryServe
final int port;
if (secure != null && secure && securePort != null) {
port = securePort;
port = securePort;
} else if (nonSecurePort != null) {
port = nonSecurePort;
} else {