Skip to content

Commit 12ab712

Browse files
committed
Merge branch '4.3.x'
2 parents 108e50c + 7edc261 commit 12ab712

5 files changed

Lines changed: 401 additions & 0 deletions

File tree

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.cloud.config.environment.PropertySource;
3838
import org.springframework.cloud.config.server.config.ConfigServerProperties;
3939
import org.springframework.core.Ordered;
40+
import org.springframework.core.env.Profiles;
4041
import org.springframework.core.io.InputStreamResource;
4142
import org.springframework.util.ObjectUtils;
4243
import org.springframework.util.StringUtils;
@@ -139,6 +140,11 @@ private void addPropertySources(Environment environment, List<String> apps, Stri
139140
for (String label : labels) {
140141
addPropertySourcesForApps(apps,
141142
app -> addNonProfileSpecificPropertySource(environment, app, null, label));
143+
// Even with no profiles, negated profile documents (e.g. on-profile:
144+
// "!my-profile") should be included because no profile is active,
145+
// so all negations are satisfied
146+
addPropertySourcesForApps(apps,
147+
app -> addNegatedProfilePropertySource(environment, app, profiles, label));
142148
}
143149
}
144150
else {
@@ -154,6 +160,11 @@ private void addPropertySources(Environment environment, List<String> apps, Stri
154160
addPropertySourcesForApps(apps,
155161
app -> addNonProfileSpecificPropertySource(environment, app, profile, label));
156162
}
163+
// Handle documents with negated profile expressions (e.g. on-profile:
164+
// "!my-profile")
165+
// once per label rather than once per profile to avoid duplicates
166+
addPropertySourcesForApps(apps,
167+
app -> addNegatedProfilePropertySource(environment, app, profiles, label));
157168
}
158169
}
159170
}
@@ -162,6 +173,32 @@ private void addPropertySourcesForApps(List<String> apps, Consumer<String> addPr
162173
apps.forEach(addPropertySource);
163174
}
164175

176+
private void addNegatedProfilePropertySource(Environment environment, String app, String[] allProfiles,
177+
String label) {
178+
List<S3ConfigFile> s3ConfigFiles = getNegatedProfileS3ConfigFileYaml(app, allProfiles, label);
179+
addPropertySource(environment, s3ConfigFiles);
180+
}
181+
182+
private List<S3ConfigFile> getNegatedProfileS3ConfigFileYaml(String application, String[] allProfiles,
183+
String label) {
184+
List<S3ConfigFile> configFiles = new ArrayList<>();
185+
try {
186+
S3ConfigFile configFile = new NegatedProfileYamlDocumentS3ConfigFile(application, label, bucketName,
187+
useApplicationAsDirectory, s3Client, allProfiles);
188+
configFiles.add(configFile);
189+
}
190+
catch (IllegalStateException e) {
191+
LOG.warn("Could not read YAML file using application <" + application + "> label <" + label + ">.", e);
192+
}
193+
catch (Exception e) {
194+
if (LOG.isDebugEnabled()) {
195+
LOG.debug("Did not find negated profile yaml document in non-profile specific file using application <"
196+
+ application + "> label <" + label + ">.");
197+
}
198+
}
199+
return configFiles;
200+
}
201+
165202
private void addProfileSpecificPropertySource(Environment environment, String app, String profile, String label) {
166203
List<S3ConfigFile> s3ConfigFiles = getS3ConfigFile(app, profile, label, this::getS3PropertiesOrJsonConfigFile,
167204
this::getProfileSpecificS3ConfigFileYaml);
@@ -576,3 +613,45 @@ protected List<String> getExtensions() {
576613
}
577614

578615
}
616+
617+
class NegatedProfileYamlDocumentS3ConfigFile extends YamlS3ConfigFile {
618+
619+
NegatedProfileYamlDocumentS3ConfigFile(String application, String label, String bucketName,
620+
boolean useApplicationAsDirectory, S3Client s3Client, String[] allProfiles) {
621+
super(application, null, label, bucketName, useApplicationAsDirectory, s3Client, properties -> {
622+
Object onProfileValue = properties.get("spring.config.activate.on-profile");
623+
if (onProfileValue == null) {
624+
onProfileValue = properties.get("spring.config.activate.onProfile");
625+
}
626+
if (onProfileValue == null) {
627+
return YamlProcessor.MatchStatus.NOT_FOUND;
628+
}
629+
String expression = onProfileValue.toString().trim();
630+
// Simple positive profile names are already handled by
631+
// ProfileSpecificYamlDocumentS3ConfigFile. Only process complex or negated
632+
// expressions here to avoid adding duplicate property sources.
633+
if (isSimpleProfileName(expression)) {
634+
return YamlProcessor.MatchStatus.NOT_FOUND;
635+
}
636+
List<String> allProfilesList = Arrays.asList(allProfiles);
637+
boolean matches = Profiles.of(expression).matches(allProfilesList::contains);
638+
return matches ? YamlProcessor.MatchStatus.FOUND : YamlProcessor.MatchStatus.NOT_FOUND;
639+
});
640+
}
641+
642+
private static boolean isSimpleProfileName(String expression) {
643+
return !expression.contains("!") && !expression.contains("&") && !expression.contains("|")
644+
&& !expression.contains("(") && !expression.contains(",");
645+
}
646+
647+
@Override
648+
protected String buildObjectKeyPrefix() {
649+
return super.buildObjectKeyPrefix(false);
650+
}
651+
652+
@Override
653+
public boolean isShouldIncludeWithEmptyProperties() {
654+
return false;
655+
}
656+
657+
}

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

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.file.Paths;
2222
import java.util.ArrayList;
2323
import java.util.List;
24+
import java.util.Optional;
2425
import java.util.Properties;
2526

