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