diff --git a/README.md b/README.md index ee43a7cfb..7ae2adf7b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ already defined in the Jenkins global configuration, an ephemeral resource is used: These resources only exist as long as any running build is referencing them. -Examples: +#### Locking Examples *Acquire lock* @@ -90,8 +90,6 @@ lock(label: 'some_resource', variable: 'LOCKED_RESOURCE', quantity: 2) { } ``` - - *Skip executing the block if there is a queue* ```groovy @@ -100,6 +98,33 @@ lock(resource: 'some_resource', skipIfLocked: true) { } ``` +#### Update Examples + +*Set the note on a lock* + +```groovy +updateLock(resource: 'printer', setNote: 'this might take a long time...') +``` + +*Changing labels of a lock* + +```groovy +updateLock(resource: 'printer', addLabel: 'offline') +``` +*Adding/Deleting locks dynamically* + +```groovy +discoveredPrinters.each { p -> + updateLock(resource: p.name, setLabels:'printer', createResource:true) +} +``` + +```groovy +brokenPrinters.each { p -> + updateLock(resource: p.name, deleteResource:true) +} +``` + Detailed documentation can be found as part of the [Pipeline Steps](https://jenkins.io/doc/pipeline/steps/lockable-resources/) documentation. diff --git a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java index 3f7f8a517..db5835e2c 100644 --- a/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java +++ b/src/main/java/org/jenkins/plugins/lockableresources/LockableResourcesManager.java @@ -119,6 +119,26 @@ public synchronized void setDeclaredResources(List declaredRes this.resources = mergedResources; } + public void deleteResource(String name) { + LockableResource resource = fromName(name); + if (resource == null) { + return; + } + + if (resource.isLocked()) { + // Removed locks became ephemeral. + resource.setDescription(""); + resource.setLabels(""); + resource.setNote(""); + resource.setEphemeral(true); + } + else { + this.resources.remove(resource); + } + + save(); + } + public List getResourcesFromProject(String fullName) { List matching = new ArrayList<>(); for (LockableResource r : resources) { diff --git a/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStep.java b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStep.java new file mode 100644 index 000000000..0a4f8b6a6 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStep.java @@ -0,0 +1,150 @@ +package org.jenkins.plugins.lockableresources; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.AutoCompletionCandidates; +import hudson.model.TaskListener; +import hudson.util.FormValidation; +import java.io.Serializable; +import java.util.Collections; +import java.util.Set; +import org.apache.commons.lang.StringUtils; +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 org.kohsuke.stapler.QueryParameter; + +public class UpdateLockStep extends Step implements Serializable { + + private static final long serialVersionUID = -7955849755535282258L; + + @CheckForNull + public String resource = null; + + @CheckForNull + public String addLabels = null; + + @CheckForNull + public String setLabels = null; + + @CheckForNull + public String removeLabels = null; + + @CheckForNull + public String setNote = null; + + public boolean createResource = false; + public boolean deleteResource = false; + + @DataBoundSetter + public void setResource(String resource) { + this.resource = resource; + } + + @DataBoundSetter + public void setAddLabels(String addLabels) { + if (StringUtils.isNotBlank(addLabels)) { + this.addLabels = addLabels; + } + } + + @DataBoundSetter + public void setSetLabels(String setLabels) { + if (StringUtils.isNotBlank(setLabels)) { + this.setLabels = setLabels; + } + } + + @DataBoundSetter + public void setRemoveLabels(String removeLabels) { + if (StringUtils.isNotBlank(removeLabels)) { + this.removeLabels = removeLabels; + } + } + + @DataBoundSetter + public void setCreateResource(boolean createResource) { + this.createResource = createResource; + } + + @DataBoundSetter + public void setDeleteResource(boolean deleteResource) { + this.deleteResource = deleteResource; + } + + @DataBoundSetter + public void setSetNote(String setNote) { + if (StringUtils.isNotBlank(setNote)) { + this.setNote = setNote; + } + } + + @DataBoundConstructor + public UpdateLockStep() { + } + + + @Extension + public static final class DescriptorImpl extends StepDescriptor { + + @Override + public String getFunctionName() { + return "updateLock"; + } + + @NonNull + @Override + public String getDisplayName() { + return "Update the definition of a lock"; + } + + @Override + public boolean takesImplicitBlockArgument() { + return false; + } + + public AutoCompletionCandidates doAutoCompleteResource(@QueryParameter String value) { + return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value); + } + + public static FormValidation doCheckResource( + @QueryParameter String value) { + return UpdateLockStepResource.DescriptorImpl.doCheckResource(value); + } + + public static FormValidation doCheckAddLabels( + @QueryParameter String value, @QueryParameter String setLabels) { + return UpdateLockStepResource.DescriptorImpl.doCheckLabelOperations(value, setLabels); + } + + public static FormValidation doCheckRemoveLabels( + @QueryParameter String value, @QueryParameter String setLabels) { + return UpdateLockStepResource.DescriptorImpl.doCheckLabelOperations(value, setLabels); + } + + public static FormValidation doCheckDelete( + @QueryParameter boolean value, @QueryParameter String setLabels, @QueryParameter String addLabels, @QueryParameter String removeLabels, @QueryParameter String setNote, @QueryParameter boolean createResource) { + return UpdateLockStepResource.DescriptorImpl.doCheckDelete(value, setLabels,addLabels, removeLabels, setNote, createResource); + } + + @Override + public Set> getRequiredContext() { + return Collections.singleton(TaskListener.class); + } + } + + @Override + public StepExecution start(StepContext context) { + return new UpdateLockStepExecution(this, context); + } + + public void validate() { + if (StringUtils.isBlank(resource)) { + throw new IllegalArgumentException("The resource name must be specified."); + } + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepExecution.java b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepExecution.java new file mode 100644 index 000000000..1777e5ca9 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepExecution.java @@ -0,0 +1,67 @@ +package org.jenkins.plugins.lockableresources; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; +import org.jenkinsci.plugins.workflow.steps.StepContext; + +public class UpdateLockStepExecution extends AbstractStepExecutionImpl implements Serializable { + + private static final long serialVersionUID = 1583205294263267002L; + + private static final Logger LOGGER = Logger.getLogger(UpdateLockStepExecution.class.getName()); + + private final UpdateLockStep step; + + public UpdateLockStepExecution(UpdateLockStep step, StepContext context) { + super(context); + this.step = step; + } + + @Override + public boolean start() throws Exception { + this.step.validate(); + + if (this.step.deleteResource) { + LockableResourcesManager.get().deleteResource(this.step.resource); + } + else { + LockableResource resource = LockableResourcesManager.get().fromName(this.step.resource); + LockableResourcesManager lockableResourcesManager = LockableResourcesManager.get(); + if (resource == null && this.step.createResource) { + lockableResourcesManager.createResource(this.step.resource); + resource = lockableResourcesManager.fromName(this.step.resource); + resource.setEphemeral(false); + } + + if (this.step.setLabels != null) { + List setLabels = Arrays.asList(this.step.setLabels.trim().split("\\s+")); + resource.setLabels(setLabels.stream().collect(Collectors.joining(" "))); + } else if (this.step.addLabels != null || this.step.removeLabels != null) { + List labels = new ArrayList<>(Arrays.asList(resource.getLabels().split("\\s+"))); + if (this.step.addLabels != null) { + List addLabels = Arrays.asList(this.step.addLabels.trim().split("\\s+")); + addLabels.stream().filter(l -> labels.contains(l) == false).forEach(labels::add); + } + if (this.step.removeLabels != null) { + List removeLabels = Arrays.asList(this.step.removeLabels.trim().split("\\s+")); + labels.removeAll(removeLabels); + } + resource.setLabels(labels.stream().collect(Collectors.joining(" "))); + } + + if (this.step.setNote != null) { + resource.setNote(this.step.setNote); + } + + lockableResourcesManager.save(); + } + + getContext().onSuccess(null); + return true; + } +} diff --git a/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepResource.java b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepResource.java new file mode 100644 index 000000000..5ac3d97b9 --- /dev/null +++ b/src/main/java/org/jenkins/plugins/lockableresources/UpdateLockStepResource.java @@ -0,0 +1,64 @@ +package org.jenkins.plugins.lockableresources; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.Util; +import hudson.model.AbstractDescribableImpl; +import hudson.model.AutoCompletionCandidates; +import hudson.model.Descriptor; +import hudson.util.FormValidation; +import java.io.Serializable; +import org.kohsuke.stapler.QueryParameter; + +public class UpdateLockStepResource extends AbstractDescribableImpl implements Serializable { + + private static final long serialVersionUID = -3689811142454137183L; + + + @Extension + public static class DescriptorImpl extends Descriptor { + + @NonNull + @Override + public String getDisplayName() { + return "Resource Update"; + } + + public AutoCompletionCandidates doAutoCompleteResource(@QueryParameter String value) { + return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value); + } + + public static FormValidation doCheckLabelOperations(String value, String setLabels) { + String updateLabel = Util.fixEmpty(value); + setLabels = Util.fixEmpty(setLabels); + + if (setLabels != null && updateLabel != null) { + return FormValidation.error("Cannot set and update labels at the same time."); + } + return FormValidation.ok(); + } + + public static FormValidation doCheckResource(@QueryParameter String value) { + String resourceName = Util.fixEmpty(value); + if (resourceName == null) { + return FormValidation.error("Resource name cannot be empty."); + } + return FormValidation.ok(); + } + + public static FormValidation doCheckDelete(boolean value, String setLabels, String addLabels, String removeLabels, String setNote, boolean createResource) { + if (!value) { + return FormValidation.ok(); + } + + if (createResource) { + return FormValidation.error("Cannot create and delete a resource."); + } + + if (Util.fixEmpty(setLabels) != null || Util.fixEmpty(addLabels) != null || Util.fixEmpty(removeLabels) != null || Util.fixEmpty(setNote) != null) { + return FormValidation.error("Cannot update and delete a resource."); + } + return FormValidation.ok(); + } + } +} diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.jelly b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.jelly new file mode 100644 index 000000000..d800632a6 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/config.jelly @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-addLabels.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-addLabels.html new file mode 100644 index 000000000..1ab5e9aa1 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-addLabels.html @@ -0,0 +1,5 @@ +
+

