Skip to content

Commit

Permalink
Add support for reading configuration from a ConfigMap
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Apr 3, 2020
1 parent cebc523 commit 4576876
Show file tree
Hide file tree
Showing 13 changed files with 570 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.kubernetes.client.deployment;

import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.RunTimeConfigurationSourceValueBuildItem;
import io.quarkus.kubernetes.client.runtime.KubernetesClientBuildConfig;
import io.quarkus.kubernetes.client.runtime.KubernetesConfigRecorder;
import io.quarkus.kubernetes.client.runtime.KubernetesConfigSourceConfig;

public class KubernetesConfigProcessor {

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
public RunTimeConfigurationSourceValueBuildItem configure(KubernetesConfigRecorder recorder,
KubernetesConfigSourceConfig config, KubernetesClientBuildConfig clientConfig) {
return new RunTimeConfigurationSourceValueBuildItem(
recorder.configMaps(config, clientConfig));
}
}
15 changes: 15 additions & 0 deletions extensions/kubernetes-client/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@
<groupId>org.jboss.spec.javax.xml.bind</groupId>
<artifactId>jboss-jaxb-api_2.3_spec</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-source-yaml</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.quarkus.kubernetes.client.runtime;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.jboss.logging.Logger;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.client.KubernetesClient;

class ConfigMapConfigSourceProvider implements ConfigSourceProvider {

private static final Logger log = Logger.getLogger(ConfigMapConfigSourceProvider.class);

private final KubernetesConfigSourceConfig config;
private final KubernetesClient client;

public ConfigMapConfigSourceProvider(KubernetesConfigSourceConfig config, KubernetesClient client) {
this.config = config;
this.client = client;
}

@Override
public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) {
if (!config.configMaps.isPresent()) {
log.debug("No ConfigMaps were configured for config source lookup");
return Collections.emptyList();
}

List<String> configMapNames = config.configMaps.orElse(Collections.emptyList());
List<ConfigSource> result = new ArrayList<>(configMapNames.size());

try {
for (String configMapName : configMapNames) {
if (log.isDebugEnabled()) {
log.debug("Attempting to read ConfigMap " + configMapName);
}
ConfigMap configMap = client.configMaps().withName(configMapName).get();
if (configMap == null) {
String message = "ConfigMap '" + configMap + "' not found in namespace '"
+ client.getConfiguration().getNamespace() + "'";
if (config.failOnMissingConfig) {
throw new RuntimeException(message);
} else {
log.info(message);
}
} else {
result.addAll(ConfigMapUtil.toConfigSources(configMap));
if (log.isDebugEnabled()) {
log.debug("Done reading ConfigMap " + configMap);
}
}
}
return result;
} catch (Exception e) {
throw new RuntimeException("Unable to obtain configuration for ConfigMap objects for Kubernetes API Server at: "
+ client.getConfiguration().getMasterUrl(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package io.quarkus.kubernetes.client.runtime;

import java.io.IOException;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.jboss.logging.Logger;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.smallrye.config.common.MapBackedConfigSource;
import io.smallrye.config.source.yaml.YamlConfigSource;

final class ConfigMapUtil {

private static final Logger log = Logger.getLogger(ConfigMapUtil.class);

private static final String APPLICATION_YML = "application.yml";
private static final String APPLICATION_YAML = "application.yaml";
private static final String APPLICATION_PROPERTIES = "application.properties";

private static final int ORDINAL = 270; // this is higher than the file system or jar ordinals, but lower than env vars

private ConfigMapUtil() {
}

/**
* Returns a list of {@code ConfigSource} for the literal data that is contained in the ConfigMap
* and for the application.{properties|yaml|yml} files that might be contained in it as well
*
* All the {@code ConfigSource} objects use the same ordinal which is higher than the ordinal
* of normal configuration files, but lower than that of environment variables
*/
static List<ConfigSource> toConfigSources(ConfigMap configMap) {
String configMapName = configMap.getMetadata().getName();
if (log.isDebugEnabled()) {
log.debug("Attempting to convert data in ConfigMap '" + configMapName + "' to a list of ConfigSource objects");
}

ConfigMapData configMapData = parse(configMap);
List<ConfigSource> result = new ArrayList<>(configMapData.fileData.size() + 1);

if (!configMapData.literalData.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Adding a ConfigSource for the literal data of ConfigMap '" + configMapName + "'");
}
result.add(new ConfigMapLiteralDataPropertiesConfigSource(configMapName + "-literalData", configMapData.literalData,
ORDINAL));
}
for (Map.Entry<String, String> entry : configMapData.fileData) {
String fileName = entry.getKey();
String rawFileData = entry.getValue();
if (APPLICATION_PROPERTIES.equals(fileName)) {
if (log.isDebugEnabled()) {
log.debug("Adding a Properties ConfigSource for file '" + fileName + "' of ConfigMap '" + configMapName
+ "'");
}
result.add(new ConfigMapStringInputPropertiesConfigSource(configMapName, fileName, rawFileData, ORDINAL));
} else if (APPLICATION_YAML.equals(fileName) || APPLICATION_YML.equals(fileName)) {
if (log.isDebugEnabled()) {
log.debug("Adding a YAML ConfigSource for file '" + fileName + "' of ConfigMap '" + configMapName + "'");
}
result.add(new ConfigMapStringInputYamlConfigSource(configMapName, fileName, rawFileData, ORDINAL));
}
}

if (log.isDebugEnabled()) {
log.debug("ConfigMap's '" + configMapName + "' converted into " + result.size() + "ConfigSource objects");
}
return result;
}

private static ConfigMapData parse(ConfigMap configMap) {
Map<String, String> data = configMap.getData();
if ((data == null) || data.isEmpty()) {
return new ConfigMapData(Collections.emptyMap(), Collections.emptyList());
}

Map<String, String> literalData = new HashMap<>();
List<Map.Entry<String, String>> fileData = new ArrayList<>();
for (Map.Entry<String, String> entry : data.entrySet()) {
String key = entry.getKey();
if (key.endsWith(".yml") || key.endsWith(".yaml") || key.endsWith(".properties")) {
fileData.add(entry);
} else {
literalData.put(key, entry.getValue());
}
}

return new ConfigMapData(literalData, fileData);
}

private static class ConfigMapData {
final Map<String, String> literalData;
final List<Map.Entry<String, String>> fileData;

ConfigMapData(Map<String, String> literalData, List<Map.Entry<String, String>> fileData) {
this.literalData = literalData;
this.fileData = fileData;
}
}

private static final class ConfigMapLiteralDataPropertiesConfigSource extends MapBackedConfigSource {

private static final String NAME_PREFIX = "ConfigMapLiteralDataPropertiesConfigSource[configMap=";

public ConfigMapLiteralDataPropertiesConfigSource(String configMapName, Map<String, String> propertyMap, int ordinal) {
super(NAME_PREFIX + configMapName + "]", propertyMap, ordinal);
}
}

private static class ConfigMapStringInputPropertiesConfigSource extends MapBackedConfigSource {

private static final String NAME_FORMAT = "ConfigMapStringInputPropertiesConfigSource[configMap=%s,file=%s]";

ConfigMapStringInputPropertiesConfigSource(String configMapName, String fileName, String input, int ordinal) {
super(String.format(NAME_FORMAT, configMapName, fileName), readProperties(input), ordinal);
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private static Map<String, String> readProperties(String rawData) {
try (StringReader br = new StringReader(rawData)) {
final Properties properties = new Properties();
properties.load(br);
return (Map<String, String>) (Map) properties;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

private static class ConfigMapStringInputYamlConfigSource extends YamlConfigSource {

private static final String NAME_FORMAT = "ConfigMapStringInputYamlConfigSource[configMap=%s,file=%s]";

public ConfigMapStringInputYamlConfigSource(String configMapName, String fileName, String input, int ordinal) {
super(String.format(NAME_FORMAT, configMapName, fileName), input, ordinal);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkus.kubernetes.client.runtime;

import java.util.Collections;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.jboss.logging.Logger;

import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;

@Recorder
public class KubernetesConfigRecorder {

private static final Logger log = Logger.getLogger(KubernetesConfigRecorder.class);

public RuntimeValue<ConfigSourceProvider> configMaps(KubernetesConfigSourceConfig kubernetesConfigSourceConfig,
KubernetesClientBuildConfig clientConfig) {
if (!kubernetesConfigSourceConfig.enabled) {
log.debug(
"No attempt will be made to obtain configuration from the Kubernetes API server because the functionality has been disabled via configuration");
return emptyRuntimeValue();
}

return new RuntimeValue<>(new ConfigMapConfigSourceProvider(kubernetesConfigSourceConfig,
KubernetesClientUtils.createClient(clientConfig)));
}

private RuntimeValue<ConfigSourceProvider> emptyRuntimeValue() {
return new RuntimeValue<>(new EmptyConfigSourceProvider());
}

private static class EmptyConfigSourceProvider implements ConfigSourceProvider {

@Override
public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) {
return Collections.emptyList();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.kubernetes.client.runtime;

import java.util.List;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(name = "kubernetes-config", phase = ConfigPhase.BOOTSTRAP)
public class KubernetesConfigSourceConfig {

/**
* If set to true, the application will attempt to look up the configuration from the API server
*/
@ConfigItem(defaultValue = "false")
public boolean enabled;

/**
* If set to true, the application will not start if any of the configured config sources cannot be located
*/
@ConfigItem(defaultValue = "true")
public boolean failOnMissingConfig;

/**
* ConfigMaps to look for in the namespace that the Kubernetes Client has been configured for
*/
@ConfigItem
public Optional<List<String>> configMaps;

}
Loading

0 comments on commit 4576876

Please sign in to comment.