Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import hudson.util.FormValidation;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
Expand Down Expand Up @@ -149,6 +150,9 @@
@Extension
public static class DescriptorImpl extends JobPropertyDescriptor {

/** Detects {@code ${...}} variable references that are resolved at build time. */
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[^}]+}");

@NonNull
@Override
public String getDisplayName() {
Expand Down Expand Up @@ -188,10 +192,17 @@
} else {
List<String> wrongNames = new ArrayList<>();
for (String name : names.split("\\s+")) {
// Skip validation for names containing build-parameter references
if (VARIABLE_PATTERN.matcher(name).find()) {

Check warning on line 196 in src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 196 is only partially covered, one branch is missing
continue;

Check warning on line 197 in src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 197 is not covered by tests
}
boolean found = LockableResourcesManager.get().resourceExist(name);
if (!found) wrongNames.add(name);
}
if (wrongNames.isEmpty()) {
if (VARIABLE_PATTERN.matcher(names).find()) {

Check warning on line 203 in src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 203 is only partially covered, one branch is missing
return FormValidation.warning(Messages.warning_resourceNameContainsVariable());

Check warning on line 204 in src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 204 is not covered by tests
}
return FormValidation.ok();
} else {
return FormValidation.error(Messages.error_resourceDoesNotExist(wrongNames));
Expand All @@ -216,33 +227,42 @@
} else if (names != null || script) {
return FormValidation.error(Messages.error_labelAndNameOrGroovySpecified());
} else {
// Skip label validation when it contains build-parameter references
if (VARIABLE_PATTERN.matcher(label).find()) {

Check warning on line 231 in src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 231 is only partially covered, one branch is missing
return FormValidation.warning(Messages.warning_labelContainsVariable());
}
if (LockableResourcesManager.get().isValidLabel(label)) {
return FormValidation.ok();
} else {
return FormValidation.error(Messages.error_labelDoesNotExist(label));
}
}
}

