[JENKINS-75465] Delete RunIdMigrator as it has been 10 years since this migration (#10456)

This commit is contained in:
Kris Stern 2025-04-04 11:47:08 +08:00 committed by GitHub
commit dd0d68d5e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 12 additions and 888 deletions

View File

@ -97,7 +97,6 @@ import jenkins.model.JenkinsLocationConfiguration;
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.PeepholePermalink;
import jenkins.model.ProjectNamingStrategy;
import jenkins.model.RunIdMigrator;
import jenkins.model.lazy.LazyBuildMixIn;
import jenkins.scm.RunWithSCM;
import jenkins.security.HexStringConfidentialKey;
@ -192,10 +191,6 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
// this should have been DescribableList but now it's too late
protected CopyOnWriteList<JobProperty<? super JobT>> properties = new CopyOnWriteList<>();
@Restricted(NoExternalUse.class)
@SuppressFBWarnings(value = "PA_PUBLIC_PRIMITIVE_ATTRIBUTE", justification = "Preserve API compatibility")
public transient RunIdMigrator runIdMigrator;
protected Job(ItemGroup parent, String name) {
super(parent, name);
}
@ -206,20 +201,19 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
holdOffBuildUntilSave = holdOffBuildUntilUserSave;
}
@Override public void onCreatedFromScratch() {
super.onCreatedFromScratch();
runIdMigrator = new RunIdMigrator();
runIdMigrator.created(getBuildDir());
}
@Override
public void onLoad(ItemGroup<? extends Item> parent, String name)
throws IOException {
super.onLoad(parent, name);
File buildDir = getBuildDir();
runIdMigrator = new RunIdMigrator();
runIdMigrator.migrate(buildDir, Jenkins.get().getRootDir());
// see https://github.com/jenkinsci/jenkins/pull/10456#issuecomment-2748112449
// This code can be deleted after several Jenkins releases,
// when it is likely that everyone is running a version equal or higher to this version.
var buildDirPath = getBuildDir().toPath();
if (Files.deleteIfExists(buildDirPath.resolve("legacyIds"))) {
LOGGER.info("Deleting legacyIds file in " + buildDirPath + ". See https://issues.jenkins"
+ ".io/browse/JENKINS-75465 for more information.");
}
TextFile f = getNextBuildNumberFile();
if (f.exists()) {

View File

@ -45,10 +45,8 @@ import java.util.Objects;
import java.util.SortedMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.RunIdMigrator;
import jenkins.model.lazy.AbstractLazyLoadRunMap;
import jenkins.model.lazy.BuildReference;
import jenkins.model.lazy.LazyBuildMixIn;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@ -72,10 +70,6 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
private Constructor<R> cons;
/** Normally overwritten by {@link LazyBuildMixIn#onLoad} or {@link LazyBuildMixIn#onCreatedFromScratch}, in turn created during {@link Job#onLoad}. */
@Restricted(NoExternalUse.class)
public RunIdMigrator runIdMigrator = new RunIdMigrator();
// TODO: before first complete build
// patch up next/previous build link
@ -156,7 +150,6 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
@Override
public boolean removeValue(R run) {
run.dropLinks();
runIdMigrator.delete(dir, run.getId());
return super.removeValue(run);
}
@ -227,14 +220,13 @@ public final class RunMap<R extends Run<?, R>> extends AbstractLazyLoadRunMap<R>
return super._put(r);
}
@CheckForNull
@Override public R getById(String id) {
int n;
try {
n = Integer.parseInt(id);
} catch (NumberFormatException x) {
n = runIdMigrator.findNumber(id);
return getByNumber(Integer.parseInt(id));
} catch (NumberFormatException e) { // see https://issues.jenkins.io/browse/JENKINS-75476
return null;
}
return getByNumber(n);
}
/**

View File

@ -1,271 +0,0 @@
/*
* The MIT License
*
* Copyright 2014 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.FINER;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Util;
import hudson.model.Job;
import hudson.model.Run;
import hudson.util.AtomicFileWriter;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Converts legacy {@code builds} directories to the current format.
*
* There would be one instance associated with each {@link Job}, to retain ID build# mapping.
*
* The {@link Job#getBuildDir} is passed to every method call (rather than being cached) in case it is moved.
*/
@Restricted(NoExternalUse.class)
public final class RunIdMigrator {
private final DateFormat legacyIdFormatter = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
static final Logger LOGGER = Logger.getLogger(RunIdMigrator.class.getName());
private static final String MAP_FILE = "legacyIds";
/** avoids wasting a map for new jobs */
private static final Map<String, Integer> EMPTY = new TreeMap<>();
private @NonNull Map<String, Integer> idToNumber = EMPTY;
public RunIdMigrator() {}
/**
* @return whether there was a file to load
*/
private boolean load(File dir) {
File f = new File(dir, MAP_FILE);
if (!f.isFile()) {
return false;
}
if (f.length() == 0) {
return true;
}
idToNumber = new TreeMap<>();
try {
for (String line : Files.readAllLines(Util.fileToPath(f), StandardCharsets.UTF_8)) {
int i = line.indexOf(' ');
idToNumber.put(line.substring(0, i), Integer.parseInt(line.substring(i + 1)));
}
} catch (Exception x) { // IOException, IndexOutOfBoundsException, NumberFormatException
LOGGER.log(WARNING, "could not read from " + f, x);
}
return true;
}
private void save(File dir) {
File f = new File(dir, MAP_FILE);
try (AtomicFileWriter w = new AtomicFileWriter(f)) {
try {
synchronized (this) {
for (Map.Entry<String, Integer> entry : idToNumber.entrySet()) {
w.write(entry.getKey() + ' ' + entry.getValue() + '\n');
}
}
w.commit();
} finally {
w.abort();
}
} catch (IOException x) {
LOGGER.log(WARNING, "could not save changes to " + f, x);
}
}
/**
* Called when a job is first created.
* Just saves an empty marker indicating that this job needs no migration.
* @param dir as in {@link Job#getBuildDir}
*/
public void created(File dir) {
save(dir);
}
/**
* Perform one-time migration if this has not been done already.
* Where previously there would be a {@code 2014-01-02_03-04-05/build.xml} specifying {@code <number>99</number>} plus a symlink {@code 99 2014-01-02_03-04-05},
* after migration there will be just {@code 99/build.xml} specifying {@code <id>2014-01-02_03-04-05</id>} and {@code <timestamp></timestamp>} according to local time zone at time of migration.
* Newly created builds are untouched.
* Does not throw {@link IOException} since we make a best effort to migrate but do not consider it fatal to job loading if we cannot.
* @param dir as in {@link Job#getBuildDir}
* @param jenkinsHome root directory of Jenkins (for logging only)
* @return true if migration was performed
*/
public synchronized boolean migrate(File dir, @CheckForNull File jenkinsHome) {
if (load(dir)) {
LOGGER.log(FINER, "migration already performed for {0}", dir);
return false;
}
if (!dir.isDirectory()) {
LOGGER.log(/* normal during Job.movedTo */FINE, "{0} was unexpectedly missing", dir);
return false;
}
LOGGER.log(INFO, "Migrating build records in {0}. See https://www.jenkins.io/redirect/build-record-migration for more information.", dir);
doMigrate(dir);
save(dir);
return true;
}
private static final Pattern NUMBER_ELT = Pattern.compile("(?m)^ <number>(\\d+)</number>(\r?\n)");
private void doMigrate(File dir) {
idToNumber = new TreeMap<>();
File[] kids = dir.listFiles();
// Need to process symlinks first so we can rename to them.
List<File> kidsList = new ArrayList<>(Arrays.asList(kids));
Iterator<File> it = kidsList.iterator();
while (it.hasNext()) {
File kid = it.next();
String name = kid.getName();
try {
Integer.parseInt(name);
} catch (NumberFormatException x) {
LOGGER.log(FINE, "ignoring nonnumeric entry {0}", name);
continue;
}
try {
if (Util.isSymlink(kid)) {
LOGGER.log(FINE, "deleting build number symlink {0} → {1}", new Object[] {name, Util.resolveSymlink(kid)});
} else if (kid.isDirectory()) {
LOGGER.log(FINE, "ignoring build directory {0}", name);
continue;
} else {
LOGGER.log(WARNING, "need to delete anomalous file entry {0}", name);
}
Util.deleteFile(kid);
it.remove();
} catch (Exception x) {
LOGGER.log(WARNING, "failed to process " + kid, x);
}
}
it = kidsList.iterator();
while (it.hasNext()) {
File kid = it.next();
try {
String name = kid.getName();
try {
Integer.parseInt(name);
LOGGER.log(FINE, "skipping new build dir {0}", name);
continue;
} catch (NumberFormatException x) {
// OK, next
}
if (!kid.isDirectory()) {
LOGGER.log(FINE, "skipping non-directory {0}", name);
continue;
}
long timestamp;
try {
synchronized (legacyIdFormatter) {
timestamp = legacyIdFormatter.parse(name).getTime();
}
} catch (ParseException x) {
LOGGER.log(WARNING, "found unexpected dir {0}", name);
continue;
}
File buildXml = new File(kid, "build.xml");
if (!buildXml.isFile()) {
LOGGER.log(WARNING, "found no build.xml in {0}", name);
continue;
}
String xml = Files.readString(Util.fileToPath(buildXml), StandardCharsets.UTF_8);
Matcher m = NUMBER_ELT.matcher(xml);
if (!m.find()) {
LOGGER.log(WARNING, "could not find <number> in {0}/build.xml", name);
continue;
}
int number = Integer.parseInt(m.group(1));
String nl = m.group(2);
xml = m.replaceFirst(" <id>" + name + "</id>" + nl + " <timestamp>" + timestamp + "</timestamp>" + nl);
File newKid = new File(dir, Integer.toString(number));
move(kid, newKid);
Files.writeString(Util.fileToPath(newKid).resolve("build.xml"), xml, StandardCharsets.UTF_8);
LOGGER.log(FINE, "fully processed {0} → {1}", new Object[] {name, number});
idToNumber.put(name, number);
} catch (Exception x) {
LOGGER.log(WARNING, "failed to process " + kid, x);
}
}
}
/**
* Tries to move/rename a file from one path to another.
* Uses {@link java.nio.file.Files#move} when available.
* Does not use {@link java.nio.file.StandardCopyOption#REPLACE_EXISTING} or any other options.
* TODO candidate for moving to {@link Util}
*/
static void move(File src, File dest) throws IOException {
try {
Files.move(src.toPath(), dest.toPath());
} catch (IOException x) {
throw x;
} catch (RuntimeException x) {
throw new IOException(x);
}
}
/**
* Look up a historical run by ID.
* @param id a nonnumeric ID which may be a valid {@link Run#getId}
* @return the corresponding {@link Run#number}, or 0 if unknown
*/
public synchronized int findNumber(@NonNull String id) {
Integer number = idToNumber.get(id);
return number != null ? number : 0;
}
/**
* Delete the record of a build.
* @param dir as in {@link Job#getBuildDir}
* @param id a {@link Run#getId}
*/
public synchronized void delete(File dir, String id) {
if (idToNumber.remove(id) != null) {
save(dir);
}
}
}

View File

@ -49,7 +49,6 @@ import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.RunIdMigrator;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
@ -147,9 +146,6 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT, RunT> & Queue.Task &
return loadBuild(dir);
}
});
RunIdMigrator runIdMigrator = asJob().runIdMigrator;
assert runIdMigrator != null;
r.runIdMigrator = runIdMigrator;
return r;
}

