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.
This commit is contained in:
Stefan Larsson
2018-01-30 01:31:22 +01:00
committed by Spencer Gibb
parent 88b7ea65ab
commit 7507e75b43
9 changed files with 199 additions and 89 deletions

View File

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

View File

@@ -71,6 +71,11 @@
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-server-mock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>

View File

@@ -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<String> paths = new LinkedList<>();
public boolean isEnableApi() {
return enableApi;
}
public void setEnableApi(boolean enableApi) {
this.enableApi = enableApi;
}
public void setPaths(List<String> paths) {
this.paths = paths;
}
public List<String> getPaths() {
return paths;
}
@Override
public String getConfigurationTarget() {
return TARGET;
}

View File

@@ -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<String, String> getData(KubernetesClient client, String name, String namespace, String[] profiles) {
private static Map<String, String> getData(KubernetesClient client, String name, String namespace,
String[] profiles, ConfigMapConfigProperties config) {
Map<String, String> 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<String, String> 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<String, String> 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<String, Object> asObjectMap(Map<String, String> source) {
@@ -99,7 +105,7 @@ public class ConfigMapPropertySource extends MapPropertySource {
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static final Function<String, Properties> yamlParserGenerator(final String[] profiles) {
private static Function<String, Properties> yamlParserGenerator(final String[] profiles) {
return s -> {
YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
if (profiles == null) {

View File

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

View File

@@ -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<String, Object> source) {
super(name, source);
}
protected static void putPathConfig(Map<String, ? super String> result, List<String> paths) {
paths
.stream()
.map(Paths::get)
.filter(Files::exists)
.forEach(p -> putAll(p, result));
}
private static void putAll(Path path, Map<String, ? super String> 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<String, ? super String> result) {
try {
result.put(
path.getFileName().toString(),
new String(Files.readAllBytes(path)).trim());
} catch (IOException e) {
LOG.warn("Error reading properties file", e);
}
}
}

View File

@@ -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<String, Object> result) {
@@ -126,24 +115,4 @@ public class SecretsPropertySource extends MapPropertySource {
);
}
}
private static void putAll(Path path, Map<String, Object> 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<String, Object> result) {
try {
result.put(
path.getFileName().toString(),
new String(Files.readAllBytes(path)).trim());
} catch (IOException e) {
LOG.warn("", e);
}
}
}

View File

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

View File

@@ -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<String,String> data = (HashMap<String, String>) configMapList.getAdditionalProperties().get("data");
@SuppressWarnings("unchecked")
Map<String,String> data = (Map<String, String>) 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));
}
}