Skip to content

Commit

Permalink
Merge pull request #276 from jenkinsci/podtemplatebuilder
Browse files Browse the repository at this point in the history
Move PodTemplate -> Pod conversion to PodTemplateBuilder
  • Loading branch information
carlossg authored Jan 29, 2018
2 parents 0d27f96 + cc04762 commit 2dd6ab9
Show file tree
Hide file tree
Showing 5 changed files with 388 additions and 313 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,56 +25,27 @@
package org.csanchez.jenkins.plugins.kubernetes;

import static java.util.logging.Level.*;
import static org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud.*;
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.*;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Paths;
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.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang.StringUtils;
import org.csanchez.jenkins.plugins.kubernetes.model.TemplateEnvVar;
import org.csanchez.jenkins.plugins.kubernetes.pipeline.PodTemplateStepExecution;
import org.csanchez.jenkins.plugins.kubernetes.volumes.PodVolume;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;

import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import hudson.model.TaskListener;
import hudson.slaves.JNLPLauncher;
import hudson.slaves.SlaveComputer;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.ContainerPort;
import io.fabric8.kubernetes.api.model.ContainerStatus;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.ExecAction;
import io.fabric8.kubernetes.api.model.LocalObjectReference;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
import io.fabric8.kubernetes.api.model.PodFluent;
import io.fabric8.kubernetes.api.model.Probe;
import io.fabric8.kubernetes.api.model.ProbeBuilder;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.Volume;
import io.fabric8.kubernetes.api.model.VolumeBuilder;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.LogWatch;
import io.fabric8.kubernetes.client.dsl.PrettyLoggable;
Expand All @@ -83,17 +54,7 @@
* Launches on Kubernetes the specified {@link KubernetesComputer} instance.
*/
public class KubernetesLauncher extends JNLPLauncher {
private static final Pattern SPLIT_IN_SPACES = Pattern.compile("([^\"]\\S*|\".+?\")\\s*");

private static final String WORKSPACE_VOLUME_NAME = "workspace-volume";

private static final String DEFAULT_JNLP_ARGUMENTS = "${computer.jnlpmac} ${computer.name}";

private static final String DEFAULT_JNLP_IMAGE = System
.getProperty(PodTemplateStepExecution.class.getName() + ".defaultImage", "jenkins/jnlp-slave:alpine");

private static final String JNLPMAC_REF = "\\$\\{computer.jnlpmac\\}";
private static final String NAME_REF = "\\$\\{computer.name\\}";
private static final Logger LOGGER = Logger.getLogger(KubernetesLauncher.class.getName());

private boolean launched;
Expand Down Expand Up @@ -249,168 +210,9 @@ public void launch(SlaveComputer computer, TaskListener listener) {
}

private Pod getPodTemplate(KubernetesSlave slave, PodTemplate template) {
if (template == null) {
return null;
}

// Build volumes and volume mounts.
List<Volume> volumes = new ArrayList<>();
Map<String, VolumeMount> volumeMounts = new HashMap();

int i = 0;
for (final PodVolume volume : template.getVolumes()) {
final String volumeName = "volume-" + i;
//We need to normalize the path or we can end up in really hard to debug issues.
final String mountPath = substituteEnv(Paths.get(volume.getMountPath()).normalize().toString());
if (!volumeMounts.containsKey(mountPath)) {
volumeMounts.put(mountPath, new VolumeMount(mountPath, volumeName, false, null));
volumes.add(volume.buildVolume(volumeName));
i++;
}
}

if (template.getWorkspaceVolume() != null) {
volumes.add(template.getWorkspaceVolume().buildVolume(WORKSPACE_VOLUME_NAME));
} else {
// add an empty volume to share the workspace across the pod
volumes.add(new VolumeBuilder().withName(WORKSPACE_VOLUME_NAME).withNewEmptyDir("").build());
}

Map<String, Container> containers = new HashMap<>();

for (ContainerTemplate containerTemplate : template.getContainers()) {
containers.put(containerTemplate.getName(), createContainer(slave, containerTemplate, template.getEnvVars(), volumeMounts.values()));
}

if (!containers.containsKey(JNLP_NAME)) {
ContainerTemplate containerTemplate = new ContainerTemplate(DEFAULT_JNLP_IMAGE);
containerTemplate.setName(JNLP_NAME);
containerTemplate.setArgs(DEFAULT_JNLP_ARGUMENTS);
containers.put(JNLP_NAME, createContainer(slave, containerTemplate, template.getEnvVars(), volumeMounts.values()));
}

List<LocalObjectReference> imagePullSecrets = template.getImagePullSecrets().stream()
.map((x) -> x.toLocalObjectReference()).collect(Collectors.toList());

PodFluent.SpecNested<PodBuilder> builder = new PodBuilder()
.withNewMetadata()
.withName(substituteEnv(slave.getNodeName()))
.withLabels(slave.getKubernetesCloud().getLabelsMap(template.getLabelSet()))
.withAnnotations(getAnnotationsMap(template.getAnnotations()))
.endMetadata()
.withNewSpec();

if(template.getActiveDeadlineSeconds() > 0) {
builder = builder.withActiveDeadlineSeconds(Long.valueOf(template.getActiveDeadlineSeconds()));
}

Pod pod = builder.withVolumes(volumes)
.withServiceAccount(substituteEnv(template.getServiceAccount()))
.withImagePullSecrets(imagePullSecrets)
.withContainers(containers.values().toArray(new Container[containers.size()]))
.withNodeSelector(getNodeSelectorMap(template.getNodeSelector()))
.withRestartPolicy("Never")
.endSpec()
.build();

return pod;

}


private Container createContainer(KubernetesSlave slave, ContainerTemplate containerTemplate, Collection<TemplateEnvVar> globalEnvVars, Collection<VolumeMount> volumeMounts) {
// Last-write wins map of environment variable names to values
HashMap<String, String> env = new HashMap<>();

// Add some default env vars for Jenkins
env.put("JENKINS_SECRET", slave.getComputer().getJnlpMac());
env.put("JENKINS_NAME", slave.getComputer().getName());

KubernetesCloud cloud = slave.getKubernetesCloud();

String url = cloud.getJenkinsUrlOrDie();

env.put("JENKINS_URL", url);
if (!StringUtils.isBlank(cloud.getJenkinsTunnel())) {
env.put("JENKINS_TUNNEL", cloud.getJenkinsTunnel());
}

// Running on OpenShift Enterprise, security concerns force use of arbitrary user ID
// As a result, container is running without a home set for user, resulting into using `/` for some tools,
// and `?` for java build tools. So we force HOME to a safe location.
env.put("HOME", containerTemplate.getWorkingDir());

Map<String, EnvVar> envVarsMap = new HashMap<>();

env.entrySet().forEach(item ->
envVarsMap.put(item.getKey(), new EnvVar(item.getKey(), item.getValue(), null))
);

if (globalEnvVars != null) {
globalEnvVars.forEach(item ->
envVarsMap.put(item.getKey(), item.buildEnvVar())
);
}

if (containerTemplate.getEnvVars() != null) {
containerTemplate.getEnvVars().forEach(item ->
envVarsMap.put(item.getKey(), item.buildEnvVar())
);
}

EnvVar[] envVars = envVarsMap.values().stream().toArray(EnvVar[]::new);

List<String> arguments = Strings.isNullOrEmpty(containerTemplate.getArgs()) ? Collections.emptyList()
: parseDockerCommand(containerTemplate.getArgs() //
.replaceAll(JNLPMAC_REF, slave.getComputer().getJnlpMac()) //
.replaceAll(NAME_REF, slave.getComputer().getName()));


List<VolumeMount> containerMounts = new ArrayList<>(volumeMounts);

ContainerPort[] ports = containerTemplate.getPorts().stream().map(entry -> entry.toPort()).toArray(size -> new ContainerPort[size]);

if (!Strings.isNullOrEmpty(containerTemplate.getWorkingDir())
&& !PodVolume.volumeMountExists(containerTemplate.getWorkingDir(), volumeMounts)) {
containerMounts.add(new VolumeMount(containerTemplate.getWorkingDir(), WORKSPACE_VOLUME_NAME, false, null));
}

ContainerLivenessProbe clp = containerTemplate.getLivenessProbe();
Probe livenessProbe = null;
if (clp != null && parseLivenessProbe(clp.getExecArgs()) != null) {
livenessProbe = new ProbeBuilder()
.withExec(new ExecAction(parseLivenessProbe(clp.getExecArgs())))
.withInitialDelaySeconds(clp.getInitialDelaySeconds())
.withTimeoutSeconds(clp.getTimeoutSeconds())
.withFailureThreshold(clp.getFailureThreshold())
.withPeriodSeconds(clp.getPeriodSeconds())
.withSuccessThreshold(clp.getSuccessThreshold())
.build();
}

return new ContainerBuilder()
.withName(substituteEnv(containerTemplate.getName()))
.withImage(substituteEnv(containerTemplate.getImage()))
.withImagePullPolicy(containerTemplate.isAlwaysPullImage() ? "Always" : "IfNotPresent")
.withNewSecurityContext()
.withPrivileged(containerTemplate.isPrivileged())
.endSecurityContext()
.withWorkingDir(substituteEnv(containerTemplate.getWorkingDir()))
.withVolumeMounts(containerMounts.toArray(new VolumeMount[containerMounts.size()]))
.addToEnv(envVars)
.addToPorts(ports)
.withCommand(parseDockerCommand(containerTemplate.getCommand()))
.withArgs(arguments)
.withLivenessProbe(livenessProbe)
.withTty(containerTemplate.isTtyEnabled())
.withNewResources()
.withRequests(getResourcesMap(containerTemplate.getResourceRequestMemory(), containerTemplate.getResourceRequestCpu()))
.withLimits(getResourcesMap(containerTemplate.getResourceLimitMemory(), containerTemplate.getResourceLimitCpu()))
.endResources()
.build();
return template == null ? null : template.build(slave);
}


/**
* Log the last lines of containers logs
*/
Expand All @@ -431,87 +233,4 @@ private void logLastLines(List<ContainerStatus> containers, String podId, String
}
}

