Add volume mount property source support

Add support for volume mounted directories where the filename becomes
the property key and the file contents becomes the value.

Support is provided via a dedicated `VolumeMountDirectoryPropertySource`
class which can either be used directly, or via a "volumemount:/..."
`spring.config.import` location.

Closes gh-19990

Co-authored-by: Phillip Webb <pwebb@vmware.com>
pull/22524/head
Madhura Bhave 4 years ago committed by Phillip Webb
parent eee260fc03
commit 3f76eb2097

@ -670,7 +670,7 @@ Locations will be processed in the order that they are defined, with later impor
[TIP] [TIP]
==== ====
Spring Boot includes pluggable API that allows various different location addresses to be supported. Spring Boot includes pluggable API that allows various different location addresses to be supported.
By default you can import Java Properties and YAML. By default you can import Java Properties, YAML and volume mounts.
Third-party jars can offer support for additional technologies (there's no requirement for files to be local). 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. For example, you can imagine config data being from external stores such as Consul, Apache ZooKeeper or Netflix Archaius.
@ -680,6 +680,48 @@ If you want to support your own locations, see the `ConfigDataLocationResolver`
[[boot-features-external-config-files-voumemounts]]
==== Using Volume Mount Properties
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.
As an alternative to environment variables, many cloud platforms now allow you to map configuration into mounted data volumes.
For example, Kubernetes can volume mount both https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#populate-a-volume-with-data-stored-in-a-configmap[`ConfigMaps`] and https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-files-from-a-pod[`Secrets`].
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`'.
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.
As an example, let's imagine that Kubernetes has mounted the following volume:
[source,indent=0]
----
etc/
config/
myapp/
username
password
----
The contents of the `username` file would be a config value, and the contents of `password` would be a secret.
To import these properties, you can add the following to your `application.properties` file:
[source,properties,indent=0]
----
spring.config.import=volumemount:/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.
[[boot-features-external-config-placeholders-in-properties]] [[boot-features-external-config-placeholders-in-properties]]
==== Property Placeholders ==== Property Placeholders
The values in `application.properties` and `application.yml` are filtered through the existing `Environment` when they are used, so you can refer back to previously defined values (for example, from System properties). The values in `application.properties` and `application.yml` are filtered through the existing `Environment` when they are used, so you can refer back to previously defined values (for example, from System properties).

@ -0,0 +1,41 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.context.config;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import org.springframework.boot.env.VolumeMountDirectoryPropertySource;
/**
* {@link ConfigDataLoader} for directory locations mounted as volumes.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class VolumeMountConfigDataLoader implements ConfigDataLoader<VolumeMountConfigDataLocation> {
@Override
public ConfigData load(VolumeMountConfigDataLocation location) throws IOException {
Path path = location.getPath();
String name = "Volume mount config '" + path + "'";
VolumeMountDirectoryPropertySource source = new VolumeMountDirectoryPropertySource(name, path);
return new ConfigData(Collections.singletonList(source));
}
}

@ -0,0 +1,66 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.context.config;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import org.springframework.util.Assert;
/**
* {@link ConfigDataLocation} backed by a directory mounted as a volume.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class VolumeMountConfigDataLocation extends ConfigDataLocation {
private final Path path;
VolumeMountConfigDataLocation(String path) {
Assert.notNull(path, "Path must not be null");
this.path = Paths.get(path).toAbsolutePath();
}
Path getPath() {
return this.path;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
VolumeMountConfigDataLocation other = (VolumeMountConfigDataLocation) obj;
return Objects.equals(this.path, other.path);
}
@Override
public int hashCode() {
return this.path.hashCode();
}
@Override
public String toString() {
return "volume mount [" + this.path + "]";
}
}

@ -0,0 +1,44 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.context.config;
import java.util.Collections;
import java.util.List;
/**
* {@link ConfigDataLocationResolver} for volume mounted locations such as Kubernetes
* ConfigMaps and Secrets.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class VolumeMountConfigDataLocationResolver implements ConfigDataLocationResolver<VolumeMountConfigDataLocation> {
private static final String PREFIX = "volumemount:";
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
return location.startsWith(PREFIX);
}
@Override
public List<VolumeMountConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location) {
VolumeMountConfigDataLocation resolved = new VolumeMountConfigDataLocation(location.substring(PREFIX.length()));
return Collections.singletonList(resolved);
}
}

@ -0,0 +1,313 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.env;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginLookup;
import org.springframework.boot.origin.OriginProvider;
import org.springframework.boot.origin.TextResourceOrigin;
import org.springframework.boot.origin.TextResourceOrigin.Location;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
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
* 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.
* <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
* used with an {@link Environment} configured with an
* {@link ApplicationConversionService}, property values can be converted to a
* {@code String} or {@code byte[]}.
* <p>
* This property source is typically used to read Kubernetes {@code configMap} volume
* mounts.
*
* @author Phillip Webb
* @since 2.4.0
*/
public class VolumeMountDirectoryPropertySource extends EnumerablePropertySource<Path> implements OriginLookup<String> {
private static final int MAX_DEPTH = 100;
private final Map<String, PropertyFile> propertyFiles;
private final String[] names;
private final Set<Option> options;
/**
* Create a new {@link VolumeMountDirectoryPropertySource} instance.
* @param name the name of the property source
* @param sourceDirectory the underlying source directory
*/
public VolumeMountDirectoryPropertySource(String name, Path sourceDirectory) {
this(name, sourceDirectory, EnumSet.noneOf(Option.class));
}
/**
* Create a new {@link VolumeMountDirectoryPropertySource} 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) {
this(name, sourceDirectory, EnumSet.copyOf(Arrays.asList(options)));
}
private VolumeMountDirectoryPropertySource(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");
this.propertyFiles = PropertyFile.findAll(sourceDirectory, options);
this.options = options;
this.names = StringUtils.toStringArray(this.propertyFiles.keySet());
}
@Override
public String[] getPropertyNames() {
return this.names.clone();
}
@Override
public Value getProperty(String name) {
PropertyFile propertyFile = this.propertyFiles.get(name);
return (propertyFile != null) ? propertyFile.getContent() : null;
}
@Override
public Origin getOrigin(String name) {
PropertyFile propertyFile = this.propertyFiles.get(name);
return (propertyFile != null) ? propertyFile.getOrigin() : null;
}
@Override
public boolean isImmutable() {
return !this.options.contains(Option.ALWAYS_READ);
}
/**
* Property source options.
*/
public enum Option {
/**
* Always read the value of the file when accessing the property value. When this
* option is not set the property source will cache the value when it's first
* read.
*/
ALWAYS_READ,
/**
* Convert file and directory names to lowercase.
*/
USE_LOWERCASE_NAMES
}
/**
* A value returned from the property source which exposes the contents of the
* property file. Values can either be treated as {@link CharSequence} or as an
* {@link InputStreamSource}.
*/
public interface Value extends CharSequence, InputStreamSource {
}
/**
* A single property file that was found when when the source was created.
*/
private static final class PropertyFile {
private static final Location START_OF_FILE = new Location(0, 0);
private final Path path;
private final PathResource resource;
private final Origin origin;
private final PropertyFileContent cachedContent;
private PropertyFile(Path path, Set<Option> options) {
this.path = path;
this.resource = new PathResource(path);
this.origin = new TextResourceOrigin(this.resource, START_OF_FILE);
this.cachedContent = options.contains(Option.ALWAYS_READ) ? null
: new PropertyFileContent(path, this.resource, this.origin, true);
}
PropertyFileContent getContent() {
return (this.cachedContent != null) ? this.cachedContent
: new PropertyFileContent(this.path, this.resource, this.origin, false);
}
Origin getOrigin() {
return this.origin;
}
static Map<String, PropertyFile> findAll(Path sourceDirectory, Set<Option> options) {
try {
Map<String, PropertyFile> propertyFiles = new TreeMap<>();
Files.find(sourceDirectory, MAX_DEPTH, PropertyFile::isRegularFile).forEach((path) -> {
String name = getName(sourceDirectory.relativize(path));
if (StringUtils.hasText(name)) {
if (options.contains(Option.USE_LOWERCASE_NAMES)) {
name = name.toLowerCase();
}
propertyFiles.put(name, new PropertyFile(path, options));
}
});
return Collections.unmodifiableMap(propertyFiles);
}
catch (IOException ex) {
throw new IllegalStateException("Unable to find files in '" + sourceDirectory + "'", ex);
}
}
private static boolean isRegularFile(Path path, BasicFileAttributes attributes) {
return attributes.isRegularFile();
}
private static String getName(Path relativePath) {
int nameCount = relativePath.getNameCount();
if (nameCount == 1) {
return relativePath.toString();
}
StringBuilder name = new StringBuilder();
for (int i = 0; i < nameCount; i++) {
name.append((i != 0) ? "." : "");
name.append(relativePath.getName(i));
}
return name.toString();
}
}
/**
* The contents of a found property file.
*/
private static final class PropertyFileContent implements Value, OriginProvider {
private final Path path;
private final Resource resource;
private final boolean cacheContent;
private volatile byte[] content;
private final Origin origin;
private PropertyFileContent(Path path, Resource resource, Origin origin, boolean cacheContent) {
this.path = path;
this.resource = resource;
this.origin = origin;
this.cacheContent = cacheContent;
}
@Override
public Origin getOrigin() {
return this.origin;
}
@Override
public int length() {
return toString().length();
}
@Override
public char charAt(int index) {
return toString().charAt(index);
}
@Override
public CharSequence subSequence(int start, int end) {
return toString().subSequence(start, end);
}
@Override
public String toString() {
return new String(getBytes());
}
@Override
public InputStream getInputStream() throws IOException {
if (!this.cacheContent) {
assertStillExists();
return this.resource.getInputStream();
}
return new ByteArrayInputStream(getBytes());
}
private byte[] getBytes() {
try {
if (!this.cacheContent) {
assertStillExists();
return FileCopyUtils.copyToByteArray(this.resource.getInputStream());
}
if (this.content == null) {
assertStillExists();
synchronized (this.resource) {
if (this.content == null) {
this.content = FileCopyUtils.copyToByteArray(this.resource.getInputStream());
}
}
}
return this.content;
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private void assertStillExists() {
Assert.state(Files.exists(this.path), () -> "The property file '" + this.path + "' no longer exists");
}
}
}

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

@ -0,0 +1,58 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.context.config;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.env.PropertySource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link VolumeMountConfigDataLoader}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class VolumeMountConfigDataLoaderTests {
private VolumeMountConfigDataLoader loader = new VolumeMountConfigDataLoader();
@TempDir
Path directory;
@Test
void loadReturnsConfigDataWithPropertySource() throws IOException {
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());
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.getProperty("hello").toString()).isEqualTo("world");
}
}

@ -0,0 +1,56 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.context.config;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link VolumeMountConfigDataLocationResolver}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class VolumeMountConfigDataLocationResolverTests {
private VolumeMountConfigDataLocationResolver resolver = new VolumeMountConfigDataLocationResolver();
private ConfigDataLocationResolverContext context = mock(ConfigDataLocationResolverContext.class);
@Test
void isResolvableWhenPrefixMatchesReturnsTrue() {
assertThat(this.resolver.isResolvable(this.context, "volumemount:/etc/config")).isTrue();
}
@Test
void isResolvableWhenPrefixDoesNotMatchReturnsFalse() {
assertThat(this.resolver.isResolvable(this.context, "http://etc/config")).isFalse();
assertThat(this.resolver.isResolvable(this.context, "/etc/config")).isFalse();
}
@Test
void resolveReturnsConfigVolumeMountLocation() {
List<VolumeMountConfigDataLocation> locations = this.resolver.resolve(this.context, "volumemount:/etc/config");
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString).containsExactly("volume mount [/etc/config]");
}
}

@ -0,0 +1,58 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.context.config;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link VolumeMountConfigDataLocation}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class VolumeMountConfigDataLocationTests {
@Test
void constructorWhenPathIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new VolumeMountConfigDataLocation(null))
.withMessage("Path must not be null");
}
@Test
void equalsWhenPathIsTheSameReturnsTrue() {
VolumeMountConfigDataLocation location = new VolumeMountConfigDataLocation("/etc/config");
VolumeMountConfigDataLocation other = new VolumeMountConfigDataLocation("/etc/config");
assertThat(location).isEqualTo(other);
}
@Test
void equalsWhenPathIsDifferentReturnsFalse() {
VolumeMountConfigDataLocation location = new VolumeMountConfigDataLocation("/etc/config");
VolumeMountConfigDataLocation other = new VolumeMountConfigDataLocation("other-location");
assertThat(location).isNotEqualTo(other);
}
@Test
void toStringReturnsDescriptiveString() {
VolumeMountConfigDataLocation location = new VolumeMountConfigDataLocation("/etc/config");
assertThat(location.toString()).isEqualTo("volume mount [/etc/config]");
}
}

@ -0,0 +1,217 @@
/*
* Copyright 2012-2020 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
*
* https://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.boot.env;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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.origin.TextResourceOrigin;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.InputStreamSource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link VolumeMountDirectoryPropertySource}.
*
* @author Phillip Webb
*/
class VolumeMountDirectoryPropertySourceTests {
@TempDir
Path directory;
@Test
void createWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new VolumeMountDirectoryPropertySource(null, this.directory))
.withMessageContaining("name must contain");
}
@Test
void createWhenSourceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new VolumeMountDirectoryPropertySource("test", null))
.withMessage("Property source must not be null");
}
@Test
void createWhenSourceDoesNotExistThrowsException() {
Path missing = this.directory.resolve("missing");
assertThatIllegalArgumentException().isThrownBy(() -> new VolumeMountDirectoryPropertySource("test", missing))
.withMessage("Directory '" + missing + "' does not exist");
}
@Test
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))
.withMessage("File '" + file + "' is not a directory");
}
@Test
void getPropertyNamesFromFlatReturnsPropertyNames() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getPropertyNames()).containsExactly("a", "b", "c");
}
@Test
void getPropertyNamesFromNestedReturnsPropertyNames() throws Exception {
VolumeMountDirectoryPropertySource 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);
assertThat(propertySource.getPropertyNames()).containsExactly("spring");
}
@Test
void getPropertyFromFlatReturnsFileContent() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getProperty("b")).hasToString("B");
}
@Test
void getPropertyFromFlatWhenMissingReturnsNull() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getProperty("missing")).isNull();
}
@Test
void getPropertyFromFlatWhenFileDeletedThrowsException() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
Path b = this.directory.resolve("b");
Files.delete(b);
assertThatIllegalStateException().isThrownBy(() -> propertySource.getProperty("b").toString())
.withMessage("The property file '" + b + "' no longer exists");
}
@Test
void getOriginFromFlatReturnsOrigin() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
TextResourceOrigin origin = (TextResourceOrigin) propertySource.getOrigin("b");
assertThat(origin.getResource().getFile()).isEqualTo(this.directory.resolve("b").toFile());
assertThat(origin.getLocation().getLine()).isEqualTo(0);
assertThat(origin.getLocation().getColumn()).isEqualTo(0);
}
@Test
void getOriginFromFlatWhenMissingReturnsNull() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getFlatPropertySource();
assertThat(propertySource.getOrigin("missing")).isNull();
}
@Test
void getPropertyViaEnvironmentSupportsConversion() throws Exception {
StandardEnvironment environment = new StandardEnvironment();
ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService) conversionService);
environment.getPropertySources().addFirst(getFlatPropertySource());
assertThat(environment.getProperty("a")).isEqualTo("A");
assertThat(environment.getProperty("b")).isEqualTo("B");
assertThat(environment.getProperty("c", InputStreamSource.class).getInputStream()).hasContent("C");
assertThat(environment.getProperty("c", byte[].class)).contains('C');
}
@Test
void getPropertyFromNestedReturnsFileContent() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getNestedPropertySource();
assertThat(propertySource.getProperty("fb.fa.a")).hasToString("BAA");
}
@Test
void getPropertyWhenNotAlwaysReadIgnoresUpdates() throws Exception {
VolumeMountDirectoryPropertySource propertySource = getNestedPropertySource();
Value v1 = propertySource.getProperty("fa.b");
Value v2 = propertySource.getProperty("fa.b");
assertThat(v1).isSameAs(v2);
assertThat(v1).hasToString("AB");
assertThat(FileCopyUtils.copyToByteArray(v1.getInputStream())).containsExactly('A', 'B');
addProperty("fa/b", "XX");
assertThat(v1).hasToString("AB");
assertThat(FileCopyUtils.copyToByteArray(v1.getInputStream())).containsExactly('A', 'B');
}
@Test
void getPropertyWhenAlwaysReadReflectsUpdates() throws Exception {
addNested();
VolumeMountDirectoryPropertySource propertySource = new VolumeMountDirectoryPropertySource("test",
this.directory, Option.ALWAYS_READ);
Value v1 = propertySource.getProperty("fa.b");
Value v2 = propertySource.getProperty("fa.b");
assertThat(v1).isNotSameAs(v2);
assertThat(v1).hasToString("AB");
assertThat(FileCopyUtils.copyToByteArray(v1.getInputStream())).containsExactly('A', 'B');
addProperty("fa/b", "XX");
assertThat(v1).hasToString("XX");
assertThat(FileCopyUtils.copyToByteArray(v1.getInputStream())).containsExactly('X', 'X');
assertThat(propertySource.getProperty("fa.b")).hasToString("XX");
}
@Test
void getPropertyWhenLowercaseReturnsValue() throws Exception {
addProperty("SpRiNg", "boot");
VolumeMountDirectoryPropertySource propertySource = new VolumeMountDirectoryPropertySource("test",
this.directory, Option.USE_LOWERCASE_NAMES);
assertThat(propertySource.getProperty("spring")).hasToString("boot");
}
private VolumeMountDirectoryPropertySource getFlatPropertySource() throws IOException {
addProperty("a", "A");
addProperty("b", "B");
addProperty("c", "C");
return new VolumeMountDirectoryPropertySource("test", this.directory);
}
private VolumeMountDirectoryPropertySource getNestedPropertySource() throws IOException {
addNested();
return new VolumeMountDirectoryPropertySource("test", this.directory);
}
private void addNested() throws IOException {
addProperty("fa/a", "AA");
addProperty("fa/b", "AB");
addProperty("fb/a", "BA");
addProperty("fb/fa/a", "BAA");
addProperty("c", "C");
}
private void addProperty(String path, String value) throws IOException {
File file = this.directory.resolve(path).toFile();
file.getParentFile().mkdirs();
FileCopyUtils.copy(value.getBytes(StandardCharsets.UTF_8), file);
}
}
Loading…
Cancel
Save