initial local-config connector implementation; provides ServiceData,

still needs converters to ServiceInfo
This commit is contained in:
Christopher Smith
2014-07-10 16:18:31 -05:00
parent f7269144bd
commit c9cd0c9e64
8 changed files with 504 additions and 3 deletions

View File

@@ -8,7 +8,7 @@ Pull requests for also inspecting environment variables are welcome.
Property sources
----------------
This connector first attempts to read the system properties generally and a property named
This connector first attempts to read the system properties generally and a system property named
`spring.cloud.propertiesFile` specifically. If the system properties are not readable
(the security manager denies `checkPropertiesAccess`), then they will be treated as empty.
If a system property named `spring.cloud.propertiesFile` is found, that file will be loaded
@@ -32,8 +32,7 @@ property sources in this order:
- system properties
The last definition of a specific service ID wins. The connector will log a message at
`INFO` to notify of service overrides for the same type of service and at `WARN` if you
override a service ID with a URI to a different type of service.
`WARN` if you override a service ID.
Activating the connector
------------------------

View File

@@ -2,4 +2,5 @@ description = 'Spring Cloud local-configuration connector'
dependencies {
compile project(':spring-cloud-core')
testCompile 'com.github.stefanbirkner:system-rules:1.5.0'
}

View File

@@ -0,0 +1,187 @@
package org.springframework.cloud.localconfig;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.springframework.cloud.AbstractCloudConnector;
import org.springframework.cloud.FallbackServiceInfoCreator;
import org.springframework.cloud.AbstractCloudConnector.KeyValuePair;
import org.springframework.cloud.app.ApplicationInstanceInfo;
import org.springframework.cloud.app.BasicApplicationInstanceInfo;
import org.springframework.cloud.service.BaseServiceInfo;
import org.springframework.cloud.service.FallbackBaseServiceInfoCreator;
/**
*
* @author Christopher Smith
*
*/
public class LocalConfigConnector extends AbstractCloudConnector<KeyValuePair> {
private static final Logger logger = Logger.getLogger(LocalConfigConnector.class.getName());
/*--------------- String constants for property keys ---------------*/
public static final String PROPERTY_PREFIX = "spring.cloud.";
public static final Pattern SERVICE_PROPERTY_PATTERN = Pattern.compile("\\A" + Pattern.quote(PROPERTY_PREFIX) + "(.+)" + "\\Z");
public static final String APP_ID_PROPERTY = PROPERTY_PREFIX + "appId";
public static final String PROPERTIES_FILE_PROPERTY = PROPERTY_PREFIX + "propertiesFile";
/**
* These properties configure the connector itself and aren't service definitions.
*/
public static final List<String> META_PROPERTIES = Collections.unmodifiableList(
Arrays.asList(new String[] { APP_ID_PROPERTY, PROPERTIES_FILE_PROPERTY }));
/*--------------- sources for service-definition properties ---------------*/
static Properties programmaticProperties = new Properties();
private Properties fileProperties = null;
/*--------------- API implementation ---------------*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public LocalConfigConnector() {
super((Class) LocalConfigServiceInfoCreator.class);
}
/**
* Returns {@code true} if a property named {@code spring.cloud.appId} is present in any of the property sources.
* On the first call, attempts to load properties from a file specified in {@code spring.cloud.propertiesFile}.
*/
@Override
public boolean isInMatchingCloud() {
if (fileProperties == null)
readFileProperties();
return findProperty(APP_ID_PROPERTY) != null;
}
@Override
public ApplicationInstanceInfo getApplicationInstanceInfo() {
return new BasicApplicationInstanceInfo(UUID.randomUUID().toString(), findProperty(APP_ID_PROPERTY),
Collections.<String, Object> emptyMap());
}
@Override
protected List<KeyValuePair> getServicesData() {
LinkedHashMap<String, Properties> propertySources = new LinkedHashMap<String, Properties>();
propertySources.put("programmatic properties", programmaticProperties);
propertySources.put("properties from file", fileProperties);
try {
propertySources.put("system properties", System.getProperties());
} catch (SecurityException e) {
logger.log(Level.WARNING,
"couldn't read system properties; no service definitions from system properties will be applied", e);
}
return LocalConfigUtil.readServicesData(propertySources);
}
@Override
protected FallbackServiceInfoCreator<BaseServiceInfo, KeyValuePair> getFallbackServiceInfoCreator() {
return new FallbackBaseServiceInfoCreator();
}
/*--------------- methods for manipulating properties and sources ---------------*/
/**
* Adds properties to be scanned from the supplied {@link InputStream}, overwriting
* existing properties with the same name. Closes the stream after loading.
*
* @param propertiesInputStream
* a property list
* @throws IOException
* if the underlying load operation throws an exception
*/
public static void supplyProperties(final InputStream propertiesInputStream) throws IOException {
programmaticProperties.load(propertiesInputStream);
propertiesInputStream.close();
}
/**
* Checks for the presence of a supplied or system property named {@code spring.cloud.propertiesFile}. If the property
* is present, load its contents into {@link #fileProperties}. If there's a problem, log but continue.
*/
private void readFileProperties() {
fileProperties = new Properties();
logger.fine("looking for a properties file");
String filename = null;
filename = programmaticProperties.getProperty(PROPERTIES_FILE_PROPERTY);
try {
filename = System.getProperty(PROPERTIES_FILE_PROPERTY, filename);
} catch (SecurityException e) {
logSystemReadException(PROPERTIES_FILE_PROPERTY, e);
return;
}
if (filename == null) {
logger.info("did not find a system property " + PROPERTIES_FILE_PROPERTY);
return;
}
logger.info("loading properties from file " + filename);
try {
InputStream fis = openFile(filename);
fileProperties.load(fis);
} catch (IOException e) {
logger.log(Level.SEVERE, "exception while loading properties from file " + filename, e);
return;
}
logger.info("properties loaded successfully");
}
/**
* Broken out into a separate method for mocking the filesystem.
* @param filename the file to open
* @return a {@code FileInputStream} to the file
* @throws IOException if opening the file throws
*/
InputStream openFile(String filename) throws IOException {
return new FileInputStream(filename);
}
/**
* Look for a specific property in programmatically-supplied properties, properties from a file,
* or the system properties. Last source wins.
*
* @param key
* the property to look for
* @return the highest-priority value for the key, or {@code null} if the key is not found
*/
private String findProperty(String key) {
String value = programmaticProperties.getProperty(key);
value = fileProperties.getProperty(key, value);
try {
value = System.getProperty(key, value);
} catch (SecurityException e) {
logSystemReadException(key, e);
}
return value;
}
private static void logSystemReadException(String key, SecurityException e) {
logger.log(Level.WARNING, "couldn't read system property " + key, e);
}
}

