Rename volumemount -> configtree

Closes gh-22941
pull/22948/head
Phillip Webb 4 years ago
parent 5579f9d1c4
commit 310ef6e999

@ -675,7 +675,7 @@ Locations will be processed in the order that they are defined, with later impor
[TIP]
====
Spring Boot includes pluggable API that allows various different location addresses to be supported.
By default you can import Java Properties, YAML and volume mounts.
By default you can import Java Properties, YAML and "`<<boot-features-external-config-files-configtree, configuration trees>>`".
Third-party jars can offer support for additional technologies (there's no requirement for files to be local).
For example, you can imagine config data being from external stores such as Consul, Apache ZooKeeper or Netflix Archaius.
@ -685,8 +685,8 @@ If you want to support your own locations, see the `ConfigDataLocationResolver`
[[boot-features-external-config-files-voumemounts]]
==== Using Volume Mount Properties
[[boot-features-external-config-files-configtree]]
==== Using Configuration Trees
When running applications on a cloud platform (such as Kubernetes) you often need to read config values that the platform supplies.
It's not uncommon to use environment variables for such purposes, but this can have drawbacks, especially if the value is supposed to be kept secret.
@ -696,10 +696,10 @@ For example, Kubernetes can volume mount both https://kubernetes.io/docs/tasks/c
There are two common volume mount patterns that can be use:
. A single file contains a complete set of properties (usually written as YAML).
. Multiple files are written to a directory with the filename becoming the '`key`' and the contents becoming the '`value`'.
. Multiple files are written to a directory tree, with the filename becoming the '`key`' and the contents becoming the '`value`'.
For the first case, you can import the YAML or Properties file directly using `spring.config.import` as described <<boot-features-external-config-files-importing,above>>.
For the second case, you need to use the `volumemount:` prefix so that Spring Boot knows it needs to expose all the files as properties.
For the second case, you need to use the `configtree:` prefix so that Spring Boot knows it needs to expose all the files as properties.
As an example, let's imagine that Kubernetes has mounted the following volume:
@ -718,12 +718,12 @@ To import these properties, you can add the following to your `application.prope
[source,properties,indent=0]
----
spring.config.import=volumemount:/etc/config
spring.config.import=configtree:/etc/config
----
You can then access or inject `myapp.username` and `myapp.password` properties from the `Environment` in the usual way.
TIP: Volume mounted values can be bound to both string `String` and `byte[]` types depending on the contents expected.
TIP: Configuration tree values can be bound to both string `String` and `byte[]` types depending on the contents expected.

@ -20,21 +20,21 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import org.springframework.boot.env.VolumeMountDirectoryPropertySource;
import org.springframework.boot.env.ConfigTreePropertySource;
/**
* {@link ConfigDataLoader} for directory locations mounted as volumes.
* {@link ConfigDataLoader} for config tree locations.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class VolumeMountConfigDataLoader implements ConfigDataLoader<VolumeMountConfigDataLocation> {
class ConfigTreeConfigDataLoader implements ConfigDataLoader<ConfigTreeConfigDataLocation> {
@Override
public ConfigData load(VolumeMountConfigDataLocation location) throws IOException {
public ConfigData load(ConfigTreeConfigDataLocation location) throws IOException {
Path path = location.getPath();
String name = "Volume mount config '" + path + "'";
VolumeMountDirectoryPropertySource source = new VolumeMountDirectoryPropertySource(name, path);
String name = "Config tree '" + path + "'";
ConfigTreePropertySource source = new ConfigTreePropertySource(name, path);
return new ConfigData(Collections.singletonList(source));
}

@ -20,19 +20,21 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import org.springframework.boot.env.ConfigTreePropertySource;
import org.springframework.util.Assert;
/**
* {@link ConfigDataLocation} backed by a directory mounted as a volume.
* {@link ConfigDataLocation} backed by a config tree directory.
*
* @author Madhura Bhave
* @author Phillip Webb
* @see ConfigTreePropertySource
*/
class VolumeMountConfigDataLocation extends ConfigDataLocation {
class ConfigTreeConfigDataLocation extends ConfigDataLocation {
private final Path path;
VolumeMountConfigDataLocation(String path) {
ConfigTreeConfigDataLocation(String path) {
Assert.notNull(path, "Path must not be null");
this.path = Paths.get(path).toAbsolutePath();
}
@ -49,7 +51,7 @@ class VolumeMountConfigDataLocation extends ConfigDataLocation {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
VolumeMountConfigDataLocation other = (VolumeMountConfigDataLocation) obj;
ConfigTreeConfigDataLocation other = (ConfigTreeConfigDataLocation) obj;
return Objects.equals(this.path, other.path);
}
@ -60,7 +62,7 @@ class VolumeMountConfigDataLocation extends ConfigDataLocation {
@Override
public String toString() {
return "volume mount [" + this.path + "]";
return "config tree [" + this.path + "]";
}
}

@ -20,15 +20,14 @@ import java.util.Collections;
import java.util.List;
/**
* {@link ConfigDataLocationResolver} for volume mounted locations such as Kubernetes
* ConfigMaps and Secrets.
* {@link ConfigDataLocationResolver} for config tree locations.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class VolumeMountConfigDataLocationResolver implements ConfigDataLocationResolver<VolumeMountConfigDataLocation> {
class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationResolver<ConfigTreeConfigDataLocation> {
private static final String PREFIX = "volumemount:";
private static final String PREFIX = "configtree:";
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
@ -36,8 +35,8 @@ class VolumeMountConfigDataLocationResolver implements ConfigDataLocationResolve
}
@Override
public List<VolumeMountConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location) {
VolumeMountConfigDataLocation resolved = new VolumeMountConfigDataLocation(location.substring(PREFIX.length()));
public List<ConfigTreeConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location) {
ConfigTreeConfigDataLocation resolved = new ConfigTreeConfigDataLocation(location.substring(PREFIX.length()));
return Collections.singletonList(resolved);
}

@ -46,16 +46,16 @@ import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
* {@link PropertySource} backed by a directory that contains files for each value. The
* {@link PropertySource} will recursively scan a given source directory and expose a
* {@link PropertySource} backed by a directory tree that contains files for each value.
* The {@link PropertySource} will recursively scan a given source directory and expose a
* property for each file found. The property name will be the filename, and the property
* value will be the contents of the file.
* <p>
* Directories are only scanned when the source is first created. The directory is not
* monitored for updates, so files should not be added or removed. However, the contents
* of a file can be updated as long as the property source was created with a
* {@link Option#ALWAYS_READ} option. Nested folders are included in the source, but with
* a {@code '.'} rather than {@code '/'} used as the path separator.
* {@link Option#ALWAYS_READ} option. Nested directories are included in the source, but
* with a {@code '.'} rather than {@code '/'} used as the path separator.
* <p>
* Property values are returned as {@link Value} instances which allows them to be treated
* either as an {@link InputStreamSource} or as a {@link CharSequence}. In addition, if
@ -69,7 +69,7 @@ import org.springframework.util.StringUtils;
* @author Phillip Webb
* @since 2.4.0
*/
public class VolumeMountDirectoryPropertySource extends EnumerablePropertySource<Path> implements OriginLookup<String> {
public class ConfigTreePropertySource extends EnumerablePropertySource<Path> implements OriginLookup<String> {
private static final int MAX_DEPTH = 100;
@ -80,25 +80,25 @@ public class VolumeMountDirectoryPropertySource extends EnumerablePropertySource
private final Set<Option> options;
/**
* Create a new {@link VolumeMountDirectoryPropertySource} instance.
* Create a new {@link ConfigTreePropertySource} instance.
* @param name the name of the property source
* @param sourceDirectory the underlying source directory
*/
public VolumeMountDirectoryPropertySource(String name, Path sourceDirectory) {
public ConfigTreePropertySource(String name, Path sourceDirectory) {
this(name, sourceDirectory, EnumSet.noneOf(Option.class));
}
/**
* Create a new {@link VolumeMountDirectoryPropertySource} instance.
* Create a new {@link ConfigTreePropertySource} instance.
* @param name the name of the property source
* @param sourceDirectory the underlying source directory
* @param options the property source options
*/
public VolumeMountDirectoryPropertySource(String name, Path sourceDirectory, Option... options) {
public ConfigTreePropertySource(String name, Path sourceDirectory, Option... options) {
this(name, sourceDirectory, EnumSet.copyOf(Arrays.asList(options)));
}
private VolumeMountDirectoryPropertySource(String name, Path sourceDirectory, Set<Option> options) {
private ConfigTreePropertySource(String name, Path sourceDirectory, Set<Option> options) {
super(name, sourceDirectory);
Assert.isTrue(Files.exists(sourceDirectory), () -> "Directory '" + sourceDirectory + "' does not exist");
Assert.isTrue(Files.isDirectory(sourceDirectory), () -> "File '" + sourceDirectory + "' is not a directory");

@ -894,7 +894,7 @@
"value": "classpath:"
},
{
"value": "volumemount:"
"value": "configtree:"
}
],
"providers": [

@ -5,13 +5,13 @@ org.springframework.boot.env.YamlPropertySourceLoader
# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ResourceConfigDataLocationResolver,\
org.springframework.boot.context.config.VolumeMountConfigDataLocationResolver
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.ResourceConfigDataLocationResolver
# ConfigData Loaders
org.springframework.boot.context.config.ConfigDataLoader=\
org.springframework.boot.context.config.ResourceConfigDataLoader,\
org.springframework.boot.context.config.VolumeMountConfigDataLoader
org.springframework.boot.context.config.ConfigTreeConfigDataLoader,\
org.springframework.boot.context.config.ResourceConfigDataLoader
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\

@ -30,14 +30,14 @@ import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link VolumeMountConfigDataLoader}.
* Tests for {@link ConfigTreeConfigDataLoader}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class VolumeMountConfigDataLoaderTests {
public class ConfigTreeConfigDataLoaderTests {
private VolumeMountConfigDataLoader loader = new VolumeMountConfigDataLoader();
private ConfigTreeConfigDataLoader loader = new ConfigTreeConfigDataLoader();
@TempDir
Path directory;
@ -47,11 +47,11 @@ public class VolumeMountConfigDataLoaderTests {
File file = this.directory.resolve("hello").toFile();
file.getParentFile().mkdirs();
FileCopyUtils.copy("world".getBytes(StandardCharsets.UTF_8), file);
VolumeMountConfigDataLocation location = new VolumeMountConfigDataLocation(this.directory.toString());
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation(this.directory.toString());
ConfigData configData = this.loader.load(location);
assertThat(configData.getPropertySources().size()).isEqualTo(1);
PropertySource<?> source = configData.getPropertySources().get(0);
assertThat(source.getName()).isEqualTo("Volume mount config '" + this.directory.toString() + "'");
assertThat(source.getName()).isEqualTo("Config tree '" + this.directory.toString() + "'");
assertThat(source.getProperty("hello").toString()).isEqualTo("world");
}

@ -25,20 +25,20 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link VolumeMountConfigDataLocationResolver}.
* Tests for {@link ConfigTreeConfigDataLocationResolver}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class VolumeMountConfigDataLocationResolverTests {
class ConfigTreeConfigDataLocationResolverTests {
private VolumeMountConfigDataLocationResolver resolver = new VolumeMountConfigDataLocationResolver();
private ConfigTreeConfigDataLocationResolver resolver = new ConfigTreeConfigDataLocationResolver();
private ConfigDataLocationResolverContext context = mock(ConfigDataLocationResolverContext.class);
@Test
void isResolvableWhenPrefixMatchesReturnsTrue() {
assertThat(this.resolver.isResolvable(this.context, "volumemount:/etc/config")).isTrue();
assertThat(this.resolver.isResolvable(this.context, "configtree:/etc/config")).isTrue();
}
@Test
@ -49,10 +49,10 @@ class VolumeMountConfigDataLocationResolverTests {
@Test
void resolveReturnsConfigVolumeMountLocation() {
List<VolumeMountConfigDataLocation> locations = this.resolver.resolve(this.context, "volumemount:/etc/config");
List<ConfigTreeConfigDataLocation> locations = this.resolver.resolve(this.context, "configtree:/etc/config");
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString)
.containsExactly("volume mount [" + new File("/etc/config").getAbsolutePath() + "]");
.containsExactly("config tree [" + new File("/etc/config").getAbsolutePath() + "]");
}
}

@ -24,37 +24,37 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link VolumeMountConfigDataLocation}.
* Tests for {@link ConfigTreeConfigDataLocation}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class VolumeMountConfigDataLocationTests {
public class ConfigTreeConfigDataLocationTests {
@Test
void constructorWhenPathIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new VolumeMountConfigDataLocation(null))
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataLocation(null))
.withMessage("Path must not be null");
}
@Test
void equalsWhenPathIsTheSameReturnsTrue() {
VolumeMountConfigDataLocation location = new VolumeMountConfigDataLocation("/etc/config");
VolumeMountConfigDataLocation other = new VolumeMountConfigDataLocation("/etc/config");
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation("/etc/config");
ConfigTreeConfigDataLocation other = new ConfigTreeConfigDataLocation("/etc/config");
assertThat(location).isEqualTo(other);
}
@Test
void equalsWhenPathIsDifferentReturnsFalse() {
VolumeMountConfigDataLocation location = new VolumeMountConfigDataLocation("/etc/config");
VolumeMountConfigDataLocation other = new VolumeMountConfigDataLocation("other-location");
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation("/etc/config");
ConfigTreeConfigDataLocation other = new ConfigTreeConfigDataLocation("other-location");
assertThat(location).isNotEqualTo(other);
}
@Test
void toStringReturnsDescriptiveString() {
VolumeMountConfigDataLocation location = new VolumeMountConfigDataLocation("/etc/config");
assertThat(location.toString()).isEqualTo("volume mount [" + new File("/etc/config").getAbsolutePath() + "]");
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation("/etc/config");
assertThat(location.toString()).isEqualTo("config tree [" + new File("/etc/config").getAbsolutePath() + "]");
}
}

@ -26,8 +26,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.env.VolumeMountDirectoryPropertySource.Option;
import org.springframework.boot.env.VolumeMountDirectoryPropertySource.Value;
import org.springframework.boot.env.ConfigTreePropertySource.Option;
import org.springframework.boot.env.ConfigTreePropertySource.Value;
import org.springframework.boot.origin.TextResourceOrigin;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConfigurableConversionService;
@ -40,32 +40,31 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link VolumeMountDirectoryPropertySource}.
* Tests for {@link ConfigTreePropertySource}.
*
* @author Phillip Webb
*/
class VolumeMountDirectoryPropertySourceTests {
class ConfigTreePropertySourceTests {
@TempDir
Path directory;
@Test
void createWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new VolumeMountDirectoryPropertySource(null, this.directory))
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreePropertySource(null, this.directory))
.withMessageContaining("name must contain");
}
@Test
void createWhenSourceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new VolumeMountDirectoryPropertySource("test", null))
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreePropertySource("test", null))
.withMessage("Property source must not be null");
}
@Test
void createWhenSourceDoesNotExistThrowsException() {
Path missing = this.directory.resolve("missing");
assertThatIllegalArgumentException().isThrownBy(() -> new VolumeMountDirectoryPropertySource("test", missing))
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreePropertySource("test", missing))
.withMessage("Directory '" + missing + "' does not exist");
}
@ -73,45 +72,45 @@ class VolumeMountDirectoryPropertySourceTests {
void createWhenSourceIsFileThrowsException() throws Exception {
Path file = this.directory.resolve("file");
FileCopyUtils.copy("test".getBytes(StandardCharsets.UTF_8), file.toFile());
assertThatIllegalArgumentException().isThrownBy(() -> new VolumeMountDirectoryPropertySource("test", file))
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreePropertySource("test", file))
.withMessage("File '" + file + "' is not a directory");
}
@Test
void getPropertyNamesFromFlatReturnsPropertyNames() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
ConfigTreePropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getPropertyNames()).containsExactly("a", "b", "c");
}
@Test
void getPropertyNamesFromNestedReturnsPropertyNames() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getNestedPropertySource();
ConfigTreePropertySource propertySource = getNestedPropertySource();
assertThat(propertySource.getPropertyNames()).containsExactly("c", "fa.a", "fa.b", "fb.a", "fb.fa.a");
}
@Test
void getPropertyNamesWhenLowercaseReturnsPropertyNames() throws Exception {
addProperty("SpRiNg", "boot");
VolumeMountDirectoryPropertySource propertySource = new VolumeMountDirectoryPropertySource("test",
this.directory, Option.USE_LOWERCASE_NAMES);
ConfigTreePropertySource propertySource = new ConfigTreePropertySource("test", this.directory,
Option.USE_LOWERCASE_NAMES);
assertThat(propertySource.getPropertyNames()).containsExactly("spring");
}
@Test
void getPropertyFromFlatReturnsFileContent() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
ConfigTreePropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getProperty("b")).hasToString("B");
}
@Test
void getPropertyFromFlatWhenMissingReturnsNull() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
ConfigTreePropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getProperty("missing")).isNull();
}
@Test
void getPropertyFromFlatWhenFileDeletedThrowsException() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
ConfigTreePropertySource propertySource = getFlatPropertySource();
Path b = this.directory.resolve("b");
Files.delete(b);
assertThatIllegalStateException().isThrownBy(() -> propertySource.getProperty("b").toString())
@ -120,7 +119,7 @@ class VolumeMountDirectoryPropertySourceTests {
@Test
void getOriginFromFlatReturnsOrigin() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
ConfigTreePropertySource propertySource = getFlatPropertySource();
TextResourceOrigin origin = (TextResourceOrigin) propertySource.getOrigin("b");
assertThat(origin.getResource().getFile()).isEqualTo(this.directory.resolve("b").toFile());
assertThat(origin.getLocation().getLine()).isEqualTo(0);
@ -129,7 +128,7 @@ class VolumeMountDirectoryPropertySourceTests {
@Test
void getOriginFromFlatWhenMissingReturnsNull() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
ConfigTreePropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getOrigin("missing")).isNull();
}
@ -147,13 +146,13 @@ class VolumeMountDirectoryPropertySourceTests {
@Test
void getPropertyFromNestedReturnsFileContent() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getNestedPropertySource();
ConfigTreePropertySource propertySource = getNestedPropertySource();
assertThat(propertySource.getProperty("fb.fa.a")).hasToString("BAA");
}
@Test
void getPropertyWhenNotAlwaysReadIgnoresUpdates() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getNestedPropertySource();
ConfigTreePropertySource propertySource = getNestedPropertySource();
Value v1 = propertySource.getProperty("fa.b");
Value v2 = propertySource.getProperty("fa.b");
assertThat(v1).isSameAs(v2);
@ -167,8 +166,8 @@ class VolumeMountDirectoryPropertySourceTests {
@Test
void getPropertyWhenAlwaysReadReflectsUpdates() throws Exception {
addNested();
VolumeMountDirectoryPropertySource propertySource = new VolumeMountDirectoryPropertySource("test",
this.directory, Option.ALWAYS_READ);
ConfigTreePropertySource propertySource = new ConfigTreePropertySource("test", this.directory,
Option.ALWAYS_READ);
Value v1 = propertySource.getProperty("fa.b");
Value v2 = propertySource.getProperty("fa.b");
assertThat(v1).isNotSameAs(v2);
@ -183,21 +182,21 @@ class VolumeMountDirectoryPropertySourceTests {
@Test
void getPropertyWhenLowercaseReturnsValue() throws Exception {
addProperty("SpRiNg", "boot");
VolumeMountDirectoryPropertySource propertySource = new VolumeMountDirectoryPropertySource("test",
this.directory, Option.USE_LOWERCASE_NAMES);
ConfigTreePropertySource propertySource = new ConfigTreePropertySource("test", this.directory,
Option.USE_LOWERCASE_NAMES);
assertThat(propertySource.getProperty("spring")).hasToString("boot");
}
private VolumeMountDirectoryPropertySource getFlatPropertySource() throws IOException {
private ConfigTreePropertySource getFlatPropertySource() throws IOException {
addProperty("a", "A");
addProperty("b", "B");
addProperty("c", "C");
return new VolumeMountDirectoryPropertySource("test", this.directory);
return new ConfigTreePropertySource("test", this.directory);
}
private VolumeMountDirectoryPropertySource getNestedPropertySource() throws IOException {
private ConfigTreePropertySource getNestedPropertySource() throws IOException {
addNested();
return new VolumeMountDirectoryPropertySource("test", this.directory);
return new ConfigTreePropertySource("test", this.directory);
}
private void addNested() throws IOException {
Loading…
Cancel
Save