diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaeeb2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target/ +/work/ +/.classpath +/.project +/.settings \ No newline at end of file diff --git a/pom.xml b/pom.xml index e366887..45bf28c 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ org.jenkins-ci.plugins unique-id - 1.3-SNAPSHOT + 2.0-SNAPSHOT hpi Unique ID Library Plugin http://wiki.jenkins-ci.org/display/JENKINS/Unique+Id+Plugin @@ -17,9 +17,13 @@ scm:git:ssh://github.com/jenkinsci/unique-id-plugin.git scm:git:ssh://git@github.com/jenkinsci/unique-id-plugin.git https://github.com/jenkinsci/unique-id-plugin - unique-id-1.2 + + 1.7 + 1.7 + + org.jenkins-ci.plugins @@ -27,6 +31,24 @@ 4.0 true + + org.jenkins-ci + test-annotations + 1.2 + test + + + junit + junit + 4.12 + test + + + org.hamcrest + hamcrest-core + 1.3 + test + org.jenkins-ci.plugins credentials @@ -54,7 +76,15 @@ maven-release-plugin - 2.4 + 2.5.1 + + + org.jenkins-ci.tools + maven-hpi-plugin + true + + 2.0 + diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java index ea19209..55c85f5 100644 --- a/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/IdStore.java @@ -1,16 +1,28 @@ package org.jenkinsci.plugins.uniqueid; import hudson.ExtensionPoint; + import jenkins.model.Jenkins; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + import javax.annotation.Nullable; +import org.apache.commons.codec.binary.Base64; + /** * An abstraction to persistently store and retrieve unique id's * for various Jenkins model objects. * * These keys are guaranteed to be unique with a Jenkins * and immutable across the lifetime of the given object. + * + * Implementations should not store the ID inside any specific item configuration as it is + * common for users top copy items either through the UI or manually and this will cause the + * IDs to become non-unique. + * * * @param */ @@ -26,14 +38,14 @@ public IdStore (Class forType) { * Creates an unique id for the given object. * Subsequent calls are idempotent. * - * @param object + * @param object the object to make the id for. */ public abstract void make(T object); /** * Get the id for this given object. * @param object - * @return the id or null if none assigned. + * @return the id or {@code null} if none assigned. */ @Nullable public abstract String get(T object); @@ -63,7 +75,7 @@ public static IdStore forClass(Class clazz) { * * @throws java.lang.IllegalArgumentException if the type is not supported. */ - public static void makeId(Object object) { + public static void makeId(Object object) throws IllegalArgumentException { IdStore store = forClass(object.getClass()); if (store == null) { throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName()); @@ -77,7 +89,7 @@ public static void makeId(Object object) { * * @throws java.lang.IllegalArgumentException if the type is not supported. */ - public static String getId(Object object) { + public static String getId(Object object) throws IllegalArgumentException { IdStore store = forClass(object.getClass()); if (store == null) { throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName()); @@ -86,4 +98,13 @@ public static String getId(Object object) { } } + /** + * Generates a new unique ID. + * Subclasses do not need to use this to create unique IDs and are free to create IDs by other methods. + * @return a string that should be unique against all jenkins instances. + */ + protected static String generateUniqueID() { + return Base64.encodeBase64String(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)).substring(0, 30); + } + } diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java index 3bb1115..814d166 100644 --- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/FolderIdStore.java @@ -1,37 +1,47 @@ package org.jenkinsci.plugins.uniqueid.impl; -import com.cloudbees.hudson.plugins.folder.Folder; -import com.cloudbees.hudson.plugins.folder.FolderProperty; -import com.cloudbees.hudson.plugins.folder.FolderPropertyDescriptor; import hudson.Extension; import hudson.model.Action; import hudson.model.Actionable; -import org.jenkinsci.plugins.uniqueid.IdStore; +import hudson.util.DescribableList; import java.io.IOException; import java.util.Collection; import java.util.Collections; -import java.util.logging.Level; +import java.util.Iterator; import java.util.logging.Logger; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import com.cloudbees.hudson.plugins.folder.FolderProperty; +import com.cloudbees.hudson.plugins.folder.FolderPropertyDescriptor; +import com.cloudbees.hudson.plugins.folder.Folder; + /** * Stores ids for folders as a {@link FolderIdProperty} + * @deprecated {@see PersistenceRootIdStore} */ @Extension(optional = true) -public class FolderIdStore extends IdStore { +@Deprecated +@Restricted(NoExternalUse.class) +public class FolderIdStore extends LegacyIdStore { public FolderIdStore() { super(Folder.class); } @Override - public void make(Folder folder) { - if (folder.getProperties().get(FolderIdProperty.class) == null) { - try { - folder.addProperty(new FolderIdProperty()); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Failed to add property",e); + public void remove(Folder folder) throws IOException { + DescribableList,FolderPropertyDescriptor> properties = folder.getProperties(); + + for (Iterator> itr = properties.iterator(); itr.hasNext(); ) { + FolderProperty prop = itr.next(); + + if (prop instanceof FolderIdProperty) { + itr.remove(); } } + folder.save(); } @Override diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java index 6abc0f7..03cedef 100644 --- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/Id.java @@ -1,18 +1,29 @@ package org.jenkinsci.plugins.uniqueid.impl; +import jenkins.model.RunAction2; + import hudson.model.Action; import hudson.model.Actionable; +import hudson.model.Run; + import org.apache.commons.codec.binary.Base64; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import javax.annotation.Nullable; + import java.util.UUID; import java.util.logging.Logger; /** - * An action which stores an id. + * DO NOT USE + * @deprecated users should not use this as it ties the ID explicitly to the item and as will not work when copying items (for example). */ -class Id implements Action { +@Deprecated +@Restricted(NoExternalUse.class) +class Id implements Action , RunAction2 { + private final static Logger LOGGER = Logger.getLogger(Id.class.getName()); private final String id; @@ -36,7 +47,13 @@ public String getId() { return id; } + + /** + * @deprecated Sub classes should not use this as it stores the ID in the actionable item. + * @return + */ @Nullable + @Deprecated protected static String getId(Actionable actionable) { Id id = actionable.getAction(Id.class); if (id != null) { @@ -47,5 +64,15 @@ protected static String getId(Actionable actionable) { } - private final static Logger LOGGER = Logger.getLogger(Id.class.getName()); + + public void onAttached(Run r) { + // NO-OP + } + + /** + * Migrates the run away from using this Action. + */ + public void onLoad(Run r) { + IdStoreMigratorV1ToV2.migrate(r); + } } diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2.java new file mode 100644 index 0000000..1e0e7c8 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2.java @@ -0,0 +1,166 @@ +package org.jenkinsci.plugins.uniqueid.impl; + +import hudson.Extension; +import hudson.init.InitMilestone; +import hudson.init.Initializer; +import hudson.model.Item; +import hudson.model.PersistenceRoot; +import hudson.model.Job; +import hudson.model.Run; + +import jenkins.model.Jenkins; + +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Nonnull; + +import org.jenkinsci.plugins.uniqueid.implv2.PersistenceRootIdStore; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Converts legacy UniqueIDs that are stored inside a Folder/Job/Run configuration to UniqueIDs that are stored alongside the Folder/Job/Run. + * + */ +@Restricted(NoExternalUse.class) +@Extension +public class IdStoreMigratorV1ToV2 { + + private static Logger LOGGER = Logger.getLogger(IdStoreMigratorV1ToV2.class.getName()); + + private static final String MARKER_FILE_NAME = "unique-id-migration.txt"; + + /** + * Migrates any IDs stored in Folder/Job/Run configuration + * @throws IOException + */ + + @Initializer(after=InitMilestone.JOB_LOADED, before=InitMilestone.COMPLETED, fatal=true) + public static void migrateIdStore() throws IOException { + Jenkins jenkins = Jenkins.getInstance(); + if (jenkins == null) { + throw new IllegalStateException("Jenkins is null, so it is impossible to migrate the IDs"); + } + File marker = new File(jenkins.getRootDir(), MARKER_FILE_NAME); + if (marker.exists()) { + LOGGER.log(Level.INFO, "Migration of IDStore already performed, so skipping migration."); + return; + } + LOGGER.log(Level.INFO, "Starting migration of IDs"); + + performMigration(jenkins); + + } + + @SuppressWarnings("unchecked") + static void performMigration(@Nonnull Jenkins jenkins) { + List allItems = jenkins.getAllItems(); + + for (Item item : allItems) { + // can only be Folder or Job here (not a run) - and these both implement PersistenceRoot + if (item instanceof PersistenceRoot) { + migrate((PersistenceRoot) item); + } + else { + LOGGER.log(Level.WARNING, "Expected item of type Folder or Job which implement PersistenceRoot, but got a {0} so can not migrate the IdStore for this item", + item.getClass().getName()); + } + } + LOGGER.log(Level.INFO, "migration of unique IDs for Jobs and Folders complete - will continue to process Runs in the background."); + + Thread t = new Thread(new RunIDMigrationThread(), "unique-id background migration thread"); + t.setDaemon(true); + t.start(); + } + + static void migrate(PersistenceRoot pr) { + LOGGER.log(Level.FINE, "migrating {0}" , pr.toString()); + try { + String id = LegacyIdStore.getId(pr); + if (id != null) { + PersistenceRootIdStore.create(pr, id); + LegacyIdStore.removeId(pr); + } + } catch (IOException ex) { + // need to rethrow (but add some context first) otherwise the migration will continue to run + // and it will not have migrated everything :-( + throw new IDStoreMigrationException("Failure whilst migrating " + pr.toString(), ex); + } + } + + /** + * Exception to indicate a failure to migrate the IDStore. + */ + private static class IDStoreMigrationException extends RuntimeException { + + public IDStoreMigrationException(String message, Throwable cause) { + super(message,cause); + } + } + + private static class RunIDMigrationThread implements Runnable { + + public void run() { + Jenkins jenkins = Jenkins.getInstance(); + if (jenkins == null) { + throw new IllegalStateException("Jenkins is null, so it is impossible to migrate the IDs"); + } + // if new jobs are added that is ok - as their runs will not need to be migrated. + // if jobs are deleted from Jenkins we need to handle that fact! + List allJobs = jenkins.getAllItems(Job.class); + int totalJobs = allJobs.size(); + int migratedJobs = 0; + int migratedBuilds = 0; + final long startTime = System.currentTimeMillis(); + long lastLog = System.currentTimeMillis(); + for (Job job : allJobs) { + // Force the loading of the builds. + migratedJobs++; + if (job.getConfigFile().getFile().exists()) { + // we have not been deleted! + for (Iterator iterator = job.getBuilds().iterator(); iterator.hasNext();) { + // the build is migrated by the action in Id.onLoad(Run) + // touch something in the build just to force loading incase it gets more lazy in the future. + Object r = iterator.next(); + if (r != null && r instanceof Run) { + ((Run)r).getResult(); + } + migratedBuilds++; + } + } + if ((System.currentTimeMillis() - lastLog) > (60 * 1000L) ) { + lastLog = System.currentTimeMillis(); + LOGGER.log(Level.INFO, "Processed {0} builds, and have inspected all runs from {1} out of {2} jobs.", + new Object[] {migratedBuilds, migratedJobs, totalJobs}); + } + } + // all done... + final long duration = System.currentTimeMillis() - startTime; + final long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - startTime); + final long seconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - startTime - TimeUnit.MINUTES.toMillis(minutes)); + + LOGGER.log(Level.INFO, "Finished unique-id migration of builds in {0} minutes {1} seconds. Processed {2} runs from {3} jobs.", + new Object[] {minutes, seconds, migratedBuilds, migratedJobs}); + File marker = new File(jenkins.getRootDir(), MARKER_FILE_NAME); + try { + if (!marker.createNewFile()) { + LOGGER.log(Level.WARNING, "Failed to record the completion of the IDStore Migration. " + + "This will cause performance issues on subsequent startup. " + + "Please create an empty file at '" + marker.getCanonicalPath() + "'"); + } + } + catch (IOException ex) { + LOGGER.log(Level.WARNING, "Failed to record the completion of the IDStore Migration. " + + "This will cause performance issues on subsequent startup. " + + "Please create an empty file in the Jenkins home directory called '" + MARKER_FILE_NAME + "'.", ex); + } + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java index 5e5b2af..e7299cb 100644 --- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/JobIdStore.java @@ -6,7 +6,6 @@ import hudson.model.Job; import hudson.model.JobProperty; import hudson.model.JobPropertyDescriptor; -import org.jenkinsci.plugins.uniqueid.IdStore; import java.io.IOException; import java.util.Collection; @@ -14,26 +13,31 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + /** * Stores ids for jobs in {@link JobIdProperty} + * @deprecated {@see PersistenceRootIdStore} */ @Extension -public class JobIdStore extends IdStore { +@Deprecated +@Restricted(NoExternalUse.class) +public class JobIdStore extends LegacyIdStore { public JobIdStore() { super(Job.class); } @Override - public void make(Job job) { - if (job.getProperty(JobIdProperty.class) == null) { - try { - job.addProperty(new JobIdProperty()); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Failed to add property",e); - } + public void remove(Job job) { + try { + while (job.removeProperty(JobIdProperty.class) != null) {} + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Failed to remove property from " + job.getFullName(), ex); } } + @Override public String get(Job thing) { return Id.getId((Actionable) thing); @@ -43,6 +47,7 @@ public String get(Job thing) { /** * A unique Id for Jobs. */ + @Deprecated public static class JobIdProperty extends JobProperty> { private Id id = new Id(); diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/LegacyIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/LegacyIdStore.java new file mode 100644 index 0000000..11c38b6 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/LegacyIdStore.java @@ -0,0 +1,101 @@ +package org.jenkinsci.plugins.uniqueid.impl; + +import hudson.ExtensionPoint; + +import jenkins.model.Jenkins; + +import java.io.IOException; + +import javax.annotation.Nullable; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * An abstraction to persistently store and retrieve unique id's + * for various Jenkins model objects. + * + * These keys are guaranteed to be unique with a Jenkins + * and immutable across the lifetime of the given object. + * + * Implementations should not store the ID inside any specific item configuration as it is + * common for users top copy items either through the UI or manually and this will cause the + * IDs to become non-unique. + * + * + * @param + */ +@Restricted(NoExternalUse.class) +@Deprecated +public abstract class LegacyIdStore implements ExtensionPoint { + + private final Class type; + + public LegacyIdStore (Class forType) { + this.type = forType; + } + + /** + * Remove the unique id associated with the given object. + * @param object + */ + public abstract void remove(T object) throws IOException; + + /** + * Get the id for this given object. + * @param object + * @return the id or null if none assigned. + */ + @Nullable + public abstract String get(T object); + + public boolean supports(Class clazz) { + return type.isAssignableFrom(clazz); + } + + /** + * Retrieve an {@link LegacyIdStore} for the given type + * @param clazz + * @param + * @return the store which supports the type, or null if none + */ + @Nullable + public static LegacyIdStore forClass(Class clazz) { + for (LegacyIdStore store : Jenkins.getInstance().getExtensionList(LegacyIdStore.class)) { + if (store.supports(clazz)) { + return store; + } + } + return null; + } + + /** + * Convenience method which makes the id for the given object. + * + * @throws java.lang.IllegalArgumentException if the type is not supported. + * @throws IOException if we could not remove the ID from the Object. + */ + public static void removeId(Object object) throws IOException{ + LegacyIdStore store = forClass(object.getClass()); + if (store == null) { + throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName()); + } else { + store.remove(object); + } + } + + /** + * Convenience method which retrieves the id for the given object. + * + * @throws java.lang.IllegalArgumentException if the type is not supported. + */ + public static String getId(Object object) { + LegacyIdStore store = forClass(object.getClass()); + if (store == null) { + throw new IllegalArgumentException("Unsupported type: " + object.getClass().getName()); + } else { + return store.get(object); + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java index 1f8937c..b1f4eb0 100644 --- a/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/impl/RunIdStore.java @@ -1,39 +1,41 @@ package org.jenkinsci.plugins.uniqueid.impl; import hudson.Extension; +import hudson.model.Action; import hudson.model.Actionable; import hudson.model.Run; -import org.jenkinsci.plugins.uniqueid.IdStore; import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Iterator; +import java.util.List; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** - * Stores id's for runs as an action on the Run. + * Controls id's for runs. */ @Extension -public class RunIdStore extends IdStore { +@Deprecated +@Restricted(NoExternalUse.class) +public class RunIdStore extends LegacyIdStore { + public RunIdStore() { super(Run.class); } @Override - public void make(Run run) { - if (run.getAction(Id.class) == null) { - run.addAction(new Id()); - try { - run.save(); - } catch (IOException e) { - LOGGER.log(Level.SEVERE,"Failed to save id",e); - } + public void remove(Run run) throws IOException { + List actionList = run.getActions(); + List ids = run.getActions(Id.class); + if (!ids.isEmpty()) { + actionList.removeAll(ids); + run.save(); } } + @Override public String get(Run thing) { return Id.getId((Actionable) thing); } - - private final static Logger LOGGER = Logger.getLogger(RunIdStore.class.getName()); - } diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/PersistenceRootIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/PersistenceRootIdStore.java new file mode 100644 index 0000000..43068ee --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/PersistenceRootIdStore.java @@ -0,0 +1,86 @@ +package org.jenkinsci.plugins.uniqueid.implv2; + +import hudson.Extension; +import hudson.model.PersistenceRoot; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; +import org.jenkinsci.plugins.uniqueid.IdStore; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + + +/** + * The {@link PersistenceRootIdStore} allows the storing of a Unique ID for any PersistenceRoot item. This replaces the + * need for {@link FolderIdStore}, {@link JobIdStore} and {@link RunIdStore} + */ +@Extension +public class PersistenceRootIdStore extends IdStore { + + /** Our Logger. */ + private final static Logger LOGGER = Logger.getLogger(PersistenceRootIdStore.class.getName()); + + /** The name of the file in which we store the unique ID. */ + private final static String ID_FILE = "unique-id.txt"; + + public PersistenceRootIdStore() { + super(PersistenceRoot.class); + } + + @Override + public void make(PersistenceRoot object) { + File f = new File(object.getRootDir(), ID_FILE); + if (!f.exists()) { + File tmp = null; + try { + tmp = File.createTempFile(".unique-id_", ".tmp", object.getRootDir()); + FileUtils.writeStringToFile(f, IdStore.generateUniqueID(), StandardCharsets.UTF_8); + try { + Files.move(tmp.toPath(), f.toPath(), StandardCopyOption.ATOMIC_MOVE); + } + catch (FileAlreadyExistsException ignored) { + FileUtils.deleteQuietly(tmp); + return; // we already have an id. + } + } + catch (IOException ex) { + LOGGER.log(Level.WARNING, "Failed to store unique ID for " + object.toString(), ex); + } + } + } + + @Override + public String get(PersistenceRoot object) { + File f = new File(object.getRootDir(), ID_FILE); + if (f.exists() && f.canRead()) { + try { + return FileUtils.readFileToString(f, StandardCharsets.UTF_8); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Failed to retrieve unique ID for " + object.toString(), ex); + } + } + return null; + } + + @Restricted(NoExternalUse.class) + public static void create(PersistenceRoot object, String uniqueId) throws IOException { + File f = new File(object.getRootDir(), ID_FILE); + if (!f.exists()) { + LOGGER.log(Level.FINE, "Creating file ({1}) to store ID for ({0}) whose RootDir is ({2}).", new Object[] {object.toString(), f, object.getRootDir()}); + // no need to migrate if its there to begin with! + FileUtils.writeStringToFile(f, uniqueId, StandardCharsets.UTF_8); + } + else { + LOGGER.log(Level.FINE, "**NOT** creating file ({1}) to store ID for ({0}) whose RootDir is ({2}).", new Object[] {object.toString(), f, object.getRootDir()}); + } + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/RunIdStore.java b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/RunIdStore.java new file mode 100644 index 0000000..6d91cd8 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/uniqueid/implv2/RunIdStore.java @@ -0,0 +1,48 @@ +package org.jenkinsci.plugins.uniqueid.implv2; + +import hudson.Extension; +import hudson.model.PersistenceRoot; +import hudson.model.Job; +import hudson.model.Run; + +import org.jenkinsci.plugins.uniqueid.IdStore; + +/** + * Manages Unique IDs for Runs. + * Whilst we could use the {@link PersistenceRootIdStore} that will create extra files for + * every single build. A build already has a unique identifier (build number / build id) and a parent job can have a + * unique ID, so we build one from the parent and our build number to save creating a file. + */ +@Extension(ordinal=1) // needs to take priority over the PersistenceRootIdStore +public class RunIdStore extends IdStore { + public RunIdStore() { + super(Run.class); + } + + @Override + public void make(Run run) { + // we calculate these on the fly, or serve up migrated IDs if they exist. + // in order to calculate on the fly we require the parent to have an id. + IdStore.makeId(run.getParent()); + } + + @Override + public String get(Run run) { + IdStore persistenceStore = IdStore.forClass(PersistenceRoot.class); + + String id = persistenceStore.get(run); + if (id != null) { + // migrated legacy id + return id; + } + + // calculate the ID. + Job parent = run.getParent(); + String parentID = IdStore.getId(parent); + if (parentID != null) { + return parentID + '_' + run.getId(); + } + return null; + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java b/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java index a7fa1fe..3885c65 100644 --- a/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java +++ b/src/test/java/org/jenkinsci/plugins/uniqueid/IdTest.java @@ -3,7 +3,6 @@ import com.cloudbees.hudson.plugins.folder.Folder; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; -import hudson.model.Job; import hudson.model.Project; import org.junit.Rule; import org.junit.Test; @@ -24,9 +23,14 @@ public void project() throws Exception { String id = IdStore.getId(p); AbstractBuild build = jenkinsRule.buildAndAssertSuccess(p); - assertNull(IdStore.getId(build)); - IdStore.makeId(build); + // a build will get an id computed from its parent. String buildId = IdStore.getId(build); + assertEquals(buildId, id+'_'+build.getId()); + + // should be a no-op + IdStore.makeId(build); + assertEquals(IdStore.getId(build), buildId); + jenkinsRule.jenkins.reload(); AbstractProject resurrectedProject = jenkinsRule.jenkins.getItemByFullName(p.getFullName(), AbstractProject.class); diff --git a/src/test/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test.java b/src/test/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test.java new file mode 100644 index 0000000..34f11b1 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test.java @@ -0,0 +1,89 @@ +package org.jenkinsci.plugins.uniqueid.impl; + +import hudson.model.ItemGroup; +import hudson.model.PersistenceRoot; +import hudson.model.Job; +import hudson.model.Run; + +import jenkins.model.Jenkins; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.FileUtils; +import org.jenkinsci.plugins.uniqueid.IdStore; +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.recipes.LocalData; + +import com.cloudbees.hudson.plugins.folder.Folder; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +public class IdStoreMigratorV1ToV2Test { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + @Test + @Issue("JENKINS-28843") + @LocalData + public void testMigration() throws Exception { + Jenkins jenkins = jenkinsRule.jenkins; + assertThat("All jobs loaded correctly", jenkins.getAllItems(), hasSize(4)); + + Folder folderNoID = jenkins.getItem("folderNoID", jenkins, Folder.class); + Folder folderWithID = jenkins.getItem("folderWithID", jenkins, Folder.class); + + Job jobNoID = jenkins.getItem("jobNoID", (ItemGroup)folderWithID, Job.class); + Job jobWithID = jenkins.getItem("jobWithID", (ItemGroup)folderWithID, Job.class); + + assertThat(folderNoID, notNullValue()); + assertThat(folderWithID, notNullValue()); + assertThat(jobNoID, notNullValue()); + assertThat(jobWithID, notNullValue()); + + checkID(folderNoID, null); + checkID(folderWithID, "YzUxN2JiZTYtNGVhZS00NDQxLTg5NT"); + + checkID(jobNoID, null); + checkID(jobWithID, "ZGQxMDNhYzUtMTJlOC00YTc4LTgzOT"); + + + checkID(jobNoID.getBuildByNumber(1), null); + checkID(jobNoID.getBuildByNumber(2), null); + + // build 1 had no id so its generated on the fly from the parent + checkID(jobWithID.getBuildByNumber(1), "ZGQxMDNhYzUtMTJlOC00YTc4LTgzOT_" + jobWithID.getBuildByNumber(1).getId()); + checkID(jobWithID.getBuildByNumber(2), "NGQ0ODM2NjktZGM0OS00MjdkLWE3NT"); + } + + private static void checkID(PersistenceRoot obj, String expectedID) throws Exception { + assertThat("Checking " + obj.toString(), IdStore.getId(obj), is(expectedID)); + if (expectedID != null) { + File f = new File(obj.getRootDir(), "config.xml"); + + String string = FileUtils.readFileToString(f, StandardCharsets.UTF_8); + // main config should not contain a reference to the unique ID any more. + assertThat("config.xml for " + obj.toString() + " still contains the ID", string, not(containsString(expectedID))); + } + } + + private static void checkID(Run obj, String expectedID) throws Exception { + assertThat("Checking " + obj.toString(), IdStore.getId(obj), is(expectedID)); + if (expectedID != null) { + File f = new File(obj.getRootDir(), "build.xml"); + + String string = FileUtils.readFileToString(f, StandardCharsets.UTF_8); + // main config should not contain a reference to the unique ID any more. + assertThat("build.xml for " + obj.toString() + " still contains the ID", string, not(containsString(expectedID))); + } + } +} diff --git a/src/test/resources/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test/testMigration.zip b/src/test/resources/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test/testMigration.zip new file mode 100644 index 0000000..ba51565 Binary files /dev/null and b/src/test/resources/org/jenkinsci/plugins/uniqueid/impl/IdStoreMigratorV1ToV2Test/testMigration.zip differ