View File

@@ -0,0 +1,11 @@
package org.springframework.cloud.localconfig;
import org.springframework.cloud.service.ServiceInfo;
import org.springframework.cloud.service.UriBasedServiceInfoCreator;
public abstract class LocalConfigServiceInfoCreator<SI extends ServiceInfo> extends UriBasedServiceInfoCreator<SI> {
protected LocalConfigServiceInfoCreator(String uriScheme) {
super(uriScheme);
}
}

View File

@@ -0,0 +1,81 @@
package org.springframework.cloud.localconfig;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import org.springframework.cloud.AbstractCloudConnector.KeyValuePair;
public final class LocalConfigUtil {
private static final Logger logger = Logger.getLogger(LocalConfigConnector.class.getName());
private LocalConfigUtil() {
}
static List<KeyValuePair> readServicesData(LinkedHashMap<String, Properties> propertySources) {
// we'll turn this into KVPs to return but need to eliminate duplicates first
Map<String, String> collectedServices = new HashMap<String, String>();
// iterate over the property sources in order, extracting matching properties
for (Map.Entry<String, Properties> propertySource : propertySources.entrySet()) {
logger.info("reading services from " + propertySource.getValue());
Map<String, String> services = readServices(propertySource.getValue());
// add each of the found services to the list, warning about duplicates
for (Map.Entry<String, String> service : services.entrySet()) {
String oldUri = collectedServices.put(service.getKey(), service.getValue());
if (oldUri == null)
logger.info("added service '" + service.getKey() + "' from " + propertySource.getKey());
else
logger.warning("replaced service '" + service.getKey() + "' with new URI from " + propertySource.getKey());
}
}
// now we have a collated set of service IDs and URIs
List<KeyValuePair> serviceData = new ArrayList<KeyValuePair>(collectedServices.size());
for (Map.Entry<String, String> serviceInfo : collectedServices.entrySet()) {
serviceData.add(new KeyValuePair(serviceInfo.getKey(), serviceInfo.getValue()));
}
return serviceData;
}
/**
* Goes through a {@code Properties} object, finding all service definitions (properties
* prefixed with {@code spring.cloud.} but not in {@code META_PROPERTIES}) and collects {@code (id,URI)} pairs.
*
* @param properties
* the {@code Properties} object to read
* @return all of the service definitions found
*/
static Map<String, String> readServices(Properties properties) {
Map<String, String> services = new HashMap<String, String>();
for (String propertyName : properties.stringPropertyNames()) {
if (LocalConfigConnector.META_PROPERTIES.contains(propertyName)) {
logger.finer("skipping meta property " + propertyName);
continue;
}
Matcher m = LocalConfigConnector.SERVICE_PROPERTY_PATTERN.matcher(propertyName);
if (!m.matches()) {
logger.finest("skipping non-Spring-Cloud property " + propertyName);
continue;
}
String serviceId = m.group(1);
String serviceUri = properties.getProperty(propertyName);
// no URI here because they will contain passwords
logger.fine("found service URI for service " + serviceId);
services.put(serviceId, serviceUri);
}
return services;
}
}

