Skip to content

Commit 0167112

Browse files
committed
Support relative paths in FileResolvingEnvironmentRepository
1 parent 50db4c8 commit 0167112

File tree

3 files changed

+167
-35
lines changed

3 files changed

+167
-35
lines changed

docs/modules/ROOT/pages/server/environment-repository.adoc

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,33 @@ spring:
5858

5959
WARNING: Enabling this feature allows the Config Server to read files from the local file system where the server is running. Ensure that the process has appropriate file system permissions and is running in a secure environment.
6060

61-
When this feature is enabled, you can use the `{file}` prefix in your configuration values followed by the path to the file.
61+
==== Usage
62+
63+
You can use the `{file}` prefix in your configuration values followed by the path to the file.
6264
The Config Server will read the file, encode its content to a Base64 string, and replace the property value.
6365

64-
For example, in your backing repository (e.g., Git or Native), you can define a property like this:
66+
**1. Absolute Path**
67+
68+
You can reference files on the Config Server's local file system using an absolute path:
6569

6670
[source,yaml]
6771
----
6872
server:
6973
ssl:
7074
key-store: {file}/etc/certs/keystore.jks
71-
key-password: my-secret-password
7275
----
7376

74-
In this example, the Config Server reads `/etc/certs/keystore.jks`, encodes it, and returns the Base64 string as the value of `server.ssl.key-store`.
77+
**2. Relative Path (Repository-aware)**
78+
79+
If you are using a repository that supports search paths (like Git, SVN, or Native), you can reference files **relative to the repository root** by starting the path with a dot (`.`):
80+
81+
[source,yaml]
82+
----
83+
server:
84+
ssl:
85+
# Resolves 'certs/keystore.jks' located inside the Git repository
86+
key-store: {file}./certs/keystore.jks
87+
----
88+
89+
In this case, the Config Server will look for the file inside the cloned repository directory.
90+
If the repository does not support search paths (e.g., JDBC, Vault), relative paths will be ignored.

spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/FileResolvingEnvironmentRepository.java

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package org.springframework.cloud.config.server.environment;
1818

19-
import java.io.File;
2019
import java.io.IOException;
20+
import java.util.Arrays;
2121
import java.util.Base64;
2222
import java.util.LinkedHashMap;
2323
import java.util.List;
@@ -29,16 +29,22 @@
2929

3030
import org.springframework.cloud.config.environment.Environment;
3131
import org.springframework.cloud.config.environment.PropertySource;
32+
import org.springframework.core.io.DefaultResourceLoader;
33+
import org.springframework.core.io.Resource;
34+
import org.springframework.core.io.ResourceLoader;
3235
import org.springframework.util.FileCopyUtils;
33-
import org.springframework.util.ResourceUtils;
3436

