Skip to content

Commit 3db60c5

Browse files
authored
Merge branch 'master' into feature/lock-step-reason
2 parents 810cecc + 59635fe commit 3db60c5

8 files changed

Lines changed: 336 additions & 3 deletions

File tree

src/main/java/org/jenkins/plugins/lockableresources/RequiredResourcesProperty.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import hudson.util.FormValidation;
2323
import java.util.ArrayList;
2424
import java.util.List;
25+
import java.util.regex.Pattern;
2526
import jenkins.model.Jenkins;
2627
import net.sf.json.JSONObject;
2728
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
@@ -149,6 +150,9 @@ public SecureGroovyScript getResourceMatchScript() {
149150
@Extension
150151
public static class DescriptorImpl extends JobPropertyDescriptor {
151152

153+
/** Detects {@code ${...}} variable references that are resolved at build time. */
154+
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[^}]+}");
155+
152156
@NonNull
153157
@Override
154158
public String getDisplayName() {
@@ -188,10 +192,17 @@ public FormValidation doCheckResourceNames(
188192
} else {
189193
List<String> wrongNames = new ArrayList<>();
190194
for (String name : names.split("\\s+")) {
195+
// Skip validation for names containing build-parameter references
196+
if (VARIABLE_PATTERN.matcher(name).find()) {
197+
continue;
198+
}
191199
boolean found = LockableResourcesManager.get().resourceExist(name);
192200
if (!found) wrongNames.add(name);
193201
}
194202
if (wrongNames.isEmpty()) {
203+
if (VARIABLE_PATTERN.matcher(names).find()) {
204+
return FormValidation.warning(Messages.warning_resourceNameContainsVariable());
205+
}
195206
return FormValidation.ok();
196207
} else {
197208
return FormValidation.error(Messages.error_resourceDoesNotExist(wrongNames));
@@ -216,6 +227,10 @@ public FormValidation doCheckLabelName(
216227
} else if (names != null || script) {
217228
return FormValidation.error(Messages.error_labelAndNameOrGroovySpecified());
218229
} else {
230+
// Skip label validation when it contains build-parameter references
231+
if (VARIABLE_PATTERN.matcher(label).find()) {
232+
return FormValidation.warning(Messages.warning_labelContainsVariable());
233+
}
219234
if (LockableResourcesManager.get().isValidLabel(label)) {
220235
return FormValidation.ok();
221236
} else {
@@ -243,6 +258,11 @@ public FormValidation doCheckResourceNumber(
243258
return FormValidation.ok();
244259
}
245260

261+
// Skip numeric validation when number contains build-parameter references
262+
if (VARIABLE_PATTERN.matcher(number).find()) {
263+
return FormValidation.warning(Messages.warning_resourceNumberContainsVariable());
264+
}
265+
246266
int numAsInt;
247267
try {
248268
numAsInt = Integer.parseInt(number);

src/main/java/org/jenkins/plugins/lockableresources/queue/LockRunListener.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
package org.jenkins.plugins.lockableresources.queue;
1010

1111
import edu.umd.cs.findbugs.annotations.NonNull;
12+
import hudson.EnvVars;
1213
import hudson.Extension;
1314
import hudson.model.AbstractBuild;
1415
import hudson.model.Job;
1516
import hudson.model.Run;
1617
import hudson.model.StringParameterValue;
1718
import hudson.model.TaskListener;
1819
import hudson.model.listeners.RunListener;
20+
import java.io.IOException;
1921
import java.util.ArrayList;
2022
import java.util.List;
2123
import java.util.logging.Logger;
@@ -40,12 +42,22 @@ public void onStarted(Run<?, ?> build, TaskListener listener) {
4042
}
4143

4244
if (build instanceof AbstractBuild) {
45+
AbstractBuild<?, ?> abstractBuild = (AbstractBuild<?, ?>) build;
4346
LockableResourcesManager lrm = LockableResourcesManager.get();
4447
synchronized (lrm.syncResources) {
4548
Job<?, ?> proj = Utils.getProject(build);
4649
List<LockableResource> required = new ArrayList<>();
4750

48-
LockableResourcesStruct resources = Utils.requiredResources(proj);
51+
// Resolve build parameters so that ${PARAM} references in
52+
// resource names, labels, and numbers are expanded.
53+
EnvVars buildEnv;
54+
try {
55+
buildEnv = abstractBuild.getEnvironment(listener);
56+
} catch (IOException | InterruptedException e) {
57+
buildEnv = new EnvVars();
58+
}
59+
60+
LockableResourcesStruct resources = Utils.requiredResources(proj, buildEnv);
4961

5062
if (resources != null) {
5163
if (resources.requiredNumber != null

src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesQueueTaskDispatcher.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.github.benmanes.caffeine.cache.Cache;
1212
import com.github.benmanes.caffeine.cache.Caffeine;
1313
import edu.umd.cs.findbugs.annotations.NonNull;
14+
import hudson.EnvVars;
1415
import hudson.Extension;
1516
import hudson.ExtensionList;
1617
import hudson.model.Job;
@@ -51,7 +52,10 @@ public CauseOfBlockage canRun(Queue.Item item) {
5152
Job<?, ?> project = Utils.getProject(item);
5253
if (project == null) return null;
5354

54-
LockableResourcesStruct resources = Utils.requiredResources(project);
55+
// Extract build parameters so that ${PARAM} references in resource
56+
// names, labels, and numbers are expanded before scheduling.
57+
EnvVars paramEnv = Utils.getParametersAsEnvVars(item);
58+
LockableResourcesStruct resources = Utils.requiredResources(project, paramEnv);
5559
if (resources == null
5660
|| (resources.required.isEmpty()
5761
&& resources.label.isEmpty()

src/main/java/org/jenkins/plugins/lockableresources/queue/LockableResourcesStruct.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public LockableResourcesStruct(RequiredResourcesProperty property, EnvVars env)
6969

7070
requiredVar = property.getResourceNamesVar();
7171

72-
requiredNumber = property.getResourceNumber();
72+
requiredNumber = env.expand(property.getResourceNumber());
7373
if (requiredNumber != null && requiredNumber.equals("0")) requiredNumber = null;
7474
}
7575

src/main/java/org/jenkins/plugins/lockableresources/queue/Utils.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,25 @@
1414
import hudson.ExtensionList;
1515
import hudson.matrix.MatrixConfiguration;
1616
import hudson.model.Job;
17+
import hudson.model.ParameterValue;
18+
import hudson.model.ParametersAction;
1719
import hudson.model.Queue;
1820
import hudson.model.Run;
21+
import java.util.List;
1922
import java.util.Map;
23+
import java.util.regex.Pattern;
24+
import org.jenkins.plugins.lockableresources.ExcludeFromJacocoGeneratedReport;
2025
import org.jenkins.plugins.lockableresources.RequiredResourcesProperty;
2126
import org.jenkinsci.plugins.variant.OptionalExtension;
27+
import org.kohsuke.accmod.Restricted;
28+
import org.kohsuke.accmod.restrictions.NoExternalUse;
2229

2330
public final class Utils {
2431
private Utils() {}
2532

33+
/** Pattern to detect {@code ${...}} variable references in configuration values. */
34+
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{[^}]+}");
35+
2636
@CheckForNull
2737
public static Job<?, ?> getProject(@NonNull Queue.Item item) {
2838
if (item.task instanceof Job) return (Job<?, ?>) item.task;
@@ -34,21 +44,80 @@ private Utils() {}
3444
return build.getParent();
3545
}
3646

47+
/**
48+
* Build the required-resources structure for a project, without additional environment variables.
49+
*
50+
* @see #requiredResources(Job, EnvVars)
51+
*/
52+
@Deprecated
53+
@ExcludeFromJacocoGeneratedReport
3754
@CheckForNull
55+
@Restricted(NoExternalUse.class)
3856
public static LockableResourcesStruct requiredResources(@NonNull Job<?, ?> project) {
57+
return requiredResources(project, null);
58+
}
59+
60+
/**
61+
* Build the required-resources structure for a project, merging any additional
62+
* environment variables (e.g.&nbsp;build parameters) into the expansion context.
63+
*
64+
* @param project the job whose {@link RequiredResourcesProperty} is read
65+
* @param additionalEnv extra variables to use when expanding {@code ${...}} references;
66+
* may be {@code null}
67+
* @return the struct, or {@code null} if the project has no lockable-resource property
68+
*/
69+
@CheckForNull
70+
@Restricted(NoExternalUse.class)
71+
public static LockableResourcesStruct requiredResources(
72+
@NonNull Job<?, ?> project, @CheckForNull EnvVars additionalEnv) {
3973
EnvVars env = new EnvVars();
4074

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

80+
if (additionalEnv != null) {
81+
env.putAll(additionalEnv);
82+
}
83+
4684
RequiredResourcesProperty property = project.getProperty(RequiredResourcesProperty.class);
4785
if (property != null) return new LockableResourcesStruct(property, env);
4886

4987
return null;
5088
}
5189

90+
/**
91+
* Extract build parameters from a {@link Queue.Item} and return them as {@link EnvVars}
92+
* so that {@code ${PARAM}} references in resource names, labels and numbers are expanded.
93+
*/
94+
@NonNull
95+
@Restricted(NoExternalUse.class)
96+
public static EnvVars getParametersAsEnvVars(@NonNull Queue.Item item) {
97+
EnvVars env = new EnvVars();
98+
List<ParametersAction> paramActions = item.getActions(ParametersAction.class);
99+
for (ParametersAction action : paramActions) {
100+
if (action == null) continue;
101+
for (ParameterValue p : action.getParameters()) {
102+
if (p == null) continue;
103+
Object value = p.getValue();
104+
if (value != null) {
105+
env.put(p.getName(), value.toString());
106+
}
107+
}
108+
}
109+
return env;
110+
}
111+
112+
/**
113+
* Returns {@code true} when the given string contains at least one {@code ${...}} variable
114+
* reference that will be resolved at build time.
115+
*/
116+
@Restricted(NoExternalUse.class)
117+
public static boolean containsVariable(@CheckForNull String value) {
118+
return value != null && VARIABLE_PATTERN.matcher(value).find();
119+
}
120+
52121
public interface MatrixAssist {
53122
@NonNull
54123
Map<String, String> getCombination(@NonNull Job<?, ?> project);

src/main/resources/org/jenkins/plugins/lockableresources/Messages.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ LockStepResource.displayName=Resource
4040
LockableResource.displayName=Resource
4141
LockableResourcesManager.displayName=External Resources
4242
RequiredResourcesProperty.displayName=Required Lockable Resources
43+
# warnings (build-parameter references)
44+
warning.resourceNameContainsVariable=Resource name contains build parameter references. \
45+
Validation will occur at build time.
46+
warning.labelContainsVariable=Label contains build parameter references. \
47+
Validation will occur at build time.
48+
warning.resourceNumberContainsVariable=Resource number contains build parameter references. \
49+
Validation will occur at build time.
4350
UpdateLockStep.displayName=Update lockable resource
4451
# UpdateLockStep errors
4552
UpdateLockStep.error.resourceRequired=The resource name must be specified.

src/test/java/org/jenkins/plugins/lockableresources/FreeStyleProjectTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
import hudson.model.FreeStyleBuild;
1616
import hudson.model.FreeStyleProject;
1717
import hudson.model.Item;
18+
import hudson.model.ParametersDefinitionProperty;
1819
import hudson.model.Queue;
1920
import hudson.model.Result;
21+
import hudson.model.StringParameterDefinition;
2022
import hudson.model.User;
2123
import hudson.model.queue.QueueTaskFuture;
2224
import hudson.triggers.TimerTrigger;
@@ -267,6 +269,71 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
267269
j.waitForCompletion(fb2);
268270
}
269271

272+
// ---------------------------------------------------------------------------
273+
// Parameterized resource tests (build parameters as resource / label / number)
274+
// ---------------------------------------------------------------------------
275+
276+
@Test
277+
void parameterizedResourceName(JenkinsRule j) throws Exception {
278+
LockableResourcesManager.get().createResource("my-resource");
279+
280+
FreeStyleProject p = j.createFreeStyleProject("paramResourceName");
281+
p.addProperty(new RequiredResourcesProperty("${RESOURCE_NAME}", "resourceNameVar", null, null, null));
282+
p.addProperty(new ParametersDefinitionProperty(
283+
new StringParameterDefinition("RESOURCE_NAME", "my-resource", "Resource to lock")));
284+
p.getBuildersList().add(new PrinterBuilder());
285+
286+
FreeStyleBuild b = p.scheduleBuild2(0).get();
287+
j.assertLogContains("acquired lock on [my-resource]", b);
288+
j.assertLogContains("resourceNameVar: my-resource", b);
289+
j.assertBuildStatus(Result.SUCCESS, b);
290+
}
291+
292+
@Test
293+
void parameterizedLabel(JenkinsRule j) throws Exception {
294+
LockableResourcesManager.get().createResourceWithLabel("res1", "team-alpha");
295+
LockableResourcesManager.get().createResourceWithLabel("res2", "team-alpha");
296+
297+
FreeStyleProject p = j.createFreeStyleProject("paramLabel");
298+
p.addProperty(new RequiredResourcesProperty(null, "resourceNameVar", "1", "${LABEL}", null));
299+
p.addProperty(
300+
new ParametersDefinitionProperty(new StringParameterDefinition("LABEL", "team-alpha", "Label to use")));
301+
302+
FreeStyleBuild b = p.scheduleBuild2(0).get();
303+
j.assertLogContains("acquired lock on", b);
304+
j.assertBuildStatus(Result.SUCCESS, b);
305+
}
306+
307+
@Test
308+
void parameterizedResourceNumber(JenkinsRule j) throws Exception {
309+
LockableResourcesManager.get().createResourceWithLabel("pool1", "pool");
310+
LockableResourcesManager.get().createResourceWithLabel("pool2", "pool");
311+
LockableResourcesManager.get().createResourceWithLabel("pool3", "pool");
312+
313+
FreeStyleProject p = j.createFreeStyleProject("paramNumber");
314+
p.addProperty(new RequiredResourcesProperty(null, "resourceNameVar", "${COUNT}", "pool", null));
315+
p.addProperty(
316+
new ParametersDefinitionProperty(new StringParameterDefinition("COUNT", "2", "How many resources")));
317+
318+
FreeStyleBuild b = p.scheduleBuild2(0).get();
319+
j.assertLogContains("acquired lock on", b);
320+
j.assertBuildStatus(Result.SUCCESS, b);
321+
322+
// Verify exactly 2 resources were locked via the variable
323+
String log = b.getLog();
324+
String varLine = null;
325+
for (String line : log.split("\n")) {
326+
if (line.contains("acquired lock on")) {
327+
varLine = line;
328+
break;
329+
}
330+
}
331+
assertNotNull(varLine, "Expected 'acquired lock on' in build log");
332+
// Count resource names in the log line (comma-separated inside brackets)
333+
long count = varLine.chars().filter(ch -> ch == ',').count() + 1;
334+
assertEquals(2, count, "Expected exactly 2 resources to be locked");
335+
}
336+
270337
public static class PrinterBuilder extends TestBuilder {
271338

272339
@Override

0 commit comments

Comments
 (0)