diff --git a/README.md b/README.md
index 5de7cef4..73464144 100644
--- a/README.md
+++ b/README.md
@@ -25,3 +25,63 @@ Note that, in each case, the certificates:
* Must be files reachable by the Jenkins controller.
* Must be in the [PKCS12 format](https://en.wikipedia.org/wiki/PKCS_12).
+
+## Pipeline support
+For pipelines, nodes can be started by using the label which is assigned to a predefined job template.
+```
+node ('my-label') {
+}
+```
+Or the template can be provided directly (either in JSON or HCL).
+```
+nomad (jobTemplate:'''
+job "%WORKER_NAME%" {
+ region = "global"
+ type = "batch"
+ datacenters = ["dc1"]
+ group "jenkins-worker-taskgroup" {
+ count = 1
+ restart {
+ attempts = 0
+ mode = "fail"
+ }
+ task "jenkins-worker" {
+ driver = "docker"
+ config {
+ image = "jenkins/inbound-agent"
+ force_pull = true
+ }
+ env {
+ JENKINS_URL = "https://jenkins.service.consul"
+ JENKINS_AGENT_NAME = "%WORKER_NAME%"
+ JENKINS_SECRET = "%WORKER_SECRET%"
+ JENKINS_WEB_SOCKET = "true"
+ }
+ resources {
+ cpu = 500
+ memory = 256
+ }
+ }
+ }
+}
+''') {
+ node (JOB_LABEL) {
+ sh 'hostname'
+ }
+}
+```
+Note: The `JOB_LABEL` environment variable is defined by the `nomad` pipeline step with the corresponding label of the `dynamic template`.
+
+Via `readFile/readTrusted`, the `jobTemplate` can also be referenced from the file system or from the repository.
+```
+nomad (jobTemplate: readTrusted('src/test/resources/nomad-templates/build.yaml')) {
+ node (JOB_LABEL) {
+ sh 'hostname'
+ }
+}
+```
+
+Optional parameters:
+* `cloud` - Name of the Nomad cloud configuration you want to use (Default: `null` which means the first Nomad cloud is selected automatically)
+* `remoteFS` - Absolute path to the working directory of the remote agent (Default: `null` which means the environment variable `hudson.model.slave.workspaceDir` or `JENKINS_HOME` is used instead)
+* `prefix` - Node name is prefixed with this value (Default: `jenkins-dt`)
diff --git a/pom.xml b/pom.xml
index 90119956..07eaacfb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,18 @@
+
+
+
+ io.jenkins.tools.bom
+ bom-2.289.x
+ 987.v4ade2e49fe70
+ import
+ pom
+
+
+
+
com.squareup.okhttp3
@@ -96,12 +108,14 @@
org.jenkins-ci.plugins
credentials
- 2.6.1
org.jenkins-ci.plugins
plain-credentials
- 1.7
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-step-api
@@ -131,5 +145,20 @@
+
+ org.jenkins-ci.plugins.workflow
+ workflow-job
+ test
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-cps
+ test
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-basic-steps
+ test
+
diff --git a/src/main/java/org/jenkinsci/plugins/nomad/NomadCloud.java b/src/main/java/org/jenkinsci/plugins/nomad/NomadCloud.java
index 4ebf565a..df1c50e7 100644
--- a/src/main/java/org/jenkinsci/plugins/nomad/NomadCloud.java
+++ b/src/main/java/org/jenkinsci/plugins/nomad/NomadCloud.java
@@ -20,6 +20,7 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Stream;
import org.jenkinsci.plugins.nomad.Api.JobInfo;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
@@ -65,6 +66,7 @@ public class NomadCloud extends AbstractCloudImpl {
// non persistent fields
private transient NomadApi nomad;
private transient int pending = 0;
+ private transient List dynamicTemplates;
// legacy fields (we have to keep them for backward compatibility)
private transient String jenkinsUrl;
@@ -96,6 +98,7 @@ public NomadCloud(
this.serverPassword = serverPassword;
this.prune = prune;
this.templates = Optional.ofNullable(templates).orElse(new ArrayList<>());
+ this.dynamicTemplates = new ArrayList<>();
readResolve();
}
@@ -172,15 +175,10 @@ private void pruneOrphanedWorkers(NomadWorkerTemplate template) {
// Find the correct template for job
public NomadWorkerTemplate getTemplate(Label label) {
- for (NomadWorkerTemplate t : templates) {
- if (label == null && !t.getLabels().isEmpty()) {
- continue;
- }
- if ((label == null && t.getLabels().isEmpty()) || (label != null && label.matches(Label.parse(t.getLabels())))) {
- return t;
- }
- }
- return null;
+ return Stream.concat(templates.stream(), dynamicTemplates.stream())
+ .filter(t -> label == null && t.getLabels().isEmpty() || (label != null && label.matches(Label.parse(t.getLabels()))))
+ .findFirst()
+ .orElse(null);
}
@Override
@@ -249,6 +247,22 @@ public Secret getServerPassword() {
return serverPassword;
}
+ /**
+ * Adds a so called 'dynamic jobTemplate' to this cloud. It is pretty much the same as the jobTemplate defined via the cloud
+ * management, but it is not editable and not visible. It can only be accessed via the assigned label.
+ */
+ public void addDynamicTemplate(NomadWorkerTemplate template) {
+ dynamicTemplates.add(template);
+ }
+
+ /**
+ * Removes an existing so called 'dynamic jobTemplate'.
+ * @param label the label assigned to the 'dynamic jobTemplate' you want to remove
+ */
+ public void removeDynamicTemplate(String label) {
+ dynamicTemplates.removeIf(t -> t.getLabels().equals(label));
+ }
+
@Extension
public static final class DescriptorImpl extends Descriptor {
diff --git a/src/main/java/org/jenkinsci/plugins/nomad/pipeline/NomadPipelineStep.java b/src/main/java/org/jenkinsci/plugins/nomad/pipeline/NomadPipelineStep.java
new file mode 100644
index 00000000..abd13db3
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/nomad/pipeline/NomadPipelineStep.java
@@ -0,0 +1,100 @@
+package org.jenkinsci.plugins.nomad.pipeline;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Set;
+
+import org.jenkinsci.plugins.workflow.steps.Step;
+import org.jenkinsci.plugins.workflow.steps.StepContext;
+import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
+import org.jenkinsci.plugins.workflow.steps.StepExecution;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.DataBoundSetter;
+
+import hudson.Extension;
+import hudson.model.TaskListener;
+
+/**
+ * Provides the 'nomad' method for scripted pipelines.
+ * Parameters:
+ *
+ * - jobTemplate - Nomad Job Template (required, either in JSON or HCL)
+ * - cloud - Name of the Nomad cloud configuration you want to use (Default: null, which means the first cloud is used)
+ * - prefix - Node name is prefixed with this value (Default: jenkins-dt)
+ * - remoteFS - Absolute path to the working directory of the remote agent (Default: null, which means hudson.model.slave.workspaceDir
+ * or JENKINS_HOME is used)
+ *
+ */
+public class NomadPipelineStep extends Step implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String jobTemplate;
+ private String cloud;
+ private String prefix;
+ private String remoteFS;
+
+ @DataBoundConstructor
+ public NomadPipelineStep(String jobTemplate) {
+ this.jobTemplate = jobTemplate;
+ this.prefix = "jenkins-dt";
+ }
+
+ public String getJobTemplate() {
+ return jobTemplate;
+ }
+
+ public String getCloud() {
+ return cloud;
+ }
+
+ public String getRemoteFS() {
+ return remoteFS;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ // === optional parameters start ===
+
+ @DataBoundSetter
+ public void setCloud(String cloud) {
+ this.cloud = cloud;
+ }
+
+ @DataBoundSetter
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ @DataBoundSetter
+ public void setRemoteFS(String remoteFS) {
+ this.remoteFS = remoteFS;
+ }
+
+ // === optional parameters end ===
+
+ @Override
+ public StepExecution start(StepContext stepContext) {
+ return new NomadStepExecution(stepContext, this);
+ }
+
+ @Extension
+ public static final class DescriptorImpl extends StepDescriptor {
+ @Override
+ public String getFunctionName() {
+ return "nomad";
+ }
+
+ @Override
+ public Set extends Class>> getRequiredContext() {
+ return Collections.singleton(TaskListener.class);
+ }
+
+ @Override
+ public boolean takesImplicitBlockArgument() {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/nomad/pipeline/NomadStepExecution.java b/src/main/java/org/jenkinsci/plugins/nomad/pipeline/NomadStepExecution.java
new file mode 100644
index 00000000..2caf822a
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/nomad/pipeline/NomadStepExecution.java
@@ -0,0 +1,88 @@
+package org.jenkinsci.plugins.nomad.pipeline;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.jenkinsci.plugins.nomad.NomadCloud;
+import org.jenkinsci.plugins.nomad.NomadWorkerTemplate;
+import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
+import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander;
+import org.jenkinsci.plugins.workflow.steps.StepContext;
+import org.jenkinsci.plugins.workflow.steps.StepExecution;
+
+import hudson.AbortException;
+import jenkins.model.Jenkins;
+
+/**
+ * Executes a given 'nomad' step for scripted pipelines. How it works:
+ *
+ * - Resolve nomad cloud by either the specified name or use the first Nomad cloud
+ * - Add the given job template with the cloud and assign a unique label to that template
+ * - Define the environment variable JOB_LABEL with the corresponding label
+ * - Execute the nomad step
+ * - Remove the job template from the cloud
+ *
+ * Note: The job templates added by this step are not editable, and they are also not visible via cloud management.
+ */
+public class NomadStepExecution extends StepExecution {
+
+ private final NomadPipelineStep step;
+
+ public NomadStepExecution(StepContext stepContext, NomadPipelineStep step) {
+ super(stepContext);
+ this.step = step;
+ }
+
+ @Override
+ public boolean start() throws IOException, InterruptedException {
+ NomadCloud cloud = resolveCloud();
+ NomadWorkerTemplate template = new NomadWorkerTemplate(
+ step.getPrefix(),
+ UUID.randomUUID().toString(),
+ 0,
+ false,
+ 1,
+ step.getRemoteFS(),
+ step.getJobTemplate()
+ );
+
+ cloud.addDynamicTemplate(template);
+
+ getContext().newBodyInvoker()
+ .withCallback(new RemoveTemplateCallBack(template.getLabels()))
+ .withContext(EnvironmentExpander.merge(
+ getContext().get(EnvironmentExpander.class),
+ EnvironmentExpander.constant(Collections.singletonMap("JOB_LABEL", template.getLabels()))
+ ))
+ .start();
+
+ return false;
+ }
+
+ private NomadCloud resolveCloud() throws AbortException {
+ if (step.getCloud() == null) {
+ return Optional.ofNullable(Jenkins.get().clouds.get(NomadCloud.class))
+ .orElseThrow(() -> new AbortException("No Nomad cloud was found. Please configure at least one!"));
+ }
+ return Optional.ofNullable(Jenkins.get().getCloud(step.getCloud()))
+ .filter(cloud -> cloud instanceof NomadCloud)
+ .map(cloud -> (NomadCloud) cloud)
+ .orElseThrow(() -> new AbortException(String.format("Nomad cloud does not exist: %s", step.getCloud())));
+ }
+
+ private class RemoveTemplateCallBack extends BodyExecutionCallback.TailCall {
+ private final String label;
+
+ public RemoveTemplateCallBack(String label) {
+ this.label = label;
+ }
+
+ @Override
+ protected void finished(StepContext stepContext) throws AbortException {
+ NomadCloud cloud = resolveCloud();
+ cloud.removeDynamicTemplate(label);
+ }
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/nomad/NomadCloudTest.java b/src/test/java/org/jenkinsci/plugins/nomad/NomadCloudTest.java
index 77d3e68e..897beb00 100644
--- a/src/test/java/org/jenkinsci/plugins/nomad/NomadCloudTest.java
+++ b/src/test/java/org/jenkinsci/plugins/nomad/NomadCloudTest.java
@@ -2,16 +2,18 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import hudson.model.labels.LabelAtom;
import hudson.slaves.NodeProvisioner;
+
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
+import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.UUID;
public class NomadCloudTest {
@@ -33,6 +35,21 @@ public void testCanProvision() {
assertThat(result, is(true));
}
+ @Test
+ public void testCanProvisionDynamically() {
+ // GIVEN
+ LabelAtom label = createLabel();
+ NomadCloud cloud = createCloud();
+ NomadWorkerTemplate template = createTemplate(label.getName());
+ cloud.addDynamicTemplate(template);
+
+ // WHEN
+ boolean result = cloud.canProvision(label);
+
+ // THEN
+ assertThat(result, is(true));
+ }
+
@Test
public void testProvision() {
// GIVEN
@@ -47,6 +64,21 @@ public void testProvision() {
assertThat(result.size(), is(3));
}
+ @Test
+ public void testProvisionDynamically() {
+ // GIVEN
+ LabelAtom label = createLabel();
+ NomadCloud cloud = createCloud();
+ NomadWorkerTemplate template = createTemplate(label.getName());
+ cloud.addDynamicTemplate(template);
+
+ // WHEN
+ Collection result = cloud.provision(label, 1);
+
+ // THEN
+ assertThat(result.size(), is(1));
+ }
+
@Test
public void testGetTemplateWithLabels() {
// GIVEN
@@ -100,10 +132,10 @@ public void testGetTemplateWithLabelNull() {
NomadWorkerTemplate result = cloud.getTemplate(null);
// THEN
- assertThat(result, is(result));
+ assertThat(result, notNullValue());
}
- private NomadCloud createCloud(NomadWorkerTemplate template) {
+ private NomadCloud createCloud(NomadWorkerTemplate... template) {
return new NomadCloud(
"nomad",
"nomadUrl",
@@ -115,7 +147,7 @@ private NomadCloud createCloud(NomadWorkerTemplate template) {
1,
"",
false,
- Collections.singletonList(template));
+ template == null ? null : Arrays.asList(template));
}
private NomadWorkerTemplate createTemplate(String labels) {
diff --git a/src/test/java/org/jenkinsci/plugins/nomad/pipeline/NomadPipelineStepTest.java b/src/test/java/org/jenkinsci/plugins/nomad/pipeline/NomadPipelineStepTest.java
new file mode 100644
index 00000000..850090e3
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/nomad/pipeline/NomadPipelineStepTest.java
@@ -0,0 +1,126 @@
+package org.jenkinsci.plugins.nomad.pipeline;
+
+import java.util.ArrayList;
+
+import org.jenkinsci.plugins.nomad.NomadCloud;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.BuildWatcher;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import hudson.model.Result;
+
+public class NomadPipelineStepTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @ClassRule
+ public static BuildWatcher buildWatcher = new BuildWatcher();
+
+ @Test
+ public void testNomadCloudDoesNotExist() throws Exception {
+ // GIVEN
+
+ // WHEN
+ WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p");
+ p.setDefinition(new CpsFlowDefinition("nomad (jobTemplate: '', cloud: 'aaaa') {\n" +
+ " node (JOB_LABEL) {\n" +
+ " sh hostname\n" +
+ " }\n" +
+ "}", true));
+ WorkflowRun b = p.scheduleBuild2(0).waitForStart();
+ j.waitForCompletion(b);
+
+ // THEN
+ j.assertBuildStatus(Result.FAILURE, b);
+ j.assertLogContains("ERROR: Nomad cloud does not exist: aaaa", b);
+ }
+
+ @Test
+ public void testNoNomadCloudFound() throws Exception {
+ // GIVEN
+
+ // WHEN
+ WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p");
+ p.setDefinition(new CpsFlowDefinition("nomad (jobTemplate: '') {\n" +
+ " node (JOB_LABEL) {\n" +
+ " sh hostname\n" +
+ " }\n" +
+ "}", true));
+ WorkflowRun b = p.scheduleBuild2(0).waitForStart();
+ j.waitForCompletion(b);
+
+ // THEN
+ j.assertBuildStatus(Result.FAILURE, b);
+ j.assertLogContains("ERROR: No Nomad cloud was found. Please configure at least one!", b);
+ }
+
+ @Test
+ public void testDefaultNomadCloudFound() throws Exception {
+ // GIVEN
+ NomadCloud cloud = new NomadCloud(
+ "aaa",
+ "http://localhost:4646",
+ false,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ false,
+ new ArrayList<>()
+ );
+ j.jenkins.clouds.add(cloud);
+ j.jenkins.save();
+
+ // WHEN
+ WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p");
+ p.setDefinition(new CpsFlowDefinition("nomad (jobTemplate: '') {\n" +
+ " print \"${JOB_LABEL == null ? 'JOB_LABEL null' : 'JOB_LABEL not null'}\" \n" +
+ "}", true));
+ WorkflowRun b = p.scheduleBuild2(0).waitForStart();
+ j.waitForCompletion(b);
+
+ // THEN
+ j.assertBuildStatusSuccess(b);
+ j.assertLogContains("JOB_LABEL not null", b);
+ }
+
+ @Test
+ public void testNamedNomadCloudFound() throws Exception {
+ // GIVEN
+ NomadCloud cloud = new NomadCloud(
+ "aaa",
+ "http://localhost:4646",
+ false,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ false,
+ new ArrayList<>()
+ );
+ j.jenkins.clouds.add(cloud);
+ j.jenkins.save();
+
+ // WHEN
+ WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p");
+ p.setDefinition(new CpsFlowDefinition("nomad (jobTemplate: '', cloud: 'aaa') {\n" +
+ " print \"${JOB_LABEL == null ? 'JOB_LABEL null' : 'JOB_LABEL not null'}\" \n" +
+ "}", true));
+ WorkflowRun b = p.scheduleBuild2(0).waitForStart();
+ j.waitForCompletion(b);
+
+ // THEN
+ j.assertBuildStatusSuccess(b);
+ j.assertLogContains("JOB_LABEL not null", b);
+ }
+}
\ No newline at end of file