/*******************************************************************************
* Copyright (c) 2012-2017 Red Hat, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.plugin.openshift.client.kubernetes;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.eclipse.che.plugin.docker.client.json.ContainerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converter of labels defined in {@link ContainerConfig} for matching to Kubernetes
* annotation requirements
*/
public final class KubernetesLabelConverter {
private static final Logger LOG = LoggerFactory.getLogger(KubernetesLabelConverter.class);
/** Prefix used for che server labels */
private static final String CHE_SERVER_LABEL_PREFIX = "che:server";
/** Padding to use when converting server label to DNS name */
private static final String CHE_SERVER_LABEL_PADDING = "0%s0";
/** Regex to use when matching converted labels -- should match {@link CHE_SERVER_LABEL_PADDING} */
private static final Pattern CHE_SERVER_LABEL_KEY = Pattern.compile("^0(.*)0$");
private static final String KUBERNETES_ANNOTATION_REGEX = "([A-Za-z0-9][-A-Za-z0-9_\\.]*)?[A-Za-z0-9]";
private KubernetesLabelConverter() {
}
/**
* @return prefix that is used for Che server labels
*/
public static String getCheServerLabelPrefix() {
return CHE_SERVER_LABEL_PREFIX;
}
/**
* Converts a map of labels to match Kubernetes annotation requirements. Annotations are limited
* to alphanumeric characters, {@code '.'}, {@code '_'} and {@code '-'}, and must start and end
* with an alphanumeric character, i.e. they must match the regex
* {@code ([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]}
*
* <p>Note that entry keys should begin with {@link KubernetesLabelConverter#CHE_SERVER_LABEL_PREFIX} and
* entries should not contain {@code '.'} or {@code '_'} before conversion;
* otherwise label will not be converted and included in output.
*
* <p>This implementation is relatively fragile -- changes to how Che generates labels may cause
* this method to stop working. In general, it will only be possible to convert labels that are
* alphanumeric plus up to 3 special characters (by converting the special characters to {@code '_'},
* {@code '-'}, and {@code '.'} as necessary).
*
* @param labels Map of labels to convert
* @return Map of labels converted to DNS Names
*/
public static Map<String, String> labelsToNames(Map<String, String> labels) {
Map<String, String> names = new HashMap<>();
for (Map.Entry<String, String> label : labels.entrySet()) {
if (!hasConversionProblems(label)) {
String key = label.getKey();
String value = label.getValue();
// Convert keys: e.g. "che:server:4401/tcp:ref" ->
// "che.server.4401-tcp.ref"
key = convertLabelKey(key);
// Convert values: e.g. "/api" -> ".api" -- note values may
// include '-' e.g. "tomcat-debug"
value = convertLabelValue(value);
// Add padding since DNS names must start and end with
// alphanumeric characters
key = addPadding(key);
value = addPadding(value);
if (matchesKubernetesLabelRegex(key) && matchesKubernetesLabelRegex(value)) {
names.put(key, value);
} else {
LOG.error(
"Could not convert label {} into Kubernetes annotation: labels must be alphanumeric with ':' and '/'",
label.toString());
}
}
}
return names;
}
/**
* Undoes the label conversion done by {@link KubernetesLabelConverter#labelsToNames(Map)}
*
* @param labels Map of DNS names
* @return Map of unconverted labels
*/
public static Map<String, String> namesToLabels(Map<String, String> names) {
Map<String, String> labels = new HashMap<>();
for (Map.Entry<String, String> entry: names.entrySet()){
String key = entry.getKey();
String value = entry.getValue();
// Remove padding
Matcher keyMatcher = CHE_SERVER_LABEL_KEY.matcher(key);
Matcher valueMatcher = CHE_SERVER_LABEL_KEY.matcher(value);
if (!keyMatcher.matches() || !valueMatcher.matches()) {
continue;
}
key = keyMatcher.group(1);
value = valueMatcher.group(1);
// Convert key: e.g. "che.server.4401_tcp.ref" -> "che:server:4401/tcp:ref"
key = key.replaceAll("\\.", ":").replaceAll("_", "/");
// Convert value: e.g. Convert values: e.g. "_api" -> "/api"
value = value.replaceAll("_", "/");
labels.put(key, value);
}
return labels;
}
/**
* Checks if there are any potential problems coupled with label conversion
* @param label
* @return true if label has no conversion issues, false otherwise
*/
private static boolean hasConversionProblems(final Map.Entry<String, String> label) {
boolean hasProblems = false;
String key = label.getKey();
String value = label.getValue();
if (StringUtils.isBlank(value)) {
LOG.error("The label {} is blank", label.toString());
hasProblems = true;
} else if (key.contains(".") || key.contains("_") || value.contains("_")) {
LOG.error("Cannot convert label {} to DNS Name: '-' and '.' are used as escape characters",
label.toString());
hasProblems = true;
} else if (!key.startsWith(CHE_SERVER_LABEL_PREFIX)) {
LOG.warn("Expected CreateContainerParams label key {} to start with {}", key, CHE_SERVER_LABEL_PREFIX);
}
return hasProblems;
}
/**
* Convert keys: e.g. "che:server:4401/tcp:ref" -> "che.server.4401-tcp.ref"
*/
private static String convertLabelKey(final String key) {
return key.replaceAll(":", ".").replaceAll("/", "_");
}
/**
* Convert values: e.g. "/api" -> ".api" Note: values may include '-' e.g.
* "tomcat-debug"
*/
private static String convertLabelValue(final String value) {
return value.replaceAll("/", "_");
}
/**
* Adds padding since DNS names must start and end with alphanumeric
* characters
*/
private static String addPadding(final String label) {
return String.format(CHE_SERVER_LABEL_PADDING, label);
}
private static boolean matchesKubernetesLabelRegex(final String label) {
return label.matches(KUBERNETES_ANNOTATION_REGEX);
}
}