From d32ef587bc31beb93dc8c025bb38b1460120789a Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Fri, 30 Oct 2026 14:43:57 -0400 Subject: [PATCH] Let the last label take precedence in the native repository With multiple labels the native repository passed all label locations to Spring Boot, which interleaved them by profile and gave the first label precedence. The SCM, S3, JDBC and other repositories resolve each label on its own and let the last label win. Resolve one label at a time and let the last label take precedence. The previous behavior can be restored with spring.cloud.config.server.native.lastLabelWins=false. Enable the compliance test for multiple labels. Signed-off-by: Ryan Baxter --- .../file-system-backend.adoc | 5 +++ .../NativeEnvironmentProperties.java | 15 +++++++ .../NativeEnvironmentRepository.java | 42 +++++++++++++++++++ .../NativeEnvironmentRepositoryTests.java | 13 ++++++ ...eEnvironmentRepositoryComplianceTests.java | 9 ---- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/docs/modules/ROOT/pages/server/environment-repository/file-system-backend.adoc b/docs/modules/ROOT/pages/server/environment-repository/file-system-backend.adoc index 56dbe930c6..61b8a7c04a 100644 --- a/docs/modules/ROOT/pages/server/environment-repository/file-system-backend.adoc +++ b/docs/modules/ROOT/pages/server/environment-repository/file-system-backend.adoc @@ -23,3 +23,8 @@ Thus, the default behaviour with no placeholders is the same as adding a search For example, `file:/tmp/config` is the same as `file:/tmp/config,file:/tmp/config/\{label}`. This behavior can be disabled by setting `spring.cloud.config.server.native.addLabelLocations=false`. +If the request contains several labels (a comma-separated list), the last label takes precedence over the earlier ones, as it does for the Git and SVN backends. +Each label is resolved on its own, and the locations the labels share come last. +Before this behavior was introduced, the first label took precedence. +To restore that behavior, set `spring.cloud.config.server.native.lastLabelWins=false`. + diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentProperties.java index 7926d9ab88..d5b37077ec 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentProperties.java @@ -35,6 +35,13 @@ public class NativeEnvironmentProperties implements EnvironmentRepositoryPropert */ private Boolean addLabelLocations = true; + /** + * Flag to determine whether, when several labels are requested, the last label takes + * precedence over the earlier ones (default true). If false, the first label takes + * precedence, as it did before this property was introduced. + */ + private Boolean lastLabelWins = true; + private String defaultLabel = "master"; /** @@ -66,6 +73,14 @@ public void setAddLabelLocations(Boolean addLabelLocations) { this.addLabelLocations = addLabelLocations; } + public Boolean getLastLabelWins() { + return this.lastLabelWins; + } + + public void setLastLabelWins(Boolean lastLabelWins) { + this.lastLabelWins = lastLabelWins; + } + public String getDefaultLabel() { return this.defaultLabel; } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java index 50cd36e64f..2e50dfa27d 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java @@ -17,10 +17,12 @@ package org.springframework.cloud.config.server.environment; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -85,6 +87,8 @@ public class NativeEnvironmentRepository implements EnvironmentRepository, Searc */ private boolean addLabelLocations; + private boolean lastLabelWins = true; + /** * Version string to be reported for native repository. */ @@ -100,6 +104,7 @@ public NativeEnvironmentRepository(ConfigurableEnvironment environment, NativeEn ObservationRegistry observationRegistry) { this.environment = environment; this.addLabelLocations = properties.getAddLabelLocations(); + this.lastLabelWins = properties.getLastLabelWins(); this.defaultLabel = properties.getDefaultLabel(); this.failOnError = properties.getFailOnError(); this.order = properties.getOrder(); @@ -124,6 +129,14 @@ public void setAddLabelLocations(boolean addLabelLocations) { this.addLabelLocations = addLabelLocations; } + public boolean isLastLabelWins() { + return this.lastLabelWins; + } + + public void setLastLabelWins(boolean lastLabelWins) { + this.lastLabelWins = lastLabelWins; + } + public String getDefaultLabel() { return this.defaultLabel; } @@ -139,7 +152,36 @@ public Environment findOne(String config, String profile, String label) { @Override public Environment findOne(String config, String profile, String label, boolean includeOrigin) { + if (!this.lastLabelWins || label == null || !label.contains(",")) { + return findOneForLabel(config, profile, label, includeOrigin); + } + // Resolve one label at a time, so that the last label takes precedence over the + // earlier ones as in the SCM repositories. Resolving them together would let + // Spring Boot interleave the labels by profile, giving the first label + // precedence. + List labels = new ArrayList<>(Arrays.asList(StringUtils.commaDelimitedListToStringArray(label))); + Collections.reverse(labels); + Environment result = null; + Map sources = new LinkedHashMap<>(); + for (String singleLabel : labels) { + Environment environment = findOneForLabel(config, profile, singleLabel, includeOrigin); + if (result == null) { + result = new Environment(environment.getName(), environment.getProfiles(), label, + environment.getVersion(), environment.getState()); + } + for (PropertySource source : environment.getPropertySources()) { + // A property source found for several labels comes from a location the + // labels share, which has a lower precedence than the label locations, + // so keep it at its last position + sources.remove(source.getName()); + sources.put(source.getName(), source); + } + } + sources.values().forEach(result::add); + return result; + } + private Environment findOneForLabel(String config, String profile, String label, boolean includeOrigin) { try { ConfigurableEnvironment environment = getEnvironment(config, profile, label); DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java index d592abad96..3cd1d743c5 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java @@ -261,6 +261,19 @@ public void locationAddLabelLocations() { @Test public void locationAddMultipleLabelLocations() { + this.repository.setSearchLocations("classpath:/test/dev/"); + Environment environment = this.repository.findOne("foo", "development", "ignore,applicationxyz"); + // the last label wins, the location shared by the labels comes last + assertThat(environment.getPropertySources()).hasSize(3); + assertThat(environment.getPropertySources().get(0).getSource().get("foo")).isEqualTo("default-app"); + assertThat(environment.getPropertySources().get(1).getSource().get("foo")).isEqualTo("app"); + assertThat(environment.getPropertySources().get(2).getSource().get("foo")).isEqualTo("dev_bar"); + assertThat(environment.getLabel()).isEqualTo("ignore,applicationxyz"); + } + + @Test + public void locationAddMultipleLabelLocationsFirstLabelWins() { + this.repository.setLastLabelWins(false); this.repository.setSearchLocations("classpath:/test/dev/"); Environment environment = this.repository.findOne("foo", "development", "ignore,applicationxyz"); assertThat(environment.getPropertySources()).hasSize(3); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/compliance/NativeEnvironmentRepositoryComplianceTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/compliance/NativeEnvironmentRepositoryComplianceTests.java index 00a5e00715..2676f86206 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/compliance/NativeEnvironmentRepositoryComplianceTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/compliance/NativeEnvironmentRepositoryComplianceTests.java @@ -19,8 +19,6 @@ import java.nio.file.Path; import io.micrometer.observation.ObservationRegistry; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.cloud.config.server.environment.EnvironmentRepository; @@ -57,11 +55,4 @@ public EnvironmentRepository repository() { return this.fixture; } - @Override - @Test - @Disabled("TODO-issue: Label directories are added as search locations, so labels are interleaved by profile and the first label wins") - public void lastLabelTakesPrecedence() throws Exception { - super.lastLabelTakesPrecedence(); - } - }