Skip to content

Commit 65a7d02

Browse files
authored
Merge branch 'master' into JENKINS-69731-scm-BRANCH_NAME
2 parents 5ab9713 + 24c19a8 commit 65a7d02

File tree

15 files changed

+539
-273
lines changed

15 files changed

+539
-273
lines changed

pom.xml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<parent>
2929
<groupId>org.jenkins-ci.plugins</groupId>
3030
<artifactId>plugin</artifactId>
31-
<version>4.54</version>
31+
<version>4.59</version>
3232
<relativePath/>
3333
</parent>
3434
<groupId>io.jenkins.plugins</groupId>
@@ -71,7 +71,7 @@
7171
<dependency>
7272
<groupId>io.jenkins.tools.bom</groupId>
7373
<artifactId>bom-2.361.x</artifactId>
74-
<version>1750.v0071fa_4c4a_e3</version>
74+
<version>1883.vcb_768a_7c3610</version>
7575
<scope>import</scope>
7676
<type>pom</type>
7777
</dependency>
@@ -190,6 +190,16 @@
190190
<groupId>org.jenkins-ci.plugins</groupId>
191191
<artifactId>subversion</artifactId>
192192
<scope>test</scope>
193+
<exclusions>
194+
<exclusion>
195+
<groupId>org.apache.sshd</groupId>
196+
<artifactId>sshd-common</artifactId>
197+
</exclusion>
198+
<exclusion>
199+
<groupId>org.apache.sshd</groupId>
200+
<artifactId>sshd-core</artifactId>
201+
</exclusion>
202+
</exclusions>
193203
</dependency>
194204
<dependency>
195205
<groupId>org.jenkins-ci.plugins</groupId>
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2023 CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package org.jenkinsci.plugins.workflow.libs;
26+
27+
import edu.umd.cs.findbugs.annotations.CheckForNull;
28+
import edu.umd.cs.findbugs.annotations.NonNull;
29+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
30+
import hudson.AbortException;
31+
import hudson.Extension;
32+
import hudson.FilePath;
33+
import hudson.Functions;
34+
import hudson.Util;
35+
import hudson.model.Computer;
36+
import hudson.model.Item;
37+
import hudson.model.Job;
38+
import hudson.model.Node;
39+
import hudson.model.Run;
40+
import hudson.model.TaskListener;
41+
import hudson.model.TopLevelItem;
42+
import hudson.model.listeners.ItemListener;
43+
import hudson.scm.SCM;
44+
import hudson.slaves.WorkspaceList;
45+
import hudson.util.FormValidation;
46+
import java.io.File;
47+
import java.io.IOException;
48+
import java.io.InterruptedIOException;
49+
import java.util.Collections;
50+
import java.util.HashMap;
51+
import java.util.List;
52+
import java.util.Map;
53+
import java.util.concurrent.Callable;
54+
import java.util.logging.Level;
55+
import java.util.logging.Logger;
56+
import java.util.regex.Pattern;
57+
import jenkins.model.Jenkins;
58+
import org.jenkinsci.plugins.workflow.steps.scm.GenericSCMStep;
59+
import org.jenkinsci.plugins.workflow.steps.scm.SCMStep;
60+
import org.kohsuke.accmod.Restricted;
61+
import org.kohsuke.accmod.restrictions.DoNotUse;
62+
import org.kohsuke.stapler.DataBoundSetter;
63+
import org.kohsuke.stapler.QueryParameter;
64+
import org.kohsuke.stapler.verb.POST;
65+
66+
/**
67+
* Functionality common to {@link SCMSourceRetriever} and {@link SCMRetriever}.
68+
*/
69+
public abstract class SCMBasedRetriever extends LibraryRetriever {
70+
71+
private static final Logger LOGGER = Logger.getLogger(SCMBasedRetriever.class.getName());
72+
73+
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for write access via the Script Console")
74+
public static boolean INCLUDE_SRC_TEST_IN_LIBRARIES = Boolean.getBoolean(SCMSourceRetriever.class.getName() + ".INCLUDE_SRC_TEST_IN_LIBRARIES");
75+
76+
/**
77+
* Matches ".." in positions where it would be treated as the parent directory.
78+
*
79+
* <p>Used to prevent {@link #libraryPath} from being used for directory traversal.
80+
*/
81+
static final Pattern PROHIBITED_DOUBLE_DOT = Pattern.compile("(^|.*[\\\\/])\\.\\.($|[\\\\/].*)");
82+
83+
private boolean clone;
84+
85+
/**
86+
* The path to the library inside of the SCM.
87+
*
88+
* {@code null} is the default and means that the library is in the root of the repository. Otherwise, the value is
89+
* considered to be a relative path inside of the repository and always ends in a forward slash
90+
*
91+
* @see #setLibraryPath
92+
*/
93+
private @CheckForNull String libraryPath;
94+
95+
public boolean isClone() {
96+
return clone;
97+
}
98+
99+
@DataBoundSetter public void setClone(boolean clone) {
100+
this.clone = clone;
101+
}
102+
103+
public String getLibraryPath() {
104+
return libraryPath;
105+
}
106+
107+
@DataBoundSetter public void setLibraryPath(String libraryPath) {
108+
libraryPath = Util.fixEmptyAndTrim(libraryPath);
109+
if (libraryPath != null && !libraryPath.endsWith("/")) {
110+
libraryPath += '/';
111+
}
112+
this.libraryPath = libraryPath;
113+
}
114+
115+
protected final void doRetrieve(String name, boolean changelog, @NonNull SCM scm, FilePath target, Run<?, ?> run, TaskListener listener) throws Exception {
116+
if (libraryPath != null && PROHIBITED_DOUBLE_DOT.matcher(libraryPath).matches()) {
117+
throw new AbortException("Library path may not contain '..'");
118+
}
119+
if (clone && changelog) {
120+
listener.getLogger().println("WARNING: ignoring request to compute changelog in clone mode");
121+
changelog = false;
122+
}
123+
// Adapted from CpsScmFlowDefinition:
124+
SCMStep delegate = new GenericSCMStep(scm);
125+
delegate.setPoll(false); // TODO we have no API for determining if a given SCMHead is branch-like or tag-like; would we want to turn on polling if the former?
126+
delegate.setChangelog(changelog);
127+
Node node = Jenkins.get();
128+
if (clone) {
129+
if (libraryPath == null) {
130+
retrySCMOperation(listener, () -> {
131+
delegate.checkout(run, target, listener, Jenkins.get().createLauncher(listener));
132+
WorkspaceList.tempDir(target).deleteRecursive();
133+
return null;
134+
});
135+
} else {
136+
FilePath root = target.child("root");
137+
retrySCMOperation(listener, () -> {
138+
delegate.checkout(run, root, listener, Jenkins.get().createLauncher(listener));
139+
WorkspaceList.tempDir(root).deleteRecursive();
140+
return null;
141+
});
142+
FilePath subdir = root.child(libraryPath);
143+
if (!subdir.isDirectory()) {
144+
throw new AbortException("Did not find " + libraryPath + " in checkout");
145+
}
146+
for (String content : List.of("src", "vars", "resources")) {
147+
FilePath contentDir = subdir.child(content);
148+
if (contentDir.isDirectory()) {
149+
LOGGER.fine(() -> "Moving " + content + " to top level in " + target);
150+
contentDir.renameTo(target.child(content));
151+
}
152+
}
153+
// root itself will be deleted below
154+
}
155+
if (!INCLUDE_SRC_TEST_IN_LIBRARIES) {
156+
FilePath srcTest = target.child("src/test");
157+
if (srcTest.isDirectory()) {
158+
listener.getLogger().println("Excluding src/test/ from checkout of " + scm.getKey() + " so that library test code cannot be accessed by Pipelines.");
159+
listener.getLogger().println("To remove this log message, move the test code outside of src/. To restore the previous behavior that allowed access to files in src/test/, pass -D" + SCMSourceRetriever.class.getName() + ".INCLUDE_SRC_TEST_IN_LIBRARIES=true to the java command used to start Jenkins.");
160+
srcTest.deleteRecursive();
161+
}
162+
}
163+
for (FilePath child : target.list()) {
164+
String subdir = child.getName();
165+
switch (subdir) {
166+
case "src":
167+
// TODO delete everything that is not *.groovy
168+
break;
169+
case "vars":
170+
// TODO delete everything that is not *.groovy or *.txt, incl. subdirs
171+
break;
172+
case "resources":
173+
// OK, leave it all
174+
break;
175+
default:
176+
child.deleteRecursive();
177+
LOGGER.fine(() -> "Deleted " + child);
178+
}
179+
}
180+
} else { // !clone
181+
FilePath dir;
182+
if (run.getParent() instanceof TopLevelItem) {
183+
FilePath baseWorkspace = node.getWorkspaceFor((TopLevelItem) run.getParent());
184+
if (baseWorkspace == null) {
185+
throw new IOException(node.getDisplayName() + " may be offline");
186+
}
187+
String checkoutDirName = LibraryRecord.directoryNameFor(scm.getKey());
188+
dir = baseWorkspace.withSuffix(getFilePathSuffix() + "libs").child(checkoutDirName);
189+
} else { // should not happen, but just in case:
190+
throw new AbortException("Cannot check out in non-top-level build");
191+
}
192+
Computer computer = node.toComputer();
193+
if (computer == null) {
194+
throw new IOException(node.getDisplayName() + " may be offline");
195+
}
196+
try (WorkspaceList.Lease lease = computer.getWorkspaceList().allocate(dir)) {
197+
// Write the SCM key to a file as a debugging aid.
198+
lease.path.withSuffix("-scm-key.txt").write(scm.getKey(), "UTF-8");
199+
retrySCMOperation(listener, () -> {
200+
delegate.checkout(run, lease.path, listener, node.createLauncher(listener));
201+
return null;
202+
});
203+
if (libraryPath == null) {
204+
libraryPath = ".";
205+
}
206+
String excludes = INCLUDE_SRC_TEST_IN_LIBRARIES ? null : "src/test/";
207+
if (lease.path.child(libraryPath).child("src/test").exists()) {
208+
listener.getLogger().println("Excluding src/test/ from checkout of " + scm.getKey() + " so that library test code cannot be accessed by Pipelines.");
209+
listener.getLogger().println("To remove this log message, move the test code outside of src/. To restore the previous behavior that allowed access to files in src/test/, pass -D" + SCMSourceRetriever.class.getName() + ".INCLUDE_SRC_TEST_IN_LIBRARIES=true to the java command used to start Jenkins.");
210+
}
211+
// Cannot add WorkspaceActionImpl to private CpsFlowExecution.flowStartNodeActions; do we care?
212+
// Copy sources with relevant files from the checkout:
213+
lease.path.child(libraryPath).copyRecursiveTo("src/**/*.groovy,vars/*.groovy,vars/*.txt,resources/", excludes, target);
214+
}
215+
}
216+
}
217+
218+
protected static <T> T retrySCMOperation(TaskListener listener, Callable<T> task) throws Exception{
219+
T ret = null;
220+
for (int retryCount = Jenkins.get().getScmCheckoutRetryCount(); retryCount >= 0; retryCount--) {
221+
try {
222+
ret = task.call();
223+
break;
224+
}
225+
catch (AbortException e) {
226+
// abort exception might have a null message.
227+
// If so, just skip echoing it.
228+
if (e.getMessage() != null) {
229+
listener.error(e.getMessage());
230+
}
231+
}
232+
catch (InterruptedIOException e) {
233+
throw e;
234+
}
235+
catch (Exception e) {
236+
// checkout error not yet reported
237+
Functions.printStackTrace(e, listener.error("Checkout failed"));
238+
}
239+
240+
if (retryCount == 0) // all attempts failed
241+
throw new AbortException("Maximum checkout retry attempts reached, aborting");
242+
243+
listener.getLogger().println("Retrying after 10 seconds");
244+
Thread.sleep(10000);
245+
}
246+
return ret;
247+
}
248+
249+
// TODO there is WorkspaceList.tempDir but no API to make other variants
250+
private static String getFilePathSuffix() {
251+
return System.getProperty(WorkspaceList.class.getName(), "@");
252+
}
253+
254+
protected abstract static class SCMBasedRetrieverDescriptor extends LibraryRetrieverDescriptor {
255+
256+
@POST
257+
public FormValidation doCheckLibraryPath(@QueryParameter String libraryPath) {
258+
libraryPath = Util.fixEmptyAndTrim(libraryPath);
259+
if (libraryPath == null) {
260+
return FormValidation.ok();
261+
} else if (PROHIBITED_DOUBLE_DOT.matcher(libraryPath).matches()) {
262+
return FormValidation.error(Messages.SCMSourceRetriever_library_path_no_double_dot());
263+
}
264+
return FormValidation.ok();
265+
}
266+
267+
}
268+
269+
@Restricted(DoNotUse.class)
270+
@Extension
271+
public static class WorkspaceListener extends ItemListener {
272+
273+
@Override
274+
public void onDeleted(Item item) {
275+
deleteLibsDir(item, item.getFullName());
276+
}
277+
278+
@Override
279+
public void onLocationChanged(Item item, String oldFullName, String newFullName) {
280+
deleteLibsDir(item, oldFullName);
281+
}
282+
283+
private static void deleteLibsDir(Item item, String itemFullName) {
284+
if (item instanceof Job
285+
&& item.getClass()
286+
.getName()
287+
.equals("org.jenkinsci.plugins.workflow.job.WorkflowJob")) {
288+
synchronized (item) {
289+
String base =
290+
expandVariablesForDirectory(
291+
Jenkins.get().getRawWorkspaceDir(),
292+
itemFullName,
293+
item.getRootDir().getPath());
294+
FilePath dir =
295+
new FilePath(new File(base)).withSuffix(getFilePathSuffix() + "libs");
296+
try {
297+
if (dir.isDirectory()) {
298+
LOGGER.log(
299+
Level.INFO,
300+
() -> "Deleting obsolete library workspace " + dir);
301+
dir.deleteRecursive();
302+
}
303+
} catch (IOException | InterruptedException e) {
304+
LOGGER.log(
305+
Level.WARNING,
306+
e,
307+
() -> "Could not delete obsolete library workspace " + dir);
308+
}
309+
}
310+
}
311+
}
312+
313+
private static String expandVariablesForDirectory(
314+
String base, String itemFullName, String itemRootDir) {
315+
// If the item is moved, it is too late to look up its original workspace location by
316+
// the time we get the notification. See:
317+
// https://github.com/jenkinsci/jenkins/blob/f03183ab09ce5fb8f9f4cc9ccee42a3c3e6b2d3e/core/src/main/java/jenkins/model/Jenkins.java#L2567-L2576
318+
Map<String, String> properties = new HashMap<>();
319+
properties.put("JENKINS_HOME", Jenkins.get().getRootDir().getPath());
320+
properties.put("ITEM_ROOTDIR", itemRootDir);
321+
properties.put("ITEM_FULLNAME", itemFullName); // legacy, deprecated
322+
properties.put(
323+
"ITEM_FULL_NAME", itemFullName.replace(':', '$')); // safe, see JENKINS-12251
324+
return Util.replaceMacro(base, Collections.unmodifiableMap(properties));
325+
}
326+
}
327+
328+
}

0 commit comments

Comments
 (0)