From 828df5262faae75a7c5ac306e2fee323c8714954 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Mon, 7 Mar 2016 16:41:26 -0700 Subject: [PATCH] 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 --- .../config/ConfigServerMvcConfiguration.java | 7 +- .../environment/EnvironmentController.java | 94 +++++++++++++------ .../server/resource/ResourceController.java | 44 ++------- .../support/EnvironmentPropertySource.java | 44 +++++++++ .../EnvironmentControllerTests.java | 48 ++++++++-- 5 files changed, 161 insertions(+), 76 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/EnvironmentPropertySource.java diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java index 3531b0c8..836e8dc3 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java @@ -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; } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java index 2006d7e4..9d3bc761 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java @@ -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 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 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 properties = convertToProperties( - labelled(name, profiles, label)); - return getSuccess(getPropertiesString(properties)); + Environment environment = labelled(name, profiles, label); + Map properties = convertToProperties(environment); + String propertiesString = getPropertiesString(properties); + if (resolvePlaceholders) { + propertiesString = resolvePlaceholders(prepareEnvironment(environment), propertiesString); + } + return getSuccess(propertiesString); } @RequestMapping("{name}-{profiles}.json") - public ResponseEntity> jsonProperties(@PathVariable String name, - @PathVariable String profiles) throws Exception { - return labelledJsonProperties(name, profiles, null); + public ResponseEntity 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> labelledJsonProperties( + public ResponseEntity 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 properties = convertToMap(labelled(name, profiles, label)); - return getSuccess(properties, MediaType.APPLICATION_JSON); + Environment environment = labelled(name, profiles, label); + Map 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 properties) { @@ -143,16 +169,20 @@ public class EnvironmentController { @RequestMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" }) public ResponseEntity 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 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 result = convertToMap(labelled(name, profiles, label)); + Environment environment = labelled(name, profiles, label); + Map 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 convertToMap(Environment input) throws BindException { - Map target = new LinkedHashMap(); - PropertiesConfigurationFactory> factory = new PropertiesConfigurationFactory>( - target); + Map target = new LinkedHashMap<>(); + PropertiesConfigurationFactory> factory = new PropertiesConfigurationFactory<>(target); Map data = convertToProperties(input); - LinkedHashMap properties = new LinkedHashMap(); + LinkedHashMap 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> getSuccess(Map body, - MediaType mediaType) { + private ResponseEntity getSuccess(String body, MediaType mediaType) { return new ResponseEntity<>(body, getHttpHeaders(mediaType), HttpStatus.OK); } @@ -268,8 +302,8 @@ public class EnvironmentController { } private Map convertToProperties(Environment profiles) { - Map map = new TreeMap(); - List sources = new ArrayList( + Map map = new TreeMap<>(); + List sources = new ArrayList<>( profiles.getPropertySources()); Collections.reverse(sources); for (PropertySource source : sources) { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java index 960bb0a3..fdd5c948 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java @@ -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 { - - 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; - } - - } - } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/EnvironmentPropertySource.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/EnvironmentPropertySource.java new file mode 100644 index 00000000..2a6a06c2 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/EnvironmentPropertySource.java @@ -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 { + + 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; + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java index aa1c6426..8cb09c14 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java @@ -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 map = new LinkedHashMap(); @@ -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 map = new LinkedHashMap(); + 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);