/**
* Split a command in the parts that Docker need
*
* @param dockerCommand
* @return
*/
@Restricted(NoExternalUse.class)
static List<String> parseDockerCommand(String dockerCommand) {
if (dockerCommand == null || dockerCommand.isEmpty()) {
return null;
}
// handle quoted arguments
Matcher m = SPLIT_IN_SPACES.matcher(dockerCommand);
List<String> commands = new ArrayList<String>();
while (m.find()) {
commands.add(substituteEnv(m.group(1).replace("\"", "")));
}
return commands;
}

/**
* Split a command in the parts that LivenessProbe need
*
* @param livenessProbeExec
* @return
*/
@Restricted(NoExternalUse.class)
static List<String> parseLivenessProbe(String livenessProbeExec) {
if (StringUtils.isBlank(livenessProbeExec)) {
return null;
}
// handle quoted arguments
Matcher m = SPLIT_IN_SPACES.matcher(livenessProbeExec);
List<String> commands = new ArrayList<String>();
while (m.find()) {
commands.add(substituteEnv(m.group(1).replace("\"", "").replace("?:\\\"", "")));
}
return commands;
}

private Map<String, Quantity> getResourcesMap(String memory, String cpu) {
ImmutableMap.Builder<String, Quantity> builder = ImmutableMap.<String, Quantity>builder();
String actualMemory = substituteEnv(memory);
String actualCpu = substituteEnv(cpu);
if (StringUtils.isNotBlank(actualMemory)) {
Quantity memoryQuantity = new Quantity(actualMemory);
builder.put("memory", memoryQuantity);
}
if (StringUtils.isNotBlank(actualCpu)) {
Quantity cpuQuantity = new Quantity(actualCpu);
builder.put("cpu", cpuQuantity);
}
return builder.build();
}

