Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for advanced label filters when locking #309

Closed
wants to merge 14 commits into from
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ lock(label: 'some_resource', variable: 'LOCKED_RESOURCE', quantity: 2) {
}
```



*Skip executing the block if there is a queue*

```groovy
Expand All @@ -100,6 +98,14 @@ lock(resource: 'some_resource', skipIfLocked: true) {
}
```

*Lock a resource based on a combination of labels*

```groovy
lock(anyOfLabels: 'cat dog', allOfLabels: 'friendly cute', noneOfLabels: 'dangerous dirty') {
echo "got a cat or a dog that's friendly and cute, and neither dangerous nor dirty"
}
```

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
Expand Up @@ -40,7 +40,7 @@ public static void compatibilityMigration() {
for (StepContext queuedContext : queuedContexts) {
List<String> resourcesNames = new ArrayList<>();
resourcesNames.add(resource.getName());
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resourcesNames, "", 0);
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resourcesNames, "", "", "", 0);
LockableResourcesManager.get().queueContext(queuedContext, Collections.singletonList(resourceHolder), resource.getName(), null);
}
queuedContexts.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.List;
import java.util.Set;
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;
Expand All @@ -28,6 +29,9 @@ public class LockStep extends Step implements Serializable {
@CheckForNull public String resource = null;

@CheckForNull public String label = null;
@CheckForNull public String anyOfLabels = null;
@CheckForNull public String allOfLabels = null;
@CheckForNull public String noneOfLabels = null;

public int quantity = 0;

Expand All @@ -45,7 +49,7 @@ public class LockStep extends Step implements Serializable {
// is not required)
@DataBoundConstructor
public LockStep(@Nullable String resource) {
if (resource != null && !resource.isEmpty()) {
if (StringUtils.isNotBlank(resource)) {
this.resource = resource;
}
}
Expand All @@ -62,11 +66,32 @@ public void setSkipIfLocked(boolean skipIfLocked) {

@DataBoundSetter
public void setLabel(String label) {
if (label != null && !label.isEmpty()) {
if (StringUtils.isNotBlank(label)) {
this.label = label;
}
}

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

@DataBoundSetter
public void setAllOfLabels(String allOfLabels) {
if (allOfLabels != null && !allOfLabels.isEmpty()) {
this.allOfLabels = allOfLabels;
}
}

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

@DataBoundSetter
public void setVariable(String variable) {
if (variable != null && !variable.isEmpty()) {
Expand Down Expand Up @@ -129,22 +154,22 @@ public String toString() {
return getResources().stream()
.map(res -> "{" + res.toString() + "}")
.collect(Collectors.joining(","));
} else if (resource != null || label != null) {
return LockStepResource.toString(resource, label, quantity);
} else if (resource != null || label != null || anyOfLabels != null || allOfLabels != null || noneOfLabels != null) {
return LockStepResource.toString(resource, label, anyOfLabels, allOfLabels, noneOfLabels, quantity);
} else {
return "nothing";
}
}

/** Label and resource are mutual exclusive. */
public void validate() {
LockStepResource.validate(resource, label, quantity);
LockStepResource.validate(resource, label, anyOfLabels, allOfLabels, noneOfLabels, quantity);
}

public List<LockStepResource> getResources() {
List<LockStepResource> resources = new ArrayList<>();
if (resource != null || label != null) {
resources.add(new LockStepResource(resource, label, quantity));
if (resource != null || label != null || anyOfLabels != null || allOfLabels != null || noneOfLabels != null) {
resources.add(new LockStepResource(resource, label, anyOfLabels, allOfLabels, noneOfLabels, quantity));
}

if (extra != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
Expand Down Expand Up @@ -54,8 +55,20 @@ public boolean start() throws Exception {
}
resources.add(resource.resource);
}

String allOfLabels = resource.allOfLabels;
// merge the label with the allOfLabels filter
if (StringUtils.isNotBlank(resource.label)) {
if (StringUtils.isBlank(allOfLabels)) {
allOfLabels = resource.label;
}
else {
allOfLabels = resource.label + " " + allOfLabels;
}
}

resourceHolderList.add(
new LockableResourcesStruct(resources, resource.label, resource.quantity));
new LockableResourcesStruct(resources, resource.anyOfLabels, allOfLabels, resource.noneOfLabels, resource.quantity));
}

// determine if there are enough resources available to proceed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
Expand All @@ -22,24 +29,48 @@ public class LockStepResource extends AbstractDescribableImpl<LockStepResource>
@CheckForNull
public String label = null;

@CheckForNull
public String anyOfLabels = null;

@CheckForNull
public String allOfLabels = null;

@CheckForNull
public String noneOfLabels = null;

public int quantity = 0;

LockStepResource(
@Nullable String resource,
@Nullable String label,
@Nullable String anyOfLabels,
@Nullable String allOfLabels,
@Nullable String noneOfLabels,
int quantity) {
this(resource, label, quantity);
this.anyOfLabels = anyOfLabels;
this.allOfLabels = allOfLabels;
this.noneOfLabels = noneOfLabels;
}

LockStepResource(@Nullable String resource, @Nullable String label, int quantity) {
this.resource = resource;
this.label = label;
this(resource);
if (StringUtils.isNotBlank(label)) {
this.label = label;
}
this.quantity = quantity;
}

@DataBoundConstructor
public LockStepResource(@Nullable String resource) {
if (resource != null && !resource.isEmpty()) {
if (StringUtils.isNotBlank(resource)) {
this.resource = resource;
}
}

@DataBoundSetter
public void setLabel(String label) {
if (label != null && !label.isEmpty()) {
if (StringUtils.isNotBlank(label)) {
this.label = label;
}
}
Expand All @@ -49,44 +80,91 @@ public void setQuantity(int quantity) {
this.quantity = quantity;
}

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

@DataBoundSetter
public void setAllOfLabels(String allOfLabels) {
if (allOfLabels != null && !allOfLabels.isEmpty()) {
this.allOfLabels = allOfLabels;
}
}

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

@Override
public String toString() {
return toString(resource, label, quantity);
return toString(resource, label, anyOfLabels, allOfLabels, noneOfLabels, quantity);
}

public static String toString(String resource, String label, int quantity) {
public static String toString(String resource, String label, String anyOfLabels, String allOfLabels, String noneOfLabels, int quantity) {
// a label takes always priority
if (label != null) {
if (label != null || anyOfLabels != null || allOfLabels != null || noneOfLabels != null) {
List<String> desc = new ArrayList<>();
if (label != null)
desc.add("Label: " + label);
if (anyOfLabels != null)
desc.add("AnyOfLabels: " + anyOfLabels);
if (allOfLabels != null)
desc.add("AllOfLabels: " + allOfLabels);
if (noneOfLabels != null)
desc.add("NoneOfLabels: " + noneOfLabels);
if (quantity > 0) {
return "Label: " + label + ", Quantity: " + quantity;
desc.add("Quantity: " + quantity);
}
return "Label: " + label;
return desc.stream().collect(Collectors.joining(", "));
}

// make sure there is an actual resource specified
if (resource != null) {
return resource;
}

return "[no resource/label specified - probably a bug]";
}

/**
* Label and resource are mutual exclusive.
*/
public void validate() {
validate(resource, label, quantity);
validate(resource, label, anyOfLabels, allOfLabels, noneOfLabels, quantity);
}

/**
* Label and resource are mutual exclusive.
* The label, if provided, must be configured (at least one resource must have this label).
*/
public static void validate(String resource, String label, int quantity) {
if (label != null && !label.isEmpty() && resource != null && !resource.isEmpty()) {
public static void validate(String resource, String label, String anyOfLabels, String allOfLabels, String noneOfLabels, int quantity) {
boolean filtersOnLabels =
StringUtils.isNotBlank(label)
|| StringUtils.isNotBlank(anyOfLabels)
|| StringUtils.isNotBlank(allOfLabels)
|| StringUtils.isNotBlank(noneOfLabels);

if (filtersOnLabels && StringUtils.isNotBlank(resource)) {
throw new IllegalArgumentException("Label and resource name cannot be specified simultaneously.");
}
if (label != null && !LockableResourcesManager.get().isValidLabel( label ) ) {
throw new IllegalArgumentException("The label does not exist: " + label);
}

// only validate the `allOfLabels` - it would be fine to use invalid labels in the `anyOf` or `noneOf` filters
if (allOfLabels != null) {
Set<String> allLabels = LockableResourcesManager.get().getAllLabels();
Optional<String> notFoundLabel = Arrays.stream(allOfLabels.split("\\s+")).filter(l -> allLabels.contains(l) == false).findFirst();
if (notFoundLabel.isPresent()) {
throw new IllegalArgumentException("The label does not exist: " + notFoundLabel.get());
}
}
}

private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public boolean isEphemeral() {
return ephemeral;
}

public boolean isValidLabel(String candidate, Map<String, Object> params) {
public boolean isValidLabel(String candidate) {
return labelsContain(candidate);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
Expand Down Expand Up @@ -165,14 +167,19 @@ public int getFreeResourceAmount(String label) {
return free;
}

@Deprecated//(forRemoval = true, since = "2.15")
public List<LockableResource> getResourcesWithLabel(String label, Map<String, Object> params) {
gaspardpetit marked this conversation as resolved.
Show resolved Hide resolved
List<LockableResource> found = new ArrayList<>();
for (LockableResource r : this.resources) {
if (r.isValidLabel(label, params)) found.add(r);
if (r.isValidLabel(label)) found.add(r);
}
return found;
}

public List<LockableResource> getResourcesMatchingPredicate(Predicate<LockableResource> predicate) {
return this.resources.stream().filter(predicate).collect(Collectors.toList());
}

/**
* Get a list of resources matching the script.
*
Expand Down Expand Up @@ -332,14 +339,13 @@ public synchronized List<LockableResource> tryQueue(
boolean candidatesByScript = (systemGroovyScript != null);
List<LockableResource> candidates = requiredResources.required; // default candidates

if (candidatesByScript ||
(requiredResources.label != null && !requiredResources.label.isEmpty())) {
if (candidatesByScript || requiredResources.hasLabelFilter()) {
candidates = cachedCandidates.getIfPresent(queueItemId);
if (candidates != null) {
candidates.retainAll(resources);
} else {
candidates = (systemGroovyScript == null)
? getResourcesWithLabel(requiredResources.label, params)
? getResourcesMatchingPredicate(requiredResources.getLabelsPredicate())
: getResourcesMatchingScript(systemGroovyScript, params);
cachedCandidates.put(queueItemId, candidates);
}
Expand Down Expand Up @@ -972,10 +978,11 @@ public synchronized List<LockableResource> checkResourcesAvailability(
// get possible resources
int requiredAmount = 0; // 0 means all
List<LockableResource> candidates = new ArrayList<>();
if (StringUtils.isBlank(requiredResources.label)) {
if (requiredResources.hasLabelFilter() == false) {
candidates.addAll(requiredResources.required);
} else {
candidates.addAll(getResourcesWithLabel(requiredResources.label, null));
}
else {
candidates.addAll(getResourcesMatchingPredicate(requiredResources.getLabelsPredicate()));
if (requiredResources.requiredNumber != null) {
try {
requiredAmount = Integer.parseInt(requiredResources.requiredNumber);
Expand Down
Loading