Support different formats for config.

Add support for YAML and Properties blobs.
Key/Value is still the default.
This commit is contained in:
Srikalyan Swayampakula
2015-11-18 16:56:42 -08:00
committed by Spencer Gibb
parent 518958e408
commit 1b31908eb4
5 changed files with 289 additions and 23 deletions

View File

@@ -158,6 +158,33 @@ spring:
* `profileSeparator` sets the value of the separator used to separate the profile name in property sources with profiles
[[spring-cloud-consul-config-format]]
== YAML or Properties with Config
It may be more convenient to store a blob of properties in YAML or Properties format as opposed to individual key/value pairs. Set the `spring.cloud.consul.config.format` property to `YAML` or `PROPERTIES`. For example to use YAML:
.bootstrap.yml
----
spring:
cloud:
consul:
config:
format: YAML
----
YAML must be set in the appropriate `data` key in consul. Using the defaults above the keys would look like:
----
config/testApp,dev/data
config/testApp/data
config/application,dev/data
config/application/data
----
You could store a YAML document in any of the keys listed above.
You can change the data key using `spring.cloud.consul.config.data-key`.
[[spring-cloud-consul-bus]]
== Spring Cloud Bus with Consul

View File

@@ -16,11 +16,11 @@
package org.springframework.cloud.consul.config;
import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
/**
* @author Spencer Gibb
*/
@@ -38,5 +38,59 @@ public class ConsulConfigProperties {
@NotEmpty
private String profileSeparator = ",";
@NotEmpty
private Format format = Format.KEY_VALUE;
/**
* If format is Format.PROPERTIES or Format.YAML
* then the following field is used as key to look up consul for configuration.
*/
@NotEmpty
private String dataKey = "data";
private String aclToken;
/**
* There are many ways in which we can specify configuration in consul i.e.,
*
* <ol>
* <li>
* Nested key value style: Where value is either a constant or part of the key (nested).
* For e.g., For following configuration a.b.c=something a.b.d=something else One can
* specify the configuration in consul with key as "../kv/config/application/a/b/c" and
* value as "something" and key as "../kv/config/application/a/b/d" and value as
* "something else"</li>
* <li>
* Entire contents of properties file as value For e.g., For following configuration
* a.b.c=something a.b.d=something else One can specify the configuration in consul with
* key as "../kv/config/application/properties" and value as whole configuration "
* a.b.c=something a.b.d=something else "</li>
* <li>
* as Json or YML. You get it.</li>
* </ol>
*
* This enum specifies the different Formats/styles supported for loading the
* configuration.
*
* @author srikalyan.swayampakula
*/
public static enum Format {
/**
* Indicates that the configuration specified in consul is of type native key values.
*/
KEY_VALUE,
/**
* Indicates that the configuration specified in consul is of property style i.e.,
* value of the consul key would be a list of key=value pairs separated by new lines.
*/
PROPERTIES,
/**
* Indicates that the configuration specified in consul is of YAML style i.e., value
* of the consul key would be YAML format
*/
YAML;
}
}

View File

