diff --git a/.gitignore b/.gitignore index c9d15d7..8f5b82e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .classpath .project .settings +.metadata target Servers diff --git a/cloudfoundry-connector/pom.xml b/cloudfoundry-connector/pom.xml index 398c934..0d4f5c8 100644 --- a/cloudfoundry-connector/pom.xml +++ b/cloudfoundry-connector/pom.xml @@ -17,7 +17,6 @@ 2.2.2 - true 4.11 1.9.5 diff --git a/cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ApplicationInstanceInfoCreator.java b/cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ApplicationInstanceInfoCreator.java index f897444..4a8d243 100644 --- a/cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ApplicationInstanceInfoCreator.java +++ b/cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/ApplicationInstanceInfoCreator.java @@ -3,6 +3,7 @@ package org.springframework.cloud.cloudfoundry; import java.util.Map; import org.springframework.cloud.app.ApplicationInstanceInfo; +import org.springframework.cloud.app.BasicApplicationInstanceInfo; /** * @@ -14,6 +15,6 @@ public class ApplicationInstanceInfoCreator { String instanceId = (String) applicationInstanceData.get("instance_id"); String appId = (String) applicationInstanceData.get("name"); - return new CloudFoundryApplicationInstanceInfo(instanceId, appId, applicationInstanceData); + return new BasicApplicationInstanceInfo(instanceId, appId, applicationInstanceData); } } diff --git a/cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/CloudFoundryApplicationInstanceInfo.java b/core/src/main/java/org/springframework/cloud/app/BasicApplicationInstanceInfo.java similarity index 61% rename from cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/CloudFoundryApplicationInstanceInfo.java rename to core/src/main/java/org/springframework/cloud/app/BasicApplicationInstanceInfo.java index 17855b6..e321479 100644 --- a/cloudfoundry-connector/src/main/java/org/springframework/cloud/cloudfoundry/CloudFoundryApplicationInstanceInfo.java +++ b/core/src/main/java/org/springframework/cloud/app/BasicApplicationInstanceInfo.java @@ -1,21 +1,23 @@ -package org.springframework.cloud.cloudfoundry; +package org.springframework.cloud.app; import java.util.Map; import org.springframework.cloud.app.ApplicationInstanceInfo; /** + * Basic implementation of ApplicationInstanceInfo that might suffice most typical + * cloud connectors. * * @author Ramnivas Laddad * */ -public class CloudFoundryApplicationInstanceInfo implements ApplicationInstanceInfo { +public class BasicApplicationInstanceInfo implements ApplicationInstanceInfo { private String instanceId; private String appId; private Map properties; - public CloudFoundryApplicationInstanceInfo(String instanceId, String appId, Map properties) { + public BasicApplicationInstanceInfo(String instanceId, String appId, Map properties) { this.instanceId = instanceId; this.appId = appId; this.properties = properties; diff --git a/heroku-connector/README.md b/heroku-connector/README.md new file mode 100644 index 0000000..2f733f0 --- /dev/null +++ b/heroku-connector/README.md @@ -0,0 +1,20 @@ +Heroku connector for spring-cloud +======================================= + +Provides Heroku connector with support for Postgres (Mysql, RabbitMQ, MongoDB, and Redis services coming soon). + +Supporting additional services +------------------------------ +Please see the documentation for cloudfoundry-connector, since the same mechanism applies with any cloud-connector. + +Limitations +----------- +Unlike CloudFoundry, Heroku exposes very little information about the app that is retrievable from within a running app. +For example, there is no good way to know the name of the application. Therefore, if an app desire such info, it +needs to make that available through environment variables. + +To have sensible app name available to ApplicationInstanceInfo, set SPRING_CLOUD_APP_NAME variable + +heroku config:add SPRING_CLOUD_APP_NAME= --app + +If this env variable is not set, the app name will be set to . \ No newline at end of file diff --git a/heroku-connector/pom.xml b/heroku-connector/pom.xml new file mode 100644 index 0000000..951acb4 --- /dev/null +++ b/heroku-connector/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + org.springframework.cloud + heroku-connector + 1.0.0.CI-SNAPSHOT + jar + Spring-Cloud Heroku Connector + http://www.springframework.org + + + + + 4.11 + 1.8.5 + + + + org.springframework.cloud + spring-service-connector + 1.0.0.CI-SNAPSHOT + + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-all + ${mockito.version} + test + + + log4j + log4j + 1.2.14 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/ApplicationInstanceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/ApplicationInstanceInfoCreator.java new file mode 100644 index 0000000..edb3e03 --- /dev/null +++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/ApplicationInstanceInfoCreator.java @@ -0,0 +1,43 @@ +package org.springframework.cloud.heroku; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.springframework.cloud.app.ApplicationInstanceInfo; +import org.springframework.cloud.app.BasicApplicationInstanceInfo; + +/** + * Application instance info creator. + *

+ * Relies on SPRING_CLOUD_APP_NAME environment being set (using commands such as + * heroku config:add SPRING_CLOUD_APP_NAME=myappname --app myappname + * + * @author Ramnivas Laddad + * + */ +public class ApplicationInstanceInfoCreator { + private static Logger logger = Logger.getLogger(ApplicationInstanceInfoCreator.class.getName()); + + private EnvironmentAccessor environment; + + public ApplicationInstanceInfoCreator(EnvironmentAccessor environmentAccessor) { + this.environment = environmentAccessor; + } + + public ApplicationInstanceInfo createApplicationInstanceInfo() { + String appname = environment.getValue("SPRING_CLOUD_APP_NAME"); + if (appname == null) { + logger.warning("Environment variable SPRING_CLOUD_APP_NAME not set. App name set to "); + appname = ""; + } + + String dyno = environment.getValue("DYNO"); + + Map appProperties = new HashMap(); + appProperties.put("port", environment.getValue("PORT")); + appProperties.put("host", environment.getHost()); + + return new BasicApplicationInstanceInfo(dyno, appname, appProperties); + } +} diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/EnvironmentAccessor.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/EnvironmentAccessor.java new file mode 100644 index 0000000..ec59351 --- /dev/null +++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/EnvironmentAccessor.java @@ -0,0 +1,35 @@ +package org.springframework.cloud.heroku; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; + +import org.springframework.cloud.CloudException; + +/** + * Environment available to the deployed app. + * + * @author Ramnivas Laddad + */ +public class EnvironmentAccessor { + + public Map getEnv() { + return System.getenv(); + } + + public String getValue(String key) { + return System.getenv(key); + } + + public String getPropertyValue(String key) { + return System.getProperty(key); + } + + public String getHost() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException ex) { + throw new CloudException(ex); + } + } +} diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuConnector.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuConnector.java new file mode 100644 index 0000000..ef0792a --- /dev/null +++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuConnector.java @@ -0,0 +1,94 @@ +package org.springframework.cloud.heroku; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.cloud.AbstractCloudConnector; +import org.springframework.cloud.CloudException; +import org.springframework.cloud.ServiceInfoCreator; +import org.springframework.cloud.app.ApplicationInstanceInfo; +import org.springframework.cloud.service.ServiceInfo; + +/** + * Implementation of CloudConnector for Heroku + * + * Currently support only the Postgres service. + * + * @author Ramnivas Laddad + * + */ +public class HerokuConnector extends AbstractCloudConnector { + + private EnvironmentAccessor environment = new EnvironmentAccessor(); + private ApplicationInstanceInfoCreator applicationInstanceInfoCreator + = new ApplicationInstanceInfoCreator(environment); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public HerokuConnector() { + super((Class) HerokuServiceInfoCreator.class); + } + + @Override + public boolean isInMatchingCloud() { + return environment.getValue("DYNO") != null; + } + + @Override + public ApplicationInstanceInfo getApplicationInstanceInfo() { + try { + return applicationInstanceInfoCreator.createApplicationInstanceInfo(); + } catch (Exception e) { + throw new CloudException(e); + } + } + + @Override + public List getServiceInfos() { + List serviceInfos = new ArrayList(); + for (Map.Entry serviceData : getServicesData().entrySet()) { + serviceInfos.add(getServiceInfo(serviceData)); + } + + return serviceInfos; + } + + /* package for testing purpose */ + void setCloudEnvironment(EnvironmentAccessor environment) { + this.environment = environment; + this.applicationInstanceInfoCreator = new ApplicationInstanceInfoCreator(environment); + } + + private ServiceInfo getServiceInfo(Map.Entry serviceData) { + for (ServiceInfoCreator serviceInfoCreator : serviceInfoCreators) { + if (serviceInfoCreator.accept(serviceData)) { + return serviceInfoCreator.createServiceInfo(serviceData); + } + } + + throw new CloudException("No suitable service info creator found"); + } + + /** + * Return object representation of the bound services + *

+ * Returns map whose key is the env key and value is the associated url + *

+ * @return + */ + private Map getServicesData() { + Map serviceData = new HashMap(); + + Map env = environment.getEnv(); + + for (Map.Entry envEntry : env.entrySet()) { + if (envEntry.getKey().startsWith("HEROKU_POSTGRESQL_")) { + serviceData.put(envEntry.getKey(), envEntry.getValue()); + } + } + + return serviceData; + } + +} diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuServiceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuServiceInfoCreator.java new file mode 100644 index 0000000..695b30b --- /dev/null +++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/HerokuServiceInfoCreator.java @@ -0,0 +1,27 @@ +package org.springframework.cloud.heroku; + +import java.util.Map; + +import org.springframework.cloud.ServiceInfoCreator; +import org.springframework.cloud.service.ServiceInfo; + +/** + * + * @author Ramnivas Laddad + * + */ +public abstract class HerokuServiceInfoCreator implements ServiceInfoCreator { + + private String urlProtocol; + + public HerokuServiceInfoCreator(String urlProtocol) { + this.urlProtocol = urlProtocol; + } + + public boolean accept(Object serviceData) { + @SuppressWarnings("unchecked") + Map.Entry serviceDataEntry = (Map.Entry)serviceData; + + return serviceDataEntry.getValue().toString().startsWith(urlProtocol + "://"); + } +} diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/PostgresqlServiceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/PostgresqlServiceInfoCreator.java new file mode 100644 index 0000000..177f013 --- /dev/null +++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/PostgresqlServiceInfoCreator.java @@ -0,0 +1,20 @@ +package org.springframework.cloud.heroku; + +import org.springframework.cloud.service.common.PostgresqlServiceInfo; + +/** + * + * @author Ramnivas Laddad + * + */ +public class PostgresqlServiceInfoCreator extends RelationalServiceInfoCreator { + + public PostgresqlServiceInfoCreator() { + super("postgres"); + } + + @Override + public PostgresqlServiceInfo createServiceInfo(String id, String uri) { + return new PostgresqlServiceInfo(id, uri); + } +} diff --git a/heroku-connector/src/main/java/org/springframework/cloud/heroku/RelationalServiceInfoCreator.java b/heroku-connector/src/main/java/org/springframework/cloud/heroku/RelationalServiceInfoCreator.java new file mode 100644 index 0000000..57ed66a --- /dev/null +++ b/heroku-connector/src/main/java/org/springframework/cloud/heroku/RelationalServiceInfoCreator.java @@ -0,0 +1,26 @@ +package org.springframework.cloud.heroku; + +import java.util.Map; + +import org.springframework.cloud.service.common.RelationalServiceInfo; + +/** + * + * @author Ramnivas Laddad + * + */ +public abstract class RelationalServiceInfoCreator extends HerokuServiceInfoCreator { + + public RelationalServiceInfoCreator(String urlProtocol) { + super(urlProtocol); + } + + public abstract SI createServiceInfo(String id, String uri); + + public SI createServiceInfo(Object serviceData) { + @SuppressWarnings("unchecked") + Map.Entry serviceDataEntry = (Map.Entry)serviceData; + + return createServiceInfo(serviceDataEntry.getKey(), serviceDataEntry.getValue()); + } +} diff --git a/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.CloudConnector b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.CloudConnector new file mode 100644 index 0000000..c017a8e --- /dev/null +++ b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.CloudConnector @@ -0,0 +1 @@ +org.springframework.cloud.heroku.HerokuConnector \ No newline at end of file diff --git a/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.heroku.HerokuServiceInfoCreator b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.heroku.HerokuServiceInfoCreator new file mode 100644 index 0000000..8a4a0f9 --- /dev/null +++ b/heroku-connector/src/main/resources/META-INF/services/org.springframework.cloud.heroku.HerokuServiceInfoCreator @@ -0,0 +1 @@ +org.springframework.cloud.heroku.PostgresqlServiceInfoCreator diff --git a/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTest.java b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTest.java new file mode 100644 index 0000000..e418389 --- /dev/null +++ b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTest.java @@ -0,0 +1,103 @@ +package org.springframework.cloud.heroku; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.heroku.HerokuConnectorTestHelper.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.cloud.service.ServiceInfo; +import org.springframework.cloud.service.common.PostgresqlServiceInfo; + +/** + * + * @author Ramnivas Laddad + * + */ +public class HerokuConnectorTest { + private HerokuConnector testCloudConnector = new HerokuConnector(); + @Mock EnvironmentAccessor mockEnvironment; + + private static final String host = "10.20.30.40"; + private static final int port = 1234; + private static String username = "myuser"; + private static final String password = "mypass"; + + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + testCloudConnector.setCloudEnvironment(mockEnvironment); + } + + @Test + public void isInMatchingEnvironment() { + when(mockEnvironment.getValue("DYNO")).thenReturn("web.1"); + assertTrue(testCloudConnector.isInMatchingCloud()); + + when(mockEnvironment.getValue("DYNO")).thenReturn(null); + assertFalse(testCloudConnector.isInMatchingCloud()); + } + + @Test + public void postgresqlServiceCreation() { + Map env = new HashMap(); + String postgresUrl = createPostgresUrl(host, port, "db", username, password); + env.put("HEROKU_POSTGRESQL_YELLOW_URL", postgresUrl); + when(mockEnvironment.getEnv()).thenReturn(env); + + List serviceInfos = testCloudConnector.getServiceInfos(); + ServiceInfo serviceInfo = getServiceInfo(serviceInfos, "HEROKU_POSTGRESQL_YELLOW_URL"); + assertNotNull(serviceInfo); + assertTrue(serviceInfo instanceof PostgresqlServiceInfo); + PostgresqlServiceInfo postgresqlServiceInfo = (PostgresqlServiceInfo)serviceInfo; + assertEquals(host, postgresqlServiceInfo.getHost()); + assertEquals(port, postgresqlServiceInfo.getPort()); + assertEquals(username, postgresqlServiceInfo.getUserName()); + assertEquals(password, postgresqlServiceInfo.getPassword()); + assertEquals("jdbc:" + postgresUrl, postgresqlServiceInfo.getJdbcUrl()); + } + + @Test + public void applicationInstanceInfo() { + when(mockEnvironment.getValue("SPRING_CLOUD_APP_NAME")).thenReturn("myapp"); + when(mockEnvironment.getValue("DYNO")).thenReturn("web.1"); + when(mockEnvironment.getValue("PORT")).thenReturn(Integer.toString(port)); + when(mockEnvironment.getHost()).thenReturn(host); + + assertEquals("myapp", testCloudConnector.getApplicationInstanceInfo().getAppId()); + assertEquals("web.1", testCloudConnector.getApplicationInstanceInfo().getInstanceId()); + Map appProps = testCloudConnector.getApplicationInstanceInfo().getProperties(); + assertEquals(host, appProps.get("host")); + assertEquals(Integer.toString(port), appProps.get("port")); + } + + @Test + public void applicationInstanceInfoNoSpringCloudAppName() { + when(mockEnvironment.getValue("DYNO")).thenReturn("web.1"); + when(mockEnvironment.getValue("PORT")).thenReturn(Integer.toString(port)); + when(mockEnvironment.getHost()).thenReturn(host); + assertEquals("", testCloudConnector.getApplicationInstanceInfo().getAppId()); + assertEquals("web.1", testCloudConnector.getApplicationInstanceInfo().getInstanceId()); + } + + private static ServiceInfo getServiceInfo(List serviceInfos, String serviceId) { + for (ServiceInfo serviceInfo : serviceInfos) { + if (serviceInfo.getId().equals(serviceId)) { + return serviceInfo; + } + } + return null; + } + + +} diff --git a/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTestHelper.java b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTestHelper.java new file mode 100644 index 0000000..1a598b6 --- /dev/null +++ b/heroku-connector/src/test/java/org/springframework/cloud/heroku/HerokuConnectorTestHelper.java @@ -0,0 +1,19 @@ +package org.springframework.cloud.heroku; + + +/** + * + * @author Ramnivas Laddad + * + */ +public class HerokuConnectorTestHelper { + public static String createPostgresUrl(String host, int port, String database, String username, String password) { + String template = "postgres://$username:$password@$host:$port/$database"; + + return template.replace("$username", username). + replace("$password", password). + replace("$host", host). + replace("$port", Integer.toString(port)). + replace("$database", database); + } +} diff --git a/heroku-connector/src/test/resources/log4j.properties b/heroku-connector/src/test/resources/log4j.properties new file mode 100644 index 0000000..11492f1 --- /dev/null +++ b/heroku-connector/src/test/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %40.40c:%4L - %m%n + diff --git a/pom.xml b/pom.xml index f3921dc..00a30f9 100644 --- a/pom.xml +++ b/pom.xml @@ -12,5 +12,6 @@ core spring-service-connector cloudfoundry-connector + heroku-connector diff --git a/spring-service-connector/pom.xml b/spring-service-connector/pom.xml index c3a3c31..73baa2a 100644 --- a/spring-service-connector/pom.xml +++ b/spring-service-connector/pom.xml @@ -15,7 +15,6 @@ ]]> - true 3.0.7.RELEASE 6.0.29