Relax the name application.(yaml|properties) constraint in ConfigMap (#170)
When the ConfigMap contains a single entry, then the name of the file does not have to application.(yaml|properties), but can be any yaml/properties file. This facilitates the use case where a confimap is created by something like: kubectl create configmap game-config --from-file=/path/app-config.yaml
This commit is contained in:
committed by
Ioannis Canellos
parent
c654079ab4
commit
5166090b77
22
README.md
22
README.md
@@ -98,6 +98,13 @@ If such a `ConfigMap` is found, it will be processed as follows:
|
||||
- apply as `yaml` the content of any property named `application.yaml`
|
||||
- apply as properties file the content of any property named `application.properties`
|
||||
|
||||
The single exception to the aforementioned flow is when the `ConfigMap` contains a **single** key that indicates
|
||||
the file is a YAML or Properties file. In that case the name of the key does NOT have to be `application.yaml` or
|
||||
`application.properties` (it can be anything) and the value of the property will be treated correctly.
|
||||
This features facilitates the use case where the `ConfigMap` was created using something like:
|
||||
|
||||
`kubectl create configmap game-config --from-file=/path/to/app-config.yaml`
|
||||
|
||||
Example:
|
||||
|
||||
Let's assume that we have a Spring Boot application named ``demo`` that uses properties to read its thread pool
|
||||
@@ -134,6 +141,21 @@ data:
|
||||
max:16
|
||||
```
|
||||
|
||||
The following also works:
|
||||
|
||||
```yaml
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: demo
|
||||
data:
|
||||
custom-name.yaml: |-
|
||||
pool:
|
||||
size:
|
||||
core: 1
|
||||
max:16
|
||||
```
|
||||
|
||||
Spring Boot applications can also be configured differently depending on active profiles which will be merged together
|
||||
when the ConfigMap is read. It is possible to provide different property values for different profiles using an
|
||||
`application.properties|yaml` property, specifying profile-specific values each in their own document
|
||||
|
||||
@@ -28,7 +28,9 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -94,11 +96,40 @@ public class ConfigMapPropertySource extends KubernetesPropertySource {
|
||||
|
||||
private static Map<String, String> processAllEntries(Map<String, String> input,
|
||||
String[] profiles) {
|
||||
|
||||
Set<Entry<String, String>> entrySet = input.entrySet();
|
||||
if(entrySet.size() == 1) {
|
||||
Entry<String, String> singleEntry = entrySet.iterator().next();
|
||||
String propertyName = singleEntry.getKey();
|
||||
String propertyValue = singleEntry.getValue();
|
||||
if (propertyName.endsWith(".yml") || propertyName.endsWith(".yaml")) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("The single property with name: [" + propertyName + "] will be treated as a yaml file");
|
||||
}
|
||||
|
||||
return yamlParserGenerator(profiles).andThen(PROPERTIES_TO_MAP).apply(propertyValue);
|
||||
} else if (propertyName.endsWith(".properties")) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("The single property with name: [" + propertyName + "] will be treated as a properties file");
|
||||
}
|
||||
|
||||
return KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(propertyValue);
|
||||
} else {
|
||||
return defaultProcessAllEntries(input, profiles);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultProcessAllEntries(input, profiles);
|
||||
}
|
||||
|
||||
private static Map<String, String> defaultProcessAllEntries(Map<String, String> input,
|
||||
String[] profiles) {
|
||||
|
||||
return input.entrySet().stream()
|
||||
.map(e -> extractProperties(e.getKey(), e.getValue(), profiles))
|
||||
.filter(m -> !m.isEmpty())
|
||||
.flatMap(m -> m.entrySet().stream())
|
||||
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
|
||||
.map(e -> extractProperties(e.getKey(), e.getValue(), profiles))
|
||||
.filter(m -> !m.isEmpty())
|
||||
.flatMap(m -> m.entrySet().stream())
|
||||
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
|
||||
}
|
||||
|
||||
private static Map<String, String> extractProperties(String resourceName, String content, String[] profiles) {
|
||||
|
||||
@@ -17,18 +17,24 @@
|
||||
|
||||
package org.springframework.cloud.kubernetes.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapList;
|
||||
import io.fabric8.kubernetes.api.model.ConfigMapListBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
|
||||
import io.fabric8.kubernetes.client.utils.IOHelpers;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
@@ -74,6 +80,159 @@ public class ConfigMapsTest {
|
||||
assertEquals("123",data.get("KEY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapFromSingleApplicationProperties() {
|
||||
String configMapName = "app-properties-test";
|
||||
String namespace = "app-props";
|
||||
server.expect()
|
||||
.withPath(
|
||||
String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, configMapName)
|
||||
)
|
||||
.andReturn(200, new ConfigMapBuilder()
|
||||
.withNewMetadata().withName(configMapName).endMetadata()
|
||||
.addToData("application.properties",readResourceFile("application.properties"))
|
||||
.build()
|
||||
)
|
||||
.once();
|
||||
|
||||
ConfigMapPropertySource cmps = new ConfigMapPropertySource(
|
||||
server.getClient().inNamespace(namespace), configMapName,
|
||||
new ConfigMapConfigProperties()
|
||||
);
|
||||
|
||||
assertEquals("a", cmps.getProperty("dummy.property.string1"));
|
||||
assertEquals("1", cmps.getProperty("dummy.property.int1"));
|
||||
assertEquals("true", cmps.getProperty("dummy.property.bool1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapFromSingleApplicationYaml() {
|
||||
String configMapName = "app-yaml-test";
|
||||
String namespace = "app-props";
|
||||
server.expect()
|
||||
.withPath(
|
||||
String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, configMapName)
|
||||
)
|
||||
.andReturn(200, new ConfigMapBuilder()
|
||||
.withNewMetadata().withName(configMapName).endMetadata()
|
||||
.addToData("application.yaml",readResourceFile("application.yaml"))
|
||||
.build()
|
||||
)
|
||||
.once();
|
||||
|
||||
ConfigMapPropertySource cmps = new ConfigMapPropertySource(
|
||||
server.getClient().inNamespace(namespace), configMapName,
|
||||
new ConfigMapConfigProperties()
|
||||
);
|
||||
|
||||
assertEquals("a", cmps.getProperty("dummy.property.string2"));
|
||||
assertEquals("1", cmps.getProperty("dummy.property.int2"));
|
||||
assertEquals("true", cmps.getProperty("dummy.property.bool2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapFromSingleNonStandardFileName() {
|
||||
String configMapName = "single-non-standard-test";
|
||||
String namespace = "app-props";
|
||||
server.expect()
|
||||
.withPath(
|
||||
String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, configMapName)
|
||||
)
|
||||
.andReturn(200, new ConfigMapBuilder()
|
||||
.withNewMetadata().withName(configMapName).endMetadata()
|
||||
.addToData("adhoc.yml",readResourceFile("adhoc.yml"))
|
||||
.build()
|
||||
)
|
||||
.once();
|
||||
|
||||
ConfigMapPropertySource cmps = new ConfigMapPropertySource(
|
||||
server.getClient().inNamespace(namespace), configMapName,
|
||||
new ConfigMapConfigProperties()
|
||||
);
|
||||
|
||||
assertEquals("a", cmps.getProperty("dummy.property.string3"));
|
||||
assertEquals("1", cmps.getProperty("dummy.property.int3"));
|
||||
assertEquals("true", cmps.getProperty("dummy.property.bool3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapFromSingleInvalidPropertiesContent() {
|
||||
String configMapName = "single-unparseable-properties-test";
|
||||
String namespace = "app-props";
|
||||
server.expect()
|
||||
.withPath(
|
||||
String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, configMapName)
|
||||
)
|
||||
.andReturn(200, new ConfigMapBuilder()
|
||||
.withNewMetadata().withName(configMapName).endMetadata()
|
||||
.addToData("application.properties","somevalue")
|
||||
.build()
|
||||
)
|
||||
.once();
|
||||
|
||||
new ConfigMapPropertySource(
|
||||
server.getClient().inNamespace(namespace), configMapName,
|
||||
new ConfigMapConfigProperties()
|
||||
);
|
||||
|
||||
//no exception is thrown for unparseable content
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapFromSingleInvalidYamlContent() {
|
||||
String configMapName = "single-unparseable-yaml-test";
|
||||
String namespace = "app-props";
|
||||
server.expect()
|
||||
.withPath(
|
||||
String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, configMapName)
|
||||
)
|
||||
.andReturn(200, new ConfigMapBuilder()
|
||||
.withNewMetadata().withName(configMapName).endMetadata()
|
||||
.addToData("application.yaml","somevalue")
|
||||
.build()
|
||||
)
|
||||
.once();
|
||||
|
||||
new ConfigMapPropertySource(
|
||||
server.getClient().inNamespace(namespace), configMapName,
|
||||
new ConfigMapConfigProperties()
|
||||
);
|
||||
|
||||
//no exception is thrown for unparseable content
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapFromMultipleApplicationProperties() {
|
||||
String configMapName = "app-multiple-properties-test";
|
||||
String namespace = "app-props";
|
||||
server.expect()
|
||||
.withPath(
|
||||
String.format("/api/v1/namespaces/%s/configmaps/%s", namespace, configMapName)
|
||||
)
|
||||
.andReturn(200, new ConfigMapBuilder()
|
||||
.withNewMetadata().withName(configMapName).endMetadata()
|
||||
.addToData("application.properties",readResourceFile("application.properties"))
|
||||
.addToData("adhoc.properties",readResourceFile("adhoc.properties"))
|
||||
.build()
|
||||
)
|
||||
.once();
|
||||
|
||||
ConfigMapPropertySource cmps = new ConfigMapPropertySource(
|
||||
server.getClient().inNamespace(namespace), configMapName,
|
||||
new ConfigMapConfigProperties()
|
||||
);
|
||||
|
||||
//application.properties should be read correctly
|
||||
assertEquals("a", cmps.getProperty("dummy.property.string1"));
|
||||
assertEquals("1", cmps.getProperty("dummy.property.int1"));
|
||||
assertEquals("true", cmps.getProperty("dummy.property.bool1"));
|
||||
|
||||
//the adhoc.properties file should not be parsed
|
||||
assertNull(cmps.getProperty("dummy.property.bool2"));
|
||||
assertNull(cmps.getProperty("dummy.property.bool2"));
|
||||
assertNull(cmps.getProperty("dummy.property.bool2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapGetFromVolume() throws IOException {
|
||||
KubernetesClient client = server.getClient();
|
||||
@@ -115,6 +274,32 @@ public class ConfigMapsTest {
|
||||
FileSystemUtils.deleteRecursively(tmp.toFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigMapGetSingleApplicationPropertiesFromVolume() throws IOException {
|
||||
KubernetesClient client = server.getClient();
|
||||
ConfigMapConfigProperties cmConfProperties = new ConfigMapConfigProperties();
|
||||
cmConfProperties.setEnableApi(false);
|
||||
|
||||
// create test data, as if in-container volumes mounted by k8s, see
|
||||
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume
|
||||
final Path tmp = Files.createTempDirectory("test-k8s-cm-");
|
||||
|
||||
final Path filesPath = tmp.resolve("cm/files");
|
||||
|
||||
createConfigMapFile(filesPath, "adhoc.properties", readResourceFile("adhoc.properties"));
|
||||
|
||||
// parse ConfigMaps
|
||||
cmConfProperties.setPaths(Collections.singletonList(filesPath.toString()));
|
||||
ConfigMapPropertySource cmps = new ConfigMapPropertySource(client, "testapp", cmConfProperties);
|
||||
|
||||
// assert as expected
|
||||
assertEquals("a", cmps.getProperty("dummy.property.string4"));
|
||||
assertEquals("1", cmps.getProperty("dummy.property.int4"));
|
||||
assertEquals("true", cmps.getProperty("dummy.property.bool4"));
|
||||
|
||||
FileSystemUtils.deleteRecursively(tmp.toFile());
|
||||
}
|
||||
|
||||
private void createConfigMapFile(Path basePath, String key, String value) throws IOException {
|
||||
Files.createDirectories(basePath);
|
||||
final Path apiUrlFile = Files.createFile(basePath.resolve(key));
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
dummy.property.string4=a
|
||||
dummy.property.int4=1
|
||||
dummy.property.bool4=true
|
||||
@@ -0,0 +1,5 @@
|
||||
dummy:
|
||||
property:
|
||||
string3: "a"
|
||||
int3: 1
|
||||
bool3: true
|
||||
Reference in New Issue
Block a user