diff --git a/.gitignore b/.gitignore index 05ce307..0d6179a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *~ #* *# + .#* .classpath .project @@ -11,5 +12,5 @@ *.iml build bin -/target/ +**/target/ .factorypath diff --git a/pom.xml b/pom.xml index 60ca453..2865a01 100644 --- a/pom.xml +++ b/pom.xml @@ -1,130 +1,76 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - spring-cloud-cloudfoundry - jar + spring-cloud-cloudfoundry + 1.0.2.BUILD-SNAPSHOT + pom - spring-cloud-cloudfoundry - Spring Patform Cloudfoundry integration project + + org.springframework.cloud + spring-cloud-build + 1.0.2.BUILD-SNAPSHOT + + - - org.springframework.cloud - spring-cloud-build - 1.0.0.BUILD-SNAPSHOT - - + + https://github.com/spring-cloud/spring-cloud-consul + + scm:git:git://github.com/spring-cloud/spring-cloud-consul.git + + + scm:git:ssh://git@github.com/spring-cloud/spring-cloud-consul.git + + HEAD + - - https://github.com/spring-cloud/spring-cloud-cloudfoundry - scm:git:git://github.com/spring-cloud/spring-cloud-cloudfoundry.git - scm:git:ssh://git@github.com/spring-cloud/spring-cloud-cloudfoundry.git - HEAD - + + spring-cloud-cloudfoundry-discovery + spring-cloud-cloudfoundry-web + spring-cloud-cloudfoundry-sample + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + - - + + + + org.springframework.cloud + spring-cloud-netflix + 1.0.3.BUILD-SNAPSHOT + pom + import + org.springframework.cloud - spring-cloud-netflix - 1.0.0.BUILD-SNAPSHOT - pom - import + spring-cloud-cloudfoundry-discovery + ${project.version} - - + + org.cloudfoundry + cloudfoundry-client-lib + 1.1.1 + + + + + + + 0.0.1-SNAPSHOT + - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.cloud - spring-cloud-netflix-core - true - - - org.springframework.cloud - spring-cloud-netflix-eureka-server - true - - - spring-boot-starter-log4j - org.springframework.boot - - - - - org.springframework.cloud - spring-cloud-security - 1.0.0.BUILD-SNAPSHOT - true - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.cloudfoundry - spring-boot-cf-service-broker - 2.3.2 - true - - - org.cloudfoundry - cloudfoundry-client-lib - 1.0.4 - true - - - org.projectlombok - lombok - compile - true - - - com.netflix.eureka - eureka-core - true - - - org.springframework.boot - spring-boot-starter-test - test - - - - - docs - - - - org.asciidoctor - asciidoctor-maven-plugin - false - - - org.apache.maven.plugins - maven-antrun-plugin - false - - - org.codehaus.mojo - build-helper-maven-plugin - false - - - - - diff --git a/spring-cloud-cloudfoundry-discovery/pom.xml b/spring-cloud-cloudfoundry-discovery/pom.xml new file mode 100644 index 0000000..9fb5379 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + spring-cloud-cloudfoundry-discovery + jar + Spring Cloud CloudFoundry Discovery + + + org.springframework.cloud + spring-cloud-cloudfoundry + 1.0.2.BUILD-SNAPSHOT + .. + + + + + org.springframework.cloud + spring-cloud-netflix-core + + + org.springframework.boot + spring-boot-starter-logging + + + com.netflix.archaius + archaius-core + + + com.netflix.ribbon + ribbon + + + com.netflix.ribbon + ribbon-core + + + com.netflix.ribbon + ribbon-httpclient + + + com.netflix.ribbon + ribbon-loadbalancer + + + + + + com.netflix.feign + feign-core + test + + + com.netflix.feign + feign-ribbon + test + + + com.netflix.feign + test + feign-slf4j + + + + org.springframework.cloud + spring-cloud-commons + + + org.cloudfoundry + cloudfoundry-client-lib + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/src/main/asciidoc/README.adoc b/spring-cloud-cloudfoundry-discovery/src/main/asciidoc/README.adoc similarity index 100% rename from src/main/asciidoc/README.adoc rename to spring-cloud-cloudfoundry-discovery/src/main/asciidoc/README.adoc diff --git a/src/main/asciidoc/ghpages.sh b/spring-cloud-cloudfoundry-discovery/src/main/asciidoc/ghpages.sh similarity index 100% rename from src/main/asciidoc/ghpages.sh rename to spring-cloud-cloudfoundry-discovery/src/main/asciidoc/ghpages.sh diff --git a/src/main/asciidoc/intro.adoc b/spring-cloud-cloudfoundry-discovery/src/main/asciidoc/intro.adoc similarity index 100% rename from src/main/asciidoc/intro.adoc rename to spring-cloud-cloudfoundry-discovery/src/main/asciidoc/intro.adoc diff --git a/src/main/asciidoc/quickstart.adoc b/spring-cloud-cloudfoundry-discovery/src/main/asciidoc/quickstart.adoc similarity index 100% rename from src/main/asciidoc/quickstart.adoc rename to spring-cloud-cloudfoundry-discovery/src/main/asciidoc/quickstart.adoc diff --git a/src/main/asciidoc/spring-cloud-cloudfoundry.adoc b/spring-cloud-cloudfoundry-discovery/src/main/asciidoc/spring-cloud-cloudfoundry.adoc similarity index 100% rename from src/main/asciidoc/spring-cloud-cloudfoundry.adoc rename to spring-cloud-cloudfoundry-discovery/src/main/asciidoc/spring-cloud-cloudfoundry.adoc diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClient.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClient.java new file mode 100644 index 0000000..05a14c0 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClient.java @@ -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}. + *

