io.micronaut.kubernetes.util.KubernetesUtils Maven / Gradle / Ivy
/*
* Copyright 2017-2020 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
*
* https://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 io.micronaut.kubernetes.util;
import io.kubernetes.client.common.KubernetesObject;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1ConfigMap;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Secret;
import io.micronaut.context.env.EnvironmentPropertySource;
import io.micronaut.context.env.PropertySource;
import io.micronaut.context.env.PropertySourceLoader;
import io.micronaut.context.env.PropertySourceReader;
import io.micronaut.context.exceptions.ConfigurationException;
import io.micronaut.kubernetes.client.reactor.CoreV1ApiReactorClient;
import io.micronaut.kubernetes.configuration.KubernetesConfigurationClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static io.micronaut.kubernetes.configuration.KubernetesConfigurationClient.KUBERNETES_CONFIG_MAP_NAME_SUFFIX;
import static io.micronaut.kubernetes.health.KubernetesHealthIndicator.HOSTNAME_ENV_VARIABLE;
/**
* Utility class with methods to help with ConfigMaps and Secrets.
*
* @author Álvaro Sánchez-Mariscal
* @since 1.0.0
*/
public class KubernetesUtils {
public static final String ENV_KUBERNETES_SERVICE_HOST = "KUBERNETES_SERVICE_HOST";
private static final Logger LOG = LoggerFactory.getLogger(KubernetesUtils.class);
private static final List PROPERTY_SOURCE_READERS = StreamSupport.stream(ServiceLoader.load(PropertySourceLoader.class).spliterator(), false).collect(Collectors.toList());
/**
* Converts a {@link V1ConfigMap} into a {@link PropertySource}.
*
* @param configMap the ConfigMap
* @return A PropertySource
*/
public static PropertySource configMapAsPropertySource(V1ConfigMap configMap) {
if (LOG.isTraceEnabled()) {
LOG.trace("Processing PropertySources for ConfigMap: {}", configMap);
}
String name = getPropertySourceName(configMap);
Map data = configMap.getData();
if (data == null || data.isEmpty()) {
return PropertySource.of(Collections.emptyMap());
}
Map.Entry entry = data.entrySet().iterator().next();
if (data.size() > 1 || !getExtension(entry.getKey()).isPresent()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Considering this ConfigMap as containing multiple literal key/values");
}
data.putIfAbsent(KubernetesConfigurationClient.CONFIG_MAP_RESOURCE_VERSION, configMap.getMetadata().getResourceVersion());
Map propertySourceData = new HashMap<>(data);
return PropertySource.of(name, propertySourceData);
} else {
if (LOG.isTraceEnabled()) {
LOG.trace("Considering this ConfigMap as containing values from a single file");
}
String extension = getExtension(entry.getKey()).get();
int priority = EnvironmentPropertySource.POSITION + 100;
PropertySource propertySource = PROPERTY_SOURCE_READERS.stream()
.filter(reader -> reader.getExtensions().contains(extension))
.map(reader -> reader.read(entry.getKey(), entry.getValue().getBytes()))
.peek(map -> map.putIfAbsent(KubernetesConfigurationClient.CONFIG_MAP_RESOURCE_VERSION, configMap.getMetadata().getResourceVersion()))
.map(map -> PropertySource.of(entry.getKey() + KUBERNETES_CONFIG_MAP_NAME_SUFFIX, map, priority))
.findFirst()
.orElse(PropertySource.of(Collections.emptyMap()));
KubernetesConfigurationClient.addPropertySourceToCache(propertySource);
return propertySource;
}
}
/**
* Converts config map mounted as volume into property sources.
*
* @param mountPoint the mount point
* @param data the configmaps data in the mounted volume where keys are file names and values is the file content
* @return list of property sources
*/
public static List configMapAsPropertySource(String mountPoint, Map data) {
if (LOG.isTraceEnabled()) {
LOG.trace("Creating {} PropertySources for ConfigMap mounted at: {}", data.size(), mountPoint);
}
if (data == null || data.isEmpty()) {
return Collections.singletonList(PropertySource.of(Collections.emptyMap()));
}
List propertySources = new ArrayList<>(data.size());
for (Map.Entry entry : data.entrySet()) {
Optional extension = getExtension(entry.getKey());
if (!extension.isPresent()) {
if (LOG.isInfoEnabled()) {
LOG.info("Failed to deduce the extension for file: {}", entry.getKey());
}
continue;
}
String fileExtension = extension.get();
String propertyName = mountPoint + "/" + entry.getKey() + KUBERNETES_CONFIG_MAP_NAME_SUFFIX;
int priority = EnvironmentPropertySource.POSITION + 150;
PropertySource propertySource = PROPERTY_SOURCE_READERS.stream()
.filter(reader -> reader.getExtensions().contains(fileExtension))
.map(reader -> reader.read(entry.getKey(), entry.getValue().getBytes()))
.map(map -> PropertySource.of(propertyName, map, priority))
.findFirst()
.orElse(PropertySource.of(Collections.emptyMap()));
propertySources.add(propertySource);
}
return propertySources;
}
/**
* Determines the value of a Kubernetes labelSelector filter based on the passed labels.
*
* @param labels the labels
* @return the value of the labelSelector filter
*/
public static String computeLabelSelector(Map labels) {
String labelSelector = "";
if (!labels.isEmpty()) {
labelSelector = labels.entrySet()
.stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining(","));
if (LOG.isTraceEnabled()) {
LOG.trace("labelSelector: {}", labelSelector);
}
}
return labelSelector;
}
/**
* @param secret The {@link V1Secret} to transform
* @return The converted {@link PropertySource}.
*/
public static PropertySource secretAsPropertySource(V1Secret secret) {
if (LOG.isTraceEnabled()) {
LOG.trace("Processing PropertySources for Secret: {}", secret);
}
String name = secret.getMetadata().getName() + KubernetesConfigurationClient.KUBERNETES_SECRET_NAME_SUFFIX;
Map data = secret.getData();
Map propertySourceData = null;
if (data != null) {
propertySourceData = data.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, v -> new String(v.getValue())));
} else {
propertySourceData = Collections.emptyMap();
}
int priority = EnvironmentPropertySource.POSITION + 100;
PropertySource propertySource = PropertySource.of(name, propertySourceData, priority);
KubernetesConfigurationClient.addPropertySourceToCache(propertySource);
return propertySource;
}
/**
* @param includes the objects to include
* @return a {@link Predicate} based on a collection of object names to include
*/
public static Predicate getIncludesFilter(Collection includes) {
Predicate includesFilter = s -> true;
if (!includes.isEmpty()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Includes: {}", includes);
}
includesFilter = s -> {
boolean result = includes.contains(s.getMetadata().getName());
if (LOG.isTraceEnabled()) {
if (result) {
LOG.trace("Includes filter matched: {}", s.getMetadata().getName());
} else {
LOG.trace("Includes filter not-matched: {}", s.getMetadata().getName());
}
}
return result;
};
}
return includesFilter;
}
/**
* @param excludes the objects to excludes
* @return a {@link Predicate} based on a collection of object names to excludes
*/
public static Predicate getExcludesFilter(Collection excludes) {
Predicate excludesFilter = s -> true;
if (!excludes.isEmpty()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Excludes: {}", excludes);
}
excludesFilter = s -> {
boolean result = !excludes.contains(s.getMetadata().getName());
if (LOG.isTraceEnabled()) {
if (!result) {
LOG.trace("Excludes matched: {}", s.getMetadata().getName());
} else {
LOG.trace("Excludes not-matched: {}", s.getMetadata().getName());
}
}
return result;
};
}
return excludesFilter;
}
/**
* @param labels the labels to include
* @return a {@link Predicate} based on labels the kubernetes objects has to match to return true
*/
public static Predicate getLabelsFilter(Map labels) {
Predicate labelsFilter = s -> true;
if (!labels.isEmpty()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Filter labels: {}", labels.keySet());
}
labelsFilter = kubernetesObject -> {
V1ObjectMeta objectMeta = kubernetesObject.getMetadata();
if (objectMeta == null) {
return false;
}
Map kubernetesObjectLabels = objectMeta.getLabels();
if (kubernetesObjectLabels == null) {
return false;
}
boolean result = labels.entrySet().stream().allMatch(
e -> kubernetesObjectLabels.containsKey(e.getKey()) && kubernetesObjectLabels.get(e.getKey()).equals(e.getValue()));
if (LOG.isTraceEnabled()) {
if (result) {
LOG.trace("Filter labels filter matched: {}", kubernetesObject.getMetadata().getName());
} else {
LOG.trace("Filter labels not-matched: {}", kubernetesObject.getMetadata().getName());
}
}
return result;
};
}
return labelsFilter;
}
public static String objectNameOrNull(KubernetesObject kubernetesObject) {
if (kubernetesObject.getMetadata() != null) {
return kubernetesObject.getMetadata().getName();
}
return null;
}
/**
* @param client the {@link CoreV1ApiReactorClient}
* @param podLabelKeys the list of labels inside a pod
* @param namespace in the configuration
* @param labels the labels
* @param exceptionOnPodLabelsMissing should and exception be thrown if configured pod label is not found
* @return the filtered labels of the current pod
*/
public static Mono computePodLabelSelector(CoreV1ApiReactorClient client, List podLabelKeys,
String namespace, Map labels,
boolean exceptionOnPodLabelsMissing) {
// determine if we are running inside a pod. This environment variable is always been set.
String host = System.getenv(ENV_KUBERNETES_SERVICE_HOST);
if (host == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not running on k8s");
}
return Mono.just(computeLabelSelector(labels));
}
final String podName = System.getenv(HOSTNAME_ENV_VARIABLE);
return client.readNamespacedPod(podName, namespace, null)
.doOnError(ApiException.class, throwable ->
LOG.error("Failed to read the Pod [" + podName + "] the application is running in: " + throwable.getResponseBody(), throwable))
.map(pod -> {
Map result = new HashMap<>();
Map podLabels = Objects.requireNonNull(pod.getMetadata()).getLabels();
for (String key : podLabelKeys) {
String value = podLabels.get(key);
if (value != null) {
result.put(key, value);
if (LOG.isTraceEnabled()) {
LOG.trace("Including pod label: {}={}", key, value);
}
} else {
if (LOG.isWarnEnabled()) {
LOG.warn("Pod metadata does not contain label: {}", key);
}
if (exceptionOnPodLabelsMissing) {
throw new ConfigurationException("Pod metadata does not contain label: " + key +
" and the exception-on-pod-labels-missing property is set");
}
}
}
if (LOG.isDebugEnabled()) {
LOG.debug("Computed pod label selectors {}", result);
}
result.putAll(labels);
return computeLabelSelector(result);
})
.doOnError(throwable -> LOG.error("Failed to compute the label selector [" + podLabelKeys + "] from the Pod [" + podName + "]: " + throwable.getMessage(), throwable));
}
private static String getPropertySourceName(V1ConfigMap configMap) {
return configMap.getMetadata().getName() + KUBERNETES_CONFIG_MAP_NAME_SUFFIX;
}
private static Optional getExtension(String filename) {
return Optional.of(filename)
.filter(f -> f.contains("."))
.map(f -> f.substring(filename.lastIndexOf(".") + 1));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy