Skip to content

Commit 4f59a0b

Browse files
yrumaxmb388a
authored andcommitted
Allow specific version to be deleted from cache
You can now specify a specific ref to be removed from the cache rather than having to remove all of them
1 parent bab4aa8 commit 4f59a0b

File tree

10 files changed

+113
-44
lines changed

10 files changed

+113
-44
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ static List<URL> retrieve(@NonNull LibraryRecord record, @NonNull LibraryRetriev
198198
retrieveLockFile.delete();
199199
}
200200
lastReadFile.touch(System.currentTimeMillis());
201-
versionCacheDir.withSuffix("-name.txt").write(name, "UTF-8");
201+
versionCacheDir.withSuffix("-name.txt").write(name + "@" + version, "UTF-8");
202202
versionCacheDir.copyRecursiveTo(libDir);
203203
} else {
204204
retriever.retrieve(name, version, changelog, libDir, run, listener);

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,13 @@ public LibraryCachingCleanup() {
2727
@Override protected void execute(TaskListener listener) throws IOException, InterruptedException {
2828
FilePath globalCacheDir = LibraryCachingConfiguration.getGlobalLibrariesCacheDir();
2929
for (FilePath library : globalCacheDir.list()) {
30-
if (!removeIfExpiredCacheDirectory(library)) {
31-
// Prior to the SECURITY-2586 fix, library caches had a two-level directory structure.
32-
// These caches will never be used again, so we delete any that we find.
33-
for (FilePath version: library.list()) {
34-
if (version.child(LibraryCachingConfiguration.LAST_READ_FILE).exists()) {
35-
library.deleteRecursive();
36-
break;
30+
for (FilePath versionDir : library.listDirectories()) {
31+
if (!removeIfExpiredCacheDirectory(versionDir)) {
32+
FilePath parent = versionDir.getParent();
33+
if (parent != null) {
34+
parent.deleteRecursive();
3735
}
36+
break;
3837
}
3938
}
4039
}
@@ -47,10 +46,12 @@ public LibraryCachingCleanup() {
4746
*/
4847
private boolean removeIfExpiredCacheDirectory(FilePath library) throws IOException, InterruptedException {
4948
final FilePath lastReadFile = new FilePath(library, LibraryCachingConfiguration.LAST_READ_FILE);
50-
if (lastReadFile.exists()) {
49+
if (lastReadFile.exists() && library.withSuffix("-name.txt").exists()) {
5150
if (System.currentTimeMillis() - lastReadFile.lastModified() > TimeUnit.DAYS.toMillis(EXPIRE_AFTER_READ_DAYS)) {
52-
library.deleteRecursive();
53-
library.withSuffix("-name.txt").delete();
51+
FilePath parent = library.getParent();
52+
if (parent != null) {
53+
parent.deleteRecursive();
54+
}
5455
}
5556
return true;
5657
}

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

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,29 +83,43 @@ public static FilePath getGlobalLibrariesCacheDir() {
8383
}
8484

8585
@Extension public static class DescriptorImpl extends Descriptor<LibraryCachingConfiguration> {
86-
public FormValidation doClearCache(@QueryParameter String name) throws InterruptedException {
86+
public FormValidation doClearCache(@QueryParameter String name, @QueryParameter String cachedLibraryRef) throws InterruptedException {
8787
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
88-
88+
String cacheDirName = null;
8989
try {
9090
if (LibraryCachingConfiguration.getGlobalLibrariesCacheDir().exists()) {
91-
for (FilePath libraryNamePath : LibraryCachingConfiguration.getGlobalLibrariesCacheDir().list("*-name.txt")) {
92-
// Libraries configured in distinct locations may have the same name. Since only admins are allowed here, this is not a huge issue, but it is probably unexpected.
93-
String cacheName;
94-
try (InputStream stream = libraryNamePath.read()) {
95-
cacheName = IOUtils.toString(stream, StandardCharsets.UTF_8);
96-
}
97-
if (libraryNamePath.readToString().equals(name)) {
98-
FilePath libraryCachePath = LibraryCachingConfiguration.getGlobalLibrariesCacheDir()
99-
.child(libraryNamePath.getName().replace("-name.txt", ""));
100-
libraryCachePath.deleteRecursive();
101-
libraryNamePath.delete();
91+
outer: for (FilePath libraryCache : LibraryCachingConfiguration.getGlobalLibrariesCacheDir().listDirectories()) {
92+
for (FilePath libraryNamePath : libraryCache.list("*-name.txt")) {
93+
if (libraryNamePath.readToString().startsWith(name + "@")) {
94+
FilePath libraryCachePath = libraryNamePath.getParent();
95+
if (libraryCachePath != null) {
96+
FilePath versionCachePath = new FilePath(libraryCachePath, libraryNamePath.getName().replace("-name.txt", ""));
97+
if (StringUtils.isNotEmpty(cachedLibraryRef)) {
98+
if (libraryNamePath.readToString().equals(name + "@" + cachedLibraryRef)) {
99+
cacheDirName = name + "@" + cachedLibraryRef;
100+
libraryNamePath.delete();
101+
versionCachePath.deleteRecursive();
102+
break outer;
103+
}
104+
} else {
105+
cacheDirName = name;
106+
libraryCachePath.deleteRecursive();
107+
break outer;
108+
}
109+
}
110+
}
102111
}
103112
}
104113
}
105114
} catch (IOException ex) {
106-
return FormValidation.error(ex, "The cache dir was not deleted successfully");
115+
return FormValidation.error(ex, String.format("The cache dir %s was not deleted successfully", cacheDirName));
116+
}
117+
118+
if (cacheDirName == null) {
119+
return FormValidation.ok(String.format("The version %s was not found for library %s.", cachedLibraryRef, name));
120+
} else {
121+
return FormValidation.ok(String.format("The cache dir %s was deleted successfully.", cacheDirName));
107122
}
108-
return FormValidation.ok("The cache dir was deleted successfully.");
109123
}
110124

111125
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package org.jenkinsci.plugins.workflow.libs;
2626

27+
import java.io.File;
2728
import java.util.Collections;
2829
import java.util.Set;
2930
import java.util.TreeSet;
@@ -62,7 +63,7 @@ public final class LibraryRecord {
6263
this.trusted = trusted;
6364
this.changelog = changelog;
6465
this.cachingConfiguration = cachingConfiguration;
65-
this.directoryName = directoryNameFor(name, version, String.valueOf(trusted), source);
66+
this.directoryName = directoryNameFor(name, String.valueOf(trusted), source) + File.separator + directoryNameFor(version);
6667
}
6768

6869
@Exported

src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryCachingConfiguration/config.jelly

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ THE SOFTWARE.
3232
<f:textbox />
3333
</f:entry>
3434
<j:if test="${h.hasPermission(app.ADMINISTER)}">
35-
<f:validateButton title="${%Clear cache}" progress="${%Clearing...}" method="clearCache" with="name" />
35+
<f:entry title="${%Clear cache for ref}" field="cachedLibraryRef">
36+
<f:textbox />
37+
</f:entry>
38+
<f:validateButton title="${%Clear cache}" progress="${%Clearing...}" method="clearCache" with="name,cachedLibraryRef" />
3639
</j:if>
3740
</j:jelly>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div>
2+
Specifies a specific version to clear the cache for. An empty value will clear the cache for all versions.
3+
</div>

src/test/java/org/jenkinsci/plugins/workflow/libs/LibraryCachingCleanupTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,16 @@ public void smokes() throws Throwable {
7171
assertThat(new File(cache.getRemote()), anExistingDirectory());
7272
// Run LibraryCachingCleanup and show that cache is not deleted.
7373
ExtensionList.lookupSingleton(LibraryCachingCleanup.class).execute(StreamTaskListener.fromStderr());
74+
assertThat(new File(cache.getParent().getRemote()), anExistingDirectory());
7475
assertThat(new File(cache.getRemote()), anExistingDirectory());
7576
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), anExistingFile());
7677
// Run LibraryCachingCleanup after modifying LAST_READ_FILE to be an old date and and show that cache is deleted.
7778
long oldMillis = ZonedDateTime.now().minusDays(LibraryCachingCleanup.EXPIRE_AFTER_READ_DAYS + 1).toInstant().toEpochMilli();
7879
cache.child(LibraryCachingConfiguration.LAST_READ_FILE).touch(oldMillis);
7980
ExtensionList.lookupSingleton(LibraryCachingCleanup.class).execute(StreamTaskListener.fromStderr());
80-
assertThat(new File(cache.getRemote()), not(anExistingDirectory()));
81-
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), not(anExistingDirectory()));
81+
assertThat(new File(cache.getParent().getRemote()), not(anExistingDirectory()));
82+
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), not(anExistingFile()));
83+
8284
}
8385

8486
@Test

src/test/java/org/jenkinsci/plugins/workflow/libs/LibraryCachingConfigurationTest.java

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import hudson.ExtensionList;
2828
import hudson.FilePath;
2929
import java.io.File;
30+
import java.util.Arrays;
31+
import java.util.List;
3032
import jenkins.plugins.git.GitSCMSource;
3133
import jenkins.plugins.git.GitSampleRepoRule;
3234
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
@@ -157,29 +159,68 @@ public void isExcluded() {
157159

158160
@Test
159161
public void clearCache() throws Exception {
162+
List<FilePath> caches = setupLibraryCaches();
163+
FilePath cache = caches.get(0);
164+
FilePath cache2 = caches.get(1);
165+
assertThat("Must be different paths", cache, not(equalTo(cache2)));
166+
assertThat(new File(cache.getParent().getRemote()), anExistingDirectory());
167+
assertThat(new File(cache.getRemote()), anExistingDirectory());
168+
assertThat(new File(cache2.getRemote()), anExistingDirectory());
169+
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), anExistingFile());
170+
assertThat(cache.withSuffix("-name.txt").readToString(), equalTo("library@master"));
171+
assertThat(cache2.withSuffix("-name.txt").readToString(), equalTo("library@feature/something"));
172+
// Clear the cache. TODO: Would be more realistic to set up security and use WebClient.
173+
ExtensionList.lookupSingleton(LibraryCachingConfiguration.DescriptorImpl.class).doClearCache("library", "");
174+
assertThat(new File(cache.getParent().getRemote()), not(anExistingDirectory()));
175+
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), not(anExistingFile()));
176+
}
177+
178+
@Test
179+
public void clearCacheVersion() throws Exception {
180+
181+
List<FilePath> caches = setupLibraryCaches();
182+
FilePath cache = caches.get(0);
183+
FilePath cache2 = caches.get(1);
184+
assertThat(new File(cache.getRemote()), anExistingDirectory());
185+
// Clear the cache. TODO: Would be more realistic to set up security and use WebClient.
186+
ExtensionList.lookupSingleton(LibraryCachingConfiguration.DescriptorImpl.class).doClearCache("library", "master");
187+
assertThat(new File(cache.getParent().getRemote()), anExistingDirectory());
188+
assertThat(new File(cache.getRemote()), not(anExistingDirectory()));
189+
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), not(anExistingFile()));
190+
//Other cache has not been touched
191+
assertThat(new File(cache2.getRemote()), anExistingDirectory());
192+
assertThat(new File(cache2.withSuffix("-name.txt").getRemote()), anExistingFile());
193+
}
194+
195+
196+
private List<FilePath> setupLibraryCaches() throws Exception {
160197
sampleRepo.init();
161198
sampleRepo.write("vars/foo.groovy", "def call() { echo 'foo' }");
162199
sampleRepo.git("add", "vars");
163200
sampleRepo.git("commit", "--message=init");
201+
sampleRepo.git("branch", "feature/something");
164202
LibraryConfiguration config = new LibraryConfiguration("library",
165203
new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)));
166204
config.setDefaultVersion("master");
167-
config.setImplicit(true);
205+
config.setImplicit(false);
168206
config.setCachingConfiguration(new LibraryCachingConfiguration(30, null));
207+
config.setAllowVersionOverride(true);
169208
GlobalLibraries.get().getLibraries().add(config);
170209
// Run build and check that cache gets created.
171210
WorkflowJob p = r.createProject(WorkflowJob.class);
172-
p.setDefinition(new CpsFlowDefinition("foo()", true));
211+
p.setDefinition(new CpsFlowDefinition("library identifier: 'library', changelog:false\n\nfoo()", true));
173212
WorkflowRun b = r.buildAndAssertSuccess(p);
213+
WorkflowJob p2 = r.createProject(WorkflowJob.class);
214+
p2.setDefinition(new CpsFlowDefinition("library identifier: 'library@feature/something', changelog:false\n\nfoo()", true));
215+
WorkflowRun b2 = r.buildAndAssertSuccess(p2);
174216
LibrariesAction action = b.getAction(LibrariesAction.class);
175217
LibraryRecord record = action.getLibraries().get(0);
218+
LibrariesAction action2 = b2.getAction(LibrariesAction.class);
219+
LibraryRecord record2 = action2.getLibraries().get(0);
220+
176221
FilePath cache = LibraryCachingConfiguration.getGlobalLibrariesCacheDir().child(record.getDirectoryName());
177-
assertThat(new File(cache.getRemote()), anExistingDirectory());
178-
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), anExistingFile());
179-
// Clear the cache. TODO: Would be more realistic to set up security and use WebClient.
180-
ExtensionList.lookupSingleton(LibraryCachingConfiguration.DescriptorImpl.class).doClearCache("library");
181-
assertThat(new File(cache.getRemote()), not(anExistingDirectory()));
182-
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), not(anExistingFile()));
222+
FilePath cache2 = LibraryCachingConfiguration.getGlobalLibrariesCacheDir().child(record2.getDirectoryName());
223+
224+
return Arrays.asList(cache, cache2);
183225
}
184-
185226
}

