diff --git a/README.md b/README.md index 534e7687..f24d8435 100644 --- a/README.md +++ b/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 diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySource.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySource.java index e7265052..5d69a38e 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySource.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySource.java @@ -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 processAllEntries(Map input, String[] profiles) { + + Set> entrySet = input.entrySet(); + if(entrySet.size() == 1) { + Entry 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 defaultProcessAllEntries(Map 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 extractProperties(String resourceName, String content, String[] profiles) { diff --git a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsTest.java b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsTest.java index 8d8773b3..50f860b1 100644 --- a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsTest.java +++ b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsTest.java @@ -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)); diff --git a/spring-cloud-kubernetes-config/src/test/resources/adhoc.properties b/spring-cloud-kubernetes-config/src/test/resources/adhoc.properties new file mode 100644 index 00000000..68741286 --- /dev/null +++ b/spring-cloud-kubernetes-config/src/test/resources/adhoc.properties @@ -0,0 +1,3 @@ +dummy.property.string4=a +dummy.property.int4=1 +dummy.property.bool4=true diff --git a/spring-cloud-kubernetes-config/src/test/resources/adhoc.yml b/spring-cloud-kubernetes-config/src/test/resources/adhoc.yml new file mode 100644 index 00000000..89da5ca5 --- /dev/null +++ b/spring-cloud-kubernetes-config/src/test/resources/adhoc.yml @@ -0,0 +1,5 @@ +dummy: + property: + string3: "a" + int3: 1 + bool3: true