Skip to content

Commit 7345138

Browse files
authored
Make jenkins agent more flexible (#1602)
1 parent cc37496 commit 7345138

File tree

22 files changed

+458
-277
lines changed

22 files changed

+458
-277
lines changed

README.md

+16-10
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ spec:
185185
Multiple containers can be defined for the agent pod, with shared resources, like mounts. Ports in each container can
186186
be accessed as in any Kubernetes pod, by using `localhost`.
187187

188-
The `container` step allows executing commands into each container.
188+
One container must run the Jenkins agent. If unspecified, a container named `jnlp` will be created with the inbound-agent image.
189+
The Jenkins agent requires a JRE to run, so you can avoid the extra container by providing a name using the `agentContainer`.
190+
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.
191+
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.
192+
193+
To execute commands in another container part of the pod (different from the one running the Jenkins agent), you can use the `container` step.
189194

190195
**Note**
191196
---
@@ -194,8 +199,11 @@ It is recommended to use the same uid across the different containers part of th
194199
---
195200

196201
```groovy
197-
podTemplate(containers: [
198-
containerTemplate(name: 'maven', image: 'maven:3.8.1-jdk-8', command: 'sleep', args: '99d'),
202+
podTemplate(
203+
agentContainer: 'maven',
204+
agentInjection: true,
205+
containers: [
206+
containerTemplate(name: 'maven', image: 'maven:3.9.9-eclipse-temurin-17'),
199207
containerTemplate(name: 'golang', image: 'golang:1.16.5', command: 'sleep', args: '99d')
200208
]) {
201209
@@ -229,17 +237,16 @@ podTemplate(containers: [
229237
or
230238
231239
```groovy
232-
podTemplate(yaml: '''
240+
podTemplate(
241+
agentContainer: 'maven',
242+
agentInjection: true,
243+
yaml: '''
233244
apiVersion: v1
234245
kind: Pod
235246
spec:
236247
containers:
237248
- name: maven
238-
image: maven:3.8.1-jdk-8
239-
command:
240-
- sleep
241-
args:
242-
- 99d
249+
image: maven:3.9.9-eclipse-temurin-17
243250
- name: golang
244251
image: golang:1.16.5
245252
command:
@@ -269,7 +276,6 @@ podTemplate(yaml: '''
269276
}
270277
}
271278
}
272-
273279
}
274280
}
275281
```

examples/maven-with-cache.groovy

+4-8
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@
77
* two concurrent jobs with this pipeline. Or change readOnly: true after the first run
88
*/
99

10-
podTemplate(containers: [
11-
containerTemplate(name: 'maven', image: 'maven:3.8.1-jdk-8', command: 'sleep', args: '99d')
12-
], volumes: [
13-
persistentVolumeClaim(mountPath: '/root/.m2/repository', claimName: 'maven-repo', readOnly: false)
14-
]) {
10+
podTemplate(agentContainer: 'maven', agentInjection: true, containers: [
11+
containerTemplate(name: 'maven', image: 'maven:3.9.9-eclipse-temurin-17')
12+
], volumes: [genericEphemeralVolume(accessModes: 'ReadWriteOnce', mountPath: '/root/.m2/repository', requestsSize: '1Gi')]) {
1513

1614
node(POD_LABEL) {
1715
stage('Build a Maven project') {
1816
git 'https://github.com/jenkinsci/kubernetes-plugin.git'
19-
container('maven') {
20-
sh 'mvn -B -ntp clean package -DskipTests'
21-
}
17+
sh 'mvn -B -ntp clean package -DskipTests'
2218
}
2319
}
2420
}

examples/multi-container.groovy

+16-21
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,27 @@
22
* This pipeline describes a multi container job, running Maven and Golang builds
33
*/
44

5-
podTemplate(yaml: '''
6-
apiVersion: v1
7-
kind: Pod
8-
spec:
9-
containers:
10-
- name: maven
11-
image: maven:3.8.1-jdk-8
12-
command:
13-
- sleep
14-
args:
15-
- 99d
16-
- name: golang
17-
image: golang:1.16.5
18-
command:
19-
- sleep
20-
args:
21-
- 99d
5+
podTemplate(agentContainer: 'maven',
6+
agentInjection: true,
7+
yaml: '''
8+
apiVersion: v1
9+
kind: Pod
10+
spec:
11+
containers:
12+
- name: maven
13+
image: maven:3.9.9-eclipse-temurin-17
14+
- name: golang
15+
image: golang:1.23.1-bookworm
16+
command:
17+
- sleep
18+
args:
19+
- 99d
2220
'''
2321
) {
24-
2522
node(POD_LABEL) {
2623
stage('Build a Maven project') {
2724
git 'https://github.com/jenkinsci/kubernetes-plugin.git'
28-
container('maven') {
29-
sh 'mvn -B -ntp clean package -DskipTests'
30-
}
25+
sh 'mvn -B -ntp clean package -DskipTests'
3126
}
3227
stage('Build a Golang project') {
3328
git url: 'https://github.com/hashicorp/terraform.git', branch: 'main'

src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplate.java

+25
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ protected static MessageDigest getLabelDigestFunction() {
188188

189189
private Long terminationGracePeriodSeconds;
190190

191+
private String agentContainer;
192+
193+
private boolean agentInjection;
194+
191195
/**
192196
* Persisted yaml fragment
193197
*/
@@ -638,6 +642,25 @@ public boolean isCapOnlyOnAlivePods() {
638642
return capOnlyOnAlivePods;
639643
}
640644

645+
@CheckForNull
646+
public String getAgentContainer() {
647+
return agentContainer;
648+
}
649+
650+
@DataBoundSetter
651+
public void setAgentContainer(@CheckForNull String agentContainer) {
652+
this.agentContainer = Util.fixEmpty(agentContainer);
653+
}
654+
655+
public boolean isAgentInjection() {
656+
return agentInjection;
657+
}
658+
659+
@DataBoundSetter
660+
public void setAgentInjection(boolean agentInjection) {
661+
this.agentInjection = agentInjection;
662+
}
663+
641664
public List<TemplateEnvVar> getEnvVars() {
642665
if (envVars == null) {
643666
return Collections.emptyList();
@@ -1173,6 +1196,8 @@ public String toString() {
11731196
+ (nodeProperties == null || nodeProperties.isEmpty() ? "" : ", nodeProperties=" + nodeProperties)
11741197
+ (yamls == null || yamls.isEmpty() ? "" : ", yamls=" + yamls)
11751198
+ (!unwrapped ? "" : ", unwrapped=" + unwrapped)
1199+
+ (agentContainer == null ? "" : ", agentContainer='" + agentContainer + '\'')
1200+
+ (!agentInjection ? "" : ", agentInjection=" + agentInjection)
11761201
+ '}';
11771202
}
11781203
}

src/main/java/org/csanchez/jenkins/plugins/kubernetes/PodTemplateBuilder.java

+86-27
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package org.csanchez.jenkins.plugins.kubernetes;
2626

27+
import static org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate.DEFAULT_WORKING_DIR;
2728
import static org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud.JNLP_NAME;
2829
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.combine;
2930
import static org.csanchez.jenkins.plugins.kubernetes.PodTemplateUtils.isNullOrEmpty;
@@ -41,6 +42,7 @@
4142
import io.fabric8.kubernetes.api.model.ContainerBuilder;
4243
import io.fabric8.kubernetes.api.model.ContainerPort;
4344
import io.fabric8.kubernetes.api.model.EnvVar;
45+
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
4446
import io.fabric8.kubernetes.api.model.ExecAction;
4547
import io.fabric8.kubernetes.api.model.LocalObjectReference;
4648
import io.fabric8.kubernetes.api.model.Pod;
@@ -51,6 +53,7 @@
5153
import io.fabric8.kubernetes.api.model.ResourceRequirements;
5254
import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder;
5355
import io.fabric8.kubernetes.api.model.Volume;
56+
import io.fabric8.kubernetes.api.model.VolumeBuilder;
5457
import io.fabric8.kubernetes.api.model.VolumeMount;
5558
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
5659
import io.fabric8.kubernetes.client.utils.Serialization;
@@ -101,6 +104,9 @@ public class PodTemplateBuilder {
101104
public static final String LABEL_KUBERNETES_CONTROLLER = "kubernetes.jenkins.io/controller";
102105
static final String NO_RECONNECT_AFTER_TIMEOUT =
103106
SystemProperties.getString(PodTemplateBuilder.class.getName() + ".noReconnectAfter", "1d");
107+
private static final String JENKINS_AGENT_FILE_ENVVAR = "JENKINS_AGENT_FILE";
108+
private static final String JENKINS_AGENT_AGENT_JAR = "/jenkins-agent/agent.jar";
109+
private static final String JENKINS_AGENT_LAUNCHER_SCRIPT_LOCATION = "/jenkins-agent/jenkins-agent";
104110

105111
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "tests")
106112
@Restricted(NoExternalUse.class)
@@ -124,7 +130,7 @@ public class PodTemplateBuilder {
124130
}
125131

126132
@Restricted(NoExternalUse.class)
127-
static final String DEFAULT_JNLP_IMAGE =
133+
static final String DEFAULT_AGENT_IMAGE =
128134
System.getProperty(PodTemplateStepExecution.class.getName() + ".defaultImage", defaultImageName);
129135

130136
static final String DEFAULT_JNLP_CONTAINER_MEMORY_REQUEST = System.getProperty(
@@ -309,39 +315,92 @@ public Pod build() {
309315
}
310316
}
311317

312-
// default jnlp container
313-
Optional<Container> jnlpOpt = pod.getSpec().getContainers().stream()
314-
.filter(c -> JNLP_NAME.equals(c.getName()))
318+
// default agent container
319+
String agentContainerName = StringUtils.defaultString(template.getAgentContainer(), JNLP_NAME);
320+
Optional<Container> agentOpt = pod.getSpec().getContainers().stream()
321+
.filter(c -> agentContainerName.equals(c.getName()))
315322
.findFirst();
316-
Container jnlp = jnlpOpt.orElse(new ContainerBuilder()
317-
.withName(JNLP_NAME)
318-
.withVolumeMounts(volumeMounts
319-
.values()
320-
.toArray(new VolumeMount[volumeMounts.values().size()]))
323+
Container agentContainer = agentOpt.orElse(new ContainerBuilder()
324+
.withName(agentContainerName)
325+
.withVolumeMounts(volumeMounts.values().toArray(VolumeMount[]::new))
321326
.build());
322-
if (!jnlpOpt.isPresent()) {
323-
pod.getSpec().getContainers().add(jnlp);
327+
if (agentOpt.isEmpty()) {
328+
pod.getSpec().getContainers().add(agentContainer);
324329
}
330+
var workingDir = agentContainer.getWorkingDir();
325331
pod.getSpec().getContainers().stream()
326332
.filter(c -> c.getWorkingDir() == null)
327-
.forEach(c -> c.setWorkingDir(jnlp.getWorkingDir()));
328-
if (StringUtils.isBlank(jnlp.getImage())) {
329-
String jnlpImage = DEFAULT_JNLP_IMAGE;
333+
.forEach(c -> c.setWorkingDir(workingDir));
334+
if (StringUtils.isBlank(agentContainer.getImage())) {
335+
String agentImage = DEFAULT_AGENT_IMAGE;
330336
if (cloud != null && StringUtils.isNotEmpty(cloud.getJnlpregistry())) {
331-
jnlpImage = Util.ensureEndsWith(cloud.getJnlpregistry(), "/") + jnlpImage;
337+
agentImage = Util.ensureEndsWith(cloud.getJnlpregistry(), "/") + agentImage;
332338
} else if (StringUtils.isNotEmpty(DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX)) {
333-
jnlpImage = Util.ensureEndsWith(DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX, "/") + jnlpImage;
339+
agentImage = Util.ensureEndsWith(DEFAULT_JNLP_DOCKER_REGISTRY_PREFIX, "/") + agentImage;
334340
}
335-
jnlp.setImage(jnlpImage);
341+
agentContainer.setImage(agentImage);
336342
}
337343
Map<String, EnvVar> envVars = new HashMap<>();
338-
envVars.putAll(jnlpEnvVars(jnlp.getWorkingDir()));
344+
envVars.putAll(agentEnvVars(workingDir));
339345
envVars.putAll(defaultEnvVars(template.getEnvVars()));
340-
Optional.ofNullable(jnlp.getEnv()).ifPresent(jnlpEnv -> {
341-
jnlpEnv.forEach(var -> envVars.put(var.getName(), var));
346+
Optional.ofNullable(agentContainer.getEnv()).ifPresent(agentEnv -> {
347+
agentEnv.forEach(var -> envVars.put(var.getName(), var));
342348
});
343-
jnlp.setEnv(new ArrayList<>(envVars.values()));
344-
if (jnlp.getResources() == null) {
349+
if (template.isAgentInjection()) {
350+
var agentVolumeMountBuilder =
351+
new VolumeMountBuilder().withName("jenkins-agent").withMountPath("/jenkins-agent");
352+
var oldInitContainers = pod.getSpec().getInitContainers();
353+
var jenkinsAgentInitContainer = new ContainerBuilder()
354+
.withName("set-up-jenkins-agent")
355+
.withImage(DEFAULT_AGENT_IMAGE)
356+
.withCommand(
357+
"/bin/sh",
358+
"-c",
359+
"cp $(command -v jenkins-agent) " + JENKINS_AGENT_LAUNCHER_SCRIPT_LOCATION + ";"
360+
+ "cp /usr/share/jenkins/agent.jar " + JENKINS_AGENT_AGENT_JAR)
361+
.withVolumeMounts(agentVolumeMountBuilder.build())
362+
.build();
363+
if (oldInitContainers != null) {
364+
var newInitContainers = new ArrayList<>(oldInitContainers);
365+
newInitContainers.add(jenkinsAgentInitContainer);
366+
pod.getSpec().setInitContainers(newInitContainers);
367+
} else {
368+
pod.getSpec().setInitContainers(List.of(jenkinsAgentInitContainer));
369+
}
370+
var oldVolumes = pod.getSpec().getVolumes();
371+
var jenkinsAgentSharedVolume = new VolumeBuilder()
372+
.withName("jenkins-agent")
373+
.withNewEmptyDir()
374+
.and()
375+
.build();
376+
if (oldVolumes != null) {
377+
var newVolumes = new ArrayList<>(oldVolumes);
378+
newVolumes.add(jenkinsAgentSharedVolume);
379+
pod.getSpec().setVolumes(newVolumes);
380+
} else {
381+
pod.getSpec().setVolumes(List.of(jenkinsAgentSharedVolume));
382+
}
383+
var existingVolumeMounts = agentContainer.getVolumeMounts();
384+
if (existingVolumeMounts != null) {
385+
var newVolumeMounts = new ArrayList<>(existingVolumeMounts);
386+
newVolumeMounts.add(agentVolumeMountBuilder.withReadOnly().build());
387+
agentContainer.setVolumeMounts(newVolumeMounts);
388+
} else {
389+
agentContainer.setVolumeMounts(
390+
List.of(agentVolumeMountBuilder.withReadOnly().build()));
391+
}
392+
agentContainer.setWorkingDir(DEFAULT_WORKING_DIR);
393+
agentContainer.setCommand(List.of(JENKINS_AGENT_LAUNCHER_SCRIPT_LOCATION));
394+
agentContainer.setArgs(List.of());
395+
envVars.put(
396+
JENKINS_AGENT_FILE_ENVVAR,
397+
new EnvVarBuilder()
398+
.withName(JENKINS_AGENT_FILE_ENVVAR)
399+
.withValue(JENKINS_AGENT_AGENT_JAR)
400+
.build());
401+
}
402+
agentContainer.setEnv(new ArrayList<>(envVars.values()));
403+
if (agentContainer.getResources() == null) {
345404

346405
Map<String, Quantity> reqMap = new HashMap<>();
347406
Map<String, Quantity> limMap = new HashMap<>();
@@ -361,7 +420,7 @@ public Pod build() {
361420
.withLimits(limMap)
362421
.build();
363422

364-
jnlp.setResources(reqs);
423+
agentContainer.setResources(reqs);
365424
}
366425
if (cloud != null) {
367426
pod = PodDecorator.decorateAll(cloud, pod);
@@ -406,9 +465,9 @@ private Map<String, EnvVar> defaultEnvVars(Collection<TemplateEnvVar> globalEnvV
406465
return envVarsMap;
407466
}
408467

409-
private Map<String, EnvVar> jnlpEnvVars(String workingDir) {
468+
private Map<String, EnvVar> agentEnvVars(String workingDir) {
410469
if (workingDir == null) {
411-
workingDir = ContainerTemplate.DEFAULT_WORKING_DIR;
470+
workingDir = DEFAULT_WORKING_DIR;
412471
}
413472
// Last-write wins map of environment variable names to values
414473
HashMap<String, String> env = new HashMap<>();
@@ -462,7 +521,7 @@ private Container createContainer(
462521
Map<String, EnvVar> envVarsMap = new HashMap<>();
463522
String workingDir = substituteEnv(containerTemplate.getWorkingDir());
464523
if (JNLP_NAME.equals(containerTemplate.getName())) {
465-
envVarsMap.putAll(jnlpEnvVars(workingDir));
524+
envVarsMap.putAll(agentEnvVars(workingDir));
466525
}
467526
envVarsMap.putAll(defaultEnvVars(globalEnvVars));
468527

@@ -541,7 +600,7 @@ private Container createContainer(
541600
private VolumeMount getDefaultVolumeMount(@CheckForNull String workingDir) {
542601
String wd = workingDir;
543602
if (wd == null) {
544-
wd = ContainerTemplate.DEFAULT_WORKING_DIR;
603+
wd = DEFAULT_WORKING_DIR;
545604
LOGGER.log(Level.FINE, "Container workingDir is null, defaulting to {0}", wd);
546605
}
547606
return new VolumeMountBuilder()

0 commit comments

Comments
 (0)