3537
/**
3638
* @author Johny Cho
3739
*/
38-
public class FileResolvingEnvironmentRepository implements EnvironmentRepository {
40+
public class FileResolvingEnvironmentRepository implements EnvironmentRepository, SearchPathLocator {
3941

4042
private static final Log log = LogFactory.getLog(FileResolvingEnvironmentRepository.class);
43+
4144
private final EnvironmentRepository delegate;
45+
46+
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
47+
4248
private static final String PREFIX = "{file}";
4349

4450
public FileResolvingEnvironmentRepository(EnvironmentRepository delegate) {
@@ -48,48 +54,93 @@ public FileResolvingEnvironmentRepository(EnvironmentRepository delegate) {
4854
@Override
4955
public Environment findOne(String application, String profile, String label) {
5056
Environment env = this.delegate.findOne(application, profile, label);
51-
5257
if (Objects.isNull(env)) {
5358
return null;
5459
}
5560

61+
Locations locations = resolveLocations(application, profile, label);
5662
List<PropertySource> sources = env.getPropertySources();
5763

5864
for (int i = 0; i < sources.size(); i++) {
5965
PropertySource source = sources.get(i);
60-
Map<?, ?> originalMap = source.getSource();
61-
62-
Map<Object, Object> modifiedMap = new LinkedHashMap<>(originalMap);
63-
boolean modified = false;
64-
65-
for (Map.Entry<?, ?> entry : originalMap.entrySet()) {
66-
Object value = entry.getValue();
67-
68-
if (value instanceof String str && str.startsWith(PREFIX)) {
69-
String filePath = str.substring(PREFIX.length());
70-
try {
71-
String base64Content = readFileToBase64(filePath);
72-
modifiedMap.put(entry.getKey(), base64Content);
73-
modified = true;
74-
}
75-
catch (IOException e) {
76-
log.warn(String.format("Failed to resolve file content for property '%s'. path: %s", entry.getKey(), filePath), e);
77-
}
78-
}
66+
PropertySource resolvedSource = processPropertySource(source, locations);
67+
if (Objects.nonNull(resolvedSource)) {
68+
sources.set(i, resolvedSource);
7969
}
70+
}
71+
72+
return env;
73+
}
74+
75+
@Override
76+
public Locations getLocations(String application, String profile, String label) {
77+
return resolveLocations(application, profile, label);
78+
}
79+
80+
private Locations resolveLocations(String application, String profile, String label) {
81+
if (this.delegate instanceof SearchPathLocator locator) {
82+
return locator.getLocations(application, profile, label);
83+
}
84+
return new Locations(application, profile, label, null, new String[0]);
85+
}
8086

81-
if (modified) {
82-
PropertySource newSource = new PropertySource(source.getName(), modifiedMap);
83-
sources.set(i, newSource);
87+
/**
88+
* Process a single PropertySource. Returns a new PropertySource if modification occurred, otherwise null.
89+
*/
90+
private PropertySource processPropertySource(PropertySource source, Locations locations) {
91+
Map<?, ?> originalMap = source.getSource();
92+
Map<Object, Object> modifiedMap = new LinkedHashMap<>(originalMap);
93+
boolean modified = false;
94+
95+
for (Map.Entry<?, ?> entry : originalMap.entrySet()) {
96+
Object value = entry.getValue();
97+
if (value instanceof String str && str.startsWith(PREFIX)) {
98+
String path = str.substring(PREFIX.length());
99+
String resolvedValue = resolveFileContent(entry.getKey().toString(), path, locations);
100+
if (Objects.nonNull(resolvedValue)) {
101+
modifiedMap.put(entry.getKey(), resolvedValue);
102+
modified = true;
103+
}
84104
}
85105
}
86106

87-
return env;
107+
return modified ? new PropertySource(source.getName(), modifiedMap) : null;
108+
}
109+
110+
private String resolveFileContent(String key, String path, Locations locations) {
111+
try {
112+
Resource resource = findResource(path, locations);
113+
if (Objects.nonNull(resource) && resource.isReadable()) {
114+
byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
115+
return Base64.getEncoder().encodeToString(content);
116+
}
117+
}
118+
catch (IOException e) {
119+
log.warn(String.format("Failed to resolve file content for '%s'. path: %s", key, path), e);
120+
}
121+
return null;
88122
}
89123

90-
private String readFileToBase64(String filePath) throws IOException {
91-
File file = ResourceUtils.getFile(filePath);
92-
byte[] fileContent = FileCopyUtils.copyToByteArray(file);
93-
return Base64.getEncoder().encodeToString(fileContent);
124+
private Resource findResource(String path, Locations locations) {
125+
// 1. Try relative path if locations are available
126+
if (path.startsWith(".") && Objects.nonNull(locations) && Objects.nonNull(locations.getLocations())) {
127+
for (String location : locations.getLocations()) {
128+
String resourceLocation = location + (location.endsWith("/") ? "" : "/") + path;
129+
Resource candidate = this.resourceLoader.getResource(resourceLocation);
130+
if (candidate.exists() && candidate.isReadable()) {
131+
return candidate;
132+
}
133+
}
134+
log.warn("Could not find relative file '" + path + "' in locations: " + Arrays.toString(locations.getLocations()));
135+
return null;
136+
}
137+
138+
// 2. Fallback to absolute path or standard resource loading
139+
Resource resource = this.resourceLoader.getResource("file:" + path);
140+
if (!resource.exists()) {
141+
resource = this.resourceLoader.getResource(path);
142+
}
143+
return resource;
94144
}
145+
95146
}

spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/FileResolvingEnvironmentRepositoryTests.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030

3131
import org.springframework.cloud.config.environment.Environment;
3232
import org.springframework.cloud.config.environment.PropertySource;
33+
import org.springframework.cloud.config.server.environment.SearchPathLocator.Locations;
3334

3435
import static org.assertj.core.api.Assertions.assertThat;
3536
import static org.mockito.ArgumentMatchers.any;
3637
import static org.mockito.ArgumentMatchers.anyString;
3738
import static org.mockito.BDDMockito.given;
3839
import static org.mockito.Mockito.mock;
40+
import static org.mockito.Mockito.withSettings;
3941

4042
/**
4143
* Tests for {@link FileResolvingEnvironmentRepository}.
@@ -120,4 +122,67 @@ void findOneShouldHandleUnmodifiableMapSafely() throws IOException {
120122

121123
assertThat(String.valueOf(resultMap.get("my.secret"))).isNotEqualTo("{file}" + secretFile.getAbsolutePath());
122124
}
125+
126+
@Test
127+
void findOneShouldResolveRelativePathUsingLocations() throws Exception {
128+
String filename = "relative-secret.txt";
129+
File secretFile = new File(tempDir, filename);
130+
String content = "relative-content";
131+
Files.writeString(secretFile.toPath(), content);
132+
133+
EnvironmentRepository delegate = mock(EnvironmentRepository.class, withSettings().extraInterfaces(SearchPathLocator.class));
134+
135+
Locations locations = new Locations("app", "dev", "label", "version",
136+
new String[] { "file:" + tempDir.getAbsolutePath() + "/" });
137+
138+
given(((SearchPathLocator) delegate).getLocations(anyString(), anyString(), any()))
139+
.willReturn(locations);
140+
141+
Environment originalEnv = new Environment("app", "dev");
142+
Map<String, Object> sourceMap = new HashMap<>();
143+
sourceMap.put("my.relative", "{file}./" + filename);
144+
originalEnv.add(new PropertySource("test-source", sourceMap));
145+
146+
given(delegate.findOne(anyString(), anyString(), any())).willReturn(originalEnv);
147+
148+
FileResolvingEnvironmentRepository repository = new FileResolvingEnvironmentRepository(delegate);
149+
Environment resultEnv = repository.findOne("app", "dev", null);
150+
151+
assertThat(resultEnv).isNotNull();
152+
Map<?, ?> resultMap = resultEnv.getPropertySources().get(0).getSource();
153+
154+
String expectedBase64 = Base64.getEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8));
155+
assertThat(String.valueOf(resultMap.get("my.relative"))).isEqualTo(expectedBase64);
156+
}
157+
158+
@Test
159+
void findOneShouldIgnoreRelativePathIfDelegateIsNotSearchPathLocator() {
160+
EnvironmentRepository delegate = mock(EnvironmentRepository.class);
161+
162+
Environment originalEnv = new Environment("app", "dev");
163+
Map<String, Object> sourceMap = new HashMap<>();
164+
sourceMap.put("my.ignored", "{file}./some/relative/path.txt");
165+
originalEnv.add(new PropertySource("test-source", sourceMap));
166+
167+
given(delegate.findOne(anyString(), anyString(), any())).willReturn(originalEnv);
168+
169+
FileResolvingEnvironmentRepository repository = new FileResolvingEnvironmentRepository(delegate);
170+
Environment resultEnv = repository.findOne("app", "dev", null);
171+
172+
Map<?, ?> resultMap = resultEnv.getPropertySources().get(0).getSource();
173+
assertThat(String.valueOf(resultMap.get("my.ignored"))).isEqualTo("{file}./some/relative/path.txt");
174+
}
175+
176+
@Test
177+
void getLocationsShouldDelegateToUnderlyingRepository() {
178+
EnvironmentRepository delegate = mock(EnvironmentRepository.class, withSettings().extraInterfaces(SearchPathLocator.class));
179+
180+
Locations expectedLocations = new Locations("app", "dev", "label", "v1", new String[] { "file:/tmp" });
181+
given(((SearchPathLocator) delegate).getLocations("app", "dev", null)).willReturn(expectedLocations);
182+
183+
FileResolvingEnvironmentRepository repository = new FileResolvingEnvironmentRepository(delegate);
184+
Locations result = repository.getLocations("app", "dev", null);
185+
186+
assertThat(result).isSameAs(expectedLocations);
187+
}
123188
}

0 commit comments

Comments
 (0)