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
parent
eee260fc03
commit
3f76eb2097
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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…
Reference in New Issue