commit 7409ed533753bda9b7ede02c6500f552ce7c3e56 Author: Dave Syer Date: Thu Jul 17 15:47:07 2014 +0100 Initial revision diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25427e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*~ +#* +*# +.#* +.classpath +.project +.settings +.springBeans +.gradle +build +bin +/target/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fff394f --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +Integration between [Cloudfoundry](https://github.com/cloudfoundry) +and the [Spring Platform](https://github.com/spring-platform). + +Add this project to a Spring Boot REST service and deploy to +Cloudfoundry (and use the Actuator for maximum flexibility). It will +expose service-broker endpoints automatically (look in /mappings for +/v2/*) and you can register it with the Cloud Controller as described +here: +[http://docs.cloudfoundry.org/services/managing-service-brokers.html](http://docs.cloudfoundry.org/services/managing-service-brokers.html). + +Example script to deploy and register a broker: + +``` +DOMAIN=mydomain.net +cf push app -p target/*.jar --no-start +cf env app | grep SPRING_PROFILES_ACTIVE || cf set-env app SPRING_PROFILES_ACTIVE cloud +cf env app | grep APPLICATION_DOMAIN || cf set-env app APPLICATION_DOMAIN ${DOMAIN} + +cf services | grep configserver && cf bind app configserver + +cf restart app +cf create-service-broker app user secure http://app.${DOMAIN} + +for f in `cf curl /v2/service_plans | grep '\"guid' | sed -e 's/.*: "//' -e 's/".*//'`; do + cf curl v2/service_plans/$f -X PUT -d '{"public":true}' +done + +cf create-service app free appi +``` + +At which point you have a service called "app" and a service instance called "appi": + +``` +$ cf marketplace +OK + +service plans description +app free Singleton service app +$ cf services +Getting services in org default / space development as admin... +OK + +name service plan bound apps +appi app free +``` + +Your application can define a configuration property +`application.domain` (defaults to "cfapps.io") which will be used to +construct the credentials for any app that binds to your service. Or +it can define the URI directly using +`cloudfoundry.service.definition.metadata.uri`. + +You can change some other basic metadata by setting config properties: + +* `cloudfoundry.service.definition.*` is bound to a + `ServiceDefinition` (defined in spring-boot-cf-service-broker) which + has optional setters for plans and metadata. + +* `cloudfoundry.service.broker.*` is bound to an internal bean. It has + optional setters for "name" (the service name), "description" (user + friendly description) and "prefix" (used to create a unique id from + the name). + +An app which binds to your service will get credentials that contain a +"uri" property linking to your service. A Spring Boot app can bind to +that through the `vcap.services.[service].credentials.uri` environment +property. + +If your service also has a +[Eureka core](https://github.com/Netflix/eureka) dependency, and you +can expose it as a Eureka service, then any service which registers +with Eureka will also become a Cloudfoundry service. Example app with +Eureka server (include jersey 1.13 to get the JAX-RS dependencies): + +``` +@Configuration +@EnableAutoConfiguration +public class Application extends WebMvcConfigurerAdapter { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public FilterRegistrationBean jersey() { + FilterRegistrationBean bean = new FilterRegistrationBean(); + bean.setFilter(new ServletContainer()); + bean.addInitParameter("com.sun.jersey.config.property.WebPageContentRegex", + "(/|/(flex/|images/|js/|css/|jsp/|admin/|v2/catalog|v2/service_instances).*)"); + bean.addInitParameter("com.sun.jersey.config.property.packages", + "com.sun.jersey;com.netflix"); + return bean; + } + +} + +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..aa17c0a --- /dev/null +++ b/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + org.springframework.platform + spring-platform-cloudfoundry + 1.0.0.BUILD-SNAPSHOT + jar + + spring-platform-cloudfoundry + Spring Patform Cloudfoundry integration project + + + org.springframework.boot + spring-boot-starter-parent + 1.1.5.BUILD-SNAPSHOT + + + + + + + org.springframework.platform + spring-platform-netflix + 1.0.0.BUILD-SNAPSHOT + pom + import + + + + + + + org.springframework.platform + spring-platform-netflix-core + + + org.springframework.boot + spring-boot-starter-data-mongodb + true + + + org.springframework.boot + spring-boot-starter-web + + + org.cloudfoundry + spring-boot-cf-service-broker + 2.3 + + + com.netflix.eureka + eureka-core + true + + + com.sun.jersey + jersey-servlet + 1.13 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + UTF-8 + 1.7 + + + diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/CatalogLeaseManager.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/CatalogLeaseManager.java new file mode 100644 index 0000000..f9647ed --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/CatalogLeaseManager.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.community.servicebroker.model.Catalog; +import org.cloudfoundry.community.servicebroker.model.ServiceDefinition; +import org.cloudfoundry.community.servicebroker.service.BeanCatalogService; +import org.cloudfoundry.community.servicebroker.service.CatalogService; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.InstanceInfo.ActionType; +import com.netflix.eureka.lease.LeaseManager; + +/** + * @author Dave Syer + * + */ +public class CatalogLeaseManager implements LeaseManager, LeaseManagerLite, + CatalogService { + + private Map definitions = new HashMap(); + + private List values = new ArrayList(); + + public CatalogLeaseManager(InstanceInfo config) { + register(config, false); + } + + @Override + public Catalog getCatalog() { + return new Catalog(values); + } + + @Override + public ServiceDefinition getServiceDefinition(String serviceId) { + return new BeanCatalogService(getCatalog()).getServiceDefinition(serviceId); + } + + @Override + public void register(InstanceInfo info, boolean isReplication) { + register(info, 0, isReplication); + } + + @Override + public void register(InstanceInfo info, int leaseDuration, boolean isReplication) { + if (!definitions.containsKey(info.getAppName())) { + ServiceDefinition definition = getServiceDefinition(info); + definitions.put(info.getAppName(), definition); + values.add(definition); + } + } + + @Override + public boolean cancel(String appName, String id, boolean isReplication) { + ServiceDefinition definition = definitions.remove(appName); + if (definition != null) { + values.remove(definition); + } + return true; + } + + @Override + public boolean renew(String appName, String id, boolean isReplication) { + if (definitions.containsKey(appName)) { + definitions.get(appName).getMetadata() + .put("timestamp", System.currentTimeMillis()); + } + return true; + } + + @Override + public void evict() { + Collection definitions = new ArrayList( + this.definitions.values()); + for (ServiceDefinition definition : definitions) { + InstanceInfo info = (InstanceInfo) definition.getMetadata().get("info"); + if (info.getActionType() == ActionType.DELETED) { + cancel(info.getAppName(), info.getId(), false); + } + } + } + + private ServiceDefinition getServiceDefinition(InstanceInfo info) { + String name = info.getAppName().toLowerCase(); + ServiceDefinition definition = new FreeServiceDefinitionFactory("eureka-").create(name, "Eureka-brokered service"); + Map map = new HashMap(); + map.put("info", (Object) info); + map.put("uri", info.getHomePageUrl()); + map.put("timestamp", System.currentTimeMillis()); + definition.setMetadata(map); + return definition; + } + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/FreeServiceDefinitionFactory.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/FreeServiceDefinitionFactory.java new file mode 100644 index 0000000..3dceb73 --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/FreeServiceDefinitionFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.community.servicebroker.model.Plan; +import org.cloudfoundry.community.servicebroker.model.ServiceDefinition; +import org.springframework.util.DigestUtils; + +/** + * @author Dave Syer + * + */ +public class FreeServiceDefinitionFactory { + + private final String prefix; + + public FreeServiceDefinitionFactory() { + this(""); + } + + public FreeServiceDefinitionFactory(String prefix) { + this.prefix = prefix; + } + + public ServiceDefinition create(String name, String description) { + return new ServiceDefinition(getId(name), name, description, true, getPlans(name)); + } + + private List getPlans(String appName) { + Plan plan = new Plan(getId(appName + "-free"), "free", + "This is a default service plan. All services are created equally.", + getServiceDefinitionMetadata(appName), true); + return new ArrayList(Arrays.asList(plan)); + } + + private Map getServiceDefinitionMetadata(String appName) { + Map sdMetadata = new HashMap(); + sdMetadata.put("displayName", appName + "-service"); + sdMetadata.put("longDescription", "Platform Service for " + appName); + sdMetadata.put("providerDisplayName", "Pivotal"); + sdMetadata.put("documentationUrl", "https://github.com/spring-platform"); + sdMetadata.put("supportUrl", "https://github.com/spring-platform"); + return sdMetadata; + } + + protected String getId(String name) { + try { + String id = DigestUtils.md5DigestAsHex((prefix + name).getBytes("UTF-8")); + return id; + } + catch (UnsupportedEncodingException e) { + throw new IllegalStateException(); + } + + } + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/LeaseManagerLite.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/LeaseManagerLite.java new file mode 100644 index 0000000..bff18f2 --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/LeaseManagerLite.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import com.netflix.appinfo.InstanceInfo; + +/** + * @author Dave Syer + * + */ +public interface LeaseManagerLite { + + void register(final InstanceInfo info, final boolean isReplication); + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/PiggybackMethodInterceptor.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/PiggybackMethodInterceptor.java new file mode 100644 index 0000000..2171e0c --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/PiggybackMethodInterceptor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.util.ReflectionUtils; + +/** + * @author Dave Syer + * + */ +public class PiggybackMethodInterceptor implements MethodInterceptor { + + private Object delegate; + private Class[] types; + + public PiggybackMethodInterceptor(Object delegate, Class... types) { + this.delegate = delegate; + this.types = types; + } + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + Object result = invocation.proceed(); + invokeAfter(invocation.getMethod(), invocation.getArguments()); + return result; + } + + private void invokeAfter(Method method, Object[] arguments) throws Exception { + for (Class type : types) { + Method target = getTarget(type, method); + if (target != null) { + target.invoke(delegate, arguments); + return; + } + } + } + + private Method getTarget(Class type, Method method) { + Method target = ReflectionUtils.findMethod(type, method.getName(), + method.getParameterTypes()); + if (target != null) { + return target; + } + return null; + } + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceBrokerAutoConfiguration.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceBrokerAutoConfiguration.java new file mode 100644 index 0000000..9d475b7 --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceBrokerAutoConfiguration.java @@ -0,0 +1,212 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import org.cloudfoundry.community.servicebroker.config.BrokerApiVersionConfig; +import org.cloudfoundry.community.servicebroker.model.Catalog; +import org.cloudfoundry.community.servicebroker.model.ServiceDefinition; +import org.cloudfoundry.community.servicebroker.service.BeanCatalogService; +import org.cloudfoundry.community.servicebroker.service.CatalogService; +import org.cloudfoundry.community.servicebroker.service.ServiceInstanceBindingService; +import org.cloudfoundry.community.servicebroker.service.ServiceInstanceService; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.platform.netflix.eureka.EurekaRegistryAvailableEvent; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; + +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider; +import com.netflix.eureka.PeerAwareInstanceRegistry; +import com.netflix.eureka.lease.LeaseManager; + +/** + * Autoconfiguration providing simple service-broker endpoints and Netflix eureka server + * features. The service-broker endpoints will be enabled if spring-boot-cf-service-broker is on the classpath. By default they just make the + * current app into a service brooker for itself. The Eureka features will be added if ereka-core is on the classpath. When + * active any Eureka services registered using native Netflix APIs will be available as + * Cloudfoundry services. They consist of an interceptor for the {@link LeaseManager} that + * manages the {@link Catalog} of Cloudfoundry servvices. + * + * + * @author Dave Syer + * + */ +@Configuration +@ComponentScan(basePackages = { "demo", "org.cloudfoundry.community.servicebroker" }, excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = BrokerApiVersionConfig.class), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = BeanCatalogService.class) }) +@ConditionalOnClass(ServiceInstanceRepository.class) +@ConditionalOnWebApplication +public class ServiceBrokerAutoConfiguration { + + @Configuration + @ConditionalOnMissingClass(name="com.netflix.eureka.PeerAwareInstanceRegistry") + @ConditionalOnMissingBean(CatalogService.class) + protected static class CatalogConfiguration { + + @Autowired + private BrokerProperties broker; + + @Bean + public BeanCatalogService catalogService() { + return new BeanCatalogService(catalog()); + } + + @Bean + public Catalog catalog() { + return new Catalog(Arrays.asList(serviceDefinition())); + } + + @Bean + @ConfigurationProperties("cloudfoundry.service.definition") + public ServiceDefinition serviceDefinition() { + return new FreeServiceDefinitionFactory(broker.getPrefix()).create(broker.getName(), + broker.getDescription()); + } + + @Component + @ConfigurationProperties("cloudfoundry.server.broker") + public static class BrokerProperties { + private String prefix; + @Value("${spring.application.name:application}") + private String name; + private String description; + public String getPrefix() { + return prefix==null ? name + "-" : prefix; + } + public void setPrefix(String prefix) { + this.prefix = prefix; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getDescription() { + return description == null ? "Singleton service app" : description; + } + public void setDescription(String description) { + this.description = description; + } + } + + } + + @Configuration + @ConditionalOnClass(PeerAwareInstanceRegistry.class) + protected static class EurekaCatalogConfiguration { + + @Bean + public CatalogLeaseManager catalogLeaseManager(EurekaInstanceConfig config) { + InstanceInfo info = new EurekaConfigBasedInstanceInfoProvider(config).get(); + return new CatalogLeaseManager(info); + } + + } + + @Configuration + @ConditionalOnClass(PeerAwareInstanceRegistry.class) + protected static class Initializer implements + ApplicationListener { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private CatalogLeaseManager leaseManager; + + @Override + public void onApplicationEvent(EurekaRegistryAvailableEvent event) { + ProxyFactory factory = new ProxyFactory( + PeerAwareInstanceRegistry.getInstance()); + factory.addAdvice(new PiggybackMethodInterceptor(leaseManager, + LeaseManager.class, LeaseManagerLite.class)); + factory.setProxyTargetClass(true); + Field field = ReflectionUtils.findField(PeerAwareInstanceRegistry.class, + "instance"); + try { + // Awful ugly hack to work around lack of DI in eureka + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + ReflectionUtils.setField(field, null, factory.getProxy()); + } + catch (Exception e) { + throw new IllegalStateException("Cannot modify instance registry", e); + } + } + + } + + @Bean + @ConditionalOnMissingBean(ServiceInstanceService.class) + public SimpleServiceInstanceService serviceInstanceService( + ServiceInstanceRepository repository) { + return new SimpleServiceInstanceService(repository); + } + + @Bean + @ConditionalOnMissingBean(ServiceInstanceBindingService.class) + public SimpleServiceInstanceBindingService serviceInstanceBindingService( + CatalogService catalog, ServiceInstanceBindingRepository repository) { + return new SimpleServiceInstanceBindingService(catalog, repository); + } + + @Bean + @ConditionalOnMissingBean(ServiceInstanceRepository.class) + public SimpleServiceInstanceRepository serviceInstanceRepository( + CatalogService catalog) { + List definitions = catalog.getCatalog() + .getServiceDefinitions(); + ServiceDefinition definition = definitions.iterator().next(); + return new SimpleServiceInstanceRepository(definition.getId()); + } + + @Bean + @ConditionalOnMissingBean(ServiceInstanceBindingRepository.class) + public SimpleServiceInstanceBindingRepository serviceInstanceBindingRepository( + CatalogService catalog) { + List definitions = catalog.getCatalog() + .getServiceDefinitions(); + ServiceDefinition definition = definitions.iterator().next(); + return new SimpleServiceInstanceBindingRepository(definition.getId()); + } + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceInstanceBindingRepository.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceInstanceBindingRepository.java new file mode 100644 index 0000000..50c60d3 --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceInstanceBindingRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import org.cloudfoundry.community.servicebroker.model.ServiceInstanceBinding; + +/** + * @author Dave Syer + * + */ +public interface ServiceInstanceBindingRepository { + + ServiceInstanceBinding findOne(String bindingId); + + void delete(String id); + + void save(ServiceInstanceBinding binding); + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceInstanceRepository.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceInstanceRepository.java new file mode 100644 index 0000000..c30ffca --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/ServiceInstanceRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.util.List; + +import org.cloudfoundry.community.servicebroker.model.ServiceInstance; + +/** + * @author Dave Syer + * + */ +public interface ServiceInstanceRepository { + + List findAll(); + + ServiceInstance findOne(String serviceInstanceId); + + void save(ServiceInstance instance); + + void delete(String id); + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceBindingRepository.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceBindingRepository.java new file mode 100644 index 0000000..7289db4 --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceBindingRepository.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import org.cloudfoundry.community.servicebroker.model.ServiceInstanceBinding; + +/** + * @author Dave Syer + * + */ +public class SimpleServiceInstanceBindingRepository implements ServiceInstanceBindingRepository { + + private String serviceDefinitionId; + + public SimpleServiceInstanceBindingRepository(String serviceDefinitionId) { + this.serviceDefinitionId = serviceDefinitionId; + } + + @Override + public ServiceInstanceBinding findOne(String id) { + return new ServiceInstanceBinding(id, serviceDefinitionId, null, null, null); + } + + @Override + public void save(ServiceInstanceBinding instance) { + } + + @Override + public void delete(String id) { + } + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceBindingService.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceBindingService.java new file mode 100644 index 0000000..c9561c4 --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceBindingService.java @@ -0,0 +1,116 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.util.HashMap; +import java.util.Map; + +import org.cloudfoundry.community.servicebroker.exception.ServiceBrokerException; +import org.cloudfoundry.community.servicebroker.exception.ServiceInstanceBindingExistsException; +import org.cloudfoundry.community.servicebroker.model.ServiceDefinition; +import org.cloudfoundry.community.servicebroker.model.ServiceInstance; +import org.cloudfoundry.community.servicebroker.model.ServiceInstanceBinding; +import org.cloudfoundry.community.servicebroker.service.CatalogService; +import org.cloudfoundry.community.servicebroker.service.ServiceInstanceBindingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * Simple impl to bind services. + * + * @author sgreenberg@gopivotal.com + * + */ +@Service +public class SimpleServiceInstanceBindingService implements ServiceInstanceBindingService { + + private ServiceInstanceBindingRepository repository; + private CatalogService catalog; + @Value("${application.domain:cfapps.io}") + private String applicationDomain; + + @Autowired + public SimpleServiceInstanceBindingService(CatalogService catalog, + ServiceInstanceBindingRepository repository) { + this.catalog = catalog; + this.repository = repository; + } + + @Override + public ServiceInstanceBinding createServiceInstanceBinding(String bindingId, + ServiceInstance serviceInstance, String serviceId, String planId, + String appGuid) throws ServiceInstanceBindingExistsException, + ServiceBrokerException { + + ServiceInstanceBinding binding = repository.findOne(bindingId); + if (binding != null && binding.getAppGuid()!=null) { + throw new ServiceInstanceBindingExistsException(binding); + } + + if (!isBindingPermitted(serviceInstance, serviceId, planId, appGuid)) { + throw new ServiceBrokerException( + "Binding is not permitted for this plan, app and service"); + } + + binding = new ServiceInstanceBinding(bindingId, serviceInstance.getId(), + getCredentials(serviceInstance, serviceId), null, appGuid); + repository.save(binding); + + return binding; + } + + protected boolean isBindingPermitted(ServiceInstance serviceInstance, + String serviceId, String planId, String appGuid) { + return true; + } + + protected Map getCredentials(ServiceInstance instance, + String serviceId) { + Map credentials = new HashMap(); + credentials.put("uri", findUriFromService(serviceId)); + credentials.put("domain", applicationDomain); + return credentials; + } + + protected String findUriFromService(String serviceId) { + ServiceDefinition definition = catalog.getServiceDefinition(serviceId); + if (definition != null && definition.getId().equals(serviceId)) { + String uri = (String) definition.getMetadata().get("uri"); + if (uri != null) { + return uri; + } + return "http://" + definition.getName() + "." + applicationDomain; + } + throw new IllegalStateException("Cannot locate service in catalog: " + serviceId); + } + + @Override + public ServiceInstanceBinding getServiceInstanceBinding(String id) { + return repository.findOne(id); + } + + @Override + public ServiceInstanceBinding deleteServiceInstanceBinding(String id) + throws ServiceBrokerException { + ServiceInstanceBinding binding = getServiceInstanceBinding(id); + if (binding != null) { + repository.delete(id); + } + return binding; + } + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceRepository.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceRepository.java new file mode 100644 index 0000000..c90b95f --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceRepository.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.util.ArrayList; +import java.util.List; + +import org.cloudfoundry.community.servicebroker.model.ServiceInstance; + +/** + * @author Dave Syer + * + */ +public class SimpleServiceInstanceRepository implements ServiceInstanceRepository { + + private String serviceDefinitionId; + + public SimpleServiceInstanceRepository(String serviceDefinitionId) { + // TODO: add dashboard URL + this.serviceDefinitionId = serviceDefinitionId; + } + + @Override + public List findAll() { + return new ArrayList(); + } + + @Override + public ServiceInstance findOne(String serviceInstanceId) { + return new ServiceInstance(serviceInstanceId, serviceDefinitionId, null, null, null, null); + } + + @Override + public void save(ServiceInstance instance) { + } + + @Override + public void delete(String id) { + } + +} diff --git a/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceService.java b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceService.java new file mode 100644 index 0000000..abae644 --- /dev/null +++ b/src/main/java/org/springframework/platform/cloudfoundry/broker/SimpleServiceInstanceService.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker; + +import java.util.List; + +import org.cloudfoundry.community.servicebroker.exception.ServiceBrokerException; +import org.cloudfoundry.community.servicebroker.exception.ServiceInstanceExistsException; +import org.cloudfoundry.community.servicebroker.model.ServiceDefinition; +import org.cloudfoundry.community.servicebroker.model.ServiceInstance; +import org.cloudfoundry.community.servicebroker.service.ServiceInstanceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class SimpleServiceInstanceService implements ServiceInstanceService { + + private ServiceInstanceRepository repository; + + @Autowired + public SimpleServiceInstanceService(ServiceInstanceRepository repository) { + this.repository = repository; + } + + @Override + public List getAllServiceInstances() { + return repository.findAll(); + } + + @Override + public ServiceInstance createServiceInstance(ServiceDefinition service, + String serviceInstanceId, String planId, String organizationGuid, + String spaceGuid) + throws ServiceInstanceExistsException, ServiceBrokerException { + ServiceInstance instance = repository.findOne(serviceInstanceId); + if (instance != null && instance.getOrganizationGuid()!=null) { + throw new ServiceInstanceExistsException(instance); + } + instance = new ServiceInstance(serviceInstanceId, service.getId(), + planId, organizationGuid, spaceGuid, null); + repository.save(instance); + return instance; + } + + + @Override + public ServiceInstance getServiceInstance(String id) { + return repository.findOne(id); + } + + @Override + public ServiceInstance deleteServiceInstance(String id) throws ServiceBrokerException { + ServiceInstance instance = repository.findOne(id); + repository.delete(id); + return instance; + } + +} \ No newline at end of file diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a8cd9d1 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.platform.cloudfoundry.broker.ServiceBrokerAutoConfiguration \ No newline at end of file diff --git a/src/test/java/org/springframework/platform/cloudfoundry/broker/sample/Application.java b/src/test/java/org/springframework/platform/cloudfoundry/broker/sample/Application.java new file mode 100644 index 0000000..25affea --- /dev/null +++ b/src/test/java/org/springframework/platform/cloudfoundry/broker/sample/Application.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker.sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.embedded.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import com.sun.jersey.spi.container.servlet.ServletContainer; + +@Configuration +@EnableAutoConfiguration +public class Application extends WebMvcConfigurerAdapter { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public FilterRegistrationBean jersey() { + FilterRegistrationBean bean = new FilterRegistrationBean(); + bean.setFilter(new ServletContainer()); + bean.addInitParameter("com.sun.jersey.config.property.WebPageContentRegex", + "(/|/(flex/|images/|js/|css/|jsp/|admin/|v2/catalog|v2/service_instances).*)"); + bean.addInitParameter("com.sun.jersey.config.property.packages", + "com.sun.jersey;com.netflix"); + return bean; + } + +} diff --git a/src/test/java/org/springframework/platform/cloudfoundry/broker/sample/ApplicationTests.java b/src/test/java/org/springframework/platform/cloudfoundry/broker/sample/ApplicationTests.java new file mode 100644 index 0000000..9d2ccd7 --- /dev/null +++ b/src/test/java/org/springframework/platform/cloudfoundry/broker/sample/ApplicationTests.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2014 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.platform.cloudfoundry.broker.sample; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +@WebAppConfiguration +public class ApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..ab78716 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,15 @@ +debug: true +spring: + application: + name: eureka +management: + context-path: /admin +eureka: + server: + waitTimeInMsWhenSyncEmpty: 1000 + client: + serviceUrl: + defaultZone: http://localhost:8080/v2/ + default.defaultZone: http://localhost:8080/v2/ + registerWithEureka: false + fetchRegistry: false \ No newline at end of file