From 7507e75b438a435700e7d29774e8bc3e5e8f7528 Mon Sep 17 00:00:00 2001 From: Stefan Larsson Date: Tue, 30 Jan 2018 01:31:22 +0100 Subject: [PATCH] Allow ConfigMaps to be parsed from mounted volume paths, like Secrets (#142) * Allow ConfigMaps to be parsed from a mounted volume's paths, just like Secrets * Remove temporary test case files, the Spring way * And also remove the test library dependency, not required any more. --- README.md | 4 +- spring-cloud-kubernetes-config/pom.xml | 5 ++ .../config/ConfigMapConfigProperties.java | 24 ++++++- .../config/ConfigMapPropertySource.java | 70 ++++++++++--------- .../ConfigMapPropertySourceLocator.java | 2 +- .../config/KubernetesPropertySource.java | 69 ++++++++++++++++++ .../config/SecretsPropertySource.java | 57 ++++----------- .../config/ConfigMapsSpringBootTest.java | 8 +-- .../kubernetes/config/ConfigMapsTest.java | 49 +++++++++++-- 9 files changed, 199 insertions(+), 89 deletions(-) create mode 100644 spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/KubernetesPropertySource.java diff --git a/README.md b/README.md index 8eab5891..b916dde9 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,9 @@ spec: | spring.cloud.kubernetes.config.enabled | Boolean | true | Enable Secrets PropertySource | spring.cloud.kubernetes.config.name | String | ${spring.application.name} | Sets the name of ConfigMap to lookup | spring.cloud.kubernetes.config.namespace | String | Client namespace | Sets the Kubernetes namespace where to lookup - +| spring.cloud.kubernetes.config.paths | List | null | Sets the paths were ConfigMaps are mounted +| spring.cloud.kubernetes.config.enableApi | Boolean | true | Enable/Disable consuming ConfigMaps via APIs + #### Secrets PropertySource diff --git a/spring-cloud-kubernetes-config/pom.xml b/spring-cloud-kubernetes-config/pom.xml index 5a619ad3..34ce24a7 100644 --- a/spring-cloud-kubernetes-config/pom.xml +++ b/spring-cloud-kubernetes-config/pom.xml @@ -71,6 +71,11 @@ mockwebserver test + + io.fabric8 + kubernetes-server-mock + test + org.spockframework spock-spring diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapConfigProperties.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapConfigProperties.java index df0ecc62..3d85e4b3 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapConfigProperties.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapConfigProperties.java @@ -17,6 +17,9 @@ package org.springframework.cloud.kubernetes.config; +import java.util.LinkedList; +import java.util.List; + import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("spring.cloud.kubernetes.config") @@ -24,7 +27,26 @@ public class ConfigMapConfigProperties extends AbstractConfigProperties { private static final String TARGET = "Config Map"; - @Override + private boolean enableApi = true; + private List paths = new LinkedList<>(); + + public boolean isEnableApi() { + return enableApi; + } + + public void setEnableApi(boolean enableApi) { + this.enableApi = enableApi; + } + + public void setPaths(List paths) { + this.paths = paths; + } + + public List getPaths() { + return paths; + } + + @Override public String getConfigurationTarget() { return TARGET; } 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 e106fcd8..ff8f3138 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 @@ -30,13 +30,12 @@ import io.fabric8.kubernetes.client.KubernetesClient; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.config.YamlProcessor; import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.boot.yaml.SpringProfileDocumentMatcher; -import org.springframework.core.env.MapPropertySource; import org.springframework.core.io.ByteArrayResource; +import org.springframework.util.StringUtils; -public class ConfigMapPropertySource extends MapPropertySource { +public class ConfigMapPropertySource extends KubernetesPropertySource { private static final Log LOG = LogFactory.getLog(ConfigMapPropertySource.class); private static final String APPLICATION_YML = "application.yml"; @@ -45,16 +44,16 @@ public class ConfigMapPropertySource extends MapPropertySource { private static final String PREFIX = "configmap"; - public ConfigMapPropertySource(KubernetesClient client, String name) { - this(client, name, null); + public ConfigMapPropertySource(KubernetesClient client, String name, ConfigMapConfigProperties config) { + this(client, name, null, config); } - public ConfigMapPropertySource(KubernetesClient client, String name, String[] profiles) { - this(client, name, null, profiles); + public ConfigMapPropertySource(KubernetesClient client, String name, String[] profiles, ConfigMapConfigProperties config) { + this(client, name, null, profiles, config); } - public ConfigMapPropertySource(KubernetesClient client, String name, String namespace, String[] profiles) { - super(getName(client, name, namespace), asObjectMap(getData(client, name, namespace, profiles))); + public ConfigMapPropertySource(KubernetesClient client, String name, String namespace, String[] profiles, ConfigMapConfigProperties config) { + super(getName(client, name, namespace), asObjectMap(getData(client, name, namespace, profiles, config))); } private static String getName(KubernetesClient client, String name, String namespace) { @@ -67,30 +66,37 @@ public class ConfigMapPropertySource extends MapPropertySource { .toString(); } - private static Map getData(KubernetesClient client, String name, String namespace, String[] profiles) { + private static Map getData(KubernetesClient client, String name, String namespace, + String[] profiles, ConfigMapConfigProperties config) { Map result = new HashMap<>(); - try { - ConfigMap map = namespace == null || namespace.isEmpty() - ? client.configMaps().withName(name).get() - : client.configMaps().inNamespace(namespace).withName(name).get(); + if (config.isEnableApi()) { + try { + ConfigMap map = StringUtils.isEmpty(namespace) + ? client.configMaps().withName(name).get() + : client.configMaps().inNamespace(namespace).withName(name).get(); - if (map != null) { - for (Map.Entry entry : map.getData().entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - if (key.equals(APPLICATION_YAML) || key.equals(APPLICATION_YML)) { - result.putAll(yamlParserGenerator(profiles).andThen(PROPERTIES_TO_MAP).apply(value)); - } else if (key.equals(APPLICATION_PROPERTIES)) { - result.putAll(KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(value)); - } else { - result.put(key, value); - } - } - } - } catch (Exception e) { - LOG.warn("Can't read configMap with name: [" + name + "] in namespace:[" + namespace + "]. Ignoring", e); - } - return result; + if (map != null) { + for (Map.Entry entry : map.getData().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key.equals(APPLICATION_YAML) || key.equals(APPLICATION_YML)) { + result.putAll(yamlParserGenerator(profiles).andThen(PROPERTIES_TO_MAP).apply(value)); + } else if (key.equals(APPLICATION_PROPERTIES)) { + result.putAll(KEY_VALUE_TO_PROPERTIES.andThen(PROPERTIES_TO_MAP).apply(value)); + } else { + result.put(key, value); + } + } + } + } catch (Exception e) { + LOG.warn("Can't read configMap with name: [" + name + "] in namespace:[" + namespace + "]. Ignoring", e); + } + } + + // read for secrets mount + putPathConfig(result, config.getPaths()); + + return result; } private static Map asObjectMap(Map source) { @@ -99,7 +105,7 @@ public class ConfigMapPropertySource extends MapPropertySource { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - private static final Function yamlParserGenerator(final String[] profiles) { + private static Function yamlParserGenerator(final String[] profiles) { return s -> { YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean(); if (profiles == null) { diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java index c7cae916..15104c9a 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/ConfigMapPropertySourceLocator.java @@ -41,7 +41,7 @@ public class ConfigMapPropertySourceLocator implements PropertySourceLocator { ConfigurableEnvironment env = (ConfigurableEnvironment) environment; String name = getApplicationName(environment, properties); String namespace = getApplicationNamespace(client, env, properties); - return new ConfigMapPropertySource(client, name, namespace, env.getActiveProfiles()); + return new ConfigMapPropertySource(client, name, namespace, env.getActiveProfiles(), properties); } return null; } diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/KubernetesPropertySource.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/KubernetesPropertySource.java new file mode 100644 index 00000000..b4695185 --- /dev/null +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/KubernetesPropertySource.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017 to the original 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 + * + * http://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.config; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.env.MapPropertySource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +/** + * Secrets and ConfigMaps shared features. + */ +public class KubernetesPropertySource extends MapPropertySource { + private static final Log LOG = LogFactory.getLog(KubernetesPropertySource.class); + + @SuppressWarnings("unchecked") + protected KubernetesPropertySource(String name, Map source) { + super(name, source); + } + + protected static void putPathConfig(Map result, List paths) { + paths + .stream() + .map(Paths::get) + .filter(Files::exists) + .forEach(p -> putAll(p, result)); + } + + private static void putAll(Path path, Map result) { + try { + Files.walk(path) + .filter(Files::isRegularFile) + .forEach(p -> readFile(p, result)); + } catch (IOException e) { + LOG.warn("Error walking properties files", e); + } + } + + private static void readFile(Path path, Map result) { + try { + result.put( + path.getFileName().toString(), + new String(Files.readAllBytes(path)).trim()); + } catch (IOException e) { + LOG.warn("Error reading properties file", e); + } + } +} diff --git a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/SecretsPropertySource.java b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/SecretsPropertySource.java index 39d68cd9..9299f136 100644 --- a/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/SecretsPropertySource.java +++ b/spring-cloud-kubernetes-config/src/main/java/org/springframework/cloud/kubernetes/config/SecretsPropertySource.java @@ -16,10 +16,6 @@ */ package org.springframework.cloud.kubernetes.config; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -30,12 +26,11 @@ import io.fabric8.kubernetes.client.KubernetesClient; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.env.Environment; -import org.springframework.core.env.MapPropertySource; import org.springframework.util.StringUtils; import static org.springframework.cloud.kubernetes.config.ConfigUtils.*; -public class SecretsPropertySource extends MapPropertySource { +public class SecretsPropertySource extends KubernetesPropertySource { private static final Log LOG = LogFactory.getLog(SecretsPropertySource.class); private static final String PREFIX = "secrets"; @@ -65,20 +60,18 @@ public class SecretsPropertySource extends MapPropertySource { if (config.isEnableApi()) { try { // Read for secrets api (named) + Secret secret; if (StringUtils.isEmpty(namespace)) { - putAll( - client.secrets() - .withName(name) - .get(), - result); + secret = client.secrets() + .withName(name) + .get(); } else { - putAll( - client.secrets() - .inNamespace(namespace) - .withName(name) - .get(), - result); + secret = client.secrets() + .inNamespace(namespace) + .withName(name) + .get(); } + putAll(secret, result); // Read for secrets api (label) if (!config.getLabels().isEmpty()) { @@ -106,16 +99,12 @@ public class SecretsPropertySource extends MapPropertySource { } // read for secrets mount - config.getPaths() - .stream() - .map(Paths::get) - .filter(Files::exists) - .forEach(p -> putAll(p, result)); + putPathConfig(result, config.getPaths()); - return result; + return result; } - // ***************************** + // ***************************** // Helpers // ***************************** private static void putAll(Secret secret, Map result) { @@ -126,24 +115,4 @@ public class SecretsPropertySource extends MapPropertySource { ); } } - - private static void putAll(Path path, Map result) { - try { - Files.walk(path) - .filter(Files::isRegularFile) - .forEach(p -> readFile(p, result)); - } catch (IOException e) { - LOG.warn("", e); - } - } - - private static void readFile(Path path, Map result) { - try { - result.put( - path.getFileName().toString(), - new String(Files.readAllBytes(path)).trim()); - } catch (IOException e) { - LOG.warn("", e); - } - } } diff --git a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsSpringBootTest.java b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsSpringBootTest.java index a50be32f..072e8671 100644 --- a/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsSpringBootTest.java +++ b/spring-cloud-kubernetes-config/src/test/java/org/springframework/cloud/kubernetes/config/ConfigMapsSpringBootTest.java @@ -23,7 +23,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.server.mock.KubernetesServer; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import io.restassured.RestAssured; import org.junit.Before; import org.junit.BeforeClass; @@ -53,18 +53,18 @@ public class ConfigMapsSpringBootTest { @ClassRule public static KubernetesServer server = new KubernetesServer(); - public static KubernetesClient mockClient; + private static KubernetesClient mockClient; @Autowired(required = false) Config config; - private static String APPLICATION_NAME = "configmap-example"; + private static final String APPLICATION_NAME = "configmap-example"; @Value("${local.server.port}") private int port; @BeforeClass - public static void setUpBeforeClass() throws Exception { + public static void setUpBeforeClass() { mockClient = server.getClient(); //Configure the kubernetes master url to point to the mock server 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 653406bb..c14af6c5 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,21 +17,24 @@ package org.springframework.cloud.kubernetes.config; -import java.util.HashMap; +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 io.fabric8.kubernetes.api.model.ConfigMap; 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.api.model.ListMeta; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.server.mock.KubernetesServer; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import org.junit.Rule; import org.junit.Test; +import org.springframework.util.FileSystemUtils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; /** @@ -65,9 +68,43 @@ public class ConfigMapsTest { ConfigMapList configMapList = client.configMaps().inNamespace("ns2").list(); assertNotNull(configMapList); assertEquals(1, configMapList.getAdditionalProperties().size()); - HashMap data = (HashMap) configMapList.getAdditionalProperties().get("data"); + @SuppressWarnings("unchecked") + Map data = (Map) configMapList.getAdditionalProperties().get("data"); assertEquals("123",data.get("KEY")); + } + @Test + public void testConfigMapGetFromVolume() 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 dbPath = tmp.resolve("cm/db"); + final Path apiPath = tmp.resolve("cm/api"); + createConfigMapFile(dbPath, "db.url", "http://localhost/db"); + createConfigMapFile(apiPath, "api.url", "http://localhost/api"); + createConfigMapFile(apiPath, "foo.bar", "42"); + + // parse ConfigMaps + cmConfProperties.setPaths(Arrays.asList(dbPath.toString(), apiPath.toString())); + ConfigMapPropertySource cmps = new ConfigMapPropertySource(client, "testapp", cmConfProperties); + + // assert as expected + assertEquals("42", cmps.getProperty("foo.bar")); + assertEquals("http://localhost/db", cmps.getProperty("db.url")); + assertEquals("http://localhost/api", cmps.getProperty("api.url")); + assertFalse(cmps.containsProperty("no.such.property")); + + 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)); + Files.write(apiUrlFile, value.getBytes(StandardCharsets.UTF_8)); } }