Add resolvePlaceholders request parameter to EnvironmentController.

Specifically to the `{json|yaml|properties}` endpoints to support sidecar or non-jvm apps to have placeholders resolved automatically.

See https://github.com/spring-cloud/spring-cloud-netflix/issues/877
This commit is contained in:
Spencer Gibb
2016-03-07 16:41:26 -07:00
parent 70819b1a8c
commit 828df5262f
5 changed files with 161 additions and 76 deletions

View File

@@ -29,6 +29,8 @@ import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Dave Syer
* @author Roy Clarkson
@@ -49,6 +51,9 @@ public class ConfigServerMvcConfiguration extends WebMvcConfigurerAdapter {
@Autowired(required = false)
private EnvironmentEncryptor environmentEncryptor;
@Autowired(required = false)
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("properties", MediaType.valueOf("text/plain"));
@@ -58,7 +63,7 @@ public class ConfigServerMvcConfiguration extends WebMvcConfigurerAdapter {
@Bean
public EnvironmentController environmentController() {
EnvironmentController controller = new EnvironmentController(encrypted());
EnvironmentController controller = new EnvironmentController(encrypted(), this.objectMapper);
controller.setStripDocumentFromYaml(this.server.isStripDocumentFromYaml());
return controller;
}

View File

@@ -25,7 +25,6 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.bind.PropertiesConfigurationFactory;
@@ -43,11 +42,16 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.springframework.cloud.config.server.support.EnvironmentPropertySource.*;
/**
* @author Dave Syer
* @author Spencer Gibb
@@ -63,11 +67,17 @@ public class EnvironmentController {
private static final String MAP_PREFIX = "map";
private EnvironmentRepository repository;
private ObjectMapper objectMapper;
private boolean stripDocument = true;
public EnvironmentController(EnvironmentRepository repository) {
this(repository, new ObjectMapper());
}
public EnvironmentController(EnvironmentRepository repository, ObjectMapper objectMapper) {
this.repository = repository;
this.objectMapper = objectMapper;
}
/**
@@ -100,33 +110,49 @@ public class EnvironmentController {
@RequestMapping("/{name}-{profiles}.properties")
public ResponseEntity<String> properties(@PathVariable String name,
@PathVariable String profiles) throws IOException {
return labelledProperties(name, profiles, null);
@PathVariable String profiles,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws IOException {
return labelledProperties(name, profiles, null, resolvePlaceholders);
}
@RequestMapping("/{label}/{name}-{profiles}.properties")
public ResponseEntity<String> labelledProperties(@PathVariable String name,
@PathVariable String profiles, @PathVariable String label)
throws IOException {
@PathVariable String profiles, @PathVariable String label,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws IOException {
validateNameAndProfiles(name, profiles);
Map<String, Object> properties = convertToProperties(
labelled(name, profiles, label));
return getSuccess(getPropertiesString(properties));
Environment environment = labelled(name, profiles, label);
Map<String, Object> properties = convertToProperties(environment);
String propertiesString = getPropertiesString(properties);
if (resolvePlaceholders) {
propertiesString = resolvePlaceholders(prepareEnvironment(environment), propertiesString);
}
return getSuccess(propertiesString);
}
@RequestMapping("{name}-{profiles}.json")
public ResponseEntity<Map<String, Object>> jsonProperties(@PathVariable String name,
@PathVariable String profiles) throws Exception {
return labelledJsonProperties(name, profiles, null);
public ResponseEntity<String> jsonProperties(@PathVariable String name,
@PathVariable String profiles,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
return labelledJsonProperties(name, profiles, null, resolvePlaceholders);
}
@RequestMapping("/{label}/{name}-{profiles}.json")
public ResponseEntity<Map<String, Object>> labelledJsonProperties(
public ResponseEntity<String> labelledJsonProperties(
@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) throws Exception {
@PathVariable String label,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
validateNameAndProfiles(name, profiles);
Map<String, Object> properties = convertToMap(labelled(name, profiles, label));
return getSuccess(properties, MediaType.APPLICATION_JSON);
Environment environment = labelled(name, profiles, label);
Map<String, Object> properties = convertToMap(environment);
String json = this.objectMapper.writeValueAsString(properties);
if (resolvePlaceholders) {
json = resolvePlaceholders(prepareEnvironment(environment), json);
}
return getSuccess(json, MediaType.APPLICATION_JSON);
}
private String getPropertiesString(Map<String, Object> properties) {
@@ -143,16 +169,20 @@ public class EnvironmentController {
@RequestMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" })
public ResponseEntity<String> yaml(@PathVariable String name,
@PathVariable String profiles) throws Exception {
return labelledYaml(name, profiles, null);
@PathVariable String profiles,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
return labelledYaml(name, profiles, null, resolvePlaceholders);
}
@RequestMapping({ "/{label}/{name}-{profiles}.yml",
"/{label}/{name}-{profiles}.yaml" })
@RequestMapping({ "/{label}/{name}-{profiles}.yml", "/{label}/{name}-{profiles}.yaml" })
public ResponseEntity<String> labelledYaml(@PathVariable String name,
@PathVariable String profiles, @PathVariable String label) throws Exception {
@PathVariable String profiles, @PathVariable String label,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
validateNameAndProfiles(name, profiles);
Map<String, Object> result = convertToMap(labelled(name, profiles, label));
Environment environment = labelled(name, profiles, label);
Map<String, Object> result = convertToMap(environment);
if (this.stripDocument && result.size() == 1
&& result.keySet().iterator().next().equals("document")) {
Object value = result.get("document");
@@ -163,15 +193,20 @@ public class EnvironmentController {
return getSuccess(new Yaml().dumpAs(value, Tag.STR, FlowStyle.BLOCK));
}
}
return getSuccess(new Yaml().dumpAsMap(result));
String yaml = new Yaml().dumpAsMap(result);
if (resolvePlaceholders) {
yaml = resolvePlaceholders(prepareEnvironment(environment), yaml);
}
return getSuccess(yaml);
}
private Map<String, Object> convertToMap(Environment input) throws BindException {
Map<String, Object> target = new LinkedHashMap<String, Object>();
PropertiesConfigurationFactory<Map<String, Object>> factory = new PropertiesConfigurationFactory<Map<String, Object>>(
target);
Map<String, Object> target = new LinkedHashMap<>();
PropertiesConfigurationFactory<Map<String, Object>> factory = new PropertiesConfigurationFactory<>(target);
Map<String, Object> data = convertToProperties(input);
LinkedHashMap<String, Object> properties = new LinkedHashMap<String, Object>();
LinkedHashMap<String, Object> properties = new LinkedHashMap<>();
for (String key : data.keySet()) {
properties.put(MAP_PREFIX + "." + key, data.get(key));
}
@@ -213,8 +248,7 @@ public class EnvironmentController {
HttpStatus.OK);
}
private ResponseEntity<Map<String, Object>> getSuccess(Map<String, Object> body,
MediaType mediaType) {
private ResponseEntity<String> getSuccess(String body, MediaType mediaType) {
return new ResponseEntity<>(body, getHttpHeaders(mediaType), HttpStatus.OK);
}
@@ -268,8 +302,8 @@ public class EnvironmentController {
}
private Map<String, Object> convertToProperties(Environment profiles) {
Map<String, Object> map = new TreeMap<String, Object>();
List<PropertySource> sources = new ArrayList<PropertySource>(
Map<String, Object> map = new TreeMap<>();
List<PropertySource> sources = new ArrayList<>(
profiles.getPropertySources());
Collections.reverse(sources);
for (PropertySource source : sources) {

View File

@@ -19,11 +19,8 @@ package org.springframework.cloud.config.server.resource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Map;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
@@ -36,6 +33,9 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.cloud.config.server.support.EnvironmentPropertySource.prepareEnvironment;
import static org.springframework.cloud.config.server.support.EnvironmentPropertySource.resolvePlaceholders;
/**
* An HTTP endpoint for serving up templated plain text resources from an underlying
* repository. Can be used to supply config files for consumption by a wide variety of
@@ -65,24 +65,19 @@ public class ResourceController {
public synchronized String resolve(@PathVariable String name,
@PathVariable String profile, @PathVariable String label,
@PathVariable String path) throws IOException {
StandardEnvironment environment = new StandardEnvironment();
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}
environment.getPropertySources().addAfter(
StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
new EnvironmentPropertySource(
this.environmentRepository.findOne(name, profile, label)));
StandardEnvironment environment = prepareEnvironment(
this.environmentRepository.findOne(name, profile, label));
// ensure InputStream will be closed to prevent file locks on Windows
try (InputStream is = this.resourceRepository.findOne(name, profile, label, path)
.getInputStream()) {
String text = StreamUtils.copyToString(is, Charset.forName("UTF-8"));
// Mask out escaped placeholders
text = text.replace("\\${", "$_{");
return environment.resolvePlaceholders(text).replace("$_{", "${");
return resolvePlaceholders(environment, text);
}
}
@@ -90,16 +85,13 @@ public class ResourceController {
public synchronized byte[] binary(@PathVariable String name,
@PathVariable String profile, @PathVariable String label,
@PathVariable String path) throws IOException {
StandardEnvironment environment = new StandardEnvironment();
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}
environment.getPropertySources().addAfter(
StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
new EnvironmentPropertySource(
this.environmentRepository.findOne(name, profile, label)));
//TODO: is this line needed for side effects?
prepareEnvironment(this.environmentRepository.findOne(name, profile, label));
try (InputStream is = this.resourceRepository.findOne(name, profile, label, path)
.getInputStream()) {
return StreamUtils.copyToByteArray(is);
@@ -111,24 +103,4 @@ public class ResourceController {
public void notFound(NoSuchResourceException e) {
}
private static class EnvironmentPropertySource extends PropertySource<Environment> {
public EnvironmentPropertySource(Environment sources) {
super("cloudEnvironment", sources);
}
@Override
public Object getProperty(String name) {
for (org.springframework.cloud.config.environment.PropertySource source : getSource()
.getPropertySources()) {
Map<?, ?> map = source.getSource();
if (map.containsKey(name)) {
return map.get(name);
}
}
return null;
}
}
}

View File

@@ -0,0 +1,44 @@
package org.springframework.cloud.config.server.support;
import java.util.Map;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
/**
* @author Spencer Gibb
*/
public class EnvironmentPropertySource extends PropertySource<Environment> {
public static StandardEnvironment prepareEnvironment(Environment environment) {
StandardEnvironment standardEnvironment = new StandardEnvironment();
standardEnvironment.getPropertySources().addAfter(
StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
new EnvironmentPropertySource(environment));
return standardEnvironment;
}
public static String resolvePlaceholders(StandardEnvironment preparedEnvironment, String text) {
// Mask out escaped placeholders
text = text.replace("\\${", "$_{");
return preparedEnvironment.resolvePlaceholders(text).replace("$_{", "${");
}
public EnvironmentPropertySource(Environment sources) {
super("cloudEnvironment", sources);
}
@Override
public Object getProperty(String name) {
for (org.springframework.cloud.config.environment.PropertySource source : getSource()
.getPropertySources()) {
Map<?, ?> map = source.getSource();
if (map.containsKey(name)) {
return map.get(name);
}
}
return null;
}
}

View File

@@ -24,6 +24,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@@ -62,7 +63,7 @@ public class EnvironmentControllerTests {
map.put("a.b.c", "d");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("a:\n b:\n c: d\n", yaml);
}
@@ -74,10 +75,17 @@ public class EnvironmentControllerTests {
this.environment.addFirst(new PropertySource("two", Collections.singletonMap("a.b.c",
"e")));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("a:\n b:\n c: e\n", yaml);
}
@Test
public void placeholdersResolvedInYaml() throws Exception {
whenPlaceholders();
String yaml = this.controller.yaml("foo", "bar", true).getBody();
assertEquals("a:\n b:\n c: bar\nfoo: bar\n", yaml);
}
@Test
public void arrayInYaml() throws Exception {
Map<String, Object> map = new LinkedHashMap<String, Object>();
@@ -85,7 +93,7 @@ public class EnvironmentControllerTests {
map.put("a.b[1]", "d");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("a:\n b:\n - c\n - d\n", yaml);
}
@@ -95,7 +103,7 @@ public class EnvironmentControllerTests {
map.put("document", "blah");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("blah\n", yaml);
}
@@ -106,7 +114,7 @@ public class EnvironmentControllerTests {
map.put("document[1]", "d");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("- c\n- d\n", yaml);
}
@@ -117,7 +125,7 @@ public class EnvironmentControllerTests {
map.put("document[1].a", "d");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("- a: c\n- a: d\n", yaml);
}
@@ -129,7 +137,7 @@ public class EnvironmentControllerTests {
map.put("a.b[1].c", "d");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertTrue("Wrong output: " + yaml,
"a:\n b:\n - d: e\n c: d\n - c: d\n".equals(yaml)
|| "a:\n b:\n - c: d\n d: e\n - c: d\n".equals(yaml));
@@ -142,7 +150,7 @@ public class EnvironmentControllerTests {
map.put("b[1].c", "d");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("b:\n- c: d\n- c: d\n", yaml);
}
@@ -153,10 +161,32 @@ public class EnvironmentControllerTests {
map.put("x.a.b[1].c", "d");
this.environment.add(new PropertySource("one", map));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
String yaml = this.controller.yaml("foo", "bar").getBody();
String yaml = this.controller.yaml("foo", "bar", false).getBody();
assertEquals("x:\n a:\n b:\n - c: d\n - c: d\n", yaml);
}
@Test
public void placeholdersResolvedInProperties() throws Exception {
whenPlaceholders();
String text = this.controller.properties("foo", "bar", true).getBody();
assertEquals("a.b.c: bar\nfoo: bar", text);
}
@Test
public void placeholdersResolvedInJson() throws Exception {
whenPlaceholders();
String json = this.controller.jsonProperties("foo", "bar", true).getBody();
assertEquals("{\"a\":{\"b\":{\"c\":\"bar\"}},\"foo\":\"bar\"}", json);
}
private void whenPlaceholders() {
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("foo", "bar");
this.environment.add(new PropertySource("one", map));
this.environment.addFirst(new PropertySource("two", Collections.singletonMap("a.b.c", "${foo}")));
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);
}
@Test
public void mappingForEnvironment() throws Exception {
Mockito.when(this.repository.findOne("foo", "bar", null)).thenReturn(this.environment);