src/test/java/org/jenkinsci/plugins/workflow/libs/LibraryStepTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import hudson.plugins.git.SubmoduleConfig;
3434
import hudson.plugins.git.UserRemoteConfig;
3535
import hudson.plugins.git.extensions.GitSCMExtension;
36+
37+
import java.io.File;
3638
import java.util.Arrays;
3739
import java.util.Collections;
3840
import java.util.List;
@@ -99,14 +101,15 @@ public class LibraryStepTest {
99101
r.assertLogContains("ran library", b);
100102
LibrariesAction action = b.getAction(LibrariesAction.class);
101103
assertNotNull(action);
102-
String directoryName = LibraryRecord.directoryNameFor("stuff", "master", String.valueOf(true), GlobalLibraries.ForJob.class.getName());
104+
String directoryName = LibraryRecord.directoryNameFor("stuff", String.valueOf(true), GlobalLibraries.ForJob.class.getName()) + File.separator + LibraryRecord.directoryNameFor("master");
105+
103106
assertEquals("[LibraryRecord{name=stuff, version=master, variables=[x], trusted=true, changelog=true, cachingConfiguration=null, directoryName=" + directoryName + "}]", action.getLibraries().toString());
104107
p.setDefinition(new CpsFlowDefinition("library identifier: 'otherstuff@master', retriever: modernSCM([$class: 'GitSCMSource', remote: $/" + sampleRepo + "/$, credentialsId: '']), changelog: false; x()", true));
105108
b = r.buildAndAssertSuccess(p);
106109
r.assertLogContains("ran library", b);
107110
action = b.getAction(LibrariesAction.class);
108111
assertNotNull(action);
109-
directoryName = LibraryRecord.directoryNameFor("otherstuff", "master", String.valueOf(false), LibraryStep.class.getName() + " " + b.getExternalizableId());
112+
directoryName = LibraryRecord.directoryNameFor("otherstuff", String.valueOf(false), LibraryStep.class.getName() + " " + b.getExternalizableId()) + File.separator + LibraryRecord.directoryNameFor("master");
110113
assertEquals("[LibraryRecord{name=otherstuff, version=master, variables=[x], trusted=false, changelog=false, cachingConfiguration=null, directoryName=" + directoryName + "}]", action.getLibraries().toString());
111114
}
112115

