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:
Georgios Andrianakis
2018-06-27 22:44:31 +03:00
committed by Ioannis Canellos
parent c654079ab4
commit 5166090b77
5 changed files with 256 additions and 10 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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));

View File

@@ -0,0 +1,3 @@
dummy.property.string4=a
dummy.property.int4=1
dummy.property.bool4=true

View File

@@ -0,0 +1,5 @@
dummy:
property:
string3: "a"
int3: 1
bool3: true