package org.csanchez.jenkins.plugins.kubernetes;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import org.csanchez.jenkins.plugins.kubernetes.volumes.PodVolume;
import org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.WorkspaceVolume;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import hudson.model.Label;
import hudson.tools.ToolLocationNodeProperty;
import static org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate.DEFAULT_WORKING_DIR;
public class PodTemplateUtils {
private static final String PLACEHOLDER_KEY = "key";
private static final String PLACEHOLDER_FORMAT = "\\$\\{%s\\}";
private static final String PLACEHOLDER_REGEX = String.format(PLACEHOLDER_FORMAT, "(?<" + PLACEHOLDER_KEY + ">[a-zA-Z0-9_]+)");
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(PLACEHOLDER_REGEX);
/**
* Combines a {@link ContainerTemplate} with its parent.
* @param parent The parent container template (nullable).
* @param template The actual container template
* @return The combined container template.
*/
public static ContainerTemplate combine(@CheckForNull ContainerTemplate parent, @Nonnull ContainerTemplate template) {
Preconditions.checkNotNull(template, "Container template should not be null");
if (parent == null) {
return template;
}
String name = template.getName();
String image = Strings.isNullOrEmpty(template.getImage()) ? parent.getImage() : template.getImage();
boolean privileged = template.isPrivileged() ? template.isPrivileged() : (parent.isPrivileged() ? parent.isPrivileged() : false);
boolean alwaysPullImage = template.isAlwaysPullImage() ? template.isAlwaysPullImage() : (parent.isAlwaysPullImage() ? parent.isAlwaysPullImage() : false);
String workingDir = Strings.isNullOrEmpty(template.getWorkingDir()) ? (Strings.isNullOrEmpty(parent.getWorkingDir()) ? DEFAULT_WORKING_DIR : parent.getWorkingDir()) : template.getCommand();
String command = Strings.isNullOrEmpty(template.getCommand()) ? parent.getCommand() : template.getCommand();
String args = Strings.isNullOrEmpty(template.getArgs()) ? parent.getArgs() : template.getArgs();
boolean ttyEnabled = template.isTtyEnabled() ? template.isTtyEnabled() : (parent.isTtyEnabled() ? parent.isTtyEnabled() : false);;
String resourceRequestCpu = Strings.isNullOrEmpty(template.getResourceRequestCpu()) ? parent.getResourceRequestCpu() : template.getResourceRequestCpu();
String resourceRequestMemory = Strings.isNullOrEmpty(template.getResourceRequestMemory()) ? parent.getResourceRequestMemory() : template.getResourceRequestMemory();
String resourceLimitCpu = Strings.isNullOrEmpty(template.getResourceLimitCpu()) ? parent.getResourceLimitCpu() : template.getResourceLimitCpu();
String resourceLimitMemory = Strings.isNullOrEmpty(template.getResourceLimitMemory()) ? parent.getResourceLimitMemory() : template.getResourceLimitMemory();
List<ContainerEnvVar> combinedEnvVars = new ArrayList<ContainerEnvVar>();
Map<String, String> envVars = new HashMap<>();
parent.getEnvVars().stream().filter(e -> !Strings.isNullOrEmpty(e.getKey())).forEach(
e -> envVars.put(e.getKey(), e.getValue())
);
template.getEnvVars().stream().filter(e -> !Strings.isNullOrEmpty(e.getKey())).forEach(
e -> envVars.put(e.getKey(), e.getValue())
);
envVars.entrySet().forEach(e -> combinedEnvVars.add(new ContainerEnvVar(e.getKey(), e.getValue())));
ContainerTemplate combined = new ContainerTemplate(image);
combined.setName(name);
combined.setImage(image);
combined.setAlwaysPullImage(alwaysPullImage);
combined.setCommand(command);
combined.setArgs(args);
combined.setTtyEnabled(ttyEnabled);
combined.setResourceLimitCpu(resourceLimitCpu);
combined.setResourceLimitMemory(resourceLimitMemory);
combined.setResourceRequestCpu(resourceRequestCpu);
combined.setResourceRequestMemory(resourceRequestMemory);
combined.setWorkingDir(workingDir);
combined.setPrivileged(privileged);
combined.setEnvVars(combinedEnvVars);
return combined;
}
/**
* Combines a {@link PodTemplate} with its parent.
* @param parent The parent container template (nullable).
* @param template The actual container template
* @return The combined container template.
*/
public static PodTemplate combine(PodTemplate parent, PodTemplate template) {
Preconditions.checkNotNull(template, "Pod template should not be null");
if (parent == null) {
return template;
}
String name = template.getName();
String label = template.getLabel();
String nodeSelector = Strings.isNullOrEmpty(template.getNodeSelector()) ? parent.getNodeSelector() : template.getNodeSelector();
String serviceAccount = Strings.isNullOrEmpty(template.getServiceAccount()) ? parent.getServiceAccount() : template.getServiceAccount();
Set<PodImagePullSecret> imagePullSecrets = new LinkedHashSet<>();
imagePullSecrets.addAll(parent.getImagePullSecrets());
imagePullSecrets.addAll(template.getImagePullSecrets());
Map<String, ContainerTemplate> combinedContainers = new HashMap<>();
Map<String, PodVolume> combinedVolumes = new HashMap<>();
//Env Vars
Map<String, String> combinedEnvVars = new HashMap<>();
combinedEnvVars.putAll(parent.getEnvVars().stream().filter(e -> !Strings.isNullOrEmpty(e.getKey())).collect(Collectors.toMap(e -> e.getKey(),e -> e.getValue())));
combinedEnvVars.putAll(template.getEnvVars().stream().filter(e -> !Strings.isNullOrEmpty(e.getKey())).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())));
//Containers
Map<String, ContainerTemplate> parentContainers = parent.getContainers().stream().collect(Collectors.toMap(c -> c.getName(), c -> c));
combinedContainers.putAll(parentContainers);
combinedContainers.putAll(template.getContainers().stream().collect(Collectors.toMap(c -> c.getName(), c -> combine(parentContainers.get(c.getName()), c))));
//Volumes
Map<String, PodVolume> parentVolumes = parent.getVolumes().stream().collect(Collectors.toMap(v -> v.getMountPath(), v -> v));
combinedVolumes.putAll(parentVolumes);
combinedVolumes.putAll(template.getVolumes().stream().collect(Collectors.toMap(v -> v.getMountPath(), v -> v)));
WorkspaceVolume workspaceVolume = template.isCustomWorkspaceVolumeEnabled() && template.getWorkspaceVolume() != null ? template.getWorkspaceVolume() : parent.getWorkspaceVolume();
//Tool location node properties
List<ToolLocationNodeProperty> toolLocationNodeProperties = new ArrayList<>();
toolLocationNodeProperties.addAll(parent.getNodeProperties());
toolLocationNodeProperties.addAll(template.getNodeProperties());
PodTemplate podTemplate = new PodTemplate();
podTemplate.setName(name);
podTemplate.setLabel(label);
podTemplate.setNodeSelector(nodeSelector);
podTemplate.setServiceAccount(serviceAccount);
podTemplate.setEnvVars(combinedEnvVars.entrySet().stream().map(e -> new PodEnvVar(e.getKey(), e.getValue())).collect(Collectors.toList()));
podTemplate.setContainers(new ArrayList<>(combinedContainers.values()));
podTemplate.setWorkspaceVolume(workspaceVolume);
podTemplate.setVolumes(new ArrayList<>(combinedVolumes.values()));
podTemplate.setImagePullSecrets(new ArrayList<>(imagePullSecrets));
podTemplate.setNodeProperties(toolLocationNodeProperties);
return podTemplate;
}
/**
* Unwraps the hierarchy of the PodTemplate.
*
* @param template The template to unwrap.
* @param defaultProviderTemplate The name of the template that provides the default values.
* @param allTemplates A collection of all the known templates
* @return
*/
static PodTemplate unwrap(PodTemplate template, String defaultProviderTemplate, Collection<PodTemplate> allTemplates) {
if (template == null) {
return null;
}
StringBuilder sb = new StringBuilder();
if (!Strings.isNullOrEmpty(defaultProviderTemplate)) {
sb.append(defaultProviderTemplate).append(" ");
}
if (!Strings.isNullOrEmpty(template.getInheritFrom())) {
sb.append(template.getInheritFrom()).append(" ");
}
String inheritFrom = sb.toString();
if (Strings.isNullOrEmpty(inheritFrom)) {
return template;
} else {
String[] parentNames = inheritFrom.split("[ ]+");
PodTemplate parent = null;
for (String name : parentNames) {
PodTemplate next = getTemplateByName(name, allTemplates);
if (next != null) {
parent = combine(parent, unwrap(next, allTemplates));
}
}
return combine(parent, template);
}
}
/**
* Unwraps the hierarchy of the PodTemplate.
*
* @param template The template to unwrap.
* @param allTemplates A collection of all the known templates
* @return
*/
static PodTemplate unwrap(PodTemplate template, Collection<PodTemplate> allTemplates) {
return unwrap(template, null, allTemplates);
}
/**
* Gets the {@link PodTemplate} by {@link Label}.
* @param label The label.
* @param templates The list of all templates.
* @return The first pod template from the collection that has a matching label.
*/
public static PodTemplate getTemplateByLabel(@CheckForNull Label label, Collection<PodTemplate> templates) {
for (PodTemplate t : templates) {
if (label == null || label.matches(t.getLabelSet())) {
return t;
}
}
return null;
}
/**
* Gets the {@link PodTemplate} by name.
* @param name The name.
* @param templates The list of all templates.
* @return The first pod template from the collection that has a matching name.
*/
public static PodTemplate getTemplateByName(@CheckForNull String name, Collection<PodTemplate> templates) {
for (PodTemplate t : templates) {
if (name != null && name.equals(t.getName())) {
return t;
}
}
return null;
}
/**
* Substitutes a placeholder with a value found in the environment.
* @param s The placeholder. Should be use the format: ${placeholder}.
* @return The substituted value if found, or the input value otherwise.
*/
public static String substituteEnv(String s) {
return substitute(s, System.getenv());
}
/**
* Substitutes a placeholder with a value found in the environment.
* @param s The placeholder. Should be use the format: ${placeholder}.
* @param defaultValue The default value to return if no match is found.
* @return The substituted value if found, or the default value otherwise.
*/
public static String substituteEnv(String s, String defaultValue) {
return substitute(s, System.getenv(), defaultValue);
}
/**
* Substitutes a placeholder with a value found in the specified map.
* @param s The placeholder. Should be use the format: ${placeholder}.
* @param properties The map with the key value pairs to use for substitution.
* @return The substituted value if found, or the input value otherwise.
*/
public static String substitute(String s, Map<String, String> properties) {
return substitute(s, properties, null);
}
/**
* Substitutes a placeholder with a value found in the specified map.
* @param s The placeholder. Should be use the format: ${placeholder}.
* @param properties The map with the key value pairs to use for substitution.
* @param defaultValue The default value to return if no match is found.
* @return The substituted value if found, or the default value otherwise.
*/
public static String substitute(String s, Map<String, String> properties, String defaultValue) {
if (Strings.isNullOrEmpty(s)) {
return defaultValue;
}
Matcher m = PLACEHOLDER_PATTERN.matcher(s);
while (m.find()) {
String key = m.group(PLACEHOLDER_KEY);
String val = properties.get(key);
if (val != null) {
s = s.replaceAll(String.format(PLACEHOLDER_FORMAT, key), val);
} else if (defaultValue != null) {
s = s.replaceAll(String.format(PLACEHOLDER_FORMAT, key), defaultValue);
}
}
return s;
}
}