Merge branch '3.0.x'

This commit is contained in:
Ryan Baxter
2024-02-01 16:14:59 -05:00
3 changed files with 491 additions and 27 deletions

View File

@@ -16,12 +16,14 @@
package org.springframework.cloud.kubernetes.commons.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
@@ -32,7 +34,6 @@ import org.springframework.core.env.MapPropertySource;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.KEY_VALUE_TO_PROPERTIES;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.PROPERTIES_TO_MAP;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.throwingMerger;
import static org.springframework.cloud.kubernetes.commons.config.PropertySourceUtils.yamlParserGenerator;
/**
@@ -73,42 +74,77 @@ public class SourceDataEntriesProcessor extends MapPropertySource {
return defaultProcessAllEntries(input, environment);
}
private static Map<String, Object> defaultProcessAllEntries(Map<String, String> input, Environment environment) {
/**
* <pre>
* we want to sort entries coming from the k8s source in a specific way:
*
* 1. "application.yaml/yml/properties" have to come first
* (or the value from spring.application.name)
* 2. then profile specific entries, like "application-dev.yaml"
* 3. then plain properties
* </pre>
*/
static List<Map.Entry<String, String>> sorted(Map<String, String> input, Environment environment) {
record WeightedEntry(Map.Entry<String, String> entry, int weight) {
}
// we pass empty Strings on purpose, the logic here is either the value of
// "spring.application.name"
// or literal "application".
// "spring.application.name" or literal "application".
String applicationName = ConfigUtils.getApplicationName(environment, "", "");
String[] activeProfiles = environment.getActiveProfiles();
Set<String> fileNames = Stream
.concat(Stream.of(applicationName),
Arrays.stream(activeProfiles).map(profile -> applicationName + "-" + profile))
.collect(Collectors.toSet());
// the order here is important, first has to come "application.yaml" and then
// "application-dev.yaml"
List<String> orderedFileNames = Stream.concat(Stream.of(applicationName),
Arrays.stream(activeProfiles).map(profile -> applicationName + "-" + profile)).toList();
return input.entrySet().stream().map(e -> extractProperties(e.getKey(), e.getValue(), fileNames, environment))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, throwingMerger(), HashMap::new));
}
private static Map<String, Object> extractProperties(String resourceName, String content, Set<String> fileNames,
Environment environment) {
if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml") || resourceName.endsWith(".properties")) {
if (fileNames.contains(resourceName.split("\\.", 2)[0])) {
if (resourceName.endsWith(".properties")) {
LOG.debug("entry : " + resourceName + " will be treated as a single properties file");
return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content);
int current = orderedFileNames.size() - 1;
List<WeightedEntry> weightedEntries = new ArrayList<>();
for (Map.Entry<String, String> entry : input.entrySet()) {
String key = entry.getKey();
if (key.endsWith(".yml") || key.endsWith(".yaml") || key.endsWith(".properties")) {
String withoutExtension = key.split("\\.", 2)[0];
int index = orderedFileNames.indexOf(withoutExtension);
if (index >= 0) {
weightedEntries.add(new WeightedEntry(entry, index));
}
else {
LOG.debug("entry : " + resourceName + " will be treated as a single yml/yaml file");
return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content);
LOG.warn("entry : " + key + " will be skipped");
}
}
else {
LOG.warn("entry : " + resourceName + " will be skipped");
return Collections.emptyMap();
weightedEntries.add(new WeightedEntry(entry, ++current));
}
}
return weightedEntries.stream().sorted(Comparator.comparing(WeightedEntry::weight)).map(WeightedEntry::entry)
.toList();
}
private static Map<String, Object> defaultProcessAllEntries(Map<String, String> input, Environment environment) {
List<Map.Entry<String, String>> sortedEntries = sorted(input, environment);
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, String> entry : sortedEntries) {
result.putAll(extractProperties(entry.getKey(), entry.getValue(), environment));
}
return result;
}
private static Map<String, Object> extractProperties(String resourceName, String content, Environment environment) {
if (resourceName.endsWith(".yml") || resourceName.endsWith(".yaml") || resourceName.endsWith(".properties")) {
if (resourceName.endsWith(".properties")) {
LOG.debug("entry : " + resourceName + " will be treated as a single properties file");
return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(content);
}
else {
LOG.debug("entry : " + resourceName + " will be treated as a single yml/yaml file");
return yamlParserGenerator(environment).andThen(PROPERTIES_TO_MAP).apply(content);
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.commons.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
/**
* @author wind57
*/
class SourceDataEntriesProcessorOrderedPropertiesTests {
/**
* <pre>
* - a single property is present
* </pre>
*/
@Test
void testSingleNonFileProperty() {
Map<String, String> map = new LinkedHashMap<>();
map.put("my-key", "my-value");
MockEnvironment mockEnvironment = new MockEnvironment();
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment);
Assertions.assertEquals(Map.of("my-key", "my-value"), result);
}
/**
* <pre>
* - a single property from a properties file
* </pre>
*/
@Test
void testSingleFileProperty() {
Map<String, String> map = new LinkedHashMap<>();
map.put("application.properties", "my-key=from-app");
MockEnvironment mockEnvironment = new MockEnvironment();
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment);
Assertions.assertEquals(Map.of("my-key", "from-app"), result);
}
/**
* <pre>
* - application.properties contains:
* {
* firstKey=firstFromProperties
* secondKey=secondFromProperties
* }
*
* - a single property exists : {firstKey = abc}
*
* - This proves that the property overrides the value from "application.properties".
* </pre>
*/
@Test
void testThree() {
Map<String, String> map = new LinkedHashMap<>();
map.put("application.properties", """
firstKey=firstFromProperties
secondKey=secondFromProperties""");
map.put("firstKey", "abc");
MockEnvironment mockEnvironment = new MockEnvironment();
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment);
Assertions.assertEquals(Map.of("firstKey", "abc", "secondKey", "secondFromProperties"), result);
}
/**
* <pre>
* - application.properties contains:
* {
* firstKey=firstFromProperties
* secondKey=secondFromProperties
* thirdKey=thirdFromProperties
* }
*
* - application-dev.properties contains:
* {
* firstKey=firstFromDevProperties
* secondKey=secondFromDevProperties
* }
*
* - a single property exists : {firstKey = abc}
*
* - This proves that profile specific properties override non-profile
* and plain properties override everything.
* </pre>
*/
@Test
void testFour() {
Map<String, String> map = new LinkedHashMap<>();
map.put("application.properties", """
firstKey=firstFromProperties
secondKey=secondFromProperties
thirdKey=thirdFromProperties""");
map.put("application-dev.properties", """
firstKey=firstFromDevProperties
secondKey=secondFromDevProperties""");
map.put("firstKey", "abc");
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment.setActiveProfiles("dev");
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment);
Assertions.assertEquals(
Map.of("firstKey", "abc", "secondKey", "secondFromDevProperties", "thirdKey", "thirdFromProperties"),
result);
}
/**
* <pre>
* - application.properties contains:
* {
* firstKey=firstFromProperties
* secondKey=secondFromProperties
* thirdKey=thirdFromProperties
* }
*
* - application-dev.properties contains:
* {
* firstKey=firstFromDevProperties
* secondKey=secondFromDevProperties
* }
*
* - a single property exists : {firstKey = abc}
*
* - This proves that profile specific properties override non-profile
* and plain properties override everything.
* It also proves that non-active profile properties are ignored.
* </pre>
*/
@Test
void testFive() {
Map<String, String> map = new LinkedHashMap<>();
map.put("application.properties", """
firstKey=firstFromProperties
secondKey=secondFromProperties
thirdKey=thirdFromProperties""");
map.put("application-dev.properties", """
firstKey=firstFromDevProperties
secondKey=secondFromDevProperties""");
map.put("application-k8s.properties", """
firstKey=firstFromK8sProperties
secondKey=secondFromK8sProperties""");
map.put("firstKey", "abc");
map.put("fourthKey", "def");
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment.setActiveProfiles("dev");
Map<String, Object> result = SourceDataEntriesProcessor.processAllEntries(map, mockEnvironment);
Assertions.assertEquals(Map.of("firstKey", "abc", "secondKey", "secondFromDevProperties", "thirdKey",
"thirdFromProperties", "fourthKey", "def"), result);
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright 2013-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.commons.config;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
/**
* @author wind57
*/
class SourceDataEntriesProcessorSortedTests {
@Test
void testSingleNonFileProperty() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("simple-property", "value");
MockEnvironment mockEnvironment = new MockEnvironment();
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 1);
Assertions.assertEquals(result.get(0).getKey(), "simple-property");
Assertions.assertEquals(result.get(0).getValue(), "value");
}
@Test
void testTwoNonFileProperties() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("one", "1");
k8sSource.put("two", "2");
MockEnvironment mockEnvironment = new MockEnvironment();
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 2);
Assertions.assertEquals(result.get(0).getKey(), "one");
Assertions.assertEquals(result.get(0).getValue(), "1");
Assertions.assertEquals(result.get(1).getKey(), "two");
Assertions.assertEquals(result.get(1).getValue(), "2");
}
@Test
void testSingleFileProperty() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("application.properties", "key=value");
MockEnvironment mockEnvironment = new MockEnvironment();
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 1);
Assertions.assertEquals(result.get(0).getKey(), "application.properties");
Assertions.assertEquals(result.get(0).getValue(), "key=value");
}
@Test
void testApplicationAndSimpleProperty() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("application.properties", "key=value");
k8sSource.put("simple", "other_value");
MockEnvironment mockEnvironment = new MockEnvironment();
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 2);
Assertions.assertEquals(result.get(0).getKey(), "application.properties");
Assertions.assertEquals(result.get(0).getValue(), "key=value");
Assertions.assertEquals(result.get(1).getKey(), "simple");
Assertions.assertEquals(result.get(1).getValue(), "other_value");
}
@Test
void testSimplePropertyAndApplication() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("simple", "other_value");
k8sSource.put("application.properties", "key=value");
MockEnvironment mockEnvironment = new MockEnvironment();
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 2);
Assertions.assertEquals(result.get(0).getKey(), "application.properties");
Assertions.assertEquals(result.get(0).getValue(), "key=value");
Assertions.assertEquals(result.get(1).getKey(), "simple");
Assertions.assertEquals(result.get(1).getValue(), "other_value");
}
@Test
void testSimplePropertyAndTwoApplications() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("simple", "other_value");
k8sSource.put("application.properties", "key=value");
k8sSource.put("application-dev.properties", "key-dev=value-dev");
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment.setActiveProfiles("dev");
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 3);
Assertions.assertEquals(result.get(0).getKey(), "application.properties");
Assertions.assertEquals(result.get(0).getValue(), "key=value");
Assertions.assertEquals(result.get(1).getKey(), "application-dev.properties");
Assertions.assertEquals(result.get(1).getValue(), "key-dev=value-dev");
Assertions.assertEquals(result.get(2).getKey(), "simple");
Assertions.assertEquals(result.get(2).getValue(), "other_value");
}
@Test
void testComplex() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("simple", "other_value");
k8sSource.put("second-simple", "second_other_value");
k8sSource.put("application.properties", "key=value");
k8sSource.put("application-dev.properties", "key-dev=value-dev");
k8sSource.put("application-k8s.properties", "key-k8s=value-k8s");
k8sSource.put("ignored.properties", "key-ignored=value-ignored");
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment.setActiveProfiles("k8s");
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 4);
Assertions.assertEquals(result.get(0).getKey(), "application.properties");
Assertions.assertEquals(result.get(0).getValue(), "key=value");
Assertions.assertEquals(result.get(1).getKey(), "application-k8s.properties");
Assertions.assertEquals(result.get(1).getValue(), "key-k8s=value-k8s");
Assertions.assertEquals(result.get(2).getKey(), "simple");
Assertions.assertEquals(result.get(2).getValue(), "other_value");
Assertions.assertEquals(result.get(3).getKey(), "second-simple");
Assertions.assertEquals(result.get(3).getValue(), "second_other_value");
}
@Test
void testComplexWithNonDefaultApplicationName() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("simple", "other_value");
k8sSource.put("second-simple", "second_other_value");
k8sSource.put("application.properties", "key=value");
k8sSource.put("application-dev.properties", "key-dev=value-dev");
k8sSource.put("application-k8s.properties", "key-k8s=value-k8s");
k8sSource.put("ignored.properties", "key-ignored=value-ignored");
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment.setProperty("spring.application.name", "sorted");
mockEnvironment.setActiveProfiles("k8s");
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 2);
Assertions.assertEquals(result.get(0).getKey(), "simple");
Assertions.assertEquals(result.get(0).getValue(), "other_value");
Assertions.assertEquals(result.get(1).getKey(), "second-simple");
Assertions.assertEquals(result.get(1).getValue(), "second_other_value");
}
@Test
void testComplexWithNonDefaultApplicationNameMoreMatches() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("simple", "other_value");
k8sSource.put("second-simple", "second_other_value");
k8sSource.put("sorted.properties", "key=value");
k8sSource.put("application-dev.properties", "key-dev=value-dev");
k8sSource.put("sorted-k8s.properties", "key-k8s=value-k8s");
k8sSource.put("ignored.properties", "key-ignored=value-ignored");
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment.setProperty("spring.application.name", "sorted");
mockEnvironment.setActiveProfiles("k8s");
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 4);
Assertions.assertEquals(result.get(0).getKey(), "sorted.properties");
Assertions.assertEquals(result.get(0).getValue(), "key=value");
Assertions.assertEquals(result.get(1).getKey(), "sorted-k8s.properties");
Assertions.assertEquals(result.get(1).getValue(), "key-k8s=value-k8s");
Assertions.assertEquals(result.get(2).getKey(), "simple");
Assertions.assertEquals(result.get(2).getValue(), "other_value");
Assertions.assertEquals(result.get(3).getKey(), "second-simple");
Assertions.assertEquals(result.get(3).getValue(), "second_other_value");
}
@Test
void testProfileBasedOnly() {
Map<String, String> k8sSource = new LinkedHashMap<>();
k8sSource.put("simple", "other_value");
k8sSource.put("second-simple", "second_other_value");
k8sSource.put("sorted-k8s.properties", "key-k8s=value-k8s");
k8sSource.put("ignored.properties", "key-ignored=value-ignored");
MockEnvironment mockEnvironment = new MockEnvironment();
mockEnvironment.setProperty("spring.application.name", "sorted");
mockEnvironment.setActiveProfiles("k8s");
List<Map.Entry<String, String>> result = SourceDataEntriesProcessor.sorted(k8sSource, mockEnvironment);
Assertions.assertEquals(result.size(), 3);
Assertions.assertEquals(result.get(0).getKey(), "sorted-k8s.properties");
Assertions.assertEquals(result.get(0).getValue(), "key-k8s=value-k8s");
Assertions.assertEquals(result.get(1).getKey(), "simple");
Assertions.assertEquals(result.get(1).getValue(), "other_value");
Assertions.assertEquals(result.get(2).getKey(), "second-simple");
Assertions.assertEquals(result.get(2).getValue(), "second_other_value");
}
}