+ Appends to the current labels with the ones specified as a space-separated list. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-createResource.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-createResource.html new file mode 100644 index 000000000..718caa28c --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-createResource.html @@ -0,0 +1,5 @@ +
+

+ Creates a new (persistent) lock. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-deleteResource.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-deleteResource.html new file mode 100644 index 000000000..fef254ef2 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-deleteResource.html @@ -0,0 +1,5 @@ +
+

+ Deletes an existing resource - cannot be used with any other fields +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-removeLabels.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-removeLabels.html new file mode 100644 index 000000000..aed83af5d --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-removeLabels.html @@ -0,0 +1,5 @@ +
+

+ Removes from the current labels the ones specified as a space-separated list. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-resource.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-resource.html new file mode 100644 index 000000000..f84cebfe7 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-resource.html @@ -0,0 +1,5 @@ +
+

+ The resource name to update as defined in Global settings. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setLabels.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setLabels.html new file mode 100644 index 000000000..e7a3dad04 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setLabels.html @@ -0,0 +1,5 @@ +
+

+ Replaces the current labels with the ones specified as a space-separated list. +

+
diff --git a/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setNote.html b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setNote.html new file mode 100644 index 000000000..d24c52d07 --- /dev/null +++ b/src/main/resources/org/jenkins/plugins/lockableresources/UpdateLockStep/help-setNote.html @@ -0,0 +1,5 @@ +
+

