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:
committed by
Spencer Gibb
parent
88b7ea65ab
commit
7507e75b43
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user