Skip to content

Commit 743db17

Browse files
committed
Introduced SCMBasedRetriever, permitting SCMRetriever to support clone
1 parent 0aa22f4 commit 743db17

File tree

13 files changed

+357
-298
lines changed

13 files changed

+357
-298
lines changed

src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryRetriever.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import hudson.util.FormValidation;
3636
import edu.umd.cs.findbugs.annotations.CheckForNull;
3737
import edu.umd.cs.findbugs.annotations.NonNull;
38+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3839
import hudson.slaves.WorkspaceList;
3940
import hudson.util.DirScanner;
4041
import hudson.util.FileVisitor;
@@ -58,6 +59,9 @@ public abstract class LibraryRetriever extends AbstractDescribableImpl<LibraryRe
5859
*/
5960
static final String ATTR_LIBRARY_NAME = "Jenkins-Library-Name";
6061

62+
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for write access via the Script Console")
63+
public static boolean INCLUDE_SRC_TEST_IN_LIBRARIES = Boolean.getBoolean(SCMSourceRetriever.class.getName() + ".INCLUDE_SRC_TEST_IN_LIBRARIES");
64+
6165
/**
6266
* Obtains library sources.
6367
* @param name the {@link LibraryConfiguration#getName}
@@ -109,7 +113,7 @@ static void dir2Jar(@NonNull String name, @NonNull FilePath dir, @NonNull FilePa
109113
@Override public void scan(File dir, FileVisitor visitor) throws IOException {
110114
scanSingle(new File(mf.getRemote()), JarFile.MANIFEST_NAME, visitor);
111115
String excludes;
112-
if (!SCMSourceRetriever.INCLUDE_SRC_TEST_IN_LIBRARIES && new File(dir, "src/test").isDirectory()) {
116+
if (!INCLUDE_SRC_TEST_IN_LIBRARIES && new File(dir, "src/test").isDirectory()) {
113117
excludes = "test/";
114118
listener.getLogger().println("Excluding src/test/ so that library test code cannot be accessed by Pipelines.");
115119
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.");
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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 hudson.AbortException;
30+
import hudson.Extension;
31+
import hudson.FilePath;
32+
import hudson.Functions;
33+
import hudson.Util;
34+
import hudson.model.Computer;
35+
import hudson.model.Item;
36+
import hudson.model.Job;
37+
import hudson.model.Node;
38+
import hudson.model.Run;
39+
import hudson.model.TaskListener;
40+
import hudson.model.TopLevelItem;
41+
import hudson.model.listeners.ItemListener;
42+
import hudson.scm.SCM;
43+
import hudson.slaves.WorkspaceList;
44+
import hudson.util.FormValidation;
45+
import java.io.File;
46+
import java.io.IOException;
47+
import java.io.InterruptedIOException;
48+
import java.util.Collections;
49+
import java.util.HashMap;
50+
import java.util.Map;
51+
import java.util.concurrent.Callable;
52+
import java.util.logging.Level;
53+
import java.util.logging.Logger;
54+
import java.util.regex.Pattern;
55+
import jenkins.model.Jenkins;
56+
import org.jenkinsci.plugins.workflow.steps.scm.GenericSCMStep;
57+
import org.jenkinsci.plugins.workflow.steps.scm.SCMStep;
58+
import org.kohsuke.accmod.Restricted;
59+
import org.kohsuke.accmod.restrictions.DoNotUse;
60+
import org.kohsuke.stapler.DataBoundSetter;
61+
import org.kohsuke.stapler.QueryParameter;
62+
import org.kohsuke.stapler.verb.POST;
63+
64+
/**
65+
* Functionality common to {@link SCMSourceRetriever} and {@link SCMRetriever}.
66+
*/
67+
public abstract class SCMBasedRetriever extends LibraryRetriever {
68+
69+
private static final Logger LOGGER = Logger.getLogger(SCMBasedRetriever.class.getName());
70+
71+
/**
72+
* Matches ".." in positions where it would be treated as the parent directory.
73+
*
74+
* <p>Used to prevent {@link #libraryPath} from being used for directory traversal.
75+
*/
76+
static final Pattern PROHIBITED_DOUBLE_DOT = Pattern.compile("(^|.*[\\\\/])\\.\\.($|[\\\\/].*)");
77+
78+
private boolean clone;
79+
80+
/**
81+
* The path to the library inside of the SCM.
82+
*
83+
* {@code null} is the default and means that the library is in the root of the repository. Otherwise, the value is
84+
* considered to be a relative path inside of the repository and always ends in a forward slash
85+
*
86+
* @see #setLibraryPath
87+
*/
88+
private @CheckForNull String libraryPath;
89+
90+
public boolean isClone() {
91+
return clone;
92+
}
93+
94+
@DataBoundSetter public void setClone(boolean clone) {
95+
this.clone = clone;
96+
}
97+
98+
public String getLibraryPath() {
99+
return libraryPath;
100+
}
101+
102+
@DataBoundSetter public void setLibraryPath(String libraryPath) {
103+
libraryPath = Util.fixEmptyAndTrim(libraryPath);
104+
if (libraryPath != null && !libraryPath.endsWith("/")) {
105+
libraryPath += '/';
106+
}
107+
this.libraryPath = libraryPath;
108+
}
109+
110+
protected final void doRetrieve(String name, boolean changelog, @NonNull SCM scm, FilePath target, Run<?, ?> run, TaskListener listener) throws Exception {
111+
if (libraryPath != null && PROHIBITED_DOUBLE_DOT.matcher(libraryPath).matches()) {
112+
throw new AbortException("Library path may not contain '..'");
113+
}
114+
if (clone && changelog) {
115+
listener.getLogger().println("WARNING: ignoring request to compute changelog in clone mode");
116+
changelog = false;
117+
}
118+
// Adapted from CpsScmFlowDefinition:
119+
SCMStep delegate = new GenericSCMStep(scm);
120+
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?
121+
delegate.setChangelog(changelog);
122+
Node node = Jenkins.get();
123+
if (clone) {
124+
FilePath tmp = target.sibling(target.getBaseName() + "-checkout");
125+
if (tmp == null) {
126+
throw new IOException();
127+
}
128+
try {
129+
retrySCMOperation(listener, () -> {
130+
delegate.checkout(run, tmp, listener, node.createLauncher(listener));
131+
return null;
132+
});
133+
LibraryRetriever.dir2Jar(name, libraryPath != null ? tmp.child(libraryPath) : tmp, target, listener);
134+
} finally {
135+
tmp.deleteRecursive();
136+
FilePath tmp2 = WorkspaceList.tempDir(tmp);
137+
if (tmp2 != null) {
138+
tmp2.deleteRecursive();
139+
}
140+
}
141+
} else {
142+
FilePath dir;
143+
if (run.getParent() instanceof TopLevelItem) {
144+
FilePath baseWorkspace = node.getWorkspaceFor((TopLevelItem) run.getParent());
145+
if (baseWorkspace == null) {
146+
throw new IOException(node.getDisplayName() + " may be offline");
147+
}
148+
String checkoutDirName = LibraryRecord.directoryNameFor(scm.getKey());
149+
dir = baseWorkspace.withSuffix(getFilePathSuffix() + "libs").child(checkoutDirName);
150+
} else { // should not happen, but just in case:
151+
throw new AbortException("Cannot check out in non-top-level build");
152+
}
153+
Computer computer = node.toComputer();
154+
if (computer == null) {
155+
throw new IOException(node.getDisplayName() + " may be offline");
156+
}
157+
try (WorkspaceList.Lease lease = computer.getWorkspaceList().allocate(dir)) {
158+
// Write the SCM key to a file as a debugging aid.
159+
lease.path.withSuffix("-scm-key.txt").write(scm.getKey(), "UTF-8");
160+
retrySCMOperation(listener, () -> {
161+
delegate.checkout(run, lease.path, listener, node.createLauncher(listener));
162+
return null;
163+
});
164+
// Cannot add WorkspaceActionImpl to private CpsFlowExecution.flowStartNodeActions; do we care?
165+
// Copy sources with relevant files from the checkout:
166+
LibraryRetriever.dir2Jar(name, libraryPath != null ? lease.path.child(libraryPath) : lease.path, target, listener);
167+
}
168+
}
169+
}
170+
171+
protected static <T> T retrySCMOperation(TaskListener listener, Callable<T> task) throws Exception{
172+
T ret = null;
173+
for (int retryCount = Jenkins.get().getScmCheckoutRetryCount(); retryCount >= 0; retryCount--) {
174+
try {
175+
ret = task.call();
176+
break;
177+
}
178+
catch (AbortException e) {
179+
// abort exception might have a null message.
180+
// If so, just skip echoing it.
181+
if (e.getMessage() != null) {
182+
listener.error(e.getMessage());
183+
}
184+
}
185+
catch (InterruptedIOException e) {
186+
throw e;
187+
}
188+
catch (Exception e) {
189+
// checkout error not yet reported
190+
Functions.printStackTrace(e, listener.error("Checkout failed"));
191+
}
192+
193+
if (retryCount == 0) // all attempts failed
194+
throw new AbortException("Maximum checkout retry attempts reached, aborting");
195+
196+
listener.getLogger().println("Retrying after 10 seconds");
197+
Thread.sleep(10000);
198+
}
199+
return ret;
200+
}
201+
202+
// TODO there is WorkspaceList.tempDir but no API to make other variants
203+
protected static String getFilePathSuffix() {
204+
return System.getProperty(WorkspaceList.class.getName(), "@");
205+
}
206+
207+
protected abstract static class SCMBasedRetrieverDescriptor extends LibraryRetrieverDescriptor {
208+
209+
@POST
210+
public FormValidation doCheckLibraryPath(@QueryParameter String libraryPath) {
211+
libraryPath = Util.fixEmptyAndTrim(libraryPath);
212+
if (libraryPath == null) {
213+
return FormValidation.ok();
214+
} else if (PROHIBITED_DOUBLE_DOT.matcher(libraryPath).matches()) {
215+
return FormValidation.error(Messages.SCMSourceRetriever_library_path_no_double_dot());
216+
}
217+
return FormValidation.ok();
218+
}
219+
220+
}
221+
222+
@Restricted(DoNotUse.class)
223+
@Extension
224+
public static class WorkspaceListener extends ItemListener {
225+
226+
@Override
227+
public void onDeleted(Item item) {
228+
deleteLibsDir(item, item.getFullName());
229+
}
230+
231+
@Override
232+
public void onLocationChanged(Item item, String oldFullName, String newFullName) {
233+
deleteLibsDir(item, oldFullName);
234+
}
235+
236+
private static void deleteLibsDir(Item item, String itemFullName) {
237+
if (item instanceof Job
238+
&& item.getClass()
239+
.getName()
240+
.equals("org.jenkinsci.plugins.workflow.job.WorkflowJob")) {
241+
synchronized (item) {
242+
String base =
243+
expandVariablesForDirectory(
244+
Jenkins.get().getRawWorkspaceDir(),
245+
itemFullName,
246+
item.getRootDir().getPath());
247+
FilePath dir =
248+
new FilePath(new File(base)).withSuffix(getFilePathSuffix() + "libs");
249+
try {
250+
if (dir.isDirectory()) {
251+
LOGGER.log(
252+
Level.INFO,
253+
() -> "Deleting obsolete library workspace " + dir);
254+
dir.deleteRecursive();
255+
}
256+
} catch (IOException | InterruptedException e) {
257+
LOGGER.log(
258+
Level.WARNING,
259+
e,
260+
() -> "Could not delete obsolete library workspace " + dir);
261+
}
262+
}
263+
}
264+
}
265+
266+
private static String expandVariablesForDirectory(
267+
String base, String itemFullName, String itemRootDir) {
268+
// If the item is moved, it is too late to look up its original workspace location by
269+
// the time we get the notification. See:
270+
// https://github.com/jenkinsci/jenkins/blob/f03183ab09ce5fb8f9f4cc9ccee42a3c3e6b2d3e/core/src/main/java/jenkins/model/Jenkins.java#L2567-L2576
271+
Map<String, String> properties = new HashMap<>();
272+
properties.put("JENKINS_HOME", Jenkins.get().getRootDir().getPath());
273+
properties.put("ITEM_ROOTDIR", itemRootDir);
274+
properties.put("ITEM_FULLNAME", itemFullName); // legacy, deprecated
275+
properties.put(
276+
"ITEM_FULL_NAME", itemFullName.replace(':', '$')); // safe, see JENKINS-12251
277+
return Util.replaceMacro(base, Collections.unmodifiableMap(properties));
278+
}
279+
}
280+
281+
}