src/test/java/org/jenkinsci/plugins/workflow/libs/ResourceStepTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public class ResourceStepTest {
195195
Path resourcesDir = Paths.get(sampleRepo.getRoot().getPath(), "resources");
196196
Files.createDirectories(resourcesDir);
197197
Path symlinkPath = Paths.get(resourcesDir.toString(), "master.key");
198-
Files.createSymbolicLink(symlinkPath, Paths.get("../../../../../../../secrets/master.key"));
198+
Files.createSymbolicLink(symlinkPath, Paths.get("../../../../../../../../secrets/master.key"));
199199

200200
sampleRepo.git("add", "src", "resources");
201201
sampleRepo.git("commit", "--message=init");
@@ -270,8 +270,9 @@ public void clearCache(String name) throws Exception {
270270
}
271271

272272
public void modifyCacheTimestamp(String name, String version, long timestamp) throws Exception {
273-
String cacheDirName = LibraryRecord.directoryNameFor(name, version, String.valueOf(true), GlobalLibraries.ForJob.class.getName());
274-
FilePath cacheDir = new FilePath(LibraryCachingConfiguration.getGlobalLibrariesCacheDir(), cacheDirName);
273+
String cacheDirName = LibraryRecord.directoryNameFor(name, String.valueOf(true), GlobalLibraries.ForJob.class.getName());
274+
FilePath libraryDir = new FilePath(LibraryCachingConfiguration.getGlobalLibrariesCacheDir(), cacheDirName);
275+
FilePath cacheDir = new FilePath(libraryDir, LibraryRecord.directoryNameFor(version));
275276
if (cacheDir.exists()) {
276277
cacheDir.touch(timestamp);
277278
}

0 commit comments

Comments
 (0)