This commit is contained in:
Josh Long
2015-05-14 19:31:01 -05:00
parent 355d28d28a
commit e1bdaa7851
38 changed files with 1630 additions and 188 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
*~
#*
*#
.#*
.classpath
.project
@@ -11,5 +12,5 @@
*.iml
build
bin
/target/
**/target/
.factorypath

178
pom.xml
View File

@@ -1,130 +1,76 @@
<?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>
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</artifactId>
<packaging>jar</packaging>
<artifactId>spring-cloud-cloudfoundry</artifactId>
<version>1.0.2.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-cloudfoundry</name>
<description>Spring Patform Cloudfoundry integration project</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>1.0.2.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<scm>
<url>https://github.com/spring-cloud/spring-cloud-consul</url>
<connection>
scm:git:git://github.com/spring-cloud/spring-cloud-consul.git
</connection>
<developerConnection>
scm:git:ssh://git@github.com/spring-cloud/spring-cloud-consul.git
</developerConnection>
<tag>HEAD</tag>
</scm>
<scm>
<url>https://github.com/spring-cloud/spring-cloud-cloudfoundry</url>
<connection>scm:git:git://github.com/spring-cloud/spring-cloud-cloudfoundry.git</connection>
<developerConnection>scm:git:ssh://git@github.com/spring-cloud/spring-cloud-cloudfoundry.git</developerConnection>
<tag>HEAD</tag>
</scm>
<modules>
<module>spring-cloud-cloudfoundry-discovery</module>
<module>spring-cloud-cloudfoundry-web</module>
<module>spring-cloud-cloudfoundry-sample</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix</artifactId>
<version>1.0.3.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
<artifactId>spring-cloud-cloudfoundry-discovery</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-client-lib</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<cloudfoundry.version>0.0.1-SNAPSHOT</cloudfoundry.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
<optional>true</optional>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-log4j</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>spring-boot-cf-service-broker</artifactId>
<version>2.3.2</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-client-lib</artifactId>
<version>1.0.4</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>docs</id>
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<inherited>false</inherited>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<inherited>false</inherited>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<inherited>false</inherited>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,80 @@
<?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-discovery</artifactId>
<packaging>jar</packaging>
<description>Spring Cloud CloudFoundry Discovery</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry</artifactId>
<version>1.0.2.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
</dependency>
<!---->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-ribbon</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<scope>test</scope>
<artifactId>feign-slf4j</artifactId>
</dependency>
<!---->
<dependency>
<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.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,178 @@
/*
* 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.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.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.http.converter.json.Jackson2ObjectMapperBuilder;
import java.io.IOException;
import java.net.URL;
import java.util.*;
/**
* 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("${MY_CUSTOM_CF_API:https://api.run.pivotal.io}") 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/>
*
* @author <A href="mailto:josh@joshlong.com">Josh Long</A>
* @author Spencer Gibb
*/
public class CloudFoundryDiscoveryClient implements DiscoveryClient {
private static final String DESCRIPTION = "Cloud Foundry " + DiscoveryClient.class.getName() + " implementation";
private final Log log = LogFactory.getLog(getClass());
private final ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
private final CloudFoundryClient cloudFoundryClient;
private final String vcapApplicationName;
public CloudFoundryDiscoveryClient(CloudFoundryClient cloudFoundryClient, Environment environment) {
this.cloudFoundryClient = cloudFoundryClient;
String vcapApplication = environment.getProperty("VCAP_APPLICATION");
try {
JsonNode jsonNode = objectMapper.readTree(vcapApplication);
JsonNode appNameNode = jsonNode.get("application_name");
this.vcapApplicationName = appNameNode.toString().replaceAll("\"", "");
this.log.debug("Current ServiceInstance information...");
this.log.debug("\tvcapApplicationName: " + this.vcapApplicationName);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String description() {
return DESCRIPTION;
}
@Override
public ServiceInstance getLocalServiceInstance() {
CloudApplication application = this.cloudFoundryClient.getApplication(this.vcapApplicationName);
List<ServiceInstance> serviceInstances =
this.createServiceInstancesFromCloudApplications(Collections.singletonList(application));
return serviceInstances.size() > 0 ? serviceInstances.iterator().next() : null;
}
@Override
public List<ServiceInstance> getInstances(String s) {
CloudApplication applications = this.cloudFoundryClient.getApplication(s);
return this.createServiceInstancesFromCloudApplications(
Collections.singletonList(applications));
}
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;
}
@Override
public List<String> getServices() {
List<String> services = new ArrayList<>();
List<CloudApplication> applications = this.cloudFoundryClient.getApplications();
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 cloudApplication;
}
public CloudFoundryServiceInstance(CloudApplication ca) {
super(ca.getName(),
ca.getUris().iterator().next(),
80,
false);
this.cloudApplication = ca;
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.cloudfoundry.client.lib.CloudCredentials;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import java.net.MalformedURLException;
import java.net.URI;
/**
* @author <A href="mailto:josh@joshlong.com">Josh Long</A>
*/
@Configuration
@EnableConfigurationProperties
public class CloudFoundryDiscoveryClientConfiguration {
@Autowired
private CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties;
@Bean
@ConditionalOnMissingBean(CloudCredentials.class)
public CloudCredentials cloudCredentials() {
return new CloudCredentials(this.cloudFoundryDiscoveryProperties.getEmail(),
this.cloudFoundryDiscoveryProperties.getPassword());
}
@Bean
@ConditionalOnMissingBean(CloudFoundryClient.class)
public CloudFoundryClient cloudFoundryClient(CloudCredentials cc) throws MalformedURLException {
CloudFoundryClient cloudFoundryClient = new CloudFoundryClient(cc,
URI.create(this.cloudFoundryDiscoveryProperties.getCloudControllerUrl()).toURL());
cloudFoundryClient.login();
return cloudFoundryClient;
}
@Bean
@ConditionalOnMissingBean(CloudFoundryDiscoveryClient.class)
public CloudFoundryDiscoveryClient cloudFoundryDiscoveryClient(
CloudFoundryClient cloudFoundryClient, Environment environment) {
return new CloudFoundryDiscoveryClient(cloudFoundryClient, environment);
}
@Bean
public CloudFoundryDiscoveryProperties cloudFoundryDiscoveryProperties() {
return new CloudFoundryDiscoveryProperties();
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author <A href="mailto:josh@joshlong.com">Josh Long</A>
*/
@ConfigurationProperties(prefix = "spring.cloud.cloudfoundry.discovery")
public class CloudFoundryDiscoveryProperties {
private String cloudControllerUrl = "https://api.run.pivotal.io";
private String email;
private String password;
public String getCloudControllerUrl() {
return cloudControllerUrl;
}
public void setCloudControllerUrl(String cloudControllerUrl) {
this.cloudControllerUrl = cloudControllerUrl;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.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.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 javax.annotation.PostConstruct;
/**
* @author <A href="mailto:josh@joshlong.com">Josh Long</A>
*/
@Configuration
public class CloudFoundryRibbonClientConfiguration {
protected static final String DEFAULT_NAMESPACE = "ribbon";
protected static final String VALUE_NOT_SET = "__not__set__";
@Value("${ribbon.client.name}")
private String serviceId;
public CloudFoundryRibbonClientConfiguration (){ }
public CloudFoundryRibbonClientConfiguration (String svcId) {
this.serviceId = svcId;
}
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(CloudFoundryClient cloudFoundryClient, IClientConfig config) {
CloudFoundryServerList cloudFoundryServerList = new CloudFoundryServerList(cloudFoundryClient);
cloudFoundryServerList.initWithNiwsConfig(config);
return cloudFoundryServerList;
}
@PostConstruct
public void postConstruct() {
// FIXME: what should this be?
setProp(this.serviceId, CommonClientConfigKey.DeploymentContextBasedVipAddresses.key(), this.serviceId);
setProp(this.serviceId, CommonClientConfigKey.EnableZoneAffinity.key(), "true");
}
protected void setProp(String serviceId, String suffix, String value) {
// how to set the namespace properly?
String key = getKey(serviceId, suffix);
DynamicStringProperty property = getProperty(key);
if (property.get().equals(VALUE_NOT_SET)) {
ConfigurationManager.getConfigInstance().setProperty(key, value);
}
}
protected DynamicStringProperty getProperty(String key) {
return DynamicPropertyFactory.getInstance().getStringProperty(key, VALUE_NOT_SET);
}
protected String getKey(String serviceId, String suffix) {
return serviceId + "." + DEFAULT_NAMESPACE + "." + suffix;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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;
/**
* @author <a href="mailto:josh@joshlong.com">Josh Long</A>
*/
public class CloudFoundryServer extends Server {
private final MetaInfo metaInfo;
public CloudFoundryServer(final CloudApplication cloudApplication) {
super(cloudApplication.getUris().iterator().next(), 80);
this.metaInfo = new MetaInfo() {
@Override
public String getAppName() {
return cloudApplication.getName();
}
@Override
public String getServerGroup() {
return null;
}
@Override
public String getServiceIdForDiscovery() {
return cloudApplication.getName();
}
@Override
public String getInstanceId() {
return cloudApplication.getName();
}
};
}
@Override
public MetaInfo getMetaInfo() {
return metaInfo;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractServerList;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import java.util.Collections;
import java.util.List;
/**
* @author <A href="mailto:josh@joshlong.com">Josh Long</A>
*/
public class CloudFoundryServerList extends AbstractServerList<CloudFoundryServer> {
protected String serviceId;
private final CloudFoundryClient cloudFoundryClient;
public CloudFoundryServerList(CloudFoundryClient cloudFoundryClient) {
this.cloudFoundryClient = cloudFoundryClient;
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
this.serviceId = iClientConfig.getClientName();
}
@Override
public List<CloudFoundryServer> getInitialListOfServers() {
return this.cloudFoundryServers();
}
@Override
public List<CloudFoundryServer> getUpdatedListOfServers() {
return this.cloudFoundryServers();
}
protected List<CloudFoundryServer> cloudFoundryServers() {
CloudApplication cloudApplications = this.cloudFoundryClient.getApplication(this.serviceId);
return Collections.singletonList(new CloudFoundryServer(cloudApplications));
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.springframework.cloud.client.discovery.EnableDiscoveryClient;
import java.lang.annotation.*;
/**
* Convenience annotation for clients to enable Cloud Foundry discovery configuration (specifically).
* Use this (optionally) in case you want discovery and know for sure that it is Cloud Foundry you want.
* All it does is turn on discovery and let the auto-configuration find the Cloud Foundry classes.
*
* @author <a href="mailto:josh@joshlong.com">Josh Long</A>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableDiscoveryClient
public @interface EnableCloudFoundryClient {
}

View File

@@ -0,0 +1,35 @@
/*
* 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.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties
@ConditionalOnBean(SpringClientFactory.class)
@ConditionalOnProperty(value = "ribbon.cloudfoundry.enabled", matchIfMissing = true)
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = CloudFoundryRibbonClientConfiguration.class)
public class RibbonCloudFoundryAutoConfiguration {
}

View File

@@ -0,0 +1,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.cloudfoundry.discovery.RibbonCloudFoundryAutoConfiguration
# Discovery Client Configuration
org.springframework.cloud.client.discovery.EnableDiscoveryClient=\
org.springframework.cloud.cloudfoundry.discovery.CloudFoundryDiscoveryClientConfiguration

View File

@@ -0,0 +1,102 @@
/*
* 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;
import java.util.Collections;
/**
* @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(Collections.singletonMap("VCAP_APPLICATION", vcapAppl))
.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

@@ -0,0 +1,156 @@
/*
* 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.Log;
import org.apache.commons.logging.LogFactory;
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.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.env.Environment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.mock;
/**
* @author <A href="mailto:josh@Joshlong.com">Josh Long</A>
*/
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;
}
@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\":\"" + 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}");
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);
cloudApplication = cloudApplications.get(0);
given(this.cloudFoundryClient.getApplication(this.hiServiceServiceId))
.willReturn(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(cloudFoundryClient, environment);
}
@Test
public void testServiceResolution() {
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);
}
}
@Test
public void testInstances() {
List<ServiceInstance> instances = this.cloudFoundryDiscoveryClient.getInstances(
this.hiServiceServiceId);
assertEquals(instances.size(), 1);
}
@Test
public void testLocalServiceInstanceRunning() {
InstanceInfo instanceInfo = mock(InstanceInfo.class);
InstancesInfo instancesInfo = mock(InstancesInfo.class);
given(instancesInfo.getInstances()).willReturn(Collections.singletonList(instanceInfo));
given(instanceInfo.getState()).willReturn(InstanceState.RUNNING);
given(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(cloudFoundryClient.getApplicationInstances(this.cloudApplication)).willReturn(instancesInfo);
ServiceInstance localServiceInstance = this.cloudFoundryDiscoveryClient.getLocalServiceInstance();
assertNull(localServiceInstance);
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.client.config.IClientConfig;
import org.cloudfoundry.client.lib.CloudFoundryClient;
import org.cloudfoundry.client.lib.domain.CloudApplication;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
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 CloudFoundryServerListTest {
private CloudFoundryServerList cloudFoundryServerList;
private String serviceId = "foo-service";
@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 cloudFoundryClient = mock(CloudFoundryClient.class);
given(cloudFoundryClient.getApplication(this.serviceId)).willReturn(cloudApplication);
IClientConfig iClientConfig = mock(IClientConfig.class);
given(iClientConfig.getClientName()).willReturn(this.serviceId);
this.cloudFoundryServerList = new CloudFoundryServerList(cloudFoundryClient);
this.cloudFoundryServerList.initWithNiwsConfig(iClientConfig);
}
@Test
public void testListOfServers() {
List<CloudFoundryServer> initialListOfServers = this.cloudFoundryServerList.getInitialListOfServers();
List<CloudFoundryServer> updatedListOfServers = this.cloudFoundryServerList.getUpdatedListOfServers();
Assert.assertEquals(updatedListOfServers, initialListOfServers);
Assert.assertTrue(initialListOfServers.size() == 1);
}
@Test
public void testInit() {
Assert.assertEquals(this.cloudFoundryServerList.serviceId, this.serviceId);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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

@@ -0,0 +1,20 @@
// to run on ur local machine this use:
// spring run hi-service.groovy
//
// to deploy this to Cloud Foundry, use
// spring jar hi.jar hi-service.groovy
// cf push hi-service -p hi.jar
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class GreetingRestController {
@RequestMapping("/hi/{name}")
def hi(@PathVariable String name) {
[greeting: "Hello, " + name + "!"]
}
}

View File

@@ -0,0 +1,61 @@
<?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-sample</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry</artifactId>
<version>1.0.2.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<!--skip deploy -->
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-ribbon</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,112 @@
/*
* 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.sample;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
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.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
/**
* This example assumes you've registered an application on <A href="http://cloudfoundry.org">Cloud Foundry</A>
* named {@code hi-service} that responds with a String at {@code /hi/{name}}. There is a sample file in the project
* root called {@code hi-service.groovy} which you can deploy using the {@code spring} CLI and the {@code cf} CLI that
* works appropriately for this demonstration.
*
* @author <a href="mailto:josh@joshlong.com">Josh Long</a>
* @author Spencer Gibb
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
private Log log = LogFactory.getLog(getClass());
@Bean
CommandLineRunner consume(final LoadBalancerClient loadBalancerClient,
final DiscoveryClient discoveryClient,
final HiServiceClient hiServiceClient,
final RestTemplate restTemplate) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
// this demonstrates using the CF/Ribbon-aware RestTemplate interceptor
log.info("=====================================");
log.info("Hi: " + restTemplate.getForEntity("http://hi-service/hi/{name}", String.class, "Josh"));
// this demonstrates using the Spring Cloud Commons DiscoveryClient abstraction
log.info("=====================================");
for (String svc : discoveryClient.getServices()) {
log.info("service = " + svc);
List<ServiceInstance> instances = discoveryClient.getInstances(svc);
for (ServiceInstance si : instances) {
log.info("\t" + ReflectionToStringBuilder.reflectionToString(si, ToStringStyle.MULTI_LINE_STYLE));
}
}
log.info("=====================================");
log.info("local: ");
log.info("\t" + ReflectionToStringBuilder.reflectionToString(
discoveryClient.getLocalServiceInstance(), ToStringStyle.MULTI_LINE_STYLE));
// this demonstrates using a CF/Ribbon-aware Feign client
log.info("=====================================");
log.info("Hi:" + hiServiceClient.hi("Josh"));
// this demonstrates using the Spring Cloud Commons LoadBalancerClient
log.info("=====================================");
ServiceInstance choose = loadBalancerClient.choose("hi-service");
log.info("chose: " + '(' + choose.getServiceId() + ") " + choose.getHost() + ':' + choose.getPort());
}
};
}
}
@FeignClient("hi-service")
interface HiServiceClient {
@RequestMapping(value = "/hi/{name}", method = RequestMethod.GET)
Map<String, Object> hi(@PathVariable("name") String name);
}

View File

@@ -0,0 +1,4 @@
spring.application.name=test-app
spring.cloud.cloudfoundry.discovery.email=starbuxman@gmail.com
spring.cloud.cloudfoundry.discovery.password=xxxx
VCAP_APPLICATION={"limits":{"mem":1024,"disk":1024,"fds":16384},"application_version":"36eff082-96d6-498f-8214-508fda72ba65","application_name":"hi-service","application_uris":["hi-service.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}

View File

@@ -0,0 +1 @@
spring.application.name=test-app

View File

@@ -0,0 +1,71 @@
<?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-web</artifactId>
<packaging>jar</packaging>
<description>Spring Cloud CloudFoundry Web</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-cloudfoundry</artifactId>
<version>1.0.2.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
<optional>true</optional>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-log4j</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-core</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,2 @@
include::intro.adoc[]

View File

@@ -0,0 +1,46 @@
#!/bin/bash -x
git remote set-url --push origin `git config remote.origin.url | sed -e 's/^git:/https:/'`
if ! (git remote set-branches --add origin gh-pages && git fetch -q); then
echo "No gh-pages, so not syncing"
exit 0
fi
if ! [ -d target/generated-docs ]; then
echo "No gh-pages sources in target/generated-docs, so not syncing"
exit 0
fi
# Stash any outstanding changes
###################################################################
git diff-index --quiet HEAD
dirty=$?
if [ "$dirty" != "0" ]; then git stash; fi
# Switch to gh-pages branch to sync it with master
###################################################################
git checkout gh-pages
for f in target/generated-docs/*; do
file=${f#target/generated-docs/*}
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then
# Not ignored...
cp -rf $f .
git add -A $file
fi
done
git commit -a -m "Sync docs from master to gh-pages"
# Uncomment the following push if you want to auto push to
# the gh-pages branch whenever you commit to master locally.
# This is a little extreme. Use with care!
###################################################################
git push origin gh-pages
# Finally, switch back to the master branch and exit block
git checkout master
if [ "$dirty" != "0" ]; then git stash pop; fi
exit 0

View File

@@ -0,0 +1,15 @@
Spring Cloud for Cloudfoundry makes it easy to run
https://github.com/spring-cloud[Spring Cloud] apps in
https://github.com/cloudfoundry[Cloud Foundry] (the Platform as a
Service). Cloud Foundry has the notion of a "service", which is
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).
Add this project as a dependency to any Spring Cloud UI app or REST
service and deploy to Cloudfoundry. If you use Spring Cloud Security
OAuth2 features this will make them bindable to Cloud Foundry services
instead of enironment properties in `spring.oauth2.*`. For a UI app you can
declare `@EnableOAuth2Sso` and bind to a service called "sso", and for
a service you can add `@EnableOAuth2Resource` and bind to a service
called "resource" (see below for how to change the names).

View File

@@ -0,0 +1,50 @@
Here's a Spring Cloud app with OAuth2 SSO:
.app.groovy
[source,java]
----
@Controller
@EnableOAuth2Sso
class Application {
@RequestMapping('/')
String home() {
'Hello World'
}
}
----
If you run it without any service bindings:
----
$ spring jar app.jar app.groovy
$ cf push -p app.jar
----
it will be secure with (Spring Boot default) Basic authentication,
i.e. the password will be in the logs (or set it with
`security.user.password` as normal). To turn on OAuth2 SSO all you
need to do is bind the app to a service with the right
credentials. For example, a
http://docs.pivotal.io/pivotalcf/devguide/services/user-provided.html[user-provided
service] can be created like this on PWS:
----
$ cf create-user-provided-service sso -p '{clientId:"<my-client>",clientSecret:"<my-secret>",userInfoUri:"https://uaa.run.pivotal.io/userinfo",tokenUri: "https://login.run.pivotal.io/oauth/token",authorizationUri:"https://login.run.pivotal.io/oauth/authorize"}
----
Then bind and restart the app:
----
$ cf bind app sso
$ cf restart app
----
and visit it in a browser. It will redirect to the Cloud Foundry (PWS)
login server instead of challenging for Basic authentication. The
`clientId` and `clientSecret` are credentials of a registered client
in Cloud Foundry. To get a Cloud Foundry client registration for
testing please ask your local platform administrator if it's a private
instance).

View File

@@ -0,0 +1,75 @@
= Spring Cloud for Cloud Foundry
include::intro.adoc[]
== Quickstart
include::quickstart.adoc[]
== How Does it Work?
=== OAuth2 Single Sign On
Spring Cloud Security provides the `@EnableOAuth2Sso` annotation and
binds the app to environment properties in `spring.oauth2.\*`. Spring Cloud
for Cloud Foundry just sets up default environment properties so that
it all just works if you bind to a Cloud Foundry service instance
called "sso". The service credentials are mapped to the SSO
properties, i.e. (from `spring.oauth2.client.*`) `clientId`, `clientSecret`,
`tokenUri`, `authorizationUri`, (and from `spring.oauth2.resource.*`)
`userInfoUri`, `tokenInfoUri`, `keyValue`, `keyUri`. Refer to the
Spring Cloud Security documentation for details of which combinations
will work together. The main thing is that in Cloud Foundry you only
need one service to cover all the necessary credentials.
To use a different service instance name (i.e. not "sso") just set
`spring.oauth2.sso.serviceId` to your custom name.
=== JWT Tokens
Spring Cloud Security already has support for decoding JWT tokens if
you just provide the verification key (as an environment property). In
Cloud Foundry you can pick that property up from a service binding
(`keyValue` or `keyUri`).
For example the `keyUri` in PWS is
"https://uaa.run.pivotal.io/token_key":
----
$ curl https://uaa.run.pivotal.io/token_key
{"alg":"SHA256withRSA","value":"-----BEGIN PUBLIC KEY-----\nMIIBI...\n-----END PUBLIC KEY-----\n"}d
----
=== OAuth2 Resource Server
Similarly, the `@EnableOAuth2Resource` annotation will protect your
API endpoints if you bind to a service instance called "resource".
The "sso" service above will work for a resource server as well (so
just bind to that if it's there). If the OAuth2 tokens are JWTs (as in
Cloud Foundry), it is common to use a separate service for resources
to avoid a network round trip decoding the token on every access. A
user-provided-service for an OAuth2 resource can be created like this
on PWS:
----
$ cf create-user-provided-service resource -p '{keyUri:"https://uaa.run.pivotal.io/token_key"}
----
To use JWT you need to add the verification key as either
`keyValue` or `keyUri` (these could be added to the "sso"
service or the "resource" service if you have one).
To use a different sercice instance name (i.e. not "resource" or
"sso") just set `spring.oauth2.resource.serviceId` to your custom name.
=== Default Environment Keys
The precise mapppings are as follows:
* `spring.oauth2.sso.\*` to `vcap.services.${spring.oauth2.sso.serviceId:sso}.credentials.*`
* `spring.oauth2.client.\*` to `vcap.services.${spring.oauth2.sso.serviceId:sso}.credentials.tokenUri:${vcap.services.${spring.oauth2.resource.serviceId:resource}.credentials.*`
* `spring.oauth2.resource.(jwt).\*` to `vcap.services.${spring.oauth2.resource.serviceId:resource}.credentials.tokenUri:${vcap.services.${spring.oauth2.sso.serviceId:sso}.credentials.*`

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.cloudfoundry.web;
package org.springframework.cloud.cloudfoundry.discovery;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -33,4 +33,4 @@ import org.springframework.context.annotation.Import;
@Import(StickyFilterConfiguration.class)
public @interface EnableStickyFilter {
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.cloudfoundry.web;
package org.springframework.cloud.cloudfoundry.discovery;
import java.io.IOException;
import java.util.UUID;
@@ -34,34 +34,33 @@ import org.springframework.web.filter.OncePerRequestFilter;
/**
* @author Dave Syer
*
*/
@Configuration
public class StickyFilterConfiguration {
private String cookie = UUID.randomUUID().toString();
@Autowired
public void init(EurekaInstanceConfigBean eurekaInstance) {
eurekaInstance.getMetadataMap().put("cookie", cookie);
}
@Bean
public FilterRegistrationBean stickyCloudFoundryFilter() {
FilterRegistrationBean filter = new FilterRegistrationBean();
filter.setOrder(Ordered.LOWEST_PRECEDENCE);
filter.setFilter(new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!response.containsHeader("Set-Cookie")) {
response.addCookie(new Cookie("JSESSIONID", cookie));
}
filterChain.doFilter(request, response);
}
});
return filter;
}
private String cookie = UUID.randomUUID().toString();
}
@Autowired
public void init(EurekaInstanceConfigBean eurekaInstance) {
eurekaInstance.getMetadataMap().put("cookie", cookie);
}
@Bean
public FilterRegistrationBean stickyCloudFoundryFilter() {
FilterRegistrationBean filter = new FilterRegistrationBean();
filter.setOrder(Ordered.LOWEST_PRECEDENCE);
filter.setFilter(new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!response.containsHeader("Set-Cookie")) {
response.addCookie(new Cookie("JSESSIONID", cookie));
}
filterChain.doFilter(request, response);
}
});
return filter;
}
}

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env ruby
base_dir = File.join(File.dirname(__FILE__),'../../..')
src_dir = File.join(base_dir, "/src/main/asciidoc")
require 'asciidoctor'
require 'optparse'
options = {}
file = "#{src_dir}/README.adoc"
OptionParser.new do |o|
o.on('-o OUTPUT_FILE', 'Output file (default is stdout)') { |file| options[:to_file] = file unless file=='-' }
o.on('-h', '--help') { puts o; exit }
o.parse!
end
file = ARGV[0] if ARGV.length>0
srcDir = File.dirname(file)
out = "// Do not edit this file (e.g. go instead to src/main/asciidoc)\n\n"
doc = Asciidoctor.load_file file, safe: :safe, parse: false
out << doc.reader.read
unless options[:to_file]
puts out
else
File.open(options[:to_file],'w+') do |file|
file.write(out)
end
end

View File

@@ -1,42 +0,0 @@
debug: true
server:
port: 8080
spring:
application:
name: eureka
security:
basic:
enabled: false
management:
context-path: /admin
eureka:
server:
enabled: false
spring:
oauth2:
sso:
tokenUri: http://localhost:8080/uaa/oauth/token
authorizationUri: http://localhost:8080/uaa/oauth/authorize
clientId: app
clientSecret: appclientsecret
logging:
level:
com.netflix.discovery: 'OFF'
org.springframework.security: DEBUG
---
spring:
profiles: github
spring:
oauth2:
sso:
tokenUri: https://github.com/login/oauth/access_token
authorizationUri: https://github.com/login/oauth/authorize
clientId: bd1c0a783ccdd1c9b9e4
clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
authenticationScheme: form
resource:
clientId: ${spring.oauth2.sso.clientId}
userInfoUri: https://api.github.com/user
preferTokenInfo: false