@@ -16,35 +16,41 @@
package org.springframework.cloud.consul.config;
import static org.springframework.util.Base64Utils.decodeFromString;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.StringUtils;
import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.kv.model.GetValue;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.util.StringUtils;
import static org.springframework.util.Base64Utils.decodeFromString;
/**
* @author Spencer Gibb
*/
public class ConsulPropertySource extends EnumerablePropertySource<ConsulClient> {
private String context;
private String aclToken;
private ConsulConfigProperties configProperties;
private Map<String, String> properties = new LinkedHashMap<>();
private final Map<String, String> properties = new LinkedHashMap<>();
public ConsulPropertySource(String context, ConsulClient source, String aclToken) {
public ConsulPropertySource(String context, ConsulClient source,
ConsulConfigProperties configProperties) {
super(context, source);
this.context = context;
this.aclToken = aclToken;
this.configProperties = configProperties;
if (!this.context.endsWith("/")) {
this.context = this.context + "/";
@@ -53,25 +59,98 @@ public class ConsulPropertySource extends EnumerablePropertySource<ConsulClient>
public void init() {
Response<List<GetValue>> response;
if (aclToken == null) {
if (configProperties.getAclToken() == null) {
response = source.getKVValues(context, QueryParams.DEFAULT);
} else {
response = source.getKVValues(context, aclToken, QueryParams.DEFAULT);
}
List<GetValue> values = response.getValue();
else {
response = source.getKVValues(context, configProperties.getAclToken(),
QueryParams.DEFAULT);
}
if (values != null) {
for (GetValue getValue : values) {
String key = getValue.getKey();
if (!StringUtils.endsWithIgnoreCase(key, "/")) {
key = key.replace(context, "").replace('/', '.');
String value = getDecoded(getValue.getValue());
properties.put(key, value);
final List<GetValue> values = response.getValue();
ConsulConfigProperties.Format format = configProperties.getFormat();
switch (format) {
case KEY_VALUE:
parsePropertiesInKeyValueFormat(values);
break;
case PROPERTIES:
case YAML:
parsePropertiesWithNonKeyValueFormat(values, format);
}
}
/**
* Parses the properties in key value style i.e., values are expected to be either a
* sub key or a constant
*
* @param values
*/
private void parsePropertiesInKeyValueFormat(List<GetValue> values) {
if (values == null) {
return;
}
for (GetValue getValue : values) {
String key = getValue.getKey();
if (!StringUtils.endsWithIgnoreCase(key, "/")) {
key = key.replace(context, "").replace('/', '.');
String value = getDecoded(getValue.getValue());
properties.put(key, value);
}
}
}
/**
* Parses the properties using the format which is not a key value style i.e., either
* java properties style or YAML style
*
* @param values
*/
private void parsePropertiesWithNonKeyValueFormat(List<GetValue> values,
ConsulConfigProperties.Format format) {
if (values == null) {
return;
}
for (GetValue getValue : values) {
String key = getValue.getKey().replace(context, "");
if (configProperties.getDataKey().equals(key)) {
final String value = getDecoded(getValue.getValue());
final Properties props = generateProperties(value, format);
for (String propKey : props.stringPropertyNames()) {
properties.put(propKey, props.getProperty(propKey));
}
}
}
}
private Properties generateProperties(String value, ConsulConfigProperties.Format format) {
final Properties props = new Properties();
if (format == ConsulConfigProperties.Format.PROPERTIES) {
try {
// Must use the ISO-8859-1 encoding because Properties.load(stream)
// expects it.
props.load(new ByteArrayInputStream(value.getBytes("ISO-8859-1")));
}
catch (IOException e) {
throw new IllegalArgumentException(value
+ " can't be encoded using ISO-8859-1");
}
return props;
}
else if (format == ConsulConfigProperties.Format.YAML) {
final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ByteArrayResource(value.getBytes()));
return yaml.getObject();
}
return props;
}
public String getDecoded(String value) {
if (value == null)
return null;

View File

@@ -77,7 +77,7 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator {
}
private ConsulPropertySource create(String context) {
return new ConsulPropertySource(context, consul, properties.getAclToken());
return new ConsulPropertySource(context, consul, properties);
}
private void addProfiles(List<String> contexts, String baseContext,

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.consul.config;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import java.util.Random;
import com.ecwid.consul.v1.ConsulClient;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.consul.ConsulProperties;
/**
* @author Spencer Gibb
*/
public class ConsulPropertySourceTests {
private ConsulClient client;
private ConsulProperties properties;
private String prefix;
private String kvContext;
private String propertiesContext;
@Before
public void setup() {
properties = new ConsulProperties();
prefix = "consulPropertySourceTests" + new Random().nextInt(Integer.MAX_VALUE);
properties.setPrefix(prefix);
client = new ConsulClient(properties.getHost(), properties.getPort());
}
@After
public void teardown() {
client.deleteKVValues(prefix);
}
@Test
public void testKv() {
// key value properties
kvContext = prefix + "/kv";
client.setKVValue(kvContext + "/fooprop", "fookvval");
client.setKVValue(prefix+"/kv"+"/bar/prop", "barkvval");
ConsulPropertySource source = getConsulPropertySource(new ConsulConfigProperties(), kvContext);
assertProperties(source, "fookvval", "barkvval");
}
private void assertProperties(ConsulPropertySource source, String fooval, String barval) {
assertThat("fooprop was wrong", (String)source.getProperty("fooprop"), is(equalTo(fooval)));
assertThat("bar.prop was wrong", (String)source.getProperty("bar.prop"), is(equalTo(barval)));
}
@Test
public void testProperties() {
// properties file property
propertiesContext = prefix + "/properties";
client.setKVValue(propertiesContext+"/data", "fooprop=foopropval\nbar.prop=barpropval");
ConsulConfigProperties configProperties = new ConsulConfigProperties();
configProperties.setFormat(ConsulConfigProperties.Format.PROPERTIES);
ConsulPropertySource source = getConsulPropertySource(configProperties, propertiesContext);
assertProperties(source, "foopropval", "barpropval");
}
@Test
public void testYaml() {
// yaml file property
String yamlContext = prefix + "/yaml";
client.setKVValue(yamlContext+"/data", "fooprop: fooymlval\nbar:\n prop: barymlval");
ConsulConfigProperties configProperties = new ConsulConfigProperties();
configProperties.setFormat(ConsulConfigProperties.Format.YAML);
ConsulPropertySource source = getConsulPropertySource(configProperties, yamlContext);
assertProperties(source, "fooymlval", "barymlval");
}
private ConsulPropertySource getConsulPropertySource(ConsulConfigProperties configProperties, String context) {
ConsulPropertySource source = new ConsulPropertySource(context, client, configProperties);
source.init();
String[] names = source.getPropertyNames();
assertThat("names was null", names, is(notNullValue()));
assertThat("names was wrong size", names.length, is(equalTo(2)));
return source;
}
}