2627
import org.junit.jupiter.api.AfterEach;
@@ -167,6 +168,279 @@ public void multiDocumentYaml() throws IOException {
167168
// @formatter:on
168169
}
169170

171+
@Test
172+
public void negatedProfileDocumentIncludedWhenNoProfilesRequested() throws IOException {
173+
Resource resource = new ClassPathResource("awss3/application-with-negated-profile.yaml");
174+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
175+
putFiles("application.yaml", yamlString);
176+
177+
// null profiles → no active profiles → "!my-profile" should always match
178+
final Environment env = envRepo.findOne("application", null, null);
179+
180+
List<PropertySource> propertySources = env.getPropertySources();
181+
Optional<PropertySource> negatedProfileSource = propertySources.stream()
182+
.filter(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"))
183+
.findFirst();
184+
assertThat(negatedProfileSource).as("negated profile document should be present when no profiles are active")
185+
.isPresent();
186+
assertThat(negatedProfileSource.get().getSource().get("demo.negatedProfileMarker"))
187+
.isEqualTo("present-when-my-profile-is-not-active");
188+
}
189+
190+
@Test
191+
public void negatedProfileDocumentIncludedWhenProfileNotActive() throws IOException {
192+
Resource resource = new ClassPathResource("awss3/application-with-negated-profile.yaml");
193+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
194+
putFiles("application.yaml", yamlString);
195+
196+
final Environment env = envRepo.findOne("application", "default", null);
197+
198+
List<PropertySource> propertySources = env.getPropertySources();
199+
Optional<PropertySource> negatedProfileSource = propertySources.stream()
200+
.filter(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"))
201+
.findFirst();
202+
assertThat(negatedProfileSource)
203+
.as("negated profile document should be present when 'my-profile' is not active")
204+
.isPresent();
205+
assertThat(negatedProfileSource.get().getSource().get("demo.negatedProfileMarker"))
206+
.isEqualTo("present-when-my-profile-is-not-active");
207+
208+
Optional<PropertySource> baseSource = propertySources.stream()
209+
.filter(ps -> ps.getSource().containsKey("demo.base"))
210+
.findFirst();
211+
assertThat(baseSource).as("non-profile-specific document should be present").isPresent();
212+
assertThat(baseSource.get().getSource().get("demo.base")).isEqualTo("base-value");
213+
}
214+
215+
@Test
216+
public void negatedProfileDocumentExcludedWhenProfileIsActive() throws IOException {
217+
Resource resource = new ClassPathResource("awss3/application-with-negated-profile.yaml");
218+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
219+
putFiles("application.yaml", yamlString);
220+
221+
final Environment env = envRepo.findOne("application", "my-profile", null);
222+
223+
List<PropertySource> propertySources = env.getPropertySources();
224+
boolean hasNegatedProfileMarker = propertySources.stream()
225+
.anyMatch(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"));
226+
assertThat(hasNegatedProfileMarker)
227+
.as("negated profile document should NOT be present when 'my-profile' is active")
228+
.isFalse();
229+
230+
Optional<PropertySource> baseSource = propertySources.stream()
231+
.filter(ps -> ps.getSource().containsKey("demo.base"))
232+
.findFirst();
233+
assertThat(baseSource).as("non-profile-specific document should still be present").isPresent();
234+
assertThat(baseSource.get().getSource().get("demo.base")).isEqualTo("base-value");
235+
}
236+
237+
@Test
238+
public void negatedProfileDocumentExcludedWhenNegatedProfileIsOneOfMultipleActiveProfiles() throws IOException {
239+
Resource resource = new ClassPathResource("awss3/application-with-negated-profile.yaml");
240+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
241+
putFiles("application.yaml", yamlString);
242+
243+
// "my-profile" is among the active profiles, so "!my-profile" should not match
244+
final Environment env = envRepo.findOne("application", "my-profile,other-profile", null);
245+
246+
List<PropertySource> propertySources = env.getPropertySources();
247+
boolean hasNegatedProfileMarker = propertySources.stream()
248+
.anyMatch(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"));
249+
assertThat(hasNegatedProfileMarker)
250+
.as("negated profile document should NOT be present when 'my-profile' is among the active profiles")
251+
.isFalse();
252+
}
253+
254+
@Test
255+
public void negatedProfileDocumentIncludedWhenNegatedProfileIsAbsentFromMultipleActiveProfiles()
256+
throws IOException {
257+
Resource resource = new ClassPathResource("awss3/application-with-negated-profile.yaml");
258+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
259+
putFiles("application.yaml", yamlString);
260+
261+
// neither "default" nor "other-profile" is "my-profile", so "!my-profile" should
262+
// match
263+
final Environment env = envRepo.findOne("application", "default,other-profile", null);
264+
265+
List<PropertySource> propertySources = env.getPropertySources();
266+
Optional<PropertySource> negatedProfileSource = propertySources.stream()
267+
.filter(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"))
268+
.findFirst();
269+
assertThat(negatedProfileSource)
270+
.as("negated profile document should be present when 'my-profile' is absent from all active profiles")
271+
.isPresent();
272+
assertThat(negatedProfileSource.get().getSource().get("demo.negatedProfileMarker"))
273+
.isEqualTo("present-when-my-profile-is-not-active");
274+
}
275+
276+
@Test
277+
public void twoNegatedProfileDocumentsBothIncludedWhenNeitherProfileIsActive() throws IOException {
278+
Resource resource = new ClassPathResource("awss3/application-with-two-negated-profiles.yaml");
279+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
280+
putFiles("application.yaml", yamlString);
281+
282+
final Environment env = envRepo.findOne("application", "default", null);
283+
284+
List<PropertySource> propertySources = env.getPropertySources();
285+
286+
// Both negated profile documents match and are merged into one property source
287+
Optional<PropertySource> negatedSource = propertySources.stream()
288+
.filter(ps -> ps.getSource().containsKey("demo.negatedProfileMarker")
289+
|| ps.getSource().containsKey("demo.otherNegatedMarker"))
290+
.findFirst();
291+
assertThat(negatedSource)
292+
.as("negated profile documents should be present when neither 'my-profile' nor 'other-profile' is active")
293+
.isPresent();
294+
assertThat(negatedSource.get().getSource().get("demo.negatedProfileMarker"))
295+
.isEqualTo("present-when-my-profile-is-not-active");
296+
assertThat(negatedSource.get().getSource().get("demo.otherNegatedMarker"))
297+
.isEqualTo("present-when-other-profile-is-not-active");
298+
299+
Optional<PropertySource> baseSource = propertySources.stream()
300+
.filter(ps -> ps.getSource().containsKey("demo.base"))
301+
.findFirst();
302+
assertThat(baseSource).as("non-profile-specific document should be present").isPresent();
303+
assertThat(baseSource.get().getSource().get("demo.base")).isEqualTo("base-value");
304+
}
305+
306+
@Test
307+
public void twoNegatedProfileDocumentsOnlyOneIncludedWhenOneProfileIsActive() throws IOException {
308+
Resource resource = new ClassPathResource("awss3/application-with-two-negated-profiles.yaml");
309+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
310+
putFiles("application.yaml", yamlString);
311+
312+
// "my-profile" is active, so the "!my-profile" document should be excluded
313+
// but the "!other-profile" document should still be included
314+
final Environment env = envRepo.findOne("application", "my-profile", null);
315+
316+
List<PropertySource> propertySources = env.getPropertySources();
317+
318+
boolean hasNegatedProfileMarker = propertySources.stream()
319+
.anyMatch(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"));
320+
assertThat(hasNegatedProfileMarker)
321+
.as("'!my-profile' document should NOT be present when 'my-profile' is active")
322+
.isFalse();
323+
324+
Optional<PropertySource> otherNegatedSource = propertySources.stream()
325+
.filter(ps -> ps.getSource().containsKey("demo.otherNegatedMarker"))
326+
.findFirst();
327+
assertThat(otherNegatedSource)
328+
.as("'!other-profile' document should still be present when 'my-profile' is active")
329+
.isPresent();
330+
assertThat(otherNegatedSource.get().getSource().get("demo.otherNegatedMarker"))
331+
.isEqualTo("present-when-other-profile-is-not-active");
332+
333+
Optional<PropertySource> baseSource = propertySources.stream()
334+
.filter(ps -> ps.getSource().containsKey("demo.base"))
335+
.findFirst();
336+
assertThat(baseSource).as("non-profile-specific document should still be present").isPresent();
337+
assertThat(baseSource.get().getSource().get("demo.base")).isEqualTo("base-value");
338+
}
339+
340+
@Test
341+
public void complexProfileExpressionsAreEvaluatedCorrectly() throws IOException {
342+
Resource resource = new ClassPathResource("awss3/application-with-complex-profile-expressions.yaml");
343+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
344+
putFiles("application.yaml", yamlString);
345+
346+
// profile1 active, profile2 not active
347+
// "profile1 & !profile2" -> true
348+
// "!(profile1 & profile2)" -> !(true & false) -> true
349+
// "!profile1 | !profile2" -> false | true -> true
350+
Environment env = envRepo.findOne("application", "profile1", null);
351+
List<PropertySource> sources = env.getPropertySources();
352+
353+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.andExpression")))
354+
.as("'profile1 & !profile2' should match when profile1 active, profile2 not active")
355+
.isTrue();
356+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.notAndExpression")))
357+
.as("'!(profile1 & profile2)' should match when not both profiles active")
358+
.isTrue();
359+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.orNegatedExpression")))
360+
.as("'!profile1 | !profile2' should match when at least one profile is not active")
361+
.isTrue();
362+
}
363+
364+
@Test
365+
public void complexProfileExpressionsAreExcludedWhenNotSatisfied() throws IOException {
366+
Resource resource = new ClassPathResource("awss3/application-with-complex-profile-expressions.yaml");
367+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
368+
putFiles("application.yaml", yamlString);
369+
370+
// both profile1 and profile2 active
371+
// "profile1 & !profile2" -> true & false -> false
372+
// "!(profile1 & profile2)" -> !(true & true) -> false
373+
// "!profile1 | !profile2" -> false | false -> false
374+
Environment env = envRepo.findOne("application", "profile1,profile2", null);
375+
List<PropertySource> sources = env.getPropertySources();
376+
377+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.andExpression")))
378+
.as("'profile1 & !profile2' should NOT match when both profiles are active")
379+
.isFalse();
380+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.notAndExpression")))
381+
.as("'!(profile1 & profile2)' should NOT match when both profiles are active")
382+
.isFalse();
383+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.orNegatedExpression")))
384+
.as("'!profile1 | !profile2' should NOT match when both profiles are active")
385+
.isFalse();
386+
}
387+
388+
@Test
389+
public void negatedProfileDocumentIncludedWhenProfileNotActive_ApplicationDirVariant() throws IOException {
390+
Resource resource = new ClassPathResource("awss3/application-with-negated-profile.yaml");
391+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
392+
putFiles("application/application.yaml", yamlString);
393+
394+
final Environment env = envRepoApplicationDir.findOne("application", "default", null);
395+
396+
List<PropertySource> propertySources = env.getPropertySources();
397+
Optional<PropertySource> negatedProfileSource = propertySources.stream()
398+
.filter(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"))
399+
.findFirst();
400+
assertThat(negatedProfileSource)
401+
.as("negated profile document should be present when 'my-profile' is not active")
402+
.isPresent();
403+
assertThat(negatedProfileSource.get().getSource().get("demo.negatedProfileMarker"))
404+
.isEqualTo("present-when-my-profile-is-not-active");
405+
}
406+
407+
@Test
408+
public void negatedProfileDocumentExcludedWhenNegatedProfileIsOneOfMultipleActiveProfiles_ApplicationDirVariant()
409+
throws IOException {
410+
Resource resource = new ClassPathResource("awss3/application-with-negated-profile.yaml");
411+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
412+
putFiles("application/application.yaml", yamlString);
413+
414+
final Environment env = envRepoApplicationDir.findOne("application", "my-profile,other-profile", null);
415+
416+
boolean hasNegatedProfileMarker = env.getPropertySources()
417+
.stream()
418+
.anyMatch(ps -> ps.getSource().containsKey("demo.negatedProfileMarker"));
419+
assertThat(hasNegatedProfileMarker)
420+
.as("negated profile document should NOT be present when 'my-profile' is among the active profiles")
421+
.isFalse();
422+
}
423+
424+
@Test
425+
public void complexProfileExpressionsAreEvaluatedCorrectly_ApplicationDirVariant() throws IOException {
426+
Resource resource = new ClassPathResource("awss3/application-with-complex-profile-expressions.yaml");
427+
String yamlString = new String(Files.readAllBytes(Paths.get(resource.getURI())));
428+
putFiles("application/application.yaml", yamlString);
429+
430+
Environment env = envRepoApplicationDir.findOne("application", "profile1", null);
431+
List<PropertySource> sources = env.getPropertySources();
432+
433+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.andExpression")))
434+
.as("'profile1 & !profile2' should match when profile1 active, profile2 not active")
435+
.isTrue();
436+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.notAndExpression")))
437+
.as("'!(profile1 & profile2)' should match when not both profiles active")
438+
.isTrue();
439+
assertThat(sources.stream().anyMatch(ps -> ps.getSource().containsKey("demo.orNegatedExpression")))
440+
.as("'!profile1 | !profile2' should match when at least one profile is not active")
441+
.isTrue();
442+
}
443+
170444
@Test
171445
public void failToFindNonexistentObject() {
172446
Environment env = envRepo.findOne("foo", "bar", null);

0 commit comments

Comments
 (0)