+ * You need to provide an instance of the {@link CloudFoundryClient}. A workable configuration looks like this: + *

+ *

+ * @Bean
+ * CloudFoundryClient cloudFoundryClient(
+ * @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;
+ * }
+ * 
+ *

+ * 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 all spaces and organizations. Use one of the + * {@link CloudFoundryClient#CloudFoundryClient(CloudCredentials, URL, String, String)} variants to specify which space + * and organization to use. + *

+ * + * @author Josh Long + * @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 serviceInstances = + this.createServiceInstancesFromCloudApplications(Collections.singletonList(application)); + return serviceInstances.size() > 0 ? serviceInstances.iterator().next() : null; + } + + @Override + public List 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 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 getServices() { + List services = new ArrayList<>(); + List applications = this.cloudFoundryClient.getApplications(); + Set serviceIds = new HashSet<>(); + for (CloudApplication ca : applications) { + if (isRunning(ca)) { + serviceIds.add(ca.getName()); + } + } + services.addAll(serviceIds); + return services; + } + + protected List createServiceInstancesFromCloudApplications( + Collection cloudApplications) { + Set serviceInstances = new HashSet<>(); + for (CloudApplication ca : cloudApplications) { + if (isRunning(ca)) { + serviceInstances.add(new CloudFoundryServiceInstance(ca)); + } + } + List 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; + } + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfiguration.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfiguration.java new file mode 100644 index 0000000..8e01ace --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientConfiguration.java @@ -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 Josh Long + */ +@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(); + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryProperties.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryProperties.java new file mode 100644 index 0000000..1aa0f8a --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryProperties.java @@ -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 Josh Long + */ +@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; + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryRibbonClientConfiguration.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryRibbonClientConfiguration.java new file mode 100644 index 0000000..2e77484 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryRibbonClientConfiguration.java @@ -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 Josh Long + */ +@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; + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServer.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServer.java new file mode 100644 index 0000000..99a440c --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServer.java @@ -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 Josh Long + */ +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; + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerList.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerList.java new file mode 100644 index 0000000..94dd134 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerList.java @@ -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 Josh Long + */ +public class CloudFoundryServerList extends AbstractServerList { + + 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 getInitialListOfServers() { + return this.cloudFoundryServers(); + } + + @Override + public List getUpdatedListOfServers() { + return this.cloudFoundryServers(); + } + + protected List cloudFoundryServers() { + CloudApplication cloudApplications = this.cloudFoundryClient.getApplication(this.serviceId); + return Collections.singletonList(new CloudFoundryServer(cloudApplications)); + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/EnableCloudFoundryClient.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/EnableCloudFoundryClient.java new file mode 100644 index 0000000..9829d6b --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/EnableCloudFoundryClient.java @@ -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 Josh Long + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@EnableDiscoveryClient +public @interface EnableCloudFoundryClient { +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/RibbonCloudFoundryAutoConfiguration.java b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/RibbonCloudFoundryAutoConfiguration.java new file mode 100644 index 0000000..779f814 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/java/org/springframework/cloud/cloudfoundry/discovery/RibbonCloudFoundryAutoConfiguration.java @@ -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 { +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-cloudfoundry-discovery/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..5b452c3 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/src/main/ruby/generate_readme.sh b/spring-cloud-cloudfoundry-discovery/src/main/ruby/generate_readme.sh similarity index 100% rename from src/main/ruby/generate_readme.sh rename to spring-cloud-cloudfoundry-discovery/src/main/ruby/generate_readme.sh diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAutoConfigurationTest.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAutoConfigurationTest.java new file mode 100644 index 0000000..6c79845 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryAutoConfigurationTest.java @@ -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 Josh Long + */ +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); + } + +} diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientTest.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientTest.java new file mode 100644 index 0000000..5c88da8 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryDiscoveryClientTest.java @@ -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 Josh Long + */ +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 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 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 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); + } + +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerListTest.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerListTest.java new file mode 100644 index 0000000..1df7822 --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerListTest.java @@ -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 Josh Long + */ +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>() { + @Override + public List 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 initialListOfServers = this.cloudFoundryServerList.getInitialListOfServers(); + List updatedListOfServers = this.cloudFoundryServerList.getUpdatedListOfServers(); + Assert.assertEquals(updatedListOfServers, initialListOfServers); + Assert.assertTrue(initialListOfServers.size() == 1); + } + + @Test + public void testInit() { + Assert.assertEquals(this.cloudFoundryServerList.serviceId, this.serviceId); + } +} diff --git a/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerTest.java b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerTest.java new file mode 100644 index 0000000..a9487bb --- /dev/null +++ b/spring-cloud-cloudfoundry-discovery/src/test/java/org/springframework/cloud/cloudfoundry/discovery/CloudFoundryServerTest.java @@ -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 Josh Long + */ +public class CloudFoundryServerTest { + + private CloudFoundryServer cloudFoundryServer; + private List 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)); + } +} diff --git a/spring-cloud-cloudfoundry-sample/hi-service.groovy b/spring-cloud-cloudfoundry-sample/hi-service.groovy new file mode 100644 index 0000000..d70fb6b --- /dev/null +++ b/spring-cloud-cloudfoundry-sample/hi-service.groovy @@ -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 + "!"] + } +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-sample/pom.xml b/spring-cloud-cloudfoundry-sample/pom.xml new file mode 100644 index 0000000..d2ecc0c --- /dev/null +++ b/spring-cloud-cloudfoundry-sample/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + spring-cloud-cloudfoundry-sample + jar + + + org.springframework.cloud + spring-cloud-cloudfoundry + 1.0.2.BUILD-SNAPSHOT + .. + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + maven-deploy-plugin + + true + + + + + + + com.netflix.feign + feign-core + + + com.netflix.feign + feign-ribbon + + + com.netflix.feign + feign-slf4j + + + org.springframework.cloud + spring-cloud-cloudfoundry-discovery + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/spring-cloud-cloudfoundry-sample/src/main/java/org/springframework/cloud/cloudfoundry/sample/DemoApplication.java b/spring-cloud-cloudfoundry-sample/src/main/java/org/springframework/cloud/cloudfoundry/sample/DemoApplication.java new file mode 100644 index 0000000..68b34f5 --- /dev/null +++ b/spring-cloud-cloudfoundry-sample/src/main/java/org/springframework/cloud/cloudfoundry/sample/DemoApplication.java @@ -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 Cloud Foundry + * 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 Josh Long + * @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 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 hi(@PathVariable("name") String name); + +} + diff --git a/spring-cloud-cloudfoundry-sample/src/main/resources/application.properties b/spring-cloud-cloudfoundry-sample/src/main/resources/application.properties new file mode 100644 index 0000000..aaa99e9 --- /dev/null +++ b/spring-cloud-cloudfoundry-sample/src/main/resources/application.properties @@ -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} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-sample/src/main/resources/bootstrap.properties b/spring-cloud-cloudfoundry-sample/src/main/resources/bootstrap.properties new file mode 100644 index 0000000..723e99d --- /dev/null +++ b/spring-cloud-cloudfoundry-sample/src/main/resources/bootstrap.properties @@ -0,0 +1 @@ +spring.application.name=test-app diff --git a/spring-cloud-cloudfoundry-web/pom.xml b/spring-cloud-cloudfoundry-web/pom.xml new file mode 100644 index 0000000..e856923 --- /dev/null +++ b/spring-cloud-cloudfoundry-web/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + spring-cloud-cloudfoundry-web + jar + Spring Cloud CloudFoundry Web + + + org.springframework.cloud + spring-cloud-cloudfoundry + 1.0.2.BUILD-SNAPSHOT + .. + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.cloud + spring-cloud-netflix-core + true + + + org.springframework.cloud + spring-cloud-netflix-eureka-server + true + + + spring-boot-starter-log4j + org.springframework.boot + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + true + + + + + org.projectlombok + lombok + compile + true + + + com.netflix.eureka + eureka-core + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-cloud-cloudfoundry-web/src/main/asciidoc/README.adoc b/spring-cloud-cloudfoundry-web/src/main/asciidoc/README.adoc new file mode 100644 index 0000000..28c40fb --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/main/asciidoc/README.adoc @@ -0,0 +1,2 @@ +include::intro.adoc[] + diff --git a/spring-cloud-cloudfoundry-web/src/main/asciidoc/ghpages.sh b/spring-cloud-cloudfoundry-web/src/main/asciidoc/ghpages.sh new file mode 100755 index 0000000..67da0d2 --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/main/asciidoc/ghpages.sh @@ -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 diff --git a/spring-cloud-cloudfoundry-web/src/main/asciidoc/intro.adoc b/spring-cloud-cloudfoundry-web/src/main/asciidoc/intro.adoc new file mode 100644 index 0000000..913ef7b --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/main/asciidoc/intro.adoc @@ -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). diff --git a/spring-cloud-cloudfoundry-web/src/main/asciidoc/quickstart.adoc b/spring-cloud-cloudfoundry-web/src/main/asciidoc/quickstart.adoc new file mode 100644 index 0000000..d08fca6 --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/main/asciidoc/quickstart.adoc @@ -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:"",clientSecret:"",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). + diff --git a/spring-cloud-cloudfoundry-web/src/main/asciidoc/spring-cloud-cloudfoundry.adoc b/spring-cloud-cloudfoundry-web/src/main/asciidoc/spring-cloud-cloudfoundry.adoc new file mode 100644 index 0000000..8faa3b5 --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/main/asciidoc/spring-cloud-cloudfoundry.adoc @@ -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.*` + + diff --git a/src/main/java/org/springframework/cloud/cloudfoundry/web/EnableStickyFilter.java b/spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/discovery/EnableStickyFilter.java similarity index 94% rename from src/main/java/org/springframework/cloud/cloudfoundry/web/EnableStickyFilter.java rename to spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/discovery/EnableStickyFilter.java index 8962629..2828a67 100644 --- a/src/main/java/org/springframework/cloud/cloudfoundry/web/EnableStickyFilter.java +++ b/spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/discovery/EnableStickyFilter.java @@ -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 { -} +} \ No newline at end of file diff --git a/src/main/java/org/springframework/cloud/cloudfoundry/web/StickyFilterConfiguration.java b/spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/discovery/StickyFilterConfiguration.java similarity index 57% rename from src/main/java/org/springframework/cloud/cloudfoundry/web/StickyFilterConfiguration.java rename to spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/discovery/StickyFilterConfiguration.java index 10f5d53..cf63dff 100644 --- a/src/main/java/org/springframework/cloud/cloudfoundry/web/StickyFilterConfiguration.java +++ b/spring-cloud-cloudfoundry-web/src/main/java/org/springframework/cloud/cloudfoundry/discovery/StickyFilterConfiguration.java @@ -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; + } + +} \ No newline at end of file diff --git a/spring-cloud-cloudfoundry-web/src/main/ruby/generate_readme.sh b/spring-cloud-cloudfoundry-web/src/main/ruby/generate_readme.sh new file mode 100755 index 0000000..fc5b7f1 --- /dev/null +++ b/spring-cloud-cloudfoundry-web/src/main/ruby/generate_readme.sh @@ -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 diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml deleted file mode 100644 index 26fb4b3..0000000 --- a/src/test/resources/application.yml +++ /dev/null @@ -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 - \ No newline at end of file