Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
62562ac
Sketch of saving libraries as JAR files rather than unpacked
jglick Mar 3, 2023
c02ed47
Incremental deployment of https://github.com/jenkinsci/workflow-cps-p…
jglick Mar 3, 2023
e5e95d0
Avoiding an extraneous empty dir
jglick Mar 3, 2023
8598f5e
Nicer name for temp checkout dir
jglick Mar 3, 2023
10155ea
Simpler handling of `LoadedClasses.srcUrl`; no apparent need for `can…
jglick Mar 3, 2023
0a2ed44
e5e95d0d2f92f5091f1646915f82a88e7e2a5590 & 8598f5e4e032a64c48ae41df12…
jglick Mar 3, 2023
fec1f4e
Support for resuming builds
jglick Mar 3, 2023
3cd3102
Fixing `ResourceStep`
jglick Mar 3, 2023
e2b6276
Clearer name for `GLOBAL_LIBRARIES_DIR`
jglick Mar 3, 2023
f9be590
Switching `*-name.txt` to a manifest attribute; and fixes related to …
jglick Mar 3, 2023
a14ed45
Giving up on https://github.com/jenkinsci/workflow-cps-plugin/pull/66…
jglick Mar 4, 2023
1783fb0
May as well pick up https://github.com/jenkinsci/workflow-cps-plugin/…
jglick Mar 6, 2023
3062a97
Comment obsolete as of a14ed45
jglick Mar 6, 2023
859dad1
Properly migrating running builds crossing the update line
jglick Mar 6, 2023
6ba9d43
Restoring SECURITY-2479 defense (SECURITY-2476 is no longer vulnerabl…
jglick Mar 6, 2023
fa82612
`SCMSourceRetrieverTest.lease` highlighted that `dir2Jar` was imprope…
jglick Mar 6, 2023
75f2cae
Incremental build of https://github.com/jenkinsci/workflow-cps-plugin…
jglick Mar 6, 2023
5bbb1ed
Restoring support for `INCLUDE_SRC_TEST_IN_LIBRARIES`, as well as war…
jglick Mar 6, 2023
0a7ccb4
Adapting `cloneModeExcludeSrcTest` to revised message
jglick Mar 6, 2023
20729d2
Fixing `vars/*.txt`
jglick Mar 6, 2023
29a8848
`safeSymlinks` broken by requirement to have at least one source file
jglick Mar 6, 2023
af8414f
Fixing `LoadedLibraries`, though not yet actual replay
jglick Mar 6, 2023
894fbbb
https://github.com/jenkinsci/workflow-cps-plugin/pull/672 released
jglick Mar 6, 2023
38e609a
Removing comment satisfied by 859dad14fa0893fae45aecbd5532ed178c9f0d87
jglick Mar 6, 2023
2c181cc
Fixing replay
jglick Mar 6, 2023
f5b0866
Fixing `LibraryCachingConfigurationTest.clearCache`
jglick Mar 6, 2023
80395ee
`LibraryAdderTest.parallelBuildsDontInterfereWithExpiredCache` simila…
jglick Mar 6, 2023
c2c711e
Similarly `ResourceStepTest.cachingRefresh`
jglick Mar 6, 2023
81ca5a6
`symlinksInLibraryResourcesAreNotAllowedToEscapeWorkspaceContext` att…
jglick Mar 6, 2023
d1f3aca
SpotBugs
jglick Mar 6, 2023
840cd60
`LibraryCachingCleanup` was unreliable as it sometimes deleted the la…
jglick Mar 6, 2023
0aa22f4
Merged `doClone` back into `doRetrieve` for clarity
jglick Mar 6, 2023
743db17
Introduced `SCMBasedRetriever`, permitting `SCMRetriever` to support …
jglick Mar 6, 2023
444bde7
Also need `class="${descriptor.clazz}"`
jglick Mar 7, 2023
992eeaa
Skip new symlink unit tests on Windows
jglick Mar 7, 2023
b7bd774
Pick up https://github.com/jenkinsci/workflow-cps-plugin/pull/673
jglick Mar 7, 2023
10623dd
Also recommend `CloneOption.noTags`
jglick Mar 8, 2023
698ae68
`CloneOption.honorRefspec` can also be useful
jglick Mar 8, 2023
77aea3e
Migrate `LoadedClasses.srcUrl`, and switching persisted field to `Lib…
jglick Mar 9, 2023
2fb8ed9
SpotBugs
jglick Mar 9, 2023
6acba02
https://github.com/jenkinsci/workflow-cps-plugin/pull/673 released
jglick Mar 9, 2023
23125c9
Also delete `lastReadFile` when clearing cache
jglick Mar 13, 2023
68d2a33
Merge branch 'SCMSourceRetriever.clone' into dir2Jar
jglick Mar 24, 2023
d2c5eff
Merge branch 'dir2Jar' of https://github.com/jglick/pipeline-groovy-l…
jglick Mar 24, 2023
157d6f0
Removing some minor differences to `SCMSourceRetriever.clone`
jglick Mar 24, 2023
d608664
Merge branch 'master' of https://github.com/jenkinsci/pipeline-groovy…
jglick Apr 11, 2023
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.361.x</artifactId>
<version>1750.v0071fa_4c4a_e3</version>
<version>1886.va_11c9f461054</version>
<scope>import</scope>
<type>pom</type>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/
// not @Extension because these are instantiated programmatically
public class UserDefinedGlobalVariable extends GlobalVariable {
// TODO switch to URL
private final File help;
private final String name;

Expand Down
124 changes: 54 additions & 70 deletions src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryAdder.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

package org.jenkinsci.plugins.workflow.libs;

import hudson.AbortException;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.FilePath;
Expand All @@ -50,6 +49,9 @@
import java.util.logging.Logger;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
Expand Down Expand Up @@ -97,12 +99,9 @@
if (action != null) {
// Resuming a build, so just look up what we loaded before.
for (LibraryRecord record : action.getLibraries()) {
FilePath libDir = new FilePath(execution.getOwner().getRootDir()).child("libs/" + record.getDirectoryName());
for (String root : new String[] {"src", "vars"}) {
FilePath dir = libDir.child(root);
if (dir.isDirectory()) {
additions.add(new Addition(dir.toURI().toURL(), record.trusted));
}
FilePath libJar = new FilePath(execution.getOwner().getRootDir()).child("libs/" + record.getDirectoryName() + ".jar");
if (libJar.exists()) {
additions.add(new Addition(libJar.toURI().toURL(), record.trusted));
}
String unparsed = librariesUnparsed.get(record.name);
if (unparsed != null) {
Expand Down Expand Up @@ -147,9 +146,7 @@
// Now actually try to retrieve the libraries.
for (LibraryRecord record : librariesAdded.values()) {
listener.getLogger().println("Loading library " + record.name + "@" + record.version);
for (URL u : retrieve(record, retrievers.get(record.name), listener, build, execution)) {
additions.add(new Addition(u, record.trusted));
}
additions.add(new Addition(retrieve(record, retrievers.get(record.name), listener, build, execution), record.trusted));
}
return additions;
}
Expand All @@ -169,14 +166,14 @@ private enum CacheStatus {
EXPIRED;
}

private static CacheStatus getCacheStatus(@NonNull LibraryCachingConfiguration cachingConfiguration, @NonNull final FilePath versionCacheDir)
private static CacheStatus getCacheStatus(@NonNull LibraryCachingConfiguration cachingConfiguration, @NonNull final FilePath versionCacheJar)
throws IOException, InterruptedException
{
if (cachingConfiguration.isRefreshEnabled()) {
final long cachingMilliseconds = cachingConfiguration.getRefreshTimeMilliseconds();

if(versionCacheDir.exists()) {
if ((versionCacheDir.lastModified() + cachingMilliseconds) > System.currentTimeMillis()) {
if(versionCacheJar.exists()) {
if ((versionCacheJar.lastModified() + cachingMilliseconds) > System.currentTimeMillis()) {
return CacheStatus.VALID;
} else {
return CacheStatus.EXPIRED;
Expand All @@ -185,7 +182,7 @@ private static CacheStatus getCacheStatus(@NonNull LibraryCachingConfiguration c
return CacheStatus.DOES_NOT_EXIST;
}
} else {
if (versionCacheDir.exists()) {
if (versionCacheJar.exists()) {
return CacheStatus.VALID;
} else {
return CacheStatus.DOES_NOT_EXIST;
Expand All @@ -194,16 +191,16 @@ private static CacheStatus getCacheStatus(@NonNull LibraryCachingConfiguration c
}

/** Retrieve library files. */
static List<URL> retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriever retriever, @NonNull TaskListener listener, @NonNull Run<?,?> run, @NonNull CpsFlowExecution execution) throws Exception {
static URL retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriever retriever, @NonNull TaskListener listener, @NonNull Run<?,?> run, @NonNull CpsFlowExecution execution) throws Exception {
String name = record.name;
String version = record.version;
boolean changelog = record.changelog;
LibraryCachingConfiguration cachingConfiguration = record.cachingConfiguration;
FilePath libDir = new FilePath(execution.getOwner().getRootDir()).child("libs/" + record.getDirectoryName());
FilePath libJar = new FilePath(execution.getOwner().getRootDir()).child("libs/" + record.getDirectoryName() + ".jar");
Boolean shouldCache = cachingConfiguration != null;
final FilePath versionCacheDir = new FilePath(LibraryCachingConfiguration.getGlobalLibrariesCacheDir(), record.getDirectoryName());
final FilePath versionCacheJar = new FilePath(LibraryCachingConfiguration.getGlobalLibrariesCacheDir(), record.getDirectoryName() + ".jar");
ReentrantReadWriteLock retrieveLock = getReadWriteLockFor(record.getDirectoryName());
final FilePath lastReadFile = new FilePath(versionCacheDir, LibraryCachingConfiguration.LAST_READ_FILE);
final FilePath lastReadFile = versionCacheJar.sibling(record.getDirectoryName() + "." + LibraryCachingConfiguration.LAST_READ_FILE);

if(shouldCache && cachingConfiguration.isExcluded(version)) {
listener.getLogger().println("Library " + name + "@" + version + " is excluded from caching.");
Expand All @@ -213,13 +210,13 @@ static List<URL> retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriev
if(shouldCache) {
retrieveLock.readLock().lockInterruptibly();
try {
CacheStatus cacheStatus = getCacheStatus(cachingConfiguration, versionCacheDir);
CacheStatus cacheStatus = getCacheStatus(cachingConfiguration, versionCacheJar);
if (cacheStatus == CacheStatus.DOES_NOT_EXIST || cacheStatus == CacheStatus.EXPIRED) {
retrieveLock.readLock().unlock();
retrieveLock.writeLock().lockInterruptibly();
try {
boolean retrieve = false;
switch (getCacheStatus(cachingConfiguration, versionCacheDir)) {
switch (getCacheStatus(cachingConfiguration, versionCacheJar)) {
case VALID:
listener.getLogger().println("Library " + name + "@" + version + " is cached. Copying from home.");
break;
Expand All @@ -229,18 +226,16 @@ static List<URL> retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriev
case EXPIRED:
long cachingMinutes = cachingConfiguration.getRefreshTimeMinutes();
listener.getLogger().println("Library " + name + "@" + version + " is due for a refresh after " + cachingMinutes + " minutes, clearing.");
if (versionCacheDir.exists()) {
versionCacheDir.deleteRecursive();
versionCacheDir.withSuffix("-name.txt").delete();
if (versionCacheJar.exists()) {
versionCacheJar.delete();
}
retrieve = true;
break;
}

if (retrieve) {
listener.getLogger().println("Caching library " + name + "@" + version);
versionCacheDir.mkdirs();
retriever.retrieve(name, version, changelog, versionCacheDir, run, listener);
retriever.retrieveJar(name, version, changelog, versionCacheJar, run, listener);
}
retrieveLock.readLock().lock();
} finally {
Expand All @@ -251,50 +246,41 @@ static List<URL> retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriev
}

lastReadFile.touch(System.currentTimeMillis());
versionCacheDir.withSuffix("-name.txt").write(name, "UTF-8");
versionCacheDir.copyRecursiveTo(libDir);
versionCacheJar.copyTo(libJar);
} finally {
retrieveLock.readLock().unlock();
}
} else {
retriever.retrieve(name, version, changelog, libDir, run, listener);
retriever.retrieveJar(name, version, changelog, libJar, run, listener);
}
// Write the user-provided name to a file as a debugging aid.
libDir.withSuffix("-name.txt").write(name, "UTF-8");

// Replace any classes requested for replay:
if (!record.trusted) {
for (String clazz : ReplayAction.replacementsIn(execution)) {
for (String root : new String[] {"src", "vars"}) {
String rel = root + "/" + clazz.replace('.', '/') + ".groovy";
FilePath f = libDir.child(rel);
if (f.exists()) {
String replacement = ReplayAction.replace(execution, clazz);
if (replacement != null) {
listener.getLogger().println("Replacing contents of " + rel);
f.write(replacement, null); // TODO as below, unsure of encoding used by Groovy compiler
}
String rel = clazz.replace('.', '/') + ".groovy";
/* TODO need to unpack & repack I guess
FilePath f = libDir.child(rel);
if (f.exists()) {
String replacement = ReplayAction.replace(execution, clazz);
if (replacement != null) {
listener.getLogger().println("Replacing contents of " + rel);
f.write(replacement, null); // TODO as below, unsure of encoding used by Groovy compiler
}
}
*/
}
}
List<URL> urls = new ArrayList<>();
FilePath srcDir = libDir.child("src");
if (srcDir.isDirectory()) {
urls.add(srcDir.toURI().toURL());
}
FilePath varsDir = libDir.child("vars");
if (varsDir.isDirectory()) {
urls.add(varsDir.toURI().toURL());
for (FilePath var : varsDir.list("*.groovy")) {
record.variables.add(var.getBaseName());
}
}
if (urls.isEmpty()) {
throw new AbortException("Library " + name + " expected to contain at least one of src or vars directories");
try (JarFile jf = new JarFile(libJar.getRemote())) {
jf.stream().forEach(entry -> {
Matcher m = ROOT_GROOVY_SOURCE.matcher(entry.getName());
if (m.matches()) {
record.variables.add(m.group(1));
}
});
}
return urls;
return libJar.toURI().toURL();
}
private static final Pattern ROOT_GROOVY_SOURCE = Pattern.compile("([^/]+)[.]groovy");

/**
* Loads resources for {@link ResourceStep}.
Expand All @@ -311,29 +297,25 @@ static List<URL> retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriev
if (action != null) {
FilePath libs = new FilePath(run.getRootDir()).child("libs");
for (LibraryRecord library : action.getLibraries()) {
FilePath libResources = libs.child(library.getDirectoryName() + "/resources/");
FilePath f = libResources.child(name);
if (!new File(f.getRemote()).getCanonicalFile().toPath().startsWith(new File(libResources.getRemote()).getCanonicalPath())) {
throw new AbortException(name + " references a file that is not contained within the library: " + library.name);
} else if (f.exists()) {
resources.put(library.name, readResource(f, encoding));
FilePath libJar = libs.child(library.getDirectoryName() + ".jar");
try (JarFile jf = new JarFile(libJar.getRemote())) {
JarEntry je = jf.getJarEntry("resources/" + name);
if (je != null) {
try (InputStream in = jf.getInputStream(je)) {
if ("Base64".equals(encoding)) {
resources.put(library.name, Base64.getEncoder().encodeToString(IOUtils.toByteArray(in)));
} else {
resources.put(library.name, IOUtils.toString(in, encoding)); // The platform default is used if encoding is null.
}
}
}
}
}
}
}
return resources;
}

private static String readResource(FilePath file, @CheckForNull String encoding) throws IOException, InterruptedException {
try (InputStream in = file.read()) {
if ("Base64".equals(encoding)) {
return Base64.getEncoder().encodeToString(IOUtils.toByteArray(in));
} else {
return IOUtils.toString(in, encoding); // The platform default is used if encoding is null.
}
}
}

@Extension public static class GlobalVars extends GlobalVariableSet {

@Override public Collection<GlobalVariable> forRun(Run<?,?> run) {
Expand All @@ -347,6 +329,7 @@ private static String readResource(FilePath file, @CheckForNull String encoding)
List<GlobalVariable> vars = new ArrayList<>();
for (LibraryRecord library : action.getLibraries()) {
for (String variable : library.variables) {
// TODO pass URL of *.jar!/$variable.txt
vars.add(new UserDefinedGlobalVariable(variable, new File(run.getRootDir(), "libs/" + library.getDirectoryName() + "/vars/" + variable + ".txt")));
}
}
Expand All @@ -367,6 +350,7 @@ private static String readResource(FilePath file, @CheckForNull String encoding)
Run<?,?> run = (Run) executable;
LibrariesAction action = run.getAction(LibrariesAction.class);
if (action != null) {
// TODO handle *.jar
FilePath libs = new FilePath(run.getRootDir()).child("libs");
for (LibraryRecord library : action.getLibraries()) {
if (library.trusted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import jenkins.model.Jenkins;

import jenkins.util.SystemProperties;

Expand All @@ -27,41 +28,28 @@ public LibraryCachingCleanup() {

@Override protected void execute(TaskListener listener) throws IOException, InterruptedException {
FilePath globalCacheDir = LibraryCachingConfiguration.getGlobalLibrariesCacheDir();
for (FilePath library : globalCacheDir.list()) {
if (!removeIfExpiredCacheDirectory(library)) {
// Prior to the SECURITY-2586 fix, library caches had a two-level directory structure.
// These caches will never be used again, so we delete any that we find.
for (FilePath version: library.list()) {
if (version.child(LibraryCachingConfiguration.LAST_READ_FILE).exists()) {
library.deleteRecursive();
break;
}
}
}
for (FilePath libJar : globalCacheDir.list()) {
removeIfExpiredCacheJar(libJar);
}
// Old cache directory; format has changed, so just delete it:
Jenkins.get().getRootPath().child("global-libraries-cache").deleteRecursive();
}

/**
* Delete the specified cache directory if it is outdated.
* @return true if specified directory is a cache directory, regardless of whether it was outdated. Used to detect
* whether the cache was created before or after the fix for SECURITY-2586.
* Delete the specified cache JAR if it is outdated.
*/
private boolean removeIfExpiredCacheDirectory(FilePath library) throws IOException, InterruptedException {
final FilePath lastReadFile = new FilePath(library, LibraryCachingConfiguration.LAST_READ_FILE);
private void removeIfExpiredCacheJar(FilePath libJar) throws IOException, InterruptedException {
final FilePath lastReadFile = libJar.sibling(libJar.getBaseName() + "." + LibraryCachingConfiguration.LAST_READ_FILE);
if (lastReadFile.exists()) {
ReentrantReadWriteLock retrieveLock = LibraryAdder.getReadWriteLockFor(library.getName());
ReentrantReadWriteLock retrieveLock = LibraryAdder.getReadWriteLockFor(libJar.getBaseName());
retrieveLock.writeLock().lockInterruptibly();
try {
if (System.currentTimeMillis() - lastReadFile.lastModified() > TimeUnit.DAYS.toMillis(EXPIRE_AFTER_READ_DAYS)) {

library.deleteRecursive();
library.withSuffix("-name.txt").delete();
libJar.delete();
}
} finally {
retrieveLock.writeLock().unlock();
}
return true;
}
return false;
}
}
Loading