src/main/java/org/jenkinsci/plugins/workflow/libs/SCMRetriever.java

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,34 +39,20 @@
3939
import hudson.util.FormValidation;
4040
import java.util.ArrayList;
4141
import java.util.List;
42-
import edu.umd.cs.findbugs.annotations.CheckForNull;
4342
import jenkins.model.Jenkins;
4443
import org.jenkinsci.Symbol;
4544
import org.kohsuke.accmod.Restricted;
4645
import org.kohsuke.accmod.restrictions.DoNotUse;
4746
import org.kohsuke.accmod.restrictions.NoExternalUse;
4847
import org.kohsuke.stapler.DataBoundConstructor;
49-
import org.kohsuke.stapler.DataBoundSetter;
50-
import org.kohsuke.stapler.QueryParameter;
51-
import org.kohsuke.stapler.verb.POST;
5248

5349
/**
5450
* Uses legacy {@link SCM} to check out sources based on variable interpolation.
5551
*/
56-
public class SCMRetriever extends LibraryRetriever {
52+
public class SCMRetriever extends SCMBasedRetriever {
5753

5854
private final SCM scm;
5955

60-
/**
61-
* The path to the library inside of the SCM.
62-
*
63-
* {@code null} is the default and means that the library is in the root of the repository. Otherwise, the value is
64-
* considered to be a relative path inside of the repository and always ends in a forward slash
65-
*
66-
* @see #setLibraryPath
67-
*/
68-
private @CheckForNull String libraryPath;
69-
7056
@DataBoundConstructor public SCMRetriever(SCM scm) {
7157
this.scm = scm;
7258
}
@@ -75,21 +61,8 @@ public SCM getScm() {
7561
return scm;
7662
}
7763

78-
public String getLibraryPath() {
79-
return libraryPath;
80-
}
81-
82-
@DataBoundSetter
83-
public void setLibraryPath(String libraryPath) {
84-
libraryPath = Util.fixEmptyAndTrim(libraryPath);
85-
if (libraryPath != null && !libraryPath.endsWith("/")) {
86-
libraryPath += '/';
87-
}
88-
this.libraryPath = libraryPath;
89-
}
90-
9164
@Override public void retrieveJar(String name, String version, boolean changelog, FilePath target, Run<?, ?> run, TaskListener listener) throws Exception {
92-
SCMSourceRetriever.doRetrieve(name, changelog, scm, libraryPath, target, run, listener, false); // TODO support clone
65+
doRetrieve(name, changelog, scm, target, run, listener);
9366
}
9467

9568
@Override public FormValidation validateVersion(String name, String version, Item context) {
@@ -101,12 +74,7 @@ public void setLibraryPath(String libraryPath) {
10174
}
10275

10376
@Symbol("legacySCM")
104-
@Extension(ordinal=-100) public static class DescriptorImpl extends LibraryRetrieverDescriptor {
105-
106-
@POST
107-
public FormValidation doCheckLibraryPath(@QueryParameter String libraryPath) {
108-
return SCMSourceRetriever.DescriptorImpl.checkLibraryPath(libraryPath);
109-
}
77+
@Extension(ordinal=-100) public static class DescriptorImpl extends SCMBasedRetrieverDescriptor {
11078

11179
@Override public String getDisplayName() {
11280
return "Legacy SCM";

0 commit comments

Comments
 (0)