@RequirePOST
public FormValidation doCheckResourceNumber(
@QueryParameter String value,
@QueryParameter String resourceNames,
@QueryParameter String labelName,
@QueryParameter String resourceMatchScript,
@AncestorInPath Item item) {
// check permission, security first
checkPermission(item);

String number = Util.fixEmptyAndTrim(value);
String names = Util.fixEmptyAndTrim(resourceNames);
String label = Util.fixEmptyAndTrim(labelName);
String script = Util.fixEmptyAndTrim(resourceMatchScript);

if (number == null || number.isEmpty() || number.trim().equals("0")) {
return FormValidation.ok();
}

// Skip numeric validation when number contains build-parameter references
if (VARIABLE_PATTERN.matcher(number).find()) {
return FormValidation.warning(Messages.warning_resourceNumberContainsVariable());

Check warning on line 263 in src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 232-263 are not covered by tests
}

int numAsInt;
try {
numAsInt = Integer.parseInt(number);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
package org.jenkins.plugins.lockableresources.queue;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.EnvVars;
import hudson.Extension;
import hudson.model.AbstractBuild;
import hudson.model.Job;
import hudson.model.Run;
import hudson.model.StringParameterValue;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
Expand All @@ -40,12 +42,22 @@
}

if (build instanceof AbstractBuild) {
AbstractBuild<?, ?> abstractBuild = (AbstractBuild<?, ?>) build;
LockableResourcesManager lrm = LockableResourcesManager.get();
synchronized (lrm.syncResources) {
Job<?, ?> proj = Utils.getProject(build);
List<LockableResource> required = new ArrayList<>();

LockableResourcesStruct resources = Utils.requiredResources(proj);
// Resolve build parameters so that ${PARAM} references in
// resource names, labels, and numbers are expanded.
EnvVars buildEnv;
try {
buildEnv = abstractBuild.getEnvironment(listener);
} catch (IOException | InterruptedException e) {
buildEnv = new EnvVars();

Check warning on line 57 in src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 56-57 are not covered by tests
}

LockableResourcesStruct resources = Utils.requiredResources(proj, buildEnv);

if (resources != null) {
if (resources.requiredNumber != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.EnvVars;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.model.Job;
Expand Down Expand Up @@ -51,7 +52,10 @@ public CauseOfBlockage canRun(Queue.Item item) {
Job<?, ?> project = Utils.getProject(item);
if (project == null) return null;

LockableResourcesStruct resources = Utils.requiredResources(project);
// Extract build parameters so that ${PARAM} references in resource
// names, labels, and numbers are expanded before scheduling.
EnvVars paramEnv = Utils.getParametersAsEnvVars(item);
LockableResourcesStruct resources = Utils.requiredResources(project, paramEnv);
if (resources == null
|| (resources.required.isEmpty()
&& resources.label.isEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public LockableResourcesStruct(RequiredResourcesProperty property, EnvVars env)

requiredVar = property.getResourceNamesVar();

requiredNumber = property.getResourceNumber();
requiredNumber = env.expand(property.getResourceNumber());
if (requiredNumber != null && requiredNumber.equals("0")) requiredNumber = null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,25 @@
import hudson.ExtensionList;
import hudson.matrix.MatrixConfiguration;
import hudson.model.Job;
import hudson.model.ParameterValue;
import hudson.model.ParametersAction;
import hudson.model.Queue;
import hudson.model.Run;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.jenkins.plugins.lockableresources.ExcludeFromJacocoGeneratedReport;
import org.jenkins.plugins.lockableresources.RequiredResourcesProperty;
import org.jenkinsci.plugins.variant.OptionalExtension;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

public final class Utils {
private Utils() {}

/** Pattern to detect {@code ${...}} variable references in configuration values. */
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[^}]+}");

@CheckForNull
public static Job<?, ?> getProject(@NonNull Queue.Item item) {
if (item.task instanceof Job) return (Job<?, ?>) item.task;
Expand All @@ -34,21 +44,80 @@
return build.getParent();
}

/**
* Build the required-resources structure for a project, without additional environment variables.
*
* @see #requiredResources(Job, EnvVars)
*/
@Deprecated
@ExcludeFromJacocoGeneratedReport
@CheckForNull
@Restricted(NoExternalUse.class)
public static LockableResourcesStruct requiredResources(@NonNull Job<?, ?> project) {
return requiredResources(project, null);
}

/**
* Build the required-resources structure for a project, merging any additional
* environment variables (e.g.&nbsp;build parameters) into the expansion context.
*
* @param project the job whose {@link RequiredResourcesProperty} is read
* @param additionalEnv extra variables to use when expanding {@code ${...}} references;
* may be {@code null}
* @return the struct, or {@code null} if the project has no lockable-resource property
*/
@CheckForNull
@Restricted(NoExternalUse.class)
public static LockableResourcesStruct requiredResources(
@NonNull Job<?, ?> project, @CheckForNull EnvVars additionalEnv) {
EnvVars env = new EnvVars();

for (var ma : ExtensionList.lookup(MatrixAssist.class)) {
env.putAll(ma.getCombination(project));
project = ma.getMainProject(project);
}

if (additionalEnv != null) {

Check warning on line 80 in src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 80 is only partially covered, one branch is missing
env.putAll(additionalEnv);
}

RequiredResourcesProperty property = project.getProperty(RequiredResourcesProperty.class);
if (property != null) return new LockableResourcesStruct(property, env);

return null;
}

/**
* Extract build parameters from a {@link Queue.Item} and return them as {@link EnvVars}
* so that {@code ${PARAM}} references in resource names, labels and numbers are expanded.
*/
@NonNull
@Restricted(NoExternalUse.class)
public static EnvVars getParametersAsEnvVars(@NonNull Queue.Item item) {
EnvVars env = new EnvVars();
List<ParametersAction> paramActions = item.getActions(ParametersAction.class);
for (ParametersAction action : paramActions) {
if (action == null) continue;

Check warning on line 100 in src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 100 is only partially covered, one branch is missing
for (ParameterValue p : action.getParameters()) {
if (p == null) continue;

Check warning on line 102 in src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 102 is only partially covered, one branch is missing
Object value = p.getValue();
if (value != null) {
env.put(p.getName(), value.toString());
}
}
}
return env;
}

/**
* Returns {@code true} when the given string contains at least one {@code ${...}} variable
* reference that will be resolved at build time.
*/
@Restricted(NoExternalUse.class)
public static boolean containsVariable(@CheckForNull String value) {
return value != null && VARIABLE_PATTERN.matcher(value).find();
}

public interface MatrixAssist {
@NonNull
Map<String, String> getCombination(@NonNull Job<?, ?> project);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ LockStepResource.displayName=Resource
LockableResource.displayName=Resource
LockableResourcesManager.displayName=External Resources
RequiredResourcesProperty.displayName=Required Lockable Resources
# warnings (build-parameter references)
warning.resourceNameContainsVariable=Resource name contains build parameter references. \
Validation will occur at build time.
warning.labelContainsVariable=Label contains build parameter references. \
Validation will occur at build time.
warning.resourceNumberContainsVariable=Resource number contains build parameter references. \
Validation will occur at build time.
UpdateLockStep.displayName=Update lockable resource
# UpdateLockStep errors
UpdateLockStep.error.resourceRequired=The resource name must be specified.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Item;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.StringParameterDefinition;
import hudson.model.User;
import hudson.model.queue.QueueTaskFuture;
import hudson.triggers.TimerTrigger;
Expand Down Expand Up @@ -267,6 +269,71 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
j.waitForCompletion(fb2);
}

// ---------------------------------------------------------------------------
// Parameterized resource tests (build parameters as resource / label / number)
// ---------------------------------------------------------------------------

@Test
void parameterizedResourceName(JenkinsRule j) throws Exception {
LockableResourcesManager.get().createResource("my-resource");

FreeStyleProject p = j.createFreeStyleProject("paramResourceName");
p.addProperty(new RequiredResourcesProperty("${RESOURCE_NAME}", "resourceNameVar", null, null, null));
p.addProperty(new ParametersDefinitionProperty(
new StringParameterDefinition("RESOURCE_NAME", "my-resource", "Resource to lock")));
p.getBuildersList().add(new PrinterBuilder());

FreeStyleBuild b = p.scheduleBuild2(0).get();
j.assertLogContains("acquired lock on [my-resource]", b);
j.assertLogContains("resourceNameVar: my-resource", b);
j.assertBuildStatus(Result.SUCCESS, b);
}

@Test
void parameterizedLabel(JenkinsRule j) throws Exception {
LockableResourcesManager.get().createResourceWithLabel("res1", "team-alpha");
LockableResourcesManager.get().createResourceWithLabel("res2", "team-alpha");

FreeStyleProject p = j.createFreeStyleProject("paramLabel");
p.addProperty(new RequiredResourcesProperty(null, "resourceNameVar", "1", "${LABEL}", null));
p.addProperty(
new ParametersDefinitionProperty(new StringParameterDefinition("LABEL", "team-alpha", "Label to use")));

FreeStyleBuild b = p.scheduleBuild2(0).get();
j.assertLogContains("acquired lock on", b);
j.assertBuildStatus(Result.SUCCESS, b);
}

@Test
void parameterizedResourceNumber(JenkinsRule j) throws Exception {
LockableResourcesManager.get().createResourceWithLabel("pool1", "pool");
LockableResourcesManager.get().createResourceWithLabel("pool2", "pool");
LockableResourcesManager.get().createResourceWithLabel("pool3", "pool");

FreeStyleProject p = j.createFreeStyleProject("paramNumber");
p.addProperty(new RequiredResourcesProperty(null, "resourceNameVar", "${COUNT}", "pool", null));
p.addProperty(
new ParametersDefinitionProperty(new StringParameterDefinition("COUNT", "2", "How many resources")));

FreeStyleBuild b = p.scheduleBuild2(0).get();
j.assertLogContains("acquired lock on", b);
j.assertBuildStatus(Result.SUCCESS, b);

// Verify exactly 2 resources were locked via the variable
String log = b.getLog();
String varLine = null;
for (String line : log.split("\n")) {
if (line.contains("acquired lock on")) {
varLine = line;
break;
}
}
assertNotNull(varLine, "Expected 'acquired lock on' in build log");
// Count resource names in the log line (comma-separated inside brackets)
long count = varLine.chars().filter(ch -> ch == ',').count() + 1;
assertEquals(2, count, "Expected exactly 2 resources to be locked");
}

public static class PrinterBuilder extends TestBuilder {

@Override
Expand Down
Loading
Loading