Skip to content

Support Git-style searchPaths with wildcards in AWS S3 buckets (#2812)#2958

Open
tomy8964 wants to merge 2 commits into
spring-cloud:mainfrom
tomy8964:feature/gh-2812-aws-s3-searchpaths
Open

Support Git-style searchPaths with wildcards in AWS S3 buckets (#2812)#2958
tomy8964 wants to merge 2 commits into
spring-cloud:mainfrom
tomy8964:feature/gh-2812-aws-s3-searchpaths

Conversation

@tomy8964
Copy link
Copy Markdown

@tomy8964 tomy8964 commented Jul 6, 2025

Summary

Add full Git-style searchPaths support (placeholders + wildcards) to the AWS S3 backend so that users can migrate existing Git-based configurations without renaming to application.*.

  • Implements literal, dot-wildcard (.*), single (?) and double (**) wildcard matching
  • Retains the lookup order .properties → .json → .yml → .yaml when expanding literals or .* patterns
  • Scans “directory” patterns for nested files
  • Deduplicates identical keys across multiple patterns

This issue (#2812)
resolves #2812


Changes

  1. Core Logic

    • Extended getS3ConfigFileWithSearchPaths(...) to treat {application} and {profile} placeholders identically to the Git backend

    • Added support for:

      • Literal + auto-ext: stops at first existing extension
      • Dot-wildcard (.*): expands against supported extensions in priority order
      • Single-character wildcard (?)
      • Multi-level wildcard (**)
      • Directory scan for literal prefixes ending with /
    • Ensured seenKeys set to avoid duplicate property sources

  2. Tests
    Added new JUnit 5 tests in AwsS3EnvironmentRepositoryTests to cover all scenarios:

    • searchPaths_placeholderOnly_shouldResolveExactFile
    • searchPaths_wildcardOnly_shouldResolveAllProperties
    • searchPaths_placeholderAndWildcard_shouldResolveMatchingKeys
    • searchPaths_orderMatters_forPropertySourceOrder
    • searchPath_extensionPreserved (dynamic tests for each extension)
    • searchPaths_applicationAsDirectory_shouldStillHonorSearchPaths
    • multiDocumentYaml_withSearchPaths_shouldNotSplitDocuments
    • getLocations_returnsCorrect
    • searchPaths_deduplication_shouldOnlyAddOnce
    • searchPaths_singleCharacterWildcard_shouldMatchExactlyOneChar
    • searchPaths_withEmptyLabel_shouldUseDefaultLabel
    • searchPaths_multipleLabels_shouldApplyForEachLabelInReverseOrder
    • searchPaths_literalNotFound_shouldReturnEmpty

    …plus the additional edge cases for literal-stop, dot-wildcard priority, and nested directory patterns.

  3. Example Usage

spring:
  cloud:
    config:
      server:
        awss3:
          bucket: my-config-bucket
          search-paths:
            - "{label}/{application}"          # literal + auto-ext
            - "{label}/{application}.*"        # dot-wildcard expansion
            - "{label}/common/*.json"          # JSON only in common/
            - "{label}/{application}.yml"      # explicit YML

Additional Notes

Backward Compatibility

This update does not break any existing AWS S3 config setups. The default lookup behavior is unchanged if no placeholders or wildcards are used in searchPaths.

Migration

Existing users migrating from Git-backed config servers can now use their current searchPaths (including wildcards and placeholders) on S3 with no renaming or convention change required.

Documentation

Documentation and usage examples for the enhanced searchPaths will be updated in the relevant documentation files after the merge.
If there are specific locations that require documentation updates, please let me know—I will be happy to update them.

Performance and Cost

Using wildcards (such as * or **) in searchPaths may result in additional AWS S3 API calls (e.g., ListObjects), especially for large buckets or deeply nested directory patterns.
This could lead to increased latency and higher AWS costs.
Users should consider the structure and size of their buckets when designing searchPaths and monitor AWS S3 usage accordingly.


…g-cloud#2812)

Fixes spring-cloud#2812

- ListObjectsV2 + AntPathMatcher based matching for *, **, ?, dot-wildcard
- auto-ext lookup order (.properties → .json → .yml/.yaml)
- directory scan, deduplication, prefix extraction
- only active when searchPaths non-empty

Signed-off-by: Geonwook Ham <tomy8964@naver.com>
Signed-off-by: ham <tomy8964@naver.com>
Signed-off-by: Geonwook Ham <tomy8964@naver.com>
@tomy8964
Copy link
Copy Markdown
Author

Hi @ryanjbaxter!
I have updated the branch to resolve the merge conflicts with the latest main. Could you please take a look when you have some time? Thank you!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the AWS S3 environment repository to support Git-style searchPaths (placeholders + wildcard matching), enabling S3-backed config layouts to match existing Git-backed repository conventions.

Changes:

  • Add searchPaths configuration to the AWS S3 backend and wire it through the factory.
  • Implement placeholder expansion and wildcard-based S3 key discovery (including directory scans) with deduplication.
  • Add an extensive JUnit 5 test suite covering placeholder/wildcard resolution scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentRepository.java Adds searchPaths support, wildcard matching via AntPathMatcher, S3 list/head probing, and key→property source wrapping.
spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentRepositoryFactory.java Passes searchPaths from properties into the repository constructor.
spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentProperties.java Introduces searchPaths as a bindable configuration property.
spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentRepositoryTests.java Adds coverage for placeholder/wildcard behavior, ordering, and special cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 121 to 127
String[] profileArray = parseProfiles(profiles);
List<String> apps = Arrays.asList(StringUtils.commaDelimitedListToStringArray(application.replace(" ", "")));
Collections.reverse(apps);
if (!apps.contains(serverProperties.getDefaultApplicationName())) {
if (searchPaths.isEmpty() && !apps.contains(serverProperties.getDefaultApplicationName())) {
Collections.reverse(apps);
apps = new ArrayList<>(apps);
apps.add(serverProperties.getDefaultApplicationName());
}
Comment on lines 150 to +162
private void addPropertySources(Environment environment, List<String> apps, String[] profiles,
List<String> labels) {
if (!this.searchPaths.isEmpty()) {
for (String label : labels) {
for (String profile : profiles) {
for (String app : apps) {
List<S3ConfigFile> s3ConfigFiles = getS3ConfigFileWithSearchPaths(app, profile, label);
addPropertySource(environment, s3ConfigFiles);
}
}
}
return;
}
Comment on lines +300 to +305
for (String template : this.searchPaths) {
String pattern = template
.replace("{application}", application)
.replace("{profile}", profile == null ? "" : profile)
.replace("{label}", label == null ? "" : label);

Comment on lines +308 to +312
for (String ext : List.of(".properties", ".json", ".yml", ".yaml")) {
String key = pattern.endsWith(ext) ? pattern : pattern + ext;
if (!seenKeys.add(key)) {
continue;
}
Comment on lines +1042 to +1045
server.setDefaultLabel("main");
List<String> paths = List.of("{label}/foo-bar.yml");
AwsS3EnvironmentRepository repo =
new AwsS3EnvironmentRepository(s3Client, "bucket1", false, server, paths);
Comment on lines 121 to 127
String[] profileArray = parseProfiles(profiles);
List<String> apps = Arrays.asList(StringUtils.commaDelimitedListToStringArray(application.replace(" ", "")));
Collections.reverse(apps);
if (!apps.contains(serverProperties.getDefaultApplicationName())) {
if (searchPaths.isEmpty() && !apps.contains(serverProperties.getDefaultApplicationName())) {
Collections.reverse(apps);
apps = new ArrayList<>(apps);
apps.add(serverProperties.getDefaultApplicationName());
}
Comment on lines 150 to +162
private void addPropertySources(Environment environment, List<String> apps, String[] profiles,
List<String> labels) {
if (!this.searchPaths.isEmpty()) {
for (String label : labels) {
for (String profile : profiles) {
for (String app : apps) {
List<S3ConfigFile> s3ConfigFiles = getS3ConfigFileWithSearchPaths(app, profile, label);
addPropertySource(environment, s3ConfigFiles);
}
}
}
return;
}
Comment on lines +300 to +305
for (String template : this.searchPaths) {
String pattern = template
.replace("{application}", application)
.replace("{profile}", profile == null ? "" : profile)
.replace("{label}", label == null ? "" : label);

Comment on lines +308 to +312
for (String ext : List.of(".properties", ".json", ".yml", ".yaml")) {
String key = pattern.endsWith(ext) ? pattern : pattern + ext;
if (!seenKeys.add(key)) {
continue;
}
Comment on lines +1042 to +1045
server.setDefaultLabel("main");
List<String> paths = List.of("{label}/foo-bar.yml");
AwsS3EnvironmentRepository repo =
new AwsS3EnvironmentRepository(s3Client, "bucket1", false, server, paths);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AwsS3EnvironmentRepository does not comply with Git

3 participants