diff --git a/README.md b/README.md index 89fa3e5b37..85b79eb70a 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,12 @@ spec: Multiple containers can be defined for the agent pod, with shared resources, like mounts. Ports in each container can be accessed as in any Kubernetes pod, by using `localhost`. -The `container` step allows executing commands into each container. +One container must run the Jenkins agent. If unspecified, a container named `jnlp` will be created with the inbound-agent image. +The Jenkins agent requires a JRE to run, so you can avoid the extra container by providing a name using the `agentContainer`. +To get the Jenkins agent injected, you will also need to set `agentInjection` to `true`, and leave the command and argument fields empty for this container. +The container specified by `agentContainer` will be the one where shell steps (or any other step running remote commands on the agent) will run on. + +To execute commands in another container part of the pod (different from the one running the Jenkins agent), you can use the `container` step. **Note** --- @@ -194,8 +199,11 @@ It is recommended to use the same uid across the different containers part of th --- ```groovy -podTemplate(containers: [ - containerTemplate(name: 'maven', image: 'maven:3.8.1-jdk-8', command: 'sleep', args: '99d'), +podTemplate( + agentContainer: 'maven', + agentInjection: true, + containers: [ + containerTemplate(name: 'maven', image: 'maven:3.9.9-eclipse-temurin-17'), containerTemplate(name: 'golang', image: 'golang:1.16.5', command: 'sleep', args: '99d') ]) { @@ -229,17 +237,16 @@ podTemplate(containers: [ or ```groovy -podTemplate(yaml: ''' +podTemplate( + agentContainer: 'maven', + agentInjection: true, + yaml: ''' apiVersion: v1 kind: Pod spec: containers: - name: maven - image: maven:3.8.1-jdk-8 - command: - - sleep - args: - - 99d + image: maven:3.9.9-eclipse-temurin-17 - name: golang image: golang:1.16.5 command: @@ -269,7 +276,6 @@ podTemplate(yaml: ''' } } } - } } ``` diff --git a/examples/maven-with-cache.groovy b/examples/maven-with-cache.groovy index f7924d4711..43e144bc59 100644 --- a/examples/maven-with-cache.groovy +++ b/examples/maven-with-cache.groovy @@ -7,18 +7,14 @@ * two concurrent jobs with this pipeline. Or change readOnly: true after the first run */ -podTemplate(containers: [ - containerTemplate(name: 'maven', image: 'maven:3.8.1-jdk-8', command: 'sleep', args: '99d') - ], volumes: [ - persistentVolumeClaim(mountPath: '/root/.m2/repository', claimName: 'maven-repo', readOnly: false) - ]) { +podTemplate(agentContainer: 'maven', agentInjection: true, containers: [ + containerTemplate(name: 'maven', image: 'maven:3.9.9-eclipse-temurin-17') + ], volumes: [genericEphemeralVolume(accessModes: 'ReadWriteOnce', mountPath: '/root/.m2/repository', requestsSize: '1Gi')]) { node(POD_LABEL) { stage('Build a Maven project') { git 'https://github.com/jenkinsci/kubernetes-plugin.git' - container('maven') { - sh 'mvn -B -ntp clean package -DskipTests' - } + sh 'mvn -B -ntp clean package -DskipTests' } } } diff --git a/examples/multi-container.groovy b/examples/multi-container.groovy index 3a7225bdaa..685a22fac5 100644 --- a/examples/multi-container.groovy +++ b/examples/multi-container.groovy @@ -2,32 +2,27 @@ * This pipeline describes a multi container job, running Maven and Golang builds */ -podTemplate(yaml: ''' - apiVersion: v1 - kind: Pod - spec: - containers: - - name: maven - image: maven:3.8.1-jdk-8 - command: - - sleep - args: - - 99d - - name: golang - image: golang:1.16.5 - command: - - sleep - args: - - 99d +podTemplate(agentContainer: 'maven', + agentInjection: true, + yaml: ''' +apiVersion: v1 +kind: Pod +spec: + containers: + - name: maven + image: maven:3.9.9-eclipse-temurin-17 + - name: golang + image: golang:1.23.1-bookworm + command: + - sleep + args: + - 99d ''' ) { - node(POD_LABEL) { stage('Build a Maven project') { git 'https://github.com/jenkinsci/kubernetes-plugin.git' - container('maven') { - sh 'mvn -B -ntp clean package -DskipTests' - } + sh 'mvn -B -ntp clean package -DskipTests' } stage('Build a Golang project') { git url: 'https://github.com/hashicorp/terraform.git', branch: 'main' diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java index e4c3e385fb..b255374c97 100644 --- a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java @@ -188,6 +188,10 @@ protected static MessageDigest getLabelDigestFunction() { private Long terminationGracePeriodSeconds; + private String agentContainer; + + private boolean agentInjection; + /** * Persisted yaml fragment */ @@ -638,6 +642,25 @@ public boolean isCapOnlyOnAlivePods() { return capOnlyOnAlivePods; } + @CheckForNull + public String getAgentContainer() { + return agentContainer; + } + + @DataBoundSetter + public void setAgentContainer(@CheckForNull String agentContainer) { + this.agentContainer = Util.fixEmpty(agentContainer); + } + + public boolean isAgentInjection() { + return agentInjection; + } + + @DataBoundSetter + public void setAgentInjection(boolean agentInjection) { + this.agentInjection = agentInjection; + } + public List getEnvVars() { if (envVars == null) { return Collections.emptyList(); @@ -1173,6 +1196,8 @@ public String toString() { + (nodeProperties == null || nodeProperties.isEmpty() ? "" : ", nodeProperties=" + nodeProperties) + (yamls == null || yamls.isEmpty() ? "" : ", yamls=" + yamls) + (!unwrapped ? "" : ", unwrapped=" + unwrapped) + + (agentContainer == null ? "" : ", agentContainer='" + agentContainer + '\'') + + (!agentInjection ? "" : ", agentInjection=" + agentInjection) + '}'; } } diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilder.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilder.java index 9a8428631e..4a6b1d2eb2 100644 --- a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilder.java +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilder.java @@ -24,6 +24,7 @@ package org.csanchez.jenkins.plugins.kubernetes; +import static org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate.DEFAULT_WORKING_DIR; import static org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud.JNLP_NAME; import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.combine; import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.isNullOrEmpty; @@ -41,6 +42,7 @@ import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ContainerPort; import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.ExecAction; import io.fabric8.kubernetes.api.model.LocalObjectReference; import io.fabric8.kubernetes.api.model.Pod; @@ -51,6 +53,7 @@ import io.fabric8.kubernetes.api.model.ResourceRequirements; import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder; 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.api.model.VolumeMountBuilder; import io.fabric8.kubernetes.client.utils.Serialization; @@ -101,6 +104,9 @@ public class PodTemplateBuilder { public static final String LABEL_KUBERNETES_CONTROLLER = "kubernetes.jenkins.io/controller"; static final String NO_RECONNECT_AFTER_TIMEOUT = SystemProperties.getString(PodTemplateBuilder.class.getName() + ".noReconnectAfter", "1d"); + private static final String JENKINS_AGENT_FILE_ENVVAR = "JENKINS_AGENT_FILE"; + private static final String JENKINS_AGENT_AGENT_JAR = "/jenkins-agent/agent.jar"; + private static final String JENKINS_AGENT_LAUNCHER_SCRIPT_LOCATION = "/jenkins-agent/jenkins-agent"; @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "tests") @Restricted(NoExternalUse.class) @@ -124,7 +130,7 @@ public class PodTemplateBuilder { } @Restricted(NoExternalUse.class) - static final String DEFAULT_JNLP_IMAGE = + static final String DEFAULT_AGENT_IMAGE = System.getProperty(PodTemplateStepExecution.class.getName() + ".defaultImage", defaultImageName); static final String DEFAULT_JNLP_CONTAINER_MEMORY_REQUEST = System.getProperty( @@ -309,39 +315,92 @@ public Pod build() { } } - // default jnlp container - Optional jnlpOpt = pod.getSpec().getContainers().stream() - .filter(c -> JNLP_NAME.equals(c.getName())) + // default agent container + String agentContainerName = StringUtils.defaultString(template.getAgentContainer(), JNLP_NAME); + Optional agentOpt = pod.getSpec().getContainers().stream() + .filter(c -> agentContainerName.equals(c.getName())) .findFirst(); - Container jnlp = jnlpOpt.orElse(new ContainerBuilder() - .withName(JNLP_NAME) - .withVolumeMounts(volumeMounts - .values() - .toArray(new VolumeMount[volumeMounts.values().size()])) + Container agentContainer = agentOpt.orElse(new ContainerBuilder() + .withName(agentContainerName) + .withVolumeMounts(volumeMounts.values().toArray(VolumeMount[]::new)) .build()); - if (!jnlpOpt.isPresent()) { - pod.getSpec().getContainers().add(jnlp); + if (agentOpt.isEmpty()) { + pod.getSpec().getContainers().add(agentContainer); } + var workingDir = agentContainer.getWorkingDir(); pod.getSpec().getContainers().stream() .filter(c -> c.getWorkingDir() == null) - .forEach(c -> c.setWorkingDir(jnlp.getWorkingDir())); - if (StringUtils.isBlank(jnlp.getImage())) { - String jnlpImage = DEFAULT_JNLP_IMAGE; + .forEach(c -> c.setWorkingDir(workingDir)); + if (StringUtils.isBlank(agentContainer.getImage())) { + String agentImage = DEFAULT_AGENT_IMAGE; if (cloud != null && StringUtils.isNotEmpty(cloud.getJnlpregistry())) { - jnlpImage = Util.ensureEndsWith(cloud.getJnlpregistry(), "/") + jnlpImage; + agentImage = Util.ensureEndsWith(cloud.getJnlpregistry(), "/") + agentImage; } else if (StringUtils.isNotEmpty(DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX)) { - jnlpImage = Util.ensureEndsWith(DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX, "/") + jnlpImage; + agentImage = Util.ensureEndsWith(DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX, "/") + agentImage; } - jnlp.setImage(jnlpImage); + agentContainer.setImage(agentImage); } Map envVars = new HashMap<>(); - envVars.putAll(jnlpEnvVars(jnlp.getWorkingDir())); + envVars.putAll(agentEnvVars(workingDir)); envVars.putAll(defaultEnvVars(template.getEnvVars())); - Optional.ofNullable(jnlp.getEnv()).ifPresent(jnlpEnv -> { - jnlpEnv.forEach(var -> envVars.put(var.getName(), var)); + Optional.ofNullable(agentContainer.getEnv()).ifPresent(agentEnv -> { + agentEnv.forEach(var -> envVars.put(var.getName(), var)); }); - jnlp.setEnv(new ArrayList<>(envVars.values())); - if (jnlp.getResources() == null) { + if (template.isAgentInjection()) { + var agentVolumeMountBuilder = + new VolumeMountBuilder().withName("jenkins-agent").withMountPath("/jenkins-agent"); + var oldInitContainers = pod.getSpec().getInitContainers(); + var jenkinsAgentInitContainer = new ContainerBuilder() + .withName("set-up-jenkins-agent") + .withImage(DEFAULT_AGENT_IMAGE) + .withCommand( + "/bin/sh", + "-c", + "cp $(command -v jenkins-agent) " + JENKINS_AGENT_LAUNCHER_SCRIPT_LOCATION + ";" + + "cp /usr/share/jenkins/agent.jar " + JENKINS_AGENT_AGENT_JAR) + .withVolumeMounts(agentVolumeMountBuilder.build()) + .build(); + if (oldInitContainers != null) { + var newInitContainers = new ArrayList<>(oldInitContainers); + newInitContainers.add(jenkinsAgentInitContainer); + pod.getSpec().setInitContainers(newInitContainers); + } else { + pod.getSpec().setInitContainers(List.of(jenkinsAgentInitContainer)); + } + var oldVolumes = pod.getSpec().getVolumes(); + var jenkinsAgentSharedVolume = new VolumeBuilder() + .withName("jenkins-agent") + .withNewEmptyDir() + .and() + .build(); + if (oldVolumes != null) { + var newVolumes = new ArrayList<>(oldVolumes); + newVolumes.add(jenkinsAgentSharedVolume); + pod.getSpec().setVolumes(newVolumes); + } else { + pod.getSpec().setVolumes(List.of(jenkinsAgentSharedVolume)); + } + var existingVolumeMounts = agentContainer.getVolumeMounts(); + if (existingVolumeMounts != null) { + var newVolumeMounts = new ArrayList<>(existingVolumeMounts); + newVolumeMounts.add(agentVolumeMountBuilder.withReadOnly().build()); + agentContainer.setVolumeMounts(newVolumeMounts); + } else { + agentContainer.setVolumeMounts( + List.of(agentVolumeMountBuilder.withReadOnly().build())); + } + agentContainer.setWorkingDir(DEFAULT_WORKING_DIR); + agentContainer.setCommand(List.of(JENKINS_AGENT_LAUNCHER_SCRIPT_LOCATION)); + agentContainer.setArgs(List.of()); + envVars.put( + JENKINS_AGENT_FILE_ENVVAR, + new EnvVarBuilder() + .withName(JENKINS_AGENT_FILE_ENVVAR) + .withValue(JENKINS_AGENT_AGENT_JAR) + .build()); + } + agentContainer.setEnv(new ArrayList<>(envVars.values())); + if (agentContainer.getResources() == null) { Map reqMap = new HashMap<>(); Map limMap = new HashMap<>(); @@ -361,7 +420,7 @@ public Pod build() { .withLimits(limMap) .build(); - jnlp.setResources(reqs); + agentContainer.setResources(reqs); } if (cloud != null) { pod = PodDecorator.decorateAll(cloud, pod); @@ -406,9 +465,9 @@ private Map defaultEnvVars(Collection globalEnvV return envVarsMap; } - private Map jnlpEnvVars(String workingDir) { + private Map agentEnvVars(String workingDir) { if (workingDir == null) { - workingDir = ContainerTemplate.DEFAULT_WORKING_DIR; + workingDir = DEFAULT_WORKING_DIR; } // Last-write wins map of environment variable names to values HashMap env = new HashMap<>(); @@ -462,7 +521,7 @@ private Container createContainer( Map envVarsMap = new HashMap<>(); String workingDir = substituteEnv(containerTemplate.getWorkingDir()); if (JNLP_NAME.equals(containerTemplate.getName())) { - envVarsMap.putAll(jnlpEnvVars(workingDir)); + envVarsMap.putAll(agentEnvVars(workingDir)); } envVarsMap.putAll(defaultEnvVars(globalEnvVars)); @@ -541,7 +600,7 @@ private Container createContainer( private VolumeMount getDefaultVolumeMount(@CheckForNull String workingDir) { String wd = workingDir; if (wd == null) { - wd = ContainerTemplate.DEFAULT_WORKING_DIR; + wd = DEFAULT_WORKING_DIR; LOGGER.log(Level.FINE, "Container workingDir is null, defaulting to {0}", wd); } return new VolumeMountBuilder() diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtils.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtils.java index 09a4064ca8..7f75c11c83 100644 --- a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtils.java +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtils.java @@ -4,7 +4,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; -import static org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate.DEFAULT_WORKING_DIR; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -46,6 +45,7 @@ import java.util.Set; import java.util.function.BinaryOperator; import java.util.function.Function; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -81,70 +81,38 @@ public static ContainerTemplate combine( return template; } - String name = template.getName(); - String image = isNullOrEmpty(template.getImage()) ? parent.getImage() : template.getImage(); - boolean privileged = template.isPrivileged() - ? template.isPrivileged() - : (parent.isPrivileged() ? parent.isPrivileged() : false); - String runAsUser = template.getRunAsUser() != null ? template.getRunAsUser() : parent.getRunAsUser(); - String runAsGroup = template.getRunAsGroup() != null ? template.getRunAsGroup() : parent.getRunAsGroup(); - boolean alwaysPullImage = template.isAlwaysPullImage() - ? template.isAlwaysPullImage() - : (parent.isAlwaysPullImage() ? parent.isAlwaysPullImage() : false); - String workingDir = isNullOrEmpty(template.getWorkingDir()) - ? (isNullOrEmpty(parent.getWorkingDir()) ? DEFAULT_WORKING_DIR : parent.getWorkingDir()) - : template.getWorkingDir(); - String command = isNullOrEmpty(template.getCommand()) ? parent.getCommand() : template.getCommand(); - String args = isNullOrEmpty(template.getArgs()) ? parent.getArgs() : template.getArgs(); - boolean ttyEnabled = template.isTtyEnabled() - ? template.isTtyEnabled() - : (parent.isTtyEnabled() ? parent.isTtyEnabled() : false); - String resourceRequestCpu = isNullOrEmpty(template.getResourceRequestCpu()) - ? parent.getResourceRequestCpu() - : template.getResourceRequestCpu(); - String resourceRequestMemory = isNullOrEmpty(template.getResourceRequestMemory()) - ? parent.getResourceRequestMemory() - : template.getResourceRequestMemory(); - String resourceRequestEphemeralStorage = isNullOrEmpty(template.getResourceRequestEphemeralStorage()) - ? parent.getResourceRequestEphemeralStorage() - : template.getResourceRequestEphemeralStorage(); - String resourceLimitCpu = isNullOrEmpty(template.getResourceLimitCpu()) - ? parent.getResourceLimitCpu() - : template.getResourceLimitCpu(); - String resourceLimitMemory = isNullOrEmpty(template.getResourceLimitMemory()) - ? parent.getResourceLimitMemory() - : template.getResourceLimitMemory(); - String resourceLimitEphemeralStorage = isNullOrEmpty(template.getResourceLimitEphemeralStorage()) - ? parent.getResourceLimitEphemeralStorage() - : template.getResourceLimitEphemeralStorage(); - String shell = isNullOrEmpty(template.getShell()) ? parent.getShell() : template.getShell(); Map ports = parent.getPorts().stream().collect(Collectors.toMap(PortMapping::getName, Function.identity())); - template.getPorts().stream().forEach(p -> ports.put(p.getName(), p)); - ContainerLivenessProbe livenessProbe = - template.getLivenessProbe() != null ? template.getLivenessProbe() : parent.getLivenessProbe(); - - 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.setResourceLimitEphemeralStorage(resourceLimitEphemeralStorage); - combined.setResourceRequestCpu(resourceRequestCpu); - combined.setResourceRequestMemory(resourceRequestMemory); - combined.setResourceRequestEphemeralStorage(resourceRequestEphemeralStorage); - combined.setShell(shell); - combined.setWorkingDir(workingDir); - combined.setPrivileged(privileged); - combined.setRunAsUser(runAsUser); - combined.setRunAsGroup(runAsGroup); + template.getPorts().forEach(p -> ports.put(p.getName(), p)); + + var h = new HierarchyResolver<>(parent, template); + ContainerTemplate combined = new ContainerTemplate( + template.getName(), h.resolve(ContainerTemplate::getImage, PodTemplateUtils::isNullOrEmpty)); + + combined.setAlwaysPullImage(h.resolve(ContainerTemplate::isAlwaysPullImage, v -> !v)); + combined.setCommand(h.resolve(ContainerTemplate::getCommand, PodTemplateUtils::isNullOrEmpty)); + combined.setArgs(h.resolve(ContainerTemplate::getArgs, PodTemplateUtils::isNullOrEmpty)); + combined.setTtyEnabled(h.resolve(ContainerTemplate::isTtyEnabled, v -> !v)); + combined.setResourceLimitCpu( + h.resolve(ContainerTemplate::getResourceLimitCpu, PodTemplateUtils::isNullOrEmpty)); + combined.setResourceLimitMemory( + h.resolve(ContainerTemplate::getResourceLimitMemory, PodTemplateUtils::isNullOrEmpty)); + combined.setResourceLimitEphemeralStorage( + h.resolve(ContainerTemplate::getResourceLimitEphemeralStorage, PodTemplateUtils::isNullOrEmpty)); + combined.setResourceRequestCpu( + h.resolve(ContainerTemplate::getResourceRequestCpu, PodTemplateUtils::isNullOrEmpty)); + combined.setResourceRequestMemory( + h.resolve(ContainerTemplate::getResourceRequestMemory, PodTemplateUtils::isNullOrEmpty)); + combined.setResourceRequestEphemeralStorage( + h.resolve(ContainerTemplate::getResourceRequestEphemeralStorage, PodTemplateUtils::isNullOrEmpty)); + combined.setShell(h.resolve(ContainerTemplate::getShell, PodTemplateUtils::isNullOrEmpty)); + combined.setWorkingDir(h.resolve(ContainerTemplate::getWorkingDir, PodTemplateUtils::isNullOrEmpty)); + combined.setPrivileged(h.resolve(ContainerTemplate::isPrivileged, v -> !v)); + combined.setRunAsUser(h.resolve(ContainerTemplate::getRunAsUser, Objects::isNull)); + combined.setRunAsGroup(h.resolve(ContainerTemplate::getRunAsGroup, Objects::isNull)); combined.setEnvVars(combineEnvVars(parent, template)); combined.setPorts(new ArrayList<>(ports.values())); - combined.setLivenessProbe(livenessProbe); + combined.setLivenessProbe(h.resolve(ContainerTemplate::getLivenessProbe, Objects::isNull)); return combined; } @@ -164,70 +132,61 @@ public static Container combine(@CheckForNull Container parent, @NonNull Contain if (parent == null) { return template; } + var h = new HierarchyResolver<>(parent, template); - String name = template.getName(); - String image = isNullOrEmpty(template.getImage()) ? parent.getImage() : template.getImage(); - Boolean privileged = template.getSecurityContext() != null - && template.getSecurityContext().getPrivileged() != null - ? template.getSecurityContext().getPrivileged() - : (parent.getSecurityContext() != null - ? parent.getSecurityContext().getPrivileged() - : Boolean.FALSE); - Long runAsUser = template.getSecurityContext() != null - && template.getSecurityContext().getRunAsUser() != null - ? template.getSecurityContext().getRunAsUser() - : (parent.getSecurityContext() != null - ? parent.getSecurityContext().getRunAsUser() - : null); - Long runAsGroup = template.getSecurityContext() != null - && template.getSecurityContext().getRunAsGroup() != null - ? template.getSecurityContext().getRunAsGroup() - : (parent.getSecurityContext() != null - ? parent.getSecurityContext().getRunAsGroup() - : null); - Capabilities capabilities = combineCapabilities(parent, template); - String imagePullPolicy = isNullOrEmpty(template.getImagePullPolicy()) - ? parent.getImagePullPolicy() - : template.getImagePullPolicy(); - String workingDir = isNullOrEmpty(template.getWorkingDir()) - ? (isNullOrEmpty(parent.getWorkingDir()) ? DEFAULT_WORKING_DIR : parent.getWorkingDir()) - : template.getWorkingDir(); - List command = - template.getCommand() == null || template.getCommand().isEmpty() - ? parent.getCommand() - : template.getCommand(); - List args = - template.getArgs() == null || template.getArgs().isEmpty() ? parent.getArgs() : template.getArgs(); - Boolean tty = template.getTty() != null ? template.getTty() : parent.getTty(); - Map requests = combineResources(parent, template, ResourceRequirements::getRequests); - Map limits = combineResources(parent, template, ResourceRequirements::getLimits); - + Boolean privileged; + Long runAsUser; + Long runAsGroup; + if (template.getSecurityContext() != null) { + if (parent.getSecurityContext() != null) { + privileged = h.resolve(c -> c.getSecurityContext().getPrivileged(), Objects::isNull); + runAsUser = h.resolve(c -> c.getSecurityContext().getRunAsUser(), Objects::isNull); + runAsGroup = h.resolve(c -> c.getSecurityContext().getRunAsGroup(), Objects::isNull); + } else { + privileged = template.getSecurityContext().getPrivileged(); + runAsUser = template.getSecurityContext().getRunAsUser(); + runAsGroup = template.getSecurityContext().getRunAsGroup(); + } + } else { + if (parent.getSecurityContext() != null) { + privileged = parent.getSecurityContext().getPrivileged(); + runAsUser = parent.getSecurityContext().getRunAsUser(); + runAsGroup = parent.getSecurityContext().getRunAsGroup(); + } else { + privileged = Boolean.FALSE; + runAsUser = null; + runAsGroup = null; + } + } Map volumeMounts = parent.getVolumeMounts().stream() .collect(Collectors.toMap(VolumeMount::getMountPath, Function.identity())); - template.getVolumeMounts().stream().forEach(vm -> volumeMounts.put(vm.getMountPath(), vm)); + template.getVolumeMounts().forEach(vm -> volumeMounts.put(vm.getMountPath(), vm)); ContainerBuilder containerBuilder = new ContainerBuilder(parent) // - .withImage(image) // - .withName(name) // - .withImagePullPolicy(imagePullPolicy) // - .withCommand(command) // - .withWorkingDir(workingDir) // - .withArgs(args) // - .withTty(tty) // + .withImage(h.resolve(Container::getImage, PodTemplateUtils::isNullOrEmpty)) // + .withName(template.getName()) // + .withImagePullPolicy(h.resolve(Container::getImagePullPolicy, PodTemplateUtils::isNullOrEmpty)) // + .withCommand(h.resolve(Container::getCommand, PodTemplateUtils::isNullOrEmpty)) // + .withWorkingDir(h.resolve(Container::getWorkingDir, PodTemplateUtils::isNullOrEmpty)) // + .withArgs(h.resolve(Container::getArgs, PodTemplateUtils::isNullOrEmpty)) // + .withTty(h.resolve(Container::getTty, Objects::isNull)) // .withNewResources() // - .withRequests(Collections.unmodifiableMap(new HashMap<>(requests))) // - .withLimits(Collections.unmodifiableMap(new HashMap<>(limits))) // + .withRequests(Map.copyOf(combineResources(parent, template, ResourceRequirements::getRequests))) // + .withLimits(Map.copyOf(combineResources(parent, template, ResourceRequirements::getLimits))) // .endResources() // .withEnv(combineEnvVars(parent, template)) // .withEnvFrom(combinedEnvFromSources(parent, template)) - .withVolumeMounts(new ArrayList<>(volumeMounts.values())); - if ((privileged != null && privileged) || runAsUser != null || runAsGroup != null || capabilities != null) { + .withVolumeMounts(List.copyOf(volumeMounts.values())); + if ((privileged != null && privileged) + || runAsUser != null + || runAsGroup != null + || combineCapabilities(parent, template) != null) { containerBuilder = containerBuilder .withNewSecurityContextLike(parent.getSecurityContext()) .withPrivileged(privileged) .withRunAsUser(runAsUser) .withRunAsGroup(runAsGroup) - .withCapabilities(capabilities) + .withCapabilities(combineCapabilities(parent, template)) .endSecurityContext(); } return containerBuilder.build(); @@ -317,26 +276,7 @@ public static Pod combine(Pod parent, Pod template) { Map nodeSelector = mergeMaps(parent.getSpec().getNodeSelector(), template.getSpec().getNodeSelector()); - String serviceAccount = isNullOrEmpty(template.getSpec().getServiceAccount()) - ? parent.getSpec().getServiceAccount() - : template.getSpec().getServiceAccount(); - String serviceAccountName = isNullOrEmpty(template.getSpec().getServiceAccountName()) - ? parent.getSpec().getServiceAccountName() - : template.getSpec().getServiceAccountName(); - String schedulerName = isNullOrEmpty(template.getSpec().getSchedulerName()) - ? parent.getSpec().getSchedulerName() - : template.getSpec().getSchedulerName(); - - Long activeDeadlineSeconds = template.getSpec().getActiveDeadlineSeconds() != null - ? template.getSpec().getActiveDeadlineSeconds() - : parent.getSpec().getActiveDeadlineSeconds(); - Boolean hostNetwork = template.getSpec().getHostNetwork() != null - ? template.getSpec().getHostNetwork() - : parent.getSpec().getHostNetwork(); - - Boolean shareProcessNamespace = template.getSpec().getShareProcessNamespace() != null - ? template.getSpec().getShareProcessNamespace() - : parent.getSpec().getShareProcessNamespace(); + var h = new HierarchyResolver<>(parent.getSpec(), template.getSpec()); Map podAnnotations = mergeMaps( parent.getMetadata().getAnnotations(), template.getMetadata().getAnnotations()); @@ -387,12 +327,12 @@ public static Pod combine(Pod parent, Pod template) { .endMetadata() // .withNewSpecLike(parent.getSpec()) // .withNodeSelector(nodeSelector) // - .withServiceAccount(serviceAccount) // - .withServiceAccountName(serviceAccountName) // - .withSchedulerName(schedulerName) - .withActiveDeadlineSeconds(activeDeadlineSeconds) // - .withHostNetwork(hostNetwork) // - .withShareProcessNamespace(shareProcessNamespace) // + .withServiceAccount(h.resolve(PodSpec::getServiceAccount, PodTemplateUtils::isNullOrEmpty)) // + .withServiceAccountName(h.resolve(PodSpec::getServiceAccountName, PodTemplateUtils::isNullOrEmpty)) // + .withSchedulerName(h.resolve(PodSpec::getSchedulerName, PodTemplateUtils::isNullOrEmpty)) + .withActiveDeadlineSeconds(h.resolve(PodSpec::getActiveDeadlineSeconds, Objects::isNull)) // + .withHostNetwork(h.resolve(PodSpec::getHostNetwork, Objects::isNull)) // + .withShareProcessNamespace(h.resolve(PodSpec::getShareProcessNamespace, Objects::isNull)) // .withContainers(combinedContainers) // .withInitContainers(combinedInitContainers) // .withVolumes(combinedVolumes) // @@ -487,19 +427,10 @@ public static PodTemplate combine(PodTemplate parent, PodTemplate template) { return template; } - LOGGER.log(Level.FINEST, "Combining pod templates, parent: {0}", parent); - LOGGER.log(Level.FINEST, "Combining pod templates, template: {0}", template); + LOGGER.log(Level.FINEST, () -> "Combining pod templates, parent: " + parent + ", template: " + template); String name = template.getName(); String label = template.getLabel(); - String nodeSelector = - isNullOrEmpty(template.getNodeSelector()) ? parent.getNodeSelector() : template.getNodeSelector(); - String serviceAccount = - isNullOrEmpty(template.getServiceAccount()) ? parent.getServiceAccount() : template.getServiceAccount(); - String schedulerName = - isNullOrEmpty(template.getSchedulerName()) ? parent.getSchedulerName() : template.getSchedulerName(); - Node.Mode nodeUsageMode = - template.getNodeUsageMode() == null ? parent.getNodeUsageMode() : template.getNodeUsageMode(); Set podAnnotations = new LinkedHashSet<>(); podAnnotations.addAll(template.getAnnotations()); @@ -514,16 +445,16 @@ public static PodTemplate combine(PodTemplate parent, PodTemplate template) { // Containers Map parentContainers = - parent.getContainers().stream().collect(toMap(c -> c.getName(), c -> c)); + parent.getContainers().stream().collect(toMap(ContainerTemplate::getName, c -> c)); combinedContainers.putAll(parentContainers); combinedContainers.putAll(template.getContainers().stream() - .collect(toMap(c -> c.getName(), c -> combine(parentContainers.get(c.getName()), c)))); + .collect(toMap(ContainerTemplate::getName, c -> combine(parentContainers.get(c.getName()), c)))); // Volumes Map parentVolumes = - parent.getVolumes().stream().collect(toMap(v -> v.getMountPath(), v -> v)); + parent.getVolumes().stream().collect(toMap(PodVolume::getMountPath, v -> v)); combinedVolumes.putAll(parentVolumes); - combinedVolumes.putAll(template.getVolumes().stream().collect(toMap(v -> v.getMountPath(), v -> v))); + combinedVolumes.putAll(template.getVolumes().stream().collect(toMap(PodVolume::getMountPath, v -> v))); WorkspaceVolume workspaceVolume = WorkspaceVolume.merge(parent.getWorkspaceVolume(), template.getWorkspaceVolume()); @@ -533,13 +464,13 @@ public static PodTemplate combine(PodTemplate parent, PodTemplate template) { nodeProperties.addAll(template.getNodeProperties()); PodTemplate podTemplate = new PodTemplate(template.getId()); + var h = new HierarchyResolver<>(parent, template); podTemplate.setName(name); - podTemplate.setNamespace( - !isNullOrEmpty(template.getNamespace()) ? template.getNamespace() : parent.getNamespace()); + podTemplate.setNamespace(h.resolve(PodTemplate::getNamespace, PodTemplateUtils::isNullOrEmpty)); podTemplate.setLabel(label); - podTemplate.setNodeSelector(nodeSelector); - podTemplate.setServiceAccount(serviceAccount); - podTemplate.setSchedulerName(schedulerName); + podTemplate.setNodeSelector(h.resolve(PodTemplate::getNodeSelector, PodTemplateUtils::isNullOrEmpty)); + podTemplate.setServiceAccount(h.resolve(PodTemplate::getServiceAccount, PodTemplateUtils::isNullOrEmpty)); + podTemplate.setSchedulerName(h.resolve(PodTemplate::getSchedulerName, PodTemplateUtils::isNullOrEmpty)); podTemplate.setEnvVars(combineEnvVars(parent, template)); podTemplate.setContainers(new ArrayList<>(combinedContainers.values())); podTemplate.setWorkspaceVolume(workspaceVolume); @@ -547,50 +478,29 @@ public static PodTemplate combine(PodTemplate parent, PodTemplate template) { podTemplate.setImagePullSecrets(new ArrayList<>(imagePullSecrets)); podTemplate.setAnnotations(new ArrayList<>(podAnnotations)); podTemplate.setNodeProperties(nodeProperties); - podTemplate.setNodeUsageMode(nodeUsageMode); - podTemplate.setYamlMergeStrategy( - template.getYamlMergeStrategy() == null && parent.isInheritYamlMergeStrategy() - ? parent.getYamlMergeStrategy() - : template.getYamlMergeStrategy()); + podTemplate.setNodeUsageMode( + template.getNodeUsageMode() == null ? parent.getNodeUsageMode() : template.getNodeUsageMode()); + podTemplate.setYamlMergeStrategy(h.resolve( + PodTemplate::getYamlMergeStrategy, + childValue -> childValue == null && parent.isInheritYamlMergeStrategy())); podTemplate.setInheritYamlMergeStrategy(parent.isInheritYamlMergeStrategy()); - podTemplate.setInheritFrom( - !isNullOrEmpty(template.getInheritFrom()) ? template.getInheritFrom() : parent.getInheritFrom()); - - podTemplate.setInstanceCap( - template.getInstanceCap() != Integer.MAX_VALUE ? template.getInstanceCap() : parent.getInstanceCap()); - - podTemplate.setSlaveConnectTimeout( - template.getSlaveConnectTimeout() != PodTemplate.DEFAULT_SLAVE_JENKINS_CONNECTION_TIMEOUT - ? template.getSlaveConnectTimeout() - : parent.getSlaveConnectTimeout()); - - podTemplate.setIdleMinutes( - template.getIdleMinutes() != 0 ? template.getIdleMinutes() : parent.getIdleMinutes()); - + podTemplate.setInheritFrom(h.resolve(PodTemplate::getInheritFrom, PodTemplateUtils::isNullOrEmpty)); + podTemplate.setInstanceCap(h.resolve(PodTemplate::getInstanceCap, i -> Objects.equals(i, Integer.MAX_VALUE))); + podTemplate.setSlaveConnectTimeout(h.resolve( + PodTemplate::getSlaveConnectTimeout, + i -> Objects.equals(i, PodTemplate.DEFAULT_SLAVE_JENKINS_CONNECTION_TIMEOUT))); + podTemplate.setIdleMinutes(h.resolve(PodTemplate::getIdleMinutes, i -> Objects.equals(i, 0))); podTemplate.setActiveDeadlineSeconds( - template.getActiveDeadlineSeconds() != 0 - ? template.getActiveDeadlineSeconds() - : parent.getActiveDeadlineSeconds()); - - podTemplate.setServiceAccount( - !isNullOrEmpty(template.getServiceAccount()) - ? template.getServiceAccount() - : parent.getServiceAccount()); - - podTemplate.setSchedulerName( - !isNullOrEmpty(template.getSchedulerName()) ? template.getSchedulerName() : parent.getSchedulerName()); - + h.resolve(PodTemplate::getActiveDeadlineSeconds, i -> Objects.equals(i, 0))); + podTemplate.setServiceAccount(h.resolve(PodTemplate::getServiceAccount, PodTemplateUtils::isNullOrEmpty)); + podTemplate.setSchedulerName(h.resolve(PodTemplate::getSchedulerName, PodTemplateUtils::isNullOrEmpty)); podTemplate.setPodRetention(template.getPodRetention()); - podTemplate.setShowRawYaml(template.isShowRawYamlSet() ? template.isShowRawYaml() : parent.isShowRawYaml()); - - podTemplate.setRunAsUser(template.getRunAsUser() != null ? template.getRunAsUser() : parent.getRunAsUser()); - podTemplate.setRunAsGroup(template.getRunAsGroup() != null ? template.getRunAsGroup() : parent.getRunAsGroup()); - - podTemplate.setSupplementalGroups( - template.getSupplementalGroups() != null - ? template.getSupplementalGroups() - : parent.getSupplementalGroups()); - + podTemplate.setShowRawYaml(h.resolve(PodTemplate::isShowRawYaml, v -> v)); + podTemplate.setRunAsUser(h.resolve(PodTemplate::getRunAsUser, Objects::isNull)); + podTemplate.setRunAsGroup(h.resolve(PodTemplate::getRunAsGroup, Objects::isNull)); + podTemplate.setSupplementalGroups(h.resolve(PodTemplate::getSupplementalGroups, Objects::isNull)); + podTemplate.setAgentContainer(h.resolve(PodTemplate::getAgentContainer, PodTemplateUtils::isNullOrEmpty)); + podTemplate.setAgentInjection(h.resolve(PodTemplate::isAgentInjection, v -> !v)); if (template.isHostNetworkSet()) { podTemplate.setHostNetwork(template.isHostNetwork()); } else if (parent.isHostNetworkSet()) { @@ -606,6 +516,33 @@ public static PodTemplate combine(PodTemplate parent, PodTemplate template) { return podTemplate; } + /** + * Helps to resolve structure fields according to hierarchy. + */ + private static class HierarchyResolver

{ + private final P parent; + private final P child; + + HierarchyResolver(P parent, P child) { + this.parent = parent; + this.child = child; + } + + /** + *

Resolves a pod template field according to hierarchy. + *

If the child pod template uses a non-default value, then it is used. + *

Otherwise the parent value is used. + * @param getter the getter function to obtain the field value + * @param isDefaultValue A function to determine if the value is the default value + * @return The value for the field taking into account the hierarchy. + * @param The field type + */ + T resolve(Function getter, Predicate isDefaultValue) { + var childValue = getter.apply(child); + return !isDefaultValue.test(childValue) ? childValue : getter.apply(parent); + } + } + /** * Unwraps the hierarchy of the PodTemplate. * @@ -906,7 +843,11 @@ static Long parseLong(String value) { } public static boolean isNullOrEmpty(@Nullable String string) { - return string == null || string.length() == 0; + return string == null || string.isEmpty(); + } + + public static boolean isNullOrEmpty(@Nullable List list) { + return list == null || list.isEmpty(); } public static @Nullable String emptyToNull(@Nullable String string) { diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent.java index 88f20b480f..15bdb019c5 100644 --- a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent.java +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent.java @@ -101,6 +101,12 @@ public class KubernetesDeclarativeAgent extends RetryableDeclarativeAgent getAsArgs() { Map argMap = new TreeMap<>(); @@ -422,6 +448,12 @@ public Map getAsArgs() { if (!StringUtils.isEmpty(supplementalGroups)) { argMap.put("supplementalGroups", supplementalGroups); } + if (!StringUtils.isEmpty(agentContainer)) { + argMap.put("agentContainer", agentContainer); + } + if (agentInjection) { + argMap.put("agentInjection", agentInjection); + } return argMap; } @@ -443,7 +475,9 @@ public static class DescriptorImpl extends DeclarativeAgentDescriptor getContainers() { return containers; } @@ -436,7 +460,9 @@ public static class DescriptorImpl extends StepDescriptor { "serviceAccount", "nodeSelector", "workingDir", - "workspaceVolume" + "workspaceVolume", + "agentContainer", + "agentInjection" }; public DescriptorImpl() { diff --git a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStepExecution.java b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStepExecution.java index 4e2d957837..f18515b7f9 100755 --- a/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStepExecution.java +++ b/src/main/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStepExecution.java @@ -141,6 +141,8 @@ public boolean start() throws Exception { if (step.isShowRawYamlSet()) { newTemplate.setShowRawYaml(step.isShowRawYaml()); } + newTemplate.setAgentInjection(step.isAgentInjection()); + newTemplate.setAgentContainer(step.getAgentContainer()); newTemplate.setPodRetention(step.getPodRetention()); if (step.getActiveDeadlineSeconds() != 0) { diff --git a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/Dockerfile b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/Dockerfile index cc202acab1..68581f9b00 100644 --- a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/Dockerfile +++ b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/Dockerfile @@ -1 +1 @@ -FROM jenkins/inbound-agent:3261.v9c670a_4748a_9-1 +FROM jenkins/inbound-agent:3261.v9c670a_4748a_9-2 diff --git a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/config.jelly b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/config.jelly index a39c09c730..7547cbea5e 100644 --- a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/config.jelly +++ b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/config.jelly @@ -45,6 +45,14 @@ THE SOFTWARE. + + + + + + + + diff --git a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-agentContainer.html b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-agentContainer.html new file mode 100644 index 0000000000..6ad36d0f35 --- /dev/null +++ b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-agentContainer.html @@ -0,0 +1,5 @@ +

The name of the container running the Jenkins agent in the pod.

+ +

This container will receive environment variables required to connect to the Jenkins controller.

+ +

Defaults to jnlp.

diff --git a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-agentInjection.html b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-agentInjection.html new file mode 100644 index 0000000000..743ca3a9e4 --- /dev/null +++ b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/PodTemplate/help-agentInjection.html @@ -0,0 +1,24 @@ +

This option lets you provide a container image that does not extend the Jenkins agent image.

+ +

The provided container image must have a (headless) JRE installed (using a version compatible with the current Jenkins version).

+ +

The agent will use the Java installation designated by the JENKINS_JAVA_BIN environment variable, or the one in PATH if undefined.

+ +

Enabling this option will override the command and arguments defined in the agent container.

+ +

Example

+ +
+podTemplate(agentContainer:'foo',
+            agentInjection: true,
+            yaml:'''
+spec:
+  containers:
+  - name: foo
+    image: eclipse-temurin:22.0.2_9-jre
+''') {
+  node(POD_LABEL) {
+    sh 'true'
+  }
+}
+
diff --git a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent/config.jelly b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent/config.jelly index 78a4c0047d..b28d8c673f 100644 --- a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent/config.jelly +++ b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgent/config.jelly @@ -12,6 +12,12 @@ + + + + + + diff --git a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStep/config.jelly b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStep/config.jelly index f751412df0..fac72d05f4 100755 --- a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStep/config.jelly +++ b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/PodTemplateStep/config.jelly @@ -26,6 +26,14 @@ + + + + + + + + diff --git a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/samples/maven.groovy b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/samples/maven.groovy index ca4f01a5ee..d2564b6e02 100644 --- a/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/samples/maven.groovy +++ b/src/main/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/samples/maven.groovy @@ -1,7 +1,10 @@ // Build a Maven project using the standard image and Scripted syntax. // Rather than inline YAML, you could use: yaml: readTrusted('jenkins-pod.yaml') // Or, to avoid YAML: containers: [containerTemplate(name: 'maven', image: 'maven:3.6.3-jdk-8', command: 'sleep', args: 'infinity')] -podTemplate(yaml: ''' +podTemplate( + agentContainer: 'maven', + agentInjection: true, + yaml: ''' apiVersion: v1 kind: Pod spec: @@ -56,10 +59,8 @@ public class SomeTest { public void checks() {} } ''' - container('maven') { - // Maven needs write access to $HOME/.m2, which it doesn't have in the maven image because only root is a real user. - sh 'HOME=$WORKSPACE_TMP/maven mvn -B -ntp -Dmaven.test.failure.ignore verify' - } + // Maven needs write access to $HOME/.m2, which it doesn't have in the maven image because only root is a real user. + sh 'HOME=$WORKSPACE_TMP/maven mvn -B -ntp -Dmaven.test.failure.ignore verify' junit '**/target/surefire-reports/TEST-*.xml' archiveArtifacts '**/target/*.jar' } diff --git a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilderTest.java b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilderTest.java index f8bf08edc5..f324113122 100644 --- a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilderTest.java +++ b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilderTest.java @@ -202,7 +202,7 @@ public void testValidateDockerRegistryUIOverride() throws Exception { assertEquals("busybox", containers.get("busybox").getImage()); assertEquals( - jnlpregistry + "/" + DEFAULT_JNLP_IMAGE, containers.get("jnlp").getImage()); + jnlpregistry + "/" + DEFAULT_AGENT_IMAGE, containers.get("jnlp").getImage()); assertThat(pod.getMetadata().getLabels(), hasEntry("jenkins", "slave")); } @@ -220,7 +220,7 @@ public void testValidateDockerRegistryUIOverrideWithSlashSuffix() throws Excepti assertEquals(2, containers.size()); assertEquals("busybox", containers.get("busybox").getImage()); - assertEquals(jnlpregistry + DEFAULT_JNLP_IMAGE, containers.get("jnlp").getImage()); + assertEquals(jnlpregistry + DEFAULT_AGENT_IMAGE, containers.get("jnlp").getImage()); assertThat(pod.getMetadata().getLabels(), hasEntry("jenkins", "slave")); } @@ -240,7 +240,7 @@ public void testValidateDockerRegistryPrefixOverride(boolean directConnection) t assertEquals("busybox", containers.get("busybox").getImage()); assertEquals( - DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX + "/" + DEFAULT_JNLP_IMAGE, + DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX + "/" + DEFAULT_AGENT_IMAGE, containers.get("jnlp").getImage()); assertThat(pod.getMetadata().getLabels(), hasEntry("jenkins", "slave")); } @@ -261,7 +261,7 @@ public void testValidateDockerRegistryPrefixOverrideWithSlashSuffix(boolean dire assertEquals("busybox", containers.get("busybox").getImage()); assertEquals( - DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX + DEFAULT_JNLP_IMAGE, + DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX + DEFAULT_AGENT_IMAGE, containers.get("jnlp").getImage()); assertThat(pod.getMetadata().getLabels(), hasEntry("jenkins", "slave")); } @@ -387,7 +387,7 @@ private void validatePod(Pod pod, boolean fromYaml, boolean directConnection) { assertEquals(2, containers.size()); assertEquals("busybox", containers.get("busybox").getImage()); - assertEquals(DEFAULT_JNLP_IMAGE, containers.get("jnlp").getImage()); + assertEquals(DEFAULT_AGENT_IMAGE, containers.get("jnlp").getImage()); // check volumes and volume mounts Map volumes = diff --git a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtilsTest.java b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtilsTest.java index 9f9faca8f6..1a0765aece 100644 --- a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtilsTest.java +++ b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateUtilsTest.java @@ -254,6 +254,9 @@ public void shouldUnwrapParent() { parent.setNodeSelector("key:value"); parent.setImagePullSecrets(asList(SECRET_1)); parent.setYaml("Yaml"); + parent.setAgentContainer("agentContainer"); + parent.setAgentInjection(true); + parent.setShowRawYaml(false); PodTemplate template1 = new PodTemplate(); template1.setName("template1"); @@ -268,6 +271,9 @@ public void shouldUnwrapParent() { assertEquals("key:value", result.getNodeSelector()); assertThat(result.getYamls(), hasSize(2)); assertThat(result.getYamls(), contains("Yaml", "Yaml2")); + assertThat(result.getAgentContainer(), is("agentContainer")); + assertThat(result.isAgentInjection(), is(true)); + assertThat(result.isShowRawYaml(), is(false)); } @Test diff --git a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentUnitTest.java b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentUnitTest.java index dbca8132cb..13926cbec0 100644 --- a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentUnitTest.java +++ b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesDeclarativeAgentUnitTest.java @@ -51,6 +51,8 @@ public void serialization() throws Exception { instance.setWorkspaceVolume(workspaceVolume); instance.setIdleMinutes(1); instance.setInheritFrom("inheritFrom"); + instance.setAgentContainer("agentContainer"); + instance.setAgentInjection(true); Map args = instance.getAsArgs(); assertThat(args.get("cloud"), equalTo("cloud")); @@ -60,6 +62,8 @@ public void serialization() throws Exception { assertThat(args.get("workspaceVolume"), equalTo(workspaceVolume)); assertThat(args.get("idleMinutes"), equalTo(1)); assertThat(args.get("inheritFrom"), equalTo("inheritFrom")); + assertThat(args.get("agentContainer"), equalTo("agentContainer")); + assertThat(args.get("agentInjection"), equalTo(true)); } @Test diff --git a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesPipelineTest.java b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesPipelineTest.java index 6ac7f19ae9..b5b23ac122 100644 --- a/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesPipelineTest.java +++ b/src/test/java/org/csanchez/jenkins/plugins/kubernetes/pipeline/KubernetesPipelineTest.java @@ -951,4 +951,15 @@ public Pod decorate(@NotNull KubernetesCloud kubernetesCloud, @NotNull Pod pod) throw new PodDecoratorException("I always fail"); } } + + @Test + public void imageWithoutAgent() throws Exception { + r.assertBuildStatus(Result.SUCCESS, r.waitForCompletion(b)); + } + + @Test + public void imageWithoutAgentNoJava() throws Exception { + r.assertBuildStatus(Result.ABORTED, r.waitForCompletion(b)); + r.assertLogContains("java: not found", b); + } } diff --git a/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/imageWithoutAgent.groovy b/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/imageWithoutAgent.groovy new file mode 100644 index 0000000000..5a5905f648 --- /dev/null +++ b/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/imageWithoutAgent.groovy @@ -0,0 +1,12 @@ +podTemplate(agentContainer:'foo', + agentInjection: true, + yaml:''' +spec: + containers: + - name: foo + image: eclipse-temurin:22.0.2_9-jre +''') { + node(POD_LABEL) { + sh 'true' + } +} diff --git a/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/imageWithoutAgentNoJava.groovy b/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/imageWithoutAgentNoJava.groovy new file mode 100644 index 0000000000..9eebc5b8fa --- /dev/null +++ b/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/imageWithoutAgentNoJava.groovy @@ -0,0 +1,12 @@ +podTemplate(agentContainer:'foo', + agentInjection: true, + yaml:''' +spec: + containers: + - name: foo + image: busybox +''') { + node(POD_LABEL) { + sh 'true' + } +}