Make postgres service work on Heroku
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
.metadata
|
||||
target
|
||||
Servers
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
20
heroku-connector/README.md
Normal file
20
heroku-connector/README.md
Normal 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
60
heroku-connector/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "://");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.cloud.heroku.HerokuConnector
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.cloud.heroku.PostgresqlServiceInfoCreator
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
6
heroku-connector/src/test/resources/log4j.properties
Normal file
6
heroku-connector/src/test/resources/log4j.properties
Normal 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
|
||||
|
||||
1
pom.xml
1
pom.xml
@@ -12,5 +12,6 @@
|
||||
<module>core</module>
|
||||
<module>spring-service-connector</module>
|
||||
<module>cloudfoundry-connector</module>
|
||||
<module>heroku-connector</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user