View File

@ -1,47 +0,0 @@
<!--
The MIT License
Copyright (c) 2014, CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="Jenkins">
<l:header />
<l:main-panel>
<p>
To reverse the effect of build record migration, run the following command
on the server. See <a href="https://www.jenkins.io/redirect/build-record-migration">documentation</a>
for more details:
</p>
<table style="width:100%">
<tr>
<td style="line-height:2em; width:80%">
<input type="text" value="${it.command}" style="width:100%"/>
</td>
<td style="line-height:2em">
<l:copyButton text="${it.command}"/>
</td>
</tr>
</table>
</l:main-panel>
</l:layout>
</j:jelly>

View File

@ -1,24 +0,0 @@
# The MIT License
#
# Bulgarian translation: Copyright (c) 2016, Alexander Shopov <ash@kambanaria.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
Copied=\
Копирано

View File

@ -1,23 +0,0 @@
# The MIT License
#
# Copyright (c) 2017 Daniel Beck and a number of other of contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
Copied=Kopiert

View File

@ -1,24 +0,0 @@
# The MIT License
#
# Italian localization plugin for Jenkins
# Copyright © 2020 Alessandro Menti
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
Copied=Copiato

View File

@ -1,23 +0,0 @@
# The MIT License
#
# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number
# of other of contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

