Skip to content
Closed
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down Expand Up @@ -100,6 +100,38 @@ lock(resource: 'some_resource', skipIfLocked: true) {
}
```

#### Finding Examples

*List all resources acquired by the current build*

```groovy
lock(label:'printer') {
echo findLocks().name
}
```

*List all resources (locked or not) currently defined*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking of the repetitive "locked or not" comment, maybe filters for lock states (not-locked - build:'none' perhaps?, reserved, stolen, ephemeral/persistent...) could be useful.

Copy link
Copy Markdown
Contributor Author

@gaspardpetit gaspardpetit Mar 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial concern was to go in a feature creep that would add complexity without fulfilling actual use cases. Do we want to search in notes? what about reservedTimestamp - and do we want to allow before / after searches, etc. I don't mind adding a few, but I think the missing ones will be discovered and added through pull requests explaining which use case the new find option is fulfilling.

For reservedBy, I think it opens an interesting case where users can reserve resources and then execute jobs that work on their reserved resources, but apart from unreserving them (ex. with the update step), it would be difficult to allow a step to lock all the resources reserved by a user unless we allow locking resources reserved by a user when the job is being triggerred by that user for example.

For stolen, ephemeral, these are easy to add, but I have the same concern about mapping these to actual use cases.

What I am hoping is that the combo of findLock/updateLock and the advanced lock filters will resolved most of the us cases by using labeling. If you really need to work on your reserved locks, findLocks on all locks, look for the ones you reserved, update the labels (ex. reserved_by_<username>) and then you can lock these specific locks by label.

I do have a much narrower view on how this plugin might be used by the rest of the community, so I'll rely on your opinion on which fields are worth adding to the findLock step


```groovy
echo findLocks(build:'any').name
```

*List all resources (locked or not) with label `printer` excluding those with label `offline`*

```groovy
echo findLocks(anyOfLabels:'printer', noneOfLabels:'offline', build:'any').name
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would this behave when findLocks() returns several values? Would Groovy resolve the .name part nicely, for each entry?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the magic of groovy - if you .name on an array of object, it applies the .name on each entry and returns an array of the results

```
*List all resources (locked or not) with label `printer` and label `offline`*

```groovy
echo findLocks(allOfLabels:'printer offline', build:'any').name
```
*List all resources (locked or not) with a name starting with `PRINTER`*

```groovy
echo findLocks(matching:'^PRINTER.*', build:'any').name
```

Detailed documentation can be found as part of the
[Pipeline Steps](https://jenkins.io/doc/pipeline/steps/lockable-resources/)
documentation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
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.Run;
import hudson.model.TaskListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;
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;

public class FindLocksStep extends Step implements Serializable {

private static final Logger LOGGER = Logger.getLogger(FindLocksStepExecution.class.getName());
private static final long serialVersionUID = 148049840628540827L;
private static final String ANY_BUILD = "any";

@CheckForNull
public String anyOfLabels = null;

@CheckForNull
public String noneOfLabels = null;

@CheckForNull
public String allOfLabels = null;

@CheckForNull
public String matching = null;

@CheckForNull
public String build = null;

@DataBoundSetter
public void setAnyOfLabels(String anyOfLabels) {
if (StringUtils.isNotBlank(anyOfLabels)) {
this.anyOfLabels = anyOfLabels;
}
}

@DataBoundSetter
public void setNoneOfLabels(String noneOfLabels) {
if (StringUtils.isNotBlank(noneOfLabels)) {
this.noneOfLabels = noneOfLabels;
}
}

@DataBoundSetter
public void setAllOfLabels(String allOfLabels) {
if (StringUtils.isNotBlank(allOfLabels)) {
this.allOfLabels = allOfLabels;
}
}

@DataBoundSetter
public void setMatching(String matching) {
if (StringUtils.isNotBlank(matching)) {
this.matching = matching;
}
}

@DataBoundSetter
public void setBuild(String build) {
if (StringUtils.isNotBlank(build)) {
this.build = build;
}
}

@DataBoundConstructor
public FindLocksStep() {
}

public Predicate<LockableResource> asPredicate(StepContext context) {
String currentJobName = null;
try {
Run run = context.get(Run.class);
currentJobName = run.getExternalizableId();
} catch (Exception e) {
LOGGER.warning("Failed to resolve the current job");
}
final String finalCurrentJobName = currentJobName;
return lockableResource -> {
if (StringUtils.isNotBlank(anyOfLabels)) {
List<String> anyLabelsList = Arrays.asList(anyOfLabels.split("\\s+"));
List<String> resourceLabels = Arrays.asList(lockableResource.getLabels().split("\\s+"));
if (anyLabelsList.stream().noneMatch(l -> resourceLabels.contains(l)))
return false;
}
if (StringUtils.isNotBlank(noneOfLabels)) {
List<String> noneOfLabelsList = Arrays.asList(noneOfLabels.split("\\s+"));
List<String> resourceLabels = Arrays.asList(lockableResource.getLabels().split("\\s+"));
if (noneOfLabelsList.stream().anyMatch(l -> resourceLabels.contains(l)))
return false;
}
if (StringUtils.isNotBlank(allOfLabels)) {
List<String> allOfLabelsList = Arrays.asList(allOfLabels.split("\\s+"));
List<String> resourceLabels = Arrays.asList(lockableResource.getLabels().split("\\s+"));
if (allOfLabelsList.stream().allMatch(l -> resourceLabels.contains(l)) == false)
return false;
}
if (StringUtils.isNotBlank(matching)) {
if (lockableResource.getName().matches(matching) == false) {
return false;
}
}
if (StringUtils.isNotBlank(build)) {
if (build.equals(ANY_BUILD) == false) {
if (StringUtils.isBlank(lockableResource.getBuildId())) {
// ignore unlocked resources
return false;
}
if (lockableResource.getBuildId().equals(build) == false) {
return false;
}
}
}
else {
// when build is blank, we restrict the search on the current build
if (finalCurrentJobName == null) {
// if we are not part of a job, filter out everything
return false;
}
if (StringUtils.isBlank(lockableResource.getBuildId())) {
// ignore unlocked resources
return false;
}
if (lockableResource.getBuildId().equals(finalCurrentJobName) == false) {
// ignore resources locked by another job
return false;
}
}
return true;
};
}

@Extension
public static final class DescriptorImpl extends StepDescriptor {

@Override
public String getFunctionName() {
return "findLocks";
}

@NonNull
@Override
public String getDisplayName() {
return "Find existing shared resource";
}

@Override
public boolean takesImplicitBlockArgument() {
return false;
}

@Override
public Set<Class<?>> getRequiredContext() {
return Collections.singleton(TaskListener.class);
}
}

@Override
public String toString() {
List<String> desc = new ArrayList<>();
if (StringUtils.isNotBlank(anyOfLabels)) {
desc.add("anyLabels:" + anyOfLabels);
}
if (StringUtils.isNotBlank(allOfLabels)) {
desc.add("allOfLabels:" + allOfLabels);
}
if (StringUtils.isNotBlank(noneOfLabels)) {
desc.add("noneOfLabels:" + noneOfLabels);
}
if (StringUtils.isNotBlank(matching)) {
desc.add("matching:" + matching);
}
if (StringUtils.isNotBlank(build)) {
desc.add("build:" + build);
}
if (desc.isEmpty()) {
return "all locked by current build";
}
else {
return desc.stream().collect(Collectors.joining("; "));
}
}

@Override
public StepExecution start(StepContext context) throws Exception {
return new FindLocksStepExecution(this, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.jenkins.plugins.lockableresources;

import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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 FindLocksStepExecution extends AbstractStepExecutionImpl implements Serializable {

private static final long serialVersionUID = -5757385070025969380L;
private static final Logger LOGGER = Logger.getLogger(FindLocksStepExecution.class.getName());

private final FindLocksStep step;

public FindLocksStepExecution(FindLocksStep step, StepContext context) {
super(context);
this.step = step;
}

@Override
public boolean start() throws Exception {
List<LockableResource> allResources = LockableResourcesManager.get().getResources();

List<Map<String,Object>> resourcesAsMap = allResources.stream()
.filter(step.asPredicate(getContext()))
.map(FindLocksStepExecution::convertResourceToMap)
.collect(Collectors.toList());

getContext().onSuccess(resourcesAsMap);
return true;
}

private static Map<String, Object> convertResourceToMap(LockableResource lockableResource) {
Map<String, Object> lockAsMap = new HashMap<>();
lockAsMap.put("name", lockableResource.getName());
lockAsMap.put("labels", lockableResource.getLabels());
lockAsMap.put("note", lockableResource.getNote());
lockAsMap.put("description", lockableResource.getDescription());
lockAsMap.put("reservedBy", lockableResource.getReservedBy());
lockAsMap.put("reservedTimestamp", lockableResource.getReservedTimestamp());
lockAsMap.put("queuedItemProject", lockableResource.getQueueItemProject());
lockAsMap.put("buildName", lockableResource.getBuildName());
lockAsMap.put("queuedItemId", lockableResource.getQueueItemId());
lockAsMap.put("lockCause", lockableResource.getLockCause());
lockAsMap.put("reservedByEmail", lockableResource.getReservedByEmail());
return lockAsMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ public String getBuildName() {
else return null;
}

@Exported
public String getBuildId() {
return this.buildExternalizableId;
}

public void setBuild(Run<?, ?> lockedBy) {
this.build = lockedBy;
if (lockedBy != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core"
xmlns:f="/lib/form">
<f:entry title="${%Resource Matches Expression}" field="matching">
<f:textbox/>
</f:entry>
<f:entry title="${%Resource Labels Contain Any Of These Labels}" field="anyOfLabels">
<f:textbox/>
</f:entry>
<f:entry title="${%Resource Labels Contain All Of These Labels}" field="allOfLabels">
<f:textbox/>
</f:entry>
<f:entry title="${%Resource Labels Contain None Of These Labels}" field="noneOfLabels">
<f:textbox/>
</f:entry>
<f:entry title="${%Build Id}" field="build">
<f:textbox/>
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<p>
A list of space separated labels;
Only include resources that contain all the specified labels.
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<p>
A list of space separated labels;
Include resources that contain any (one or more) of the specified labels.
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div>
<p>
A job id, ex. <code>myJob#32</code>;
Only resources locked by the specified build will be returned. Defaults to the
current job id. Specify <code>any</code> to search across all resources - locked
or not by any build
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<p>
A regular expression;
Ignore any resource with a name that does not match the specified regular expression.
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<p>
A list of space separated labels;
Ignore resources that contain any (one or more) of the specified labels.
</p>
</div>
Loading