+ Replaces the note of the resource with the one specified. +

+
diff --git a/src/test/java/org/jenkins/plugins/lockableresources/UpdateLockStepTest.java b/src/test/java/org/jenkins/plugins/lockableresources/UpdateLockStepTest.java new file mode 100644 index 000000000..bb52b431d --- /dev/null +++ b/src/test/java/org/jenkins/plugins/lockableresources/UpdateLockStepTest.java @@ -0,0 +1,155 @@ +package org.jenkins.plugins.lockableresources; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +public class UpdateLockStepTest extends LockStepTestBase { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + //@Issue("JENKINS-XXXXX") + public void updateLockAddLabels() throws Exception { + LockableResourcesManager.get().createResourceWithLabel("resource1", "label1"); + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition( + new CpsFlowDefinition( + "updateLock(resource:'resource1', addLabels:'newLabel1 newLabel2')\n", + true)); + WorkflowRun b1 = p.scheduleBuild2(0).waitForStart(); + j.waitForCompletion(b1); + + // labels should have been added + Assert.assertEquals( + "label1 newLabel1 newLabel2", + LockableResourcesManager.get().fromName("resource1").getLabels()); + } + + @Test + //@Issue("JENKINS-XXXXX") + public void updateLockRemoveLabels() throws Exception { + LockableResourcesManager.get().createResourceWithLabel("resource1", "label1 label2"); + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition( + new CpsFlowDefinition( + "updateLock(resource:'resource1', removeLabels:'label1')\n", + true)); + WorkflowRun b1 = p.scheduleBuild2(0).waitForStart(); + j.waitForCompletion(b1); + + // label should have been removed + Assert.assertEquals( + "label2", + LockableResourcesManager.get().fromName("resource1").getLabels()); + } + + @Test + //@Issue("JENKINS-XXXXX") + public void updateLockSetLabels() throws Exception { + LockableResourcesManager.get().createResourceWithLabel("resource1", "label1 label2"); + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition( + new CpsFlowDefinition( + "updateLock(resource:'resource1', setLabels:'a b c')\n", + true)); + WorkflowRun b1 = p.scheduleBuild2(0).waitForStart(); + j.waitForCompletion(b1); + + // labels should have been set + Assert.assertEquals( + "a b c", + LockableResourcesManager.get().fromName("resource1").getLabels()); + } + + @Test + //@Issue("JENKINS-XXXXX") + public void updateLockSetNote() throws Exception { + LockableResourcesManager.get().createResource("resource1"); + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition( + new CpsFlowDefinition( + "updateLock(resource:'resource1', setNote:'hello world')\n", + true)); + WorkflowRun b1 = p.scheduleBuild2(0).waitForStart(); + j.waitForCompletion(b1); + + // Note should have been updated + Assert.assertEquals( + "hello world", + LockableResourcesManager.get().fromName("resource1").getNote()); + } + + @Test + //@Issue("JENKINS-XXXXX") + public void updateLockCreateResource() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition( + new CpsFlowDefinition( + "updateLock(resource:'newResource', createResource:true)\n", + true)); + WorkflowRun b1 = p.scheduleBuild2(0).waitForStart(); + j.waitForCompletion(b1); + + // Resource should be created (not ephemeral) + Assert.assertNotNull(LockableResourcesManager.get().fromName("newResource")); + Assert.assertFalse(LockableResourcesManager.get().fromName("newResource").isEphemeral()); + } + + @Test + //@Issue("JENKINS-XXXXX") + public void updateLockDeleteResource() throws Exception { + LockableResourcesManager.get().createResource("resource1"); + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition( + new CpsFlowDefinition( + "updateLock(resource:'resource1', deleteResource:true)\n", + true)); + WorkflowRun b1 = p.scheduleBuild2(0).waitForStart(); + j.waitForCompletion(b1); + + // Resource should be deleted + Assert.assertNull(LockableResourcesManager.get().fromName("resource1")); + } + + + @Test + //@Issue("JENKINS-XXXXX") + public void updateLockDeleteLockedResource() throws Exception { + LockableResourcesManager.get().createResource("resource1"); + LockableResourcesManager.get().fromName("resource1").setEphemeral(false); + + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition( + new CpsFlowDefinition( + "lock(resource:'resource1') {\n" + + " semaphore 'wait-inside'\n" + + "}\n" + + "echo 'Finish'", + true)); + WorkflowRun b1 = p.scheduleBuild2(0).waitForStart(); + SemaphoreStep.waitForStart("wait-inside/1", b1); + + WorkflowJob p2 = j.jenkins.createProject(WorkflowJob.class, "p2"); + p2.setDefinition( + new CpsFlowDefinition( + "updateLock(resource:'resource1', deleteResource:true)\n", + true)); + WorkflowRun b2 = p2.scheduleBuild2(0).waitForStart(); + j.waitForCompletion(b2); + + Assert.assertNotNull(LockableResourcesManager.get().fromName("resource1")); + Assert.assertTrue(LockableResourcesManager.get().fromName("resource1").isEphemeral()); + + SemaphoreStep.success("wait-inside/1", null); + j.waitForCompletion(b1); + + Assert.assertNull(LockableResourcesManager.get().fromName("resource1")); + } +}