View File

@ -1,3 +0,0 @@
# This file is under the MIT License by authors
Copied=Ископирано

View File

@ -1,3 +0,0 @@
# This file is under the MIT License by authors
Copied=Kopierades

View File

@ -1,23 +0,0 @@
# The MIT License
#
# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
Copied=已複製

View File

@ -112,7 +112,6 @@
<Class name="jenkins.fingerprints.FingerprintStorage"/>
<Class name="jenkins.install.SetupWizard"/>
<Class name="jenkins.model.Jenkins"/>
<Class name="jenkins.model.RunIdMigrator"/>
<Class name="jenkins.mvn.SettingsPathHelper"/>
<Class name="jenkins.security.CustomClassFilter$Contributed"/>
<Class name="jenkins.security.ResourceDomainConfiguration"/>

View File

@ -1,319 +0,0 @@
/*
* The MIT License
*
* Copyright 2014 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import hudson.Functions;
import hudson.Util;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import org.apache.commons.io.FileUtils;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
public class RunIdMigratorTest {
@Rule public TemporaryFolder tmp = new TemporaryFolder();
private static TimeZone defaultTimezone;
/** Ensures that legacy timestamps are interpreted in a predictable time zone. */
@BeforeClass public static void timezone() {
defaultTimezone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));
}
@AfterClass public static void tearDown() {
TimeZone.setDefault(defaultTimezone);
}
// TODO could use LoggerRule only if it were extracted to an independent library
@BeforeClass public static void logging() {
RunIdMigrator.LOGGER.setLevel(Level.ALL);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
RunIdMigrator.LOGGER.addHandler(handler);
}
private RunIdMigrator migrator;
private File dir;
@Before public void init() {
migrator = new RunIdMigrator();
dir = tmp.getRoot();
}
@Test public void newJob() throws Exception {
migrator.created(dir);
assertEquals("{legacyIds=''}", summarize());
assertEquals(0, migrator.findNumber("whatever"));
migrator.delete(dir, "1");
migrator = new RunIdMigrator();
assertFalse(migrator.migrate(dir, null));
assertEquals("{legacyIds=''}", summarize());
}
@Test public void legacy() throws Exception {
assumeFalse("Symlinks don't work well on Windows", Functions.isWindows());
write(
"2014-01-02_03-04-05/build.xml",
"<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <number>99</number>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>");
link("99", "2014-01-02_03-04-05");
link("lastFailedBuild", "-1");
link("lastSuccessfulBuild", "99");
assertEquals(
"{2014-01-02_03-04-05={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <number>99</number>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, 99=→2014-01-02_03-04-05, lastFailedBuild=→-1, lastSuccessfulBuild=→99}",
summarize());
assertTrue(migrator.migrate(dir, null));
assertEquals(
"{99={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <id>2014-01-02_03-04-05</id>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, lastFailedBuild=→-1, lastSuccessfulBuild=→99, legacyIds='2014-01-02_03-04-05 99\n"
+ "'}",
summarize());
assertEquals(99, migrator.findNumber("2014-01-02_03-04-05"));
migrator = new RunIdMigrator();
assertFalse(migrator.migrate(dir, null));
assertEquals(99, migrator.findNumber("2014-01-02_03-04-05"));
migrator.delete(dir, "2014-01-02_03-04-05");
FileUtils.deleteDirectory(new File(dir, "99"));
new File(dir, "lastSuccessfulBuild").delete();
assertEquals("{lastFailedBuild=→-1, legacyIds=''}", summarize());
}
@Test public void reRunMigration() throws Exception {
assumeFalse("Symlinks don't work well on Windows", Functions.isWindows());
write("2014-01-02_03-04-04/build.xml", "<run>\n <number>98</number>\n</run>");
link("98", "2014-01-02_03-04-04");
write(
"99/build.xml",
"<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>");
link("lastFailedBuild", "-1");
link("lastSuccessfulBuild", "99");
assertEquals(
"{2014-01-02_03-04-04={build.xml='<run>\n"
+ " <number>98</number>\n"
+ "</run>'}, 98=→2014-01-02_03-04-04, 99={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, lastFailedBuild=→-1, lastSuccessfulBuild=→99}",
summarize());
assertTrue(migrator.migrate(dir, null));
assertEquals(
"{98={build.xml='<run>\n"
+ " <id>2014-01-02_03-04-04</id>\n"
+ " <timestamp>1388649844000</timestamp>\n"
+ "</run>'}, 99={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, lastFailedBuild=→-1, lastSuccessfulBuild=→99, legacyIds='2014-01-02_03-04-04 98\n"
+ "'}",
summarize());
}
@Test public void reverseImmediately() throws Exception {
assumeFalse("Symlinks don't work well on Windows", Functions.isWindows());
File root = dir;
dir = new File(dir, "jobs/somefolder/jobs/someproject/promotions/OK/builds");
write(
"99/build.xml",
"<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <id>2014-01-02_03-04-05</id>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>");
link("lastFailedBuild", "-1");
link("lastSuccessfulBuild", "99");
write("legacyIds", "2014-01-02_03-04-05 99\n");
assertEquals(
"{99={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <id>2014-01-02_03-04-05</id>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, lastFailedBuild=→-1, lastSuccessfulBuild=→99, legacyIds='2014-01-02_03-04-05 99\n"
+ "'}",
summarize());
}
@Test public void reverseAfterNewBuilds() throws Exception {
assumeFalse("Symlinks don't work well on Windows", Functions.isWindows());
File root = dir;
dir = new File(dir, "jobs/someproject/modules/test$test/builds");
write(
"1/build.xml",
"<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>");
write("legacyIds", "");
assertEquals(
"{1={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, legacyIds=''}",
summarize());
}
@Test public void reverseMatrixAfterNewBuilds() throws Exception {
assumeFalse("Symlinks don't work well on Windows", Functions.isWindows());
File root = dir;
dir = new File(dir, "jobs/someproject/Environment=prod/builds");
write(
"1/build.xml",
"<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>");
write("legacyIds", "");
assertEquals(
"{1={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, legacyIds=''}",
summarize());
}
@Test public void reverseMavenAfterNewBuilds() throws Exception {
assumeFalse("Symlinks don't work well on Windows", Functions.isWindows());
File root = dir;
dir = new File(dir, "jobs/someproject/test$test/builds");
write(
"1/build.xml",
"<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>");
write("legacyIds", "");
assertEquals(
"{1={build.xml='<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<run>\n"
+ " <stuff>ok</stuff>\n"
+ " <timestamp>1388649845000</timestamp>\n"
+ " <otherstuff>ok</otherstuff>\n"
+ "</run>'}, legacyIds=''}",
summarize());
}
// TODO test sane recovery from various error conditions
private void write(String file, String text) throws Exception {
Path path = new File(dir, file).toPath();
Files.createDirectories(path.getParent());
Files.writeString(path, text, Charset.defaultCharset());
}
private void link(String symlink, String dest) throws Exception {
Util.createSymlink(dir, dest, symlink, new StreamTaskListener(System.out, Charset.defaultCharset()));
}
private String summarize() throws Exception {
return summarize(dir);
}
private static String summarize(File dir) throws Exception {
File[] kids = dir.listFiles();
Map<String, String> m = new TreeMap<>();
for (File kid : kids) {
String notation;
String symlink = Util.resolveSymlink(kid);
if (symlink != null) {
notation = "" + symlink;
} else if (kid.isFile()) {
notation = "'" + Files.readString(kid.toPath(), Charset.defaultCharset()) + "'";
} else if (kid.isDirectory()) {
notation = summarize(kid);
} else {
notation = "?";
}
m.put(kid.getName(), notation);
}
return m.toString();
}
@Test public void move() throws Exception {
File src = tmp.newFile();
File dest = new File(tmp.getRoot(), "dest");
RunIdMigrator.move(src, dest);
File dest2 = tmp.newFile();
assertThrows(IOException.class, () -> RunIdMigrator.move(dest, dest2));
}
}