private Map<String, String> getAnnotationsMap(List<PodAnnotation> annotations) {
ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder();
if (annotations != null) {
for (PodAnnotation podAnnotation : annotations) {
builder.put(podAnnotation.getKey(), substituteEnv(podAnnotation.getValue()));
}
}
return builder.build();
}

private Map<String, String> getNodeSelectorMap(String selectors) {
if (Strings.isNullOrEmpty(selectors)) {
return ImmutableMap.of();
} else {
ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder();

for (String selector : selectors.split(",")) {
String[] parts = selector.split("=");
if (parts.length == 2 && !parts[0].isEmpty() && !parts[1].isEmpty()) {
builder = builder.put(parts[0], substituteEnv(parts[1]));
} else {
LOGGER.log(Level.WARNING, "Ignoring selector '" + selector
+ "'. Selectors must be in the format 'label1=value1,label2=value2'.");
}
}
return builder.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import hudson.model.Node;
import hudson.model.labels.LabelAtom;
import hudson.tools.ToolLocationNodeProperty;
import io.fabric8.kubernetes.api.model.Pod;
import jenkins.model.Jenkins;

/**
Expand Down Expand Up @@ -587,6 +588,15 @@ protected Object readResolve() {
return this;
}

/**
* Build a Pod object from a PodTemplate
*
* @param slave
*/
public Pod build(KubernetesSlave slave) {
return new PodTemplateBuilder(this).build(slave);
}

@Extension
public static class DescriptorImpl extends Descriptor<PodTemplate> {

Expand Down
Loading

0 comments on commit 2dd6ab9

Please sign in to comment.