Skip to content

Commit 9657f1d

Browse files
authored
Add updateLock pipeline step for resource management (#980)
* Add updateLock pipeline step for resource management This step allows pipelines to dynamically manage lockable resources: - Create new resources (createResource: true) - Delete existing resources (deleteResource: true) - Modify labels (setLabels, addLabels, removeLabels) - Set notes (setNote) Based on the original design from PR #305 by @gaspardpetit. Fixes #305 * Remove @SInCE TODO - not applicable for plugins * Add updateLock step documentation to README * fix: Add CSRF protection and permission checks to doCheck* methods Addresses security warnings from github-advanced-security[bot]: - Add @RequirePOST annotation to doCheckResource, doCheckAddLabels, doCheckRemoveLabels, and doCheckDeleteResource - Add @AncestorInPath Item parameter and permission checks to prevent unauthorized form validation calls
1 parent 57a4a43 commit 9657f1d

14 files changed

Lines changed: 945 additions & 0 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,46 @@ lock(resource: 'some_resource', skipIfLocked: true) {
184184
}
185185
```
186186

187+
#### Update resource properties
188+
189+
The `updateLock` step allows pipelines to dynamically manage lockable resources without using the Jenkins UI.
190+
191+
**Create a new resource:**
192+
193+
```groovy
194+
updateLock(resource: 'my-resource', createResource: true, setLabels: 'env-test team-a')
195+
```
196+
197+
**Modify labels on an existing resource:**
198+
199+
```groovy
200+
// Replace all labels
201+
updateLock(resource: 'my-resource', setLabels: 'new-label1 new-label2')
202+
203+
// Add labels (keeps existing)
204+
updateLock(resource: 'my-resource', addLabels: 'additional-label')
205+
206+
// Remove specific labels
207+
updateLock(resource: 'my-resource', removeLabels: 'old-label')
208+
209+
// Add and remove in one step
210+
updateLock(resource: 'my-resource', addLabels: 'new', removeLabels: 'old')
211+
```
212+
213+
**Set a note on a resource:**
214+
215+
```groovy
216+
updateLock(resource: 'my-resource', setNote: 'Updated by build #${BUILD_NUMBER}')
217+
```
218+
219+
**Delete a resource:**
220+
221+
```groovy
222+
updateLock(resource: 'my-resource', deleteResource: true)
223+
```
224+
225+
> **Note:** Resources cannot be deleted while locked, queued, or reserved.
226+
187227
Detailed documentation can be found as part of the
188228
[Pipeline Steps](https://jenkins.io/doc/pipeline/steps/lockable-resources/)
189229
documentation.
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2+
* Copyright (c) 2013, 6WIND S.A. All rights reserved. *
3+
* *
4+
* This file is part of the Jenkins Lockable Resources Plugin and is *
5+
* published under the MIT license. *
6+
* *
7+
* See the "LICENSE.txt" file for more information. *
8+
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
9+
package org.jenkins.plugins.lockableresources;
10+
11+
import edu.umd.cs.findbugs.annotations.CheckForNull;
12+
import edu.umd.cs.findbugs.annotations.NonNull;
13+
import hudson.Extension;
14+
import hudson.Util;
15+
import hudson.model.AutoCompletionCandidates;
16+
import hudson.model.Item;
17+
import hudson.model.TaskListener;
18+
import hudson.util.FormValidation;
19+
import java.io.Serializable;
20+
import java.util.Collections;
21+
import java.util.Set;
22+
import java.util.logging.Logger;
23+
import jenkins.model.Jenkins;
24+
import org.jenkinsci.plugins.workflow.steps.Step;
25+
import org.jenkinsci.plugins.workflow.steps.StepContext;
26+
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
27+
import org.jenkinsci.plugins.workflow.steps.StepExecution;
28+
import org.kohsuke.stapler.AncestorInPath;
29+
import org.kohsuke.stapler.DataBoundConstructor;
30+
import org.kohsuke.stapler.DataBoundSetter;
31+
import org.kohsuke.stapler.QueryParameter;
32+
import org.kohsuke.stapler.interceptor.RequirePOST;
33+
34+
/**
35+
* Pipeline step to update the definition of a lockable resource.
36+
*
37+
* <p>This step allows pipelines to:
38+
* <ul>
39+
* <li>Create new resources</li>
40+
* <li>Delete existing resources</li>
41+
* <li>Add, remove, or set labels on resources</li>
42+
* <li>Set notes on resources</li>
43+
* </ul>
44+
*/
45+
public class UpdateLockStep extends Step implements Serializable {
46+
47+
private static final Logger LOG = Logger.getLogger(UpdateLockStep.class.getName());
48+
private static final long serialVersionUID = -7955849755535282258L;
49+
50+
@CheckForNull
51+
private String resource = null;
52+
53+
@CheckForNull
54+
private String addLabels = null;
55+
56+
@CheckForNull
57+
private String setLabels = null;
58+
59+
@CheckForNull
60+
private String removeLabels = null;
61+
62+
@CheckForNull
63+
private String setNote = null;
64+
65+
private boolean createResource = false;
66+
private boolean deleteResource = false;
67+
68+
@DataBoundConstructor
69+
public UpdateLockStep() {
70+
// default constructor
71+
}
72+
73+
@CheckForNull
74+
public String getResource() {
75+
return resource;
76+
}
77+
78+
@DataBoundSetter
79+
public void setResource(String resource) {
80+
if (resource != null && !resource.trim().isEmpty()) {
81+
if (!resource.equals(resource.trim())) {
82+
LOG.warning("The provided 'resource' should not start or end with spaces.");
83+
}
84+
this.resource = resource.trim();
85+
}
86+
}
87+
88+
@CheckForNull
89+
public String getAddLabels() {
90+
return addLabels;
91+
}
92+
93+
@DataBoundSetter
94+
public void setAddLabels(String addLabels) {
95+
addLabels = Util.fixEmptyAndTrim(addLabels);
96+
if (addLabels != null) {
97+
this.addLabels = addLabels;
98+
}
99+
}
100+
101+
@CheckForNull
102+
public String getSetLabels() {
103+
return setLabels;
104+
}
105+
106+
@DataBoundSetter
107+
public void setSetLabels(String setLabels) {
108+
setLabels = Util.fixEmptyAndTrim(setLabels);
109+
if (setLabels != null) {
110+
this.setLabels = setLabels;
111+
}
112+
}
113+
114+
@CheckForNull
115+
public String getRemoveLabels() {
116+
return removeLabels;
117+
}
118+
119+
@DataBoundSetter
120+
public void setRemoveLabels(String removeLabels) {
121+
removeLabels = Util.fixEmptyAndTrim(removeLabels);
122+
if (removeLabels != null) {
123+
this.removeLabels = removeLabels;
124+
}
125+
}
126+
127+
@CheckForNull
128+
public String getSetNote() {
129+
return setNote;
130+
}
131+
132+
@DataBoundSetter
133+
public void setSetNote(String setNote) {
134+
setNote = Util.fixEmptyAndTrim(setNote);
135+
if (setNote != null) {
136+
this.setNote = setNote;
137+
}
138+
}
139+
140+
public boolean isCreateResource() {
141+
return createResource;
142+
}
143+
144+
@DataBoundSetter
145+
public void setCreateResource(boolean createResource) {
146+
this.createResource = createResource;
147+
}
148+
149+
public boolean isDeleteResource() {
150+
return deleteResource;
151+
}
152+
153+
@DataBoundSetter
154+
public void setDeleteResource(boolean deleteResource) {
155+
this.deleteResource = deleteResource;
156+
}
157+
158+
/**
159+
* Validates the step configuration.
160+
*
161+
* @throws IllegalArgumentException if the configuration is invalid
162+
*/
163+
public void validate() {
164+
if (Util.fixEmptyAndTrim(resource) == null) {
165+
throw new IllegalArgumentException(Messages.UpdateLockStep_error_resourceRequired());
166+
}
167+
if (deleteResource && createResource) {
168+
throw new IllegalArgumentException(Messages.UpdateLockStep_error_deleteAndCreateConflict());
169+
}
170+
if (deleteResource && (addLabels != null || setLabels != null || removeLabels != null || setNote != null)) {
171+
throw new IllegalArgumentException(Messages.UpdateLockStep_error_deleteWithOtherOptions());
172+
}
173+
if (setLabels != null && (addLabels != null || removeLabels != null)) {
174+
throw new IllegalArgumentException(Messages.UpdateLockStep_error_setLabelsConflict());
175+
}
176+
}
177+
178+
@Override
179+
public StepExecution start(StepContext context) {
180+
return new UpdateLockStepExecution(this, context);
181+
}
182+
183+
@Override
184+
public String toString() {
185+
StringBuilder sb =
186+
new StringBuilder("UpdateLockStep{resource='").append(resource).append("'");
187+
if (createResource) sb.append(", createResource=true");
188+
if (deleteResource) sb.append(", deleteResource=true");
189+
if (setLabels != null) sb.append(", setLabels='").append(setLabels).append("'");
190+
if (addLabels != null) sb.append(", addLabels='").append(addLabels).append("'");
191+
if (removeLabels != null)
192+
sb.append(", removeLabels='").append(removeLabels).append("'");
193+
if (setNote != null) sb.append(", setNote='").append(setNote).append("'");
194+
sb.append("}");
195+
return sb.toString();
196+
}
197+
198+
@Extension
199+
public static final class DescriptorImpl extends StepDescriptor {
200+
201+
@Override
202+
public String getFunctionName() {
203+
return "updateLock";
204+
}
205+
206+
@NonNull
207+
@Override
208+
public String getDisplayName() {
209+
return Messages.UpdateLockStep_displayName();
210+
}
211+
212+
@Override
213+
public boolean takesImplicitBlockArgument() {
214+
return false;
215+
}
216+
217+
@Override
218+
public Set<Class<?>> getRequiredContext() {
219+
return Collections.singleton(TaskListener.class);
220+
}
221+
222+
/**
223+
* Provides auto-completion for resource names.
224+
*/
225+
@RequirePOST
226+
public AutoCompletionCandidates doAutoCompleteResource(
227+
@QueryParameter String value, @AncestorInPath Item item) {
228+
return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value, item);
229+
}
230+
231+
/**
232+
* Validates the resource name.
233+
*/
234+
@RequirePOST
235+
public FormValidation doCheckResource(@QueryParameter String value, @AncestorInPath Item item) {
236+
if (item != null) {
237+
item.checkPermission(Item.CONFIGURE);
238+
} else {
239+
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
240+
}
241+
value = Util.fixEmptyAndTrim(value);
242+
if (value == null) {
243+
return FormValidation.error(Messages.UpdateLockStep_error_resourceRequired());
244+
}
245+
return FormValidation.ok();
246+
}
247+
248+
/**
249+
* Validates addLabels option - cannot be used with setLabels.
250+
*/
251+
@RequirePOST
252+
public FormValidation doCheckAddLabels(
253+
@QueryParameter String value, @QueryParameter String setLabels, @AncestorInPath Item item) {
254+
if (item != null) {
255+
item.checkPermission(Item.CONFIGURE);
256+
} else {
257+
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
258+
}
259+
return doCheckLabelOperation(value, setLabels);
260+
}
261+
262+
/**
263+
* Validates removeLabels option - cannot be used with setLabels.
264+
*/
265+
@RequirePOST
266+
public FormValidation doCheckRemoveLabels(
267+
@QueryParameter String value, @QueryParameter String setLabels, @AncestorInPath Item item) {
268+
if (item != null) {
269+
item.checkPermission(Item.CONFIGURE);
270+
} else {
271+
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
272+
}
273+
return doCheckLabelOperation(value, setLabels);
274+
}
275+
276+
private FormValidation doCheckLabelOperation(String value, String setLabels) {
277+
if (Util.fixEmptyAndTrim(value) != null && Util.fixEmptyAndTrim(setLabels) != null) {
278+
return FormValidation.error(Messages.UpdateLockStep_error_setLabelsConflict());
279+
}
280+
return FormValidation.ok();
281+
}
282+
283+
/**
284+
* Validates deleteResource option - cannot be combined with other modify options.
285+
*/
286+
@RequirePOST
287+
public FormValidation doCheckDeleteResource(
288+
@QueryParameter boolean value,
289+
@QueryParameter String setLabels,
290+
@QueryParameter String addLabels,
291+
@QueryParameter String removeLabels,
292+
@QueryParameter String setNote,
293+
@QueryParameter boolean createResource,
294+
@AncestorInPath Item item) {
295+
if (item != null) {
296+
item.checkPermission(Item.CONFIGURE);
297+
} else {
298+
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
299+
}
300+
if (!value) {
301+
return FormValidation.ok();
302+
}
303+
if (createResource) {
304+
return FormValidation.error(Messages.UpdateLockStep_error_deleteAndCreateConflict());
305+
}
306+
if (Util.fixEmptyAndTrim(setLabels) != null
307+
|| Util.fixEmptyAndTrim(addLabels) != null
308+
|| Util.fixEmptyAndTrim(removeLabels) != null
309+
|| Util.fixEmptyAndTrim(setNote) != null) {
310+
return FormValidation.error(Messages.UpdateLockStep_error_deleteWithOtherOptions());
311+
}
312+
return FormValidation.ok();
313+
}
314+
}
315+
}

0 commit comments

Comments
 (0)