View File

@ -1,97 +0,0 @@
/*
* The MIT License
*
* Copyright 2023 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model;
import static hudson.cli.CLICommandInvoker.Matcher.succeededSilently;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import hudson.cli.CLICommandInvoker;
import hudson.cli.CreateJobCommand;
import hudson.model.FreeStyleProject;
import hudson.model.Item;
import hudson.model.User;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import org.htmlunit.HttpMethod;
import org.htmlunit.WebRequest;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;
public class RunIdMigratorTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Test
public void legacyIdsPresent() throws Exception {
FreeStyleProject p = j.createFreeStyleProject();
File legacyIds = new File(p.getBuildDir(), "legacyIds");
assertTrue(legacyIds.exists());
}
@Issue("JENKINS-64356")
@Test
public void legacyIdsPresentViaRestApi() throws Exception {
User user = User.getById("user", true);
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.READ, Item.CREATE)
.everywhere()
.to(user.getId()));
String jobName = "test" + j.jenkins.getItems().size();
try (JenkinsRule.WebClient wc = j.createWebClient()) {
wc.login(user.getId());
WebRequest req = new WebRequest(wc.createCrumbedUrl("createItem?name=" + jobName), HttpMethod.POST);
req.setAdditionalHeader("Content-Type", "application/xml");
req.setRequestBody("<project/>");
wc.getPage(req);
}
FreeStyleProject p = j.jenkins.getItemByFullName(jobName, FreeStyleProject.class);
assertNotNull(p);
File legacyIds = new File(p.getBuildDir(), "legacyIds");
assertTrue(legacyIds.exists());
}
@Issue("JENKINS-64356")
@Test
public void legacyIdsPresentViaCli() {
String jobName = "test" + j.jenkins.getItems().size();
CLICommandInvoker invoker = new CLICommandInvoker(j, new CreateJobCommand());
CLICommandInvoker.Result result = invoker.withStdin(
new ByteArrayInputStream("<project/>".getBytes(StandardCharsets.UTF_8)))
.invokeWithArgs(jobName);
assertThat(result, succeededSilently());
FreeStyleProject p = j.jenkins.getItemByFullName(jobName, FreeStyleProject.class);
assertNotNull(p);
File legacyIds = new File(p.getBuildDir(), "legacyIds");
assertTrue(legacyIds.exists());
}
}