diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractScmEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractScmEnvironmentRepository.java index 3b8a253c..2373afaf 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractScmEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractScmEnvironmentRepository.java @@ -16,6 +16,11 @@ package org.springframework.cloud.config.server.environment; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import io.micrometer.observation.ObservationRegistry; import org.springframework.cloud.config.environment.Environment; @@ -23,6 +28,7 @@ import org.springframework.cloud.config.server.support.AbstractScmAccessor; import org.springframework.cloud.config.server.support.AbstractScmAccessorProperties; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StringUtils; /** * @author Dave Syer @@ -57,12 +63,29 @@ public abstract class AbstractScmEnvironmentRepository extends AbstractScmAccess @Override public synchronized Environment findOne(String application, String profile, String label, boolean includeOrigin) { - NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(), - new NativeEnvironmentProperties(), this.observationRegistry); - Locations locations = getLocations(application, profile, label); - delegate.setSearchLocations(locations.getLocations()); - Environment result = delegate.findOne(application, profile, "", includeOrigin); - result.setVersion(locations.getVersion()); + Environment result; + if (StringUtils.hasText(label) && label.contains(",")) { + List labels = Arrays.asList(StringUtils.commaDelimitedListToStringArray(label)); + Collections.reverse(labels); + List environmentRepositories = new ArrayList<>(); + Environment env = new Environment(application, new String[] { profile }, label, null, null); + for (String l : labels) { + NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(), + new NativeEnvironmentProperties(), this.observationRegistry); + Locations locations = getLocations(application, profile, l); + delegate.setSearchLocations(locations.getLocations()); + env.addAll(delegate.findOne(application, profile, "", includeOrigin).getPropertySources()); + } + result = env; + } + else { + NativeEnvironmentRepository delegate = new NativeEnvironmentRepository(getEnvironment(), + new NativeEnvironmentProperties(), this.observationRegistry); + Locations locations = getLocations(application, profile, label); + delegate.setSearchLocations(locations.getLocations()); + result = delegate.findOne(application, profile, "", includeOrigin); + result.setVersion(locations.getVersion()); + } result.setLabel(label); return this.cleaner.clean(result, getWorkingDirectory().toURI().toString(), getUri()); } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentRepository.java index e815eda0..777273a6 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsS3EnvironmentRepository.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Properties; @@ -93,15 +94,28 @@ public class AwsS3EnvironmentRepository implements EnvironmentRepository, Ordere final Environment environment = new Environment(application, profileArray); environment.setLabel(label); - for (String profile : profileArray) { - for (String app : apps) { - addPropertySource(environment, app, profile, label); + List labels; + if (StringUtils.hasText(label) && label.contains(",")) { + labels = Arrays.asList(StringUtils.commaDelimitedListToStringArray(label)); + Collections.reverse(labels); + } + else { + labels = Collections.singletonList(label); + } + + for (String l : labels) { + for (String profile : profileArray) { + for (String app : apps) { + addPropertySource(environment, app, profile, l); + } } } // Add propertysources without profiles as well - for (String app : apps) { - addPropertySource(environment, app, null, label); + for (String l : labels) { + for (String app : apps) { + addPropertySource(environment, app, null, l); + } } if (LOG.isDebugEnabled()) { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepository.java index e497b8b7..2dce7d92 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepository.java @@ -101,29 +101,37 @@ public class AwsSecretsManagerEnvironmentRepository implements EnvironmentReposi environment.add(new PropertySource("overrides", overrides)); } + List labels; + if (StringUtils.hasText(label) && label.contains(",")) { + labels = Arrays.asList(StringUtils.commaDelimitedListToStringArray(label)); + Collections.reverse(labels); + } + else { + labels = Collections.singletonList(label); + } + List reversedProfiles = new ArrayList<>(Arrays.asList(profiles)); Collections.reverse(reversedProfiles); - for (String profile : reversedProfiles) { - addPropertySource(environment, application, profile, label); - if (!defaultApplication.equals(application)) { - addPropertySource(environment, defaultApplication, profile, label); + for (String l : labels) { + for (String profile : reversedProfiles) { + addPropertySource(environment, application, profile, l); + if (!defaultApplication.equals(application)) { + addPropertySource(environment, defaultApplication, profile, l); + } + } + if (!Arrays.asList(profiles).contains(defaultProfile)) { + addPropertySource(environment, application, defaultProfile, l); + } + if (!Arrays.asList(profiles).contains(defaultProfile) && !defaultApplication.equals(application)) { + addPropertySource(environment, defaultApplication, defaultProfile, l); } - } - if (!Arrays.asList(profiles).contains(defaultProfile)) { - addPropertySource(environment, application, defaultProfile, label); + if (!defaultApplication.equals(application)) { + addPropertySource(environment, application, null, l); + } + addPropertySource(environment, defaultApplication, null, l); } - if (!Arrays.asList(profiles).contains(defaultProfile) && !defaultApplication.equals(application)) { - addPropertySource(environment, defaultApplication, defaultProfile, label); - } - - if (!defaultApplication.equals(application)) { - addPropertySource(environment, application, null, label); - } - - addPropertySource(environment, defaultApplication, null, label); - return environment; } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepository.java index f0836a2a..fd96e9f1 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepository.java @@ -77,11 +77,14 @@ public class CredhubEnvironmentRepository implements EnvironmentRepository, Orde List applications = normalize(application, DEFAULT_APPLICATION); List profiles = normalize(profile, DEFAULT_PROFILE); + List labels = normalize(label, this.defaultLabel); Environment environment = new Environment(application, split(profile), label, null, null); - for (String prof : profiles) { - for (String app : applications) { - addPropertySource(environment, app, prof, label); + for (String l : labels) { + for (String prof : profiles) { + for (String app : applications) { + addPropertySource(environment, app, prof, l); + } } } 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 6d3d168b..cd23cb2a 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 @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -210,9 +211,19 @@ public class NativeEnvironmentRepository implements EnvironmentRepository, Searc if (this.addLabelLocations) { for (String location : locations) { if (StringUtils.hasText(label)) { - String labelled = location + label.trim() + "/"; - if (isDirectory(labelled)) { - output.add(labelled); + List labels; + if (label.contains(",")) { + labels = Arrays.asList(StringUtils.commaDelimitedListToStringArray(label)); + Collections.reverse(labels); + } + else { + labels = Collections.singletonList(label); + } + for (String l : labels) { + String labelled = location + l + "/"; + if (isDirectory(labelled)) { + output.add(labelled); + } } } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AwsS3IntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AwsS3IntegrationTests.java index da5e0ad1..f65b91f3 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AwsS3IntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/AwsS3IntegrationTests.java @@ -103,6 +103,15 @@ public class AwsS3IntegrationTests { objectResponse = s3Client.putObject((request) -> request.bucket("test-bucket").key("data-dev.properties"), RequestBody.fromString("bar=1")); LOG.info("object response " + objectResponse); + objectResponse = s3Client.putObject((request) -> request.bucket("test-bucket").key("main/foo.properties"), + RequestBody.fromString("foo=1")); + LOG.info("object response " + objectResponse); + objectResponse = s3Client.putObject((request) -> request.bucket("test-bucket").key("dev/foo.properties"), + RequestBody.fromString("devfoo=1")); + LOG.info("object response " + objectResponse); + objectResponse = s3Client.putObject((request) -> request.bucket("test-bucket").key("test/foo.properties"), + RequestBody.fromString("testfoo=1")); + LOG.info("object response " + objectResponse); } @AfterAll @@ -124,6 +133,16 @@ public class AwsS3IntegrationTests { .isEqualTo("this is a test"); } + @Test + public void testMultipleLabels() throws IOException { + RestTemplate rest = new RestTemplateBuilder().build(); + String configServerUrl = "http://localhost:" + configServerPort; + Environment env = rest.getForObject(configServerUrl + "/foo/default/main,dev,test", Environment.class); + assertThat(env.getPropertySources().get(0).getSource().get("testfoo")).isEqualTo("1"); + assertThat(env.getPropertySources().get(1).getSource().get("devfoo")).isEqualTo("1"); + assertThat(env.getPropertySources().get(2).getSource().get("foo")).isEqualTo("1"); + } + @Test public void defaultApplicationAndProfileIncluded() throws IOException { RestTemplate rest = new RestTemplateBuilder().build(); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeIntegrationTests.java index 660751a9..51ee665d 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeIntegrationTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/CompositeIntegrationTests.java @@ -19,6 +19,7 @@ package org.springframework.cloud.config.server; import org.eclipse.jgit.junit.MockSystemReader; import org.eclipse.jgit.util.SystemReader; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @@ -41,6 +42,7 @@ import static org.springframework.cloud.config.server.test.ConfigServerTestUtils */ public class CompositeIntegrationTests { + @Nested @SpringBootTest(classes = TestConfigServerApplication.class, properties = { "spring.config.name:compositeconfigserver", "spring.cloud.config.server.svn.uri:file:///./target/repos/svn-config-repo", @@ -49,7 +51,7 @@ public class CompositeIntegrationTests { "spring.cloud.config.server.git.order:1" }, webEnvironment = RANDOM_PORT) @ActiveProfiles({ "test", "git", "subversion" }) - public static class StaticTests { + class StaticTests { @LocalServerPort private int port; @@ -92,6 +94,7 @@ public class CompositeIntegrationTests { } + @Nested @SpringBootTest(classes = TestConfigServerApplication.class, properties = { "spring.config.name:compositeconfigserver", "spring.cloud.config.server.svn.uri:file:///./target/repos/svn-config-repo", @@ -100,7 +103,7 @@ public class CompositeIntegrationTests { "spring.cloud.config.server.git.order:1", "spring.cloud.config.server.reverseLocationOrder:true" }, webEnvironment = RANDOM_PORT) @ActiveProfiles({ "test", "git", "subversion" }) - public static class ReverseLocationOrderTest { + class ReverseLocationOrderTest { @LocalServerPort private int port; @@ -130,6 +133,7 @@ public class CompositeIntegrationTests { } + @Nested @SpringBootTest(classes = TestConfigServerApplication.class, properties = { "spring.config.name:compositeconfigserver", "spring.cloud.config.server.composite[0].uri:file:./target/repos/config-repo", @@ -138,7 +142,7 @@ public class CompositeIntegrationTests { "spring.cloud.config.server.composite[1].type:svn" }, webEnvironment = RANDOM_PORT) @ActiveProfiles({ "test", "composite" }) - public static class ListTests { + class ListTests { @LocalServerPort private int port; diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepositoryTests.java index d819f01f..7df4e7f2 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/AwsSecretsManagerEnvironmentRepositoryTests.java @@ -18,6 +18,7 @@ package org.springframework.cloud.config.server.environment; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -1595,6 +1596,41 @@ public class AwsSecretsManagerEnvironmentRepositoryTests { assertThat(resultEnv).usingRecursiveComparison().withStrictTypeChecking().isEqualTo(expectedEnv); } + @Test + public void testFindOneWithExistingApplicationAndDefaultProfileAndExistingLabelWhenMultipleLabelIsSet() { + String application = "foo"; + String profile = configServerProperties.getDefaultProfile(); + String label = "release,test"; + String[] profiles = StringUtils.commaDelimitedListToStringArray(profile); + + String fooPropertiesName = "aws:secrets:/secret/foo/"; + PropertySource fooProperties = new PropertySource(fooPropertiesName, getFooReleaseProperties()); + + String fooDefaultPropertiesName = "aws:secrets:/secret/foo-default/"; + PropertySource fooDefaultProperties = new PropertySource(fooDefaultPropertiesName, + getFooDefaultReleaseProperties()); + + String applicationDefaultPropertiesName = "aws:secrets:/secret/application-default/"; + PropertySource applicationDefaultProperties = new PropertySource(applicationDefaultPropertiesName, + getApplicationDefaultReleaseProperties()); + + String applicationPropertiesName = "aws:secrets:/secret/application/"; + PropertySource applicationProperties = new PropertySource(applicationPropertiesName, + getApplicationReleaseProperties()); + + Environment expectedEnv = new Environment(application, profiles, label, null, null); + expectedEnv.addAll(Arrays.asList(applicationDefaultProperties, fooProperties)); + + putSecrets("release", Collections.singletonList(fooProperties)); + putSecrets("dev", Collections.singletonList(fooDefaultProperties)); + putSecrets("test", Collections.singletonList(applicationDefaultProperties)); + putSecrets("", Collections.singletonList(applicationProperties)); + + Environment resultEnv = labeledRepository.findOne(application, profile, label); + + assertThat(resultEnv).usingRecursiveComparison().withStrictTypeChecking().isEqualTo(expectedEnv); + } + @Test public void testFindOneWithExistingApplicationAndNonExistingProfileAndExistingLabelWhenDefaultLabelIsSet() { String application = "foo"; @@ -2640,7 +2676,11 @@ public class AwsSecretsManagerEnvironmentRepositoryTests { private void putSecrets(Environment environment) { String label = environment.getLabel() != null ? environment.getLabel() : environmentProperties.getDefaultLabel(); - for (PropertySource ps : environment.getPropertySources()) { + putSecrets(label, environment.getPropertySources()); + } + + private void putSecrets(String label, List propertySources) { + for (PropertySource ps : propertySources) { String path = StringUtils.delete(ps.getName(), environmentProperties.getOrigin()); String secrets = getSecrets(ps); CreateSecretResponse response = smClient diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepositoryTests.java index d8d15109..aee73a8c 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/CredhubEnvironmentRepositoryTests.java @@ -121,6 +121,28 @@ public class CredhubEnvironmentRepositoryTests { assertThat(environment.getPropertySources().get(1).getSource()).isEqualTo(Map.of("k1", "v1")); } + @Test + public void shouldRetrieveGivenLabelsProfiles() { + stubCredentials("/myApp/prod/myLabel", credential("c1", "k1", "v1")); + stubCredentials("/myApp/cloud/mySecondLabel", credential("c2", "k2", "v2")); + stubCredentials("/myApp/prod/myThirdLabel", credential("c3", "k3", "v3")); + + Environment environment = this.credhubEnvironmentRepository.findOne("myApp", "prod,cloud", + "myLabel,mySecondLabel"); + + assertThat(environment.getName()).isEqualTo("myApp"); + assertThat(environment.getProfiles()).containsExactly("prod", "cloud"); + assertThat(environment.getLabel()).isEqualTo("myLabel,mySecondLabel"); + + assertThat(environment.getPropertySources()).hasSize(2); + + assertThat(environment.getPropertySources().get(0).getName()).isEqualTo("credhub-myApp-cloud-mySecondLabel"); + assertThat(environment.getPropertySources().get(0).getSource()).isEqualTo(Map.of("k2", "v2")); + + assertThat(environment.getPropertySources().get(1).getName()).isEqualTo("credhub-myApp-prod-myLabel"); + assertThat(environment.getPropertySources().get(1).getSource()).isEqualTo(Map.of("k1", "v1")); + } + @Test public void shouldRetrieveGivenMultipleApplicationNames() { stubCredentials("/app1/default/myLabel", credential("c1", "k1", "v1")); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java index 7ccae2d3..30d4e5ee 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java @@ -210,6 +210,13 @@ public class JGitEnvironmentRepositoryTests { assertVersion(environment); } + @Test + public void multipleLabels() { + this.repository.setBasedir(this.basedir); + Environment environment = this.repository.findOne("bar", "staging", "master,foo,raw"); + assertThat(environment.getPropertySources()).hasSize(6); + } + @Test public void basedirExists() throws Exception { assertThat(this.basedir.mkdirs()).isTrue(); 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 95a74dcd..20645a2c 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 @@ -244,6 +244,16 @@ public class NativeEnvironmentRepositoryTests { assertThat(environment.getPropertySources().get(0).getSource().get("foo")).isNotEqualTo("dev_bar"); } + @Test + public void locationAddMultipleLabelLocations() { + this.repository.setSearchLocations("classpath:/test/dev/"); + Environment environment = this.repository.findOne("foo", "development", "ignore,applicationxyz"); + assertThat(environment.getPropertySources()).hasSize(3); + assertThat(environment.getPropertySources().get(0).getSource().get("foo")).isEqualTo("app"); + assertThat(environment.getPropertySources().get(1).getSource().get("foo")).isEqualTo("default-app"); + assertThat(environment.getPropertySources().get(2).getSource().get("foo")).isEqualTo("dev_bar"); + } + @Test public void tryToStartReactive() { this.repository.setSearchLocations("classpath:/test/reactive/"); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/SVNKitEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/SVNKitEnvironmentRepositoryTests.java index 22cc50e1..a14095c6 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/SVNKitEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/SVNKitEnvironmentRepositoryTests.java @@ -115,6 +115,15 @@ public class SVNKitEnvironmentRepositoryTests { assertThat(environment.getPropertySources().get(1).getName()).contains("application.yml"); } + @Test + public void testMultipleLabels() { + Environment environment = this.repository.findOne("bar", "staging", "branches/demobranch,trunk"); + assertThat(environment.getPropertySources()).hasSize(3); + assertThat(environment.getPropertySources().get(0).getName()).contains("bar.properties"); + assertThat(environment.getPropertySources().get(1).getName()).contains("application.yml"); + assertThat(environment.getPropertySources().get(2).getName()).contains("branches/demobranch/bar.properties"); + } + @Test public void invalidLabel() { Assertions.assertThatThrownBy(() -> {