Skip to content

Commit 0d00e64

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 0d00e64

File tree

10 files changed

+138
-64
lines changed

10 files changed

+138
-64
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: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
package org.jenkinsci.plugins.workflow.libs;
22

3-
import hudson.Extension;
4-
import hudson.FilePath;
5-
import hudson.model.AbstractDescribableImpl;
6-
import hudson.model.Descriptor;
7-
import hudson.util.FormValidation;
8-
import jenkins.model.Jenkins;
9-
import org.kohsuke.stapler.DataBoundConstructor;
10-
import org.kohsuke.stapler.QueryParameter;
11-
12-
import java.io.File;
133
import java.io.IOException;
144
import java.io.InputStream;
155
import java.nio.charset.StandardCharsets;
166
import java.util.Arrays;
7+
import java.util.Collection;
178
import java.util.Collections;
189
import java.util.List;
10+
import java.util.stream.Collectors;
11+
1912
import org.apache.commons.io.IOUtils;
2013
import org.apache.commons.lang.StringUtils;
14+
import org.kohsuke.stapler.DataBoundConstructor;
15+
import org.kohsuke.stapler.QueryParameter;
16+
17+
import hudson.Extension;
18+
import hudson.FilePath;
19+
import hudson.model.AbstractDescribableImpl;
20+
import hudson.model.Descriptor;
21+
import hudson.util.FormValidation;
22+
import jenkins.model.Jenkins;
2123

2224
public final class LibraryCachingConfiguration extends AbstractDescribableImpl<LibraryCachingConfiguration> {
2325
private int refreshTimeMinutes;
@@ -83,29 +85,43 @@ public static FilePath getGlobalLibrariesCacheDir() {
8385
}
8486

8587
@Extension public static class DescriptorImpl extends Descriptor<LibraryCachingConfiguration> {
86-
public FormValidation doClearCache(@QueryParameter String name) throws InterruptedException {
88+
public FormValidation doClearCache(@QueryParameter String name, @QueryParameter String cachedLibraryRef) throws InterruptedException {
8789
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
88-
90+
String cacheDirName = null;
8991
try {
9092
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();
93+
outer: for (FilePath libraryCache : LibraryCachingConfiguration.getGlobalLibrariesCacheDir().listDirectories()) {
94+
for (FilePath libraryNamePath : libraryCache.list("*-name.txt")) {
95+
if (libraryNamePath.readToString().startsWith(name + "@")) {
96+
FilePath libraryCachePath = libraryNamePath.getParent();
97+
if (libraryCachePath != null) {
98+
FilePath versionCachePath = new FilePath(libraryCachePath, libraryNamePath.getName().replace("-name.txt", ""));
99+
if (cachedLibraryRef != null && !cachedLibraryRef.equals("")) {
100+
if (libraryNamePath.readToString().equals(name + "@" + cachedLibraryRef)) {
101+
cacheDirName = name + "@" + cachedLibraryRef;
102+
libraryNamePath.delete();
103+
versionCachePath.deleteRecursive();
104+
break outer;
105+
}
106+
} else {
107+
cacheDirName = name;
108+
libraryCachePath.deleteRecursive();
109+
break outer;
110+
}
111+
}
112+
}
102113
}
103114
}
104115
}
105116
} catch (IOException ex) {
106-
return FormValidation.error(ex, "The cache dir was not deleted successfully");
117+
return FormValidation.error(ex, String.format("The cache dir %s was not deleted successfully", cacheDirName));
118+
}
119+
120+
if (cacheDirName == null) {
121+
return FormValidation.ok(String.format("The version %s was not found for library %s.", cachedLibraryRef, name));
122+
} else {
123+
return FormValidation.ok(String.format("The cache dir %s was deleted successfully.", cacheDirName));
107124
}
108-
return FormValidation.ok("The cache dir was deleted successfully.");
109125
}
110126

111127
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@
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;
30-
import jenkins.security.HMACConfidentialKey;
31+
3132
import org.kohsuke.stapler.export.Exported;
3233
import org.kohsuke.stapler.export.ExportedBean;
3334

35+
import jenkins.security.HMACConfidentialKey;
36+
3437
/**
3538
* Record of a library being used in a particular build.
3639
*/
@@ -62,7 +65,7 @@ public final class LibraryRecord {
6265
this.trusted = trusted;
6366
this.changelog = changelog;
6467
this.cachingConfiguration = cachingConfiguration;
65-
this.directoryName = directoryNameFor(name, version, String.valueOf(trusted), source);
68+
this.directoryName = directoryNameFor(name, String.valueOf(trusted), source) + File.separator + directoryNameFor(version);
6669
}
6770

6871
@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: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,19 @@
2424

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

27-
import hudson.ExtensionList;
28-
import hudson.FilePath;
27+
import static org.hamcrest.MatcherAssert.assertThat;
28+
import static org.hamcrest.Matchers.equalTo;
29+
import static org.hamcrest.Matchers.is;
30+
import static org.hamcrest.Matchers.not;
31+
import static org.hamcrest.io.FileMatchers.anExistingDirectory;
32+
import static org.hamcrest.io.FileMatchers.anExistingFile;
33+
import static org.junit.Assert.assertFalse;
34+
import static org.junit.Assert.assertTrue;
35+
2936
import java.io.File;
30-
import jenkins.plugins.git.GitSCMSource;
31-
import jenkins.plugins.git.GitSampleRepoRule;
37+
import java.util.Arrays;
38+
import java.util.List;
39+
3240
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
3341
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
3442
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
@@ -39,12 +47,10 @@
3947
import org.jvnet.hudson.test.JenkinsRule;
4048
import org.jvnet.hudson.test.WithoutJenkins;
4149

42-
import static org.hamcrest.MatcherAssert.*;
43-
import static org.hamcrest.Matchers.*;
44-
import static org.hamcrest.io.FileMatchers.anExistingDirectory;
45-
import static org.hamcrest.io.FileMatchers.anExistingFile;
46-
import static org.junit.Assert.assertFalse;
47-
import static org.junit.Assert.assertTrue;
50+
import hudson.ExtensionList;
51+
import hudson.FilePath;
52+
import jenkins.plugins.git.GitSCMSource;
53+
import jenkins.plugins.git.GitSampleRepoRule;
4854

4955
public class LibraryCachingConfigurationTest {
5056
@Rule
@@ -157,29 +163,68 @@ public void isExcluded() {
157163

158164
@Test
159165
public void clearCache() throws Exception {
166+
List<FilePath> caches = setupLibraryCaches();
167+
FilePath cache = caches.get(0);
168+
FilePath cache2 = caches.get(1);
169+
assertThat("Must be different paths", cache, not(equalTo(cache2)));
170+
assertThat(new File(cache.getParent().getRemote()), anExistingDirectory());
171+
assertThat(new File(cache.getRemote()), anExistingDirectory());
172+
assertThat(new File(cache2.getRemote()), anExistingDirectory());
173+
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), anExistingFile());
174+
assertThat(cache.withSuffix("-name.txt").readToString(), equalTo("library@master"));
175+
assertThat(cache2.withSuffix("-name.txt").readToString(), equalTo("library@feature/something"));
176+
// Clear the cache. TODO: Would be more realistic to set up security and use WebClient.
177+
ExtensionList.lookupSingleton(LibraryCachingConfiguration.DescriptorImpl.class).doClearCache("library", "");
178+
assertThat(new File(cache.getParent().getRemote()), not(anExistingDirectory()));
179+
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), not(anExistingFile()));
180+
}
181+
182+
@Test
183+
public void clearCacheVersion() throws Exception {
184+
185+
List<FilePath> caches = setupLibraryCaches();
186+
FilePath cache = caches.get(0);
187+
FilePath cache2 = caches.get(1);
188+
assertThat(new File(cache.getRemote()), anExistingDirectory());
189+
// Clear the cache. TODO: Would be more realistic to set up security and use WebClient.
190+
ExtensionList.lookupSingleton(LibraryCachingConfiguration.DescriptorImpl.class).doClearCache("library", "master");
191+
assertThat(new File(cache.getParent().getRemote()), anExistingDirectory());
192+
assertThat(new File(cache.getRemote()), not(anExistingDirectory()));
193+
assertThat(new File(cache.withSuffix("-name.txt").getRemote()), not(anExistingFile()));
194+
//Other cache has not been touched
195+
assertThat(new File(cache2.getRemote()), anExistingDirectory());
196+
assertThat(new File(cache2.withSuffix("-name.txt").getRemote()), anExistingFile());
197+
}
198+
199+
200+
private List<FilePath> setupLibraryCaches() throws Exception {
160201
sampleRepo.init();
161202
sampleRepo.write("vars/foo.groovy", "def call() { echo 'foo' }");
162203
sampleRepo.git("add", "vars");
163204
sampleRepo.git("commit", "--message=init");
205+
sampleRepo.git("branch", "feature/something");
164206
LibraryConfiguration config = new LibraryConfiguration("library",
165207
new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)));
166208
config.setDefaultVersion("master");
167-
config.setImplicit(true);
209+
config.setImplicit(false);
168210
config.setCachingConfiguration(new LibraryCachingConfiguration(30, null));
211+
config.setAllowVersionOverride(true);
169212
GlobalLibraries.get().getLibraries().add(config);
170213
// Run build and check that cache gets created.
171214
WorkflowJob p = r.createProject(WorkflowJob.class);
172-
p.setDefinition(new CpsFlowDefinition("foo()", true));
215+
p.setDefinition(new CpsFlowDefinition("library identifier: 'library', changelog:false\n\nfoo()", true));
173216
WorkflowRun b = r.buildAndAssertSuccess(p);
217+
WorkflowJob p2 = r.createProject(WorkflowJob.class);
218+
p2.setDefinition(new CpsFlowDefinition("library identifier: 'library@feature/something', changelog:false\n\nfoo()", true));
219+
WorkflowRun b2 = r.buildAndAssertSuccess(p2);
174220
LibrariesAction action = b.getAction(LibrariesAction.class);
175221
LibraryRecord record = action.getLibraries().get(0);
222+
LibrariesAction action2 = b2.getAction(LibrariesAction.class);
223+
LibraryRecord record2 = action2.getLibraries().get(0);
224+
176225
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()));
226+
FilePath cache2 = LibraryCachingConfiguration.getGlobalLibrariesCacheDir().child(record2.getDirectoryName());
227+
228+
return Arrays.asList(cache, cache2);
183229
}
184-
185230
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,14 @@ public class LibraryStepTest {
9999
r.assertLogContains("ran library", b);
100100
LibrariesAction action = b.getAction(LibrariesAction.class);
101101
assertNotNull(action);
102-
String directoryName = LibraryRecord.directoryNameFor("stuff", "master", String.valueOf(true), GlobalLibraries.ForJob.class.getName());
102+
String directoryName = LibraryRecord.directoryNameFor("stuff", String.valueOf(true), GlobalLibraries.ForJob.class.getName());
103103
assertEquals("[LibraryRecord{name=stuff, version=master, variables=[x], trusted=true, changelog=true, cachingConfiguration=null, directoryName=" + directoryName + "}]", action.getLibraries().toString());
104104
p.setDefinition(new CpsFlowDefinition("library identifier: 'otherstuff@master', retriever: modernSCM([$class: 'GitSCMSource', remote: $/" + sampleRepo + "/$, credentialsId: '']), changelog: false; x()", true));
105105
b = r.buildAndAssertSuccess(p);
106106
r.assertLogContains("ran library", b);
107107
action = b.getAction(LibrariesAction.class);
108108
assertNotNull(action);
109-
directoryName = LibraryRecord.directoryNameFor("otherstuff", "master", String.valueOf(false), LibraryStep.class.getName() + " " + b.getExternalizableId());
109+
directoryName = LibraryRecord.directoryNameFor("otherstuff", String.valueOf(false), LibraryStep.class.getName() + " " + b.getExternalizableId());
110110
assertEquals("[LibraryRecord{name=otherstuff, version=master, variables=[x], trusted=false, changelog=false, cachingConfiguration=null, directoryName=" + directoryName + "}]", action.getLibraries().toString());
111111
}
112112

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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)