View File

@@ -0,0 +1,145 @@
package org.springframework.cloud.localconfig;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Properties;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.ClearSystemProperties;
import org.junit.contrib.java.lang.system.ProvideSystemProperty;
import org.springframework.cloud.AbstractCloudConnector.KeyValuePair;
public class LocalConfigConnectorTest {
static final Charset UTF_8 = Charset.forName("UTF-8");
public static final String APP_ID_1 = "appId1";
public static final String APP_ID_1_PROPERTY = LocalConfigConnector.APP_ID_PROPERTY + ": " + APP_ID_1;
public static final String APP_ID_2 = "appId2";
public static final String APP_ID_2_PROPERTY = LocalConfigConnector.APP_ID_PROPERTY + ": " + APP_ID_2;
public static final String PROPERTY_FILE_NAME = "propFile";
public static final String PROPERTY_FILE_PROPERTY = LocalConfigConnector.PROPERTIES_FILE_PROPERTY + ": " + PROPERTY_FILE_NAME;
public static class AppIdTest {
private LocalConfigConnector connector;
@Before
public void setup() {
connector = new LocalConfigConnector();
}
@After
public void clearProperties() {
LocalConfigConnector.programmaticProperties = new Properties();
}
@Rule
public final ClearSystemProperties NO_APP_ID_PROPERTY = new ClearSystemProperties(LocalConfigConnector.APP_ID_PROPERTY);
@Test
public void testNoAppIdAnywhere() {
assertFalse(connector.isInMatchingCloud());
}
@Test
public void testProgrammaticAppId() throws IOException {
LocalConfigConnector.supplyProperties(new ByteArrayInputStream(APP_ID_1_PROPERTY.getBytes(UTF_8)));
assertTrue(connector.isInMatchingCloud());
assertEquals(APP_ID_1, connector.getApplicationInstanceInfo().getAppId());
}
@Test
public void testProgrammaticAndFileAppIds() throws IOException {
LocalConfigConnector.supplyProperties(new ByteArrayInputStream(APP_ID_1_PROPERTY.getBytes(UTF_8)));
LocalConfigConnector.supplyProperties(new ByteArrayInputStream(PROPERTY_FILE_PROPERTY.getBytes(UTF_8)));
LocalConfigConnector stubConnector = new LocalConfigConnector() {
@Override
InputStream openFile(String filename) throws IOException {
assertEquals(PROPERTY_FILE_NAME, filename);
return new ByteArrayInputStream(APP_ID_2_PROPERTY.getBytes(UTF_8));
};
};
assertTrue(stubConnector.isInMatchingCloud());
assertEquals(APP_ID_2, stubConnector.getApplicationInstanceInfo().getAppId());
}
@Test
public void testProgrammaticFilenamePlusSystemAppId() throws IOException {
LocalConfigConnector.supplyProperties(new ByteArrayInputStream(PROPERTY_FILE_PROPERTY.getBytes(UTF_8)));
LocalConfigConnector stubConnector = new LocalConfigConnector() {
@Override
InputStream openFile(String filename) throws IOException {
assertEquals(PROPERTY_FILE_NAME, filename);
return new ByteArrayInputStream(APP_ID_2_PROPERTY.getBytes(UTF_8));
};
};
System.setProperty(LocalConfigConnector.APP_ID_PROPERTY, "helloApp");
assertTrue(stubConnector.isInMatchingCloud());
assertEquals("helloApp", stubConnector.getApplicationInstanceInfo().getAppId());
}
}
private LocalConfigConnector connector;
InputStream propertiesFile;
@Before
public void setup() {
connector = new LocalConfigConnector();
propertiesFile = LocalConfigConnectorTest.class.getClassLoader().getResourceAsStream("localconfig.properties");
}
@After
public void cleanup() throws IOException {
LocalConfigConnector.programmaticProperties = new Properties();
propertiesFile.close();
}
@Test
public void testLoadFromFile() throws IOException {
LocalConfigConnector.supplyProperties(propertiesFile);
assertTrue(connector.isInMatchingCloud());
assertEquals("testApp", connector.getApplicationInstanceInfo().getAppId());
List<KeyValuePair> services = connector.getServicesData();
assertEquals(2, services.size());
for (KeyValuePair service : services)
if ("foo".equals(service.getKey()))
assertEquals("bar", service.getValue());
}
@Rule
public ProvideSystemProperty BAZ_PROPERTY = new ProvideSystemProperty("spring.cloud.baz", "inline!");
@Test
public void testLoadFromInputStreamWithOverride() throws IOException {
LocalConfigConnector.supplyProperties(propertiesFile);
assertTrue(connector.isInMatchingCloud());
assertEquals("testApp", connector.getApplicationInstanceInfo().getAppId());
List<KeyValuePair> services = connector.getServicesData();
assertEquals(2, services.size());
for(KeyValuePair service: services)
if("baz".equals(service.getKey()))
assertEquals("inline!", service.getValue());
}
}

View File

@@ -0,0 +1,74 @@
package org.springframework.cloud.localconfig;
import static org.junit.Assert.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.AbstractCloudConnector.KeyValuePair;
public class LocalConfigUtilTest {
private Properties first, second;
private LinkedHashMap<String, Properties> propertySources;
@Before
public void initProperties(){
first = new Properties();
second = new Properties();
propertySources = new LinkedHashMap<String, Properties>();
propertySources.put("first", first);
propertySources.put("second", second);
}
@Test
public void testPropertyParsing() {
first.setProperty("spring.cloud.appId", "should skip me because I'm meta");
first.setProperty("spring.cloud.service1", "one");
first.setProperty("spring.cloud.", "should skip me because I don't have an ID");
first.setProperty("spring.cloud.service.two", "two");
first.setProperty("foobar", "should skip me because I don't match the prefix");
Map<String, String> services = LocalConfigUtil.readServices(first);
assertEquals(2, services.size());
assertEquals("one", services.get("service1"));
assertEquals("two", services.get("service.two"));
}
@Test
public void testCollation() {
first.setProperty("spring.cloud.first", "firstUri");
second.setProperty("spring.cloud.second", "secondUri");
List<KeyValuePair> serviceData = LocalConfigUtil.readServicesData(propertySources);
assertEquals(2, serviceData.size());
boolean foundFirst = false;
for(KeyValuePair kvp : serviceData) {
if(kvp.getKey().equals("first")) {
assertEquals("firstUri", kvp.getValue());
foundFirst = true;
}
}
assertTrue(foundFirst);
}
@Test
public void testOverride() {
first.setProperty("spring.cloud.duplicate", "firstUri");
second.setProperty("spring.cloud.duplicate", "secondUri");
List<KeyValuePair> serviceData = LocalConfigUtil.readServicesData(propertySources);
assertEquals(1, serviceData.size());
KeyValuePair kvp = serviceData.get(0);
assertEquals("duplicate", kvp.getKey());
assertEquals("secondUri", kvp.getValue());
}
}

View File

@@ -0,0 +1,3 @@
spring.cloud.appId: testApp
spring.cloud.foo: bar
spring.cloud.baz: quux