initial local-config connector implementation; provides ServiceData,
still needs converters to ServiceInfo
This commit is contained in:
@@ -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
|
||||
------------------------
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
spring.cloud.appId: testApp
|
||||
spring.cloud.foo: bar
|
||||
spring.cloud.baz: quux
|
||||
Reference in New Issue
Block a user