/* * The MIT License * * Copyright 2016 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.branch; import com.google.common.base.Charsets; import hudson.Extension; import hudson.FilePath; import hudson.model.Item; import hudson.model.Node; import hudson.model.Slave; import hudson.model.TopLevelItem; import hudson.model.listeners.ItemListener; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import jenkins.slaves.WorkspaceLocator; import org.apache.commons.codec.binary.Base32; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Chooses manageable workspace names for branch projects. * @see "JENKINS-34564" */ @Restricted(NoExternalUse.class) @Extension(ordinal = -100) public class WorkspaceLocatorImpl extends WorkspaceLocator { private static final Logger LOGGER = Logger.getLogger(WorkspaceLocatorImpl.class.getName()); /** The most characters to allow in a workspace directory name, relative to the root. Zero to disable altogether. */ // TODO 2.4+ use SystemProperties static /* not final */ int PATH_MAX = Integer.getInteger(WorkspaceLocatorImpl.class.getName() + ".PATH_MAX", 80); @Override public FilePath locate(TopLevelItem item, Node node) { if (PATH_MAX == 0) { return null; } if (!(item.getParent() instanceof MultiBranchProject)) { return null; } String minimized = minimize(item.getFullName()); if (node instanceof Jenkins) { return ((Jenkins) node).getRootPath().child("workspace/" + minimized); } else if (node instanceof Slave) { FilePath root = ((Slave) node).getWorkspaceRoot(); return root != null ? root.child(minimized) : null; } else { // ? return null; } } static String uniqueSuffix(String name) { // TODO still in beta: byte[] sha256 = Hashing.sha256().hashString(name).asBytes(); byte[] sha256; try { sha256 = MessageDigest.getInstance("SHA-256").digest(name.getBytes(Charsets.UTF_16LE)); } catch (NoSuchAlgorithmException x) { throw new AssertionError("https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest", x); } return new Base32(0).encodeToString(sha256).replaceFirst("=+$", ""); } static String minimize(String name) { String mnemonic = name.replaceAll("(%[0-9A-F]{2}|[^a-zA-Z0-9-_.])+", "_"); int maxSuffix = 53; /* ceil(256 / lg(32)) + length("-") */ int maxMnemonic = Math.max(PATH_MAX - maxSuffix, 1); if (maxSuffix + maxMnemonic > PATH_MAX) { // The whole suffix cannot be included in the path. Trim the suffix // and the mnemonic to fit inside PATH_MAX. The mnemonic always gets // at least one character. The suffix always gets 10 characters plus // the "-". The rest of PATH_MAX is split evenly between the two. LOGGER.log(Level.WARNING, "WorkspaceLocatorImpl.PATH_MAX is small enough that workspace path collisions are more likely to occur"); final int minSuffix = 10 + /* length("-") */ 1; maxMnemonic = Math.max((int)((PATH_MAX - minSuffix) / 2), 1); maxSuffix = Math.max(PATH_MAX - maxMnemonic, minSuffix); } maxSuffix = maxSuffix - 1; // Remove the "-" String result = StringUtils.right(mnemonic, maxMnemonic) + "-" + uniqueSuffix(name).substring(0, maxSuffix); return result; } /** * Cleans up workspace when an orphaned branch project is deleted. * @see "JENKINS-2111" */ @Extension public static class Deleter extends ItemListener { @Override public void onDeleted(Item item) { if (item.getParent() instanceof MultiBranchProject) { String suffix = uniqueSuffix(item.getFullName()); Jenkins jenkins = Jenkins.getActiveInstance(); cleanUp(suffix, jenkins.getRootPath().child("workspace"), jenkins); for (Node node : jenkins.getNodes()) { if (node instanceof Slave) { cleanUp(suffix, ((Slave) node).getWorkspaceRoot(), node); } } } } private void cleanUp(String suffix, FilePath root, Node node) { try { if (root == null || !root.isDirectory()) { return; } for (FilePath child : root.listDirectories()) { if (child.getName().contains(suffix)) { LOGGER.log(Level.INFO, "deleting obsolete workspace {0} on {1}", new Object[] {child, node.getNodeName()}); child.deleteRecursive(); } } } catch (IOException | InterruptedException x) { LOGGER.log(Level.WARNING, "could not clean up workspace directories under " + root + " on " + node.getNodeName(), x); } } } }