Make postgres service work on Heroku

This commit is contained in:
Ramnivas Laddad
2013-09-16 19:52:15 -07:00
parent 3c3edcc8d0
commit d9849da774
19 changed files with 464 additions and 6 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.classpath
.project
.settings
.metadata
target
Servers

View File

@@ -17,7 +17,6 @@
<properties>
<jackson.version>2.2.2</jackson.version>
<maven.test.failure.ignore>true</maven.test.failure.ignore>
<junit.version>4.11</junit.version>
<mockito.version>1.9.5</mockito.version>
</properties>

View File

@@ -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);
}
}

View File

@@ -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<String, Object> properties;
public CloudFoundryApplicationInstanceInfo(String instanceId, String appId, Map<String, Object> properties) {
public BasicApplicationInstanceInfo(String instanceId, String appId, Map<String, Object> properties) {
this.instanceId = instanceId;
this.appId = appId;
this.properties = properties;

View File

@@ -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=<myappname> --app <myappname>
If this env variable is not set, the app name will be set to <unknown>.

60
heroku-connector/pom.xml Normal file
View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud</groupId>
<artifactId>heroku-connector</artifactId>
<version>1.0.0.CI-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Spring-Cloud Heroku Connector</name>
<url>http://www.springframework.org</url>
<description>
<![CDATA[
Spring Cloud connector for CloudFoundry
]]>
</description>
<properties>
<junit.version>4.11</junit.version>
<mockito.version>1.8.5</mockito.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-service-connector</artifactId>
<version>1.0.0.CI-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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.
* <p>
* Relies on SPRING_CLOUD_APP_NAME environment being set (using commands such as
* <code>heroku config:add SPRING_CLOUD_APP_NAME=myappname --app myappname</code>
*
* @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 <unknown>");
appname = "<unknown>";
}
String dyno = environment.getValue("DYNO");
Map<String,Object> appProperties = new HashMap<String, Object>();
appProperties.put("port", environment.getValue("PORT"));
appProperties.put("host", environment.getHost());
return new BasicApplicationInstanceInfo(dyno, appname, appProperties);
}
}

View File

@@ -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<String, String> 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);
}
}
}

View File

@@ -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<ServiceInfo> getServiceInfos() {
List<ServiceInfo> serviceInfos = new ArrayList<ServiceInfo>();
for (Map.Entry<String,String> 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<String, String> 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
* <p>
* Returns map whose key is the env key and value is the associated url
* </p>
* @return
*/
private Map<String,String> getServicesData() {
Map<String,String> serviceData = new HashMap<String,String>();
Map<String,String> env = environment.getEnv();
for (Map.Entry<String, String> envEntry : env.entrySet()) {
if (envEntry.getKey().startsWith("HEROKU_POSTGRESQL_")) {
serviceData.put(envEntry.getKey(), envEntry.getValue());
}
}
return serviceData;
}
}

View File

@@ -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<T extends ServiceInfo> implements ServiceInfoCreator<T> {
private String urlProtocol;
public HerokuServiceInfoCreator(String urlProtocol) {
this.urlProtocol = urlProtocol;
}
public boolean accept(Object serviceData) {
@SuppressWarnings("unchecked")
Map.Entry<String,Object> serviceDataEntry = (Map.Entry<String,Object>)serviceData;
return serviceDataEntry.getValue().toString().startsWith(urlProtocol + "://");
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.cloud.heroku;
import org.springframework.cloud.service.common.PostgresqlServiceInfo;
/**
*
* @author Ramnivas Laddad
*
*/
public class PostgresqlServiceInfoCreator extends RelationalServiceInfoCreator<PostgresqlServiceInfo> {
public PostgresqlServiceInfoCreator() {
super("postgres");
}
@Override
public PostgresqlServiceInfo createServiceInfo(String id, String uri) {
return new PostgresqlServiceInfo(id, uri);
}
}

View File

@@ -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<SI extends RelationalServiceInfo> extends HerokuServiceInfoCreator<SI> {
public RelationalServiceInfoCreator(String urlProtocol) {
super(urlProtocol);
}
public abstract SI createServiceInfo(String id, String uri);
public SI createServiceInfo(Object serviceData) {
@SuppressWarnings("unchecked")
Map.Entry<String,String> serviceDataEntry = (Map.Entry<String,String>)serviceData;
return createServiceInfo(serviceDataEntry.getKey(), serviceDataEntry.getValue());
}
}

View File

@@ -0,0 +1 @@
org.springframework.cloud.heroku.HerokuConnector

View File

@@ -0,0 +1 @@
org.springframework.cloud.heroku.PostgresqlServiceInfoCreator

View File

@@ -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<String, String> env = new HashMap<String, String>();
String postgresUrl = createPostgresUrl(host, port, "db", username, password);
env.put("HEROKU_POSTGRESQL_YELLOW_URL", postgresUrl);
when(mockEnvironment.getEnv()).thenReturn(env);
List<ServiceInfo> 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<String,Object> 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("<unknown>", testCloudConnector.getApplicationInstanceInfo().getAppId());
assertEquals("web.1", testCloudConnector.getApplicationInstanceInfo().getInstanceId());
}
private static ServiceInfo getServiceInfo(List<ServiceInfo> serviceInfos, String serviceId) {
for (ServiceInfo serviceInfo : serviceInfos) {
if (serviceInfo.getId().equals(serviceId)) {
return serviceInfo;
}
}
return null;
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -12,5 +12,6 @@
<module>core</module>
<module>spring-service-connector</module>
<module>cloudfoundry-connector</module>
<module>heroku-connector</module>
</modules>
</project>

View File

@@ -15,7 +15,6 @@
]]>
</description>
<properties>
<maven.test.failure.ignore>true</maven.test.failure.ignore>
<spring.framework.version>3.0.7.RELEASE</spring.framework.version>
<tomcat.version>6.0.29</tomcat.version>