Skip to content

Commit

Permalink
Merge pull request #92 from batmat/JENKINS-40098-take-2
Browse files Browse the repository at this point in the history
[JENKINS-40098] Bundle naming strategy should be able to specify an instance type
  • Loading branch information
batmat committed Dec 13, 2016
2 parents fc31193 + d2d3262 commit 80b611d
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 74 deletions.
@@ -0,0 +1,108 @@
/*
* The MIT License
*
* Copyright (c) 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 com.cloudbees.jenkins.support;

import com.cloudbees.jenkins.support.api.SupportProvider;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
* <p>Extension point allowing to customize the support bundle naming strategy.</p>
* <p>
* It will work the following way:
* </p>
* <ol>
* <li>If an implementation of {@link BundleNameInstanceTypeProvider} is found, it will be used.<br>
* <strong>WARNING: </strong>if many are found, then a warning will be issued, and the first extension found will
* be used.</li>
* <li>If not, then it will check for the presence of the {@link #SUPPORT_BUNDLE_NAMING_INSTANCE_SPEC_PROPERTY}
* system property, and will use its value if provided.</li>
* <li>If not, then will fallback to the original behaviour, which is simply an empty String</li>
* </ol>
*
* @see SupportProvider#getName() for prefixing.
*/
public abstract class BundleNameInstanceTypeProvider implements ExtensionPoint {

@VisibleForTesting
static final String SUPPORT_BUNDLE_NAMING_INSTANCE_SPEC_PROPERTY = SupportPlugin.class.getName() + ".instanceType";

private static final Logger LOGGER = Logger.getLogger(BundleNameInstanceTypeProvider.class.getName());

@NonNull
static BundleNameInstanceTypeProvider getInstance() {
final ExtensionList<BundleNameInstanceTypeProvider> all = ExtensionList.lookup(BundleNameInstanceTypeProvider.class);
final int extensionCount = all.size();

if (extensionCount > 2) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "{0} implementations found for support bundle prefix naming strategy. " +
"Can be only 1 (default one) or 2 (default one, plus alternative). " +
"Choosing the first found among the following:", extensionCount);
for (BundleNameInstanceTypeProvider nameProvider : all) {
LOGGER.log(Level.WARNING, "Class {0} found", nameProvider.getClass().getName());
}
}
}

final BundleNameInstanceTypeProvider chosen = all.get(0);
if (extensionCount > 1) {
LOGGER.log(Level.FINE, "Using {0} as BundleNameInstanceTypeProvider implementation", chosen.getClass().getName());
}
return chosen;
}

/**
* Returns the <strong>non-null</strong> and non empty (default value is empty) instance type to be used for
* generated support bundle names.
*
* <p>Aims to provide informational data about the generated bundles.</p>
* <p><strong>Will be used for file name generation, so avoid funky characters.
* Please ideally stay in <code>[a-zA-Z-_.]</code></strong>.
*
* Also consider the file name length, you probably want to be defensive
* and not return crazily long strings. Something below 20 characters or so might sound reasonable.</p>
*
* @return the instance type specification to be used for generated support bundles.
*/
@NonNull
public abstract String getInstanceType();

// We want this to be always picked up last in case others are found Double.MIN_VALUE does NOT work
@Extension(ordinal = Integer.MIN_VALUE)
public static final class DEFAULT_STRATEGY extends BundleNameInstanceTypeProvider {

@Override
public String getInstanceType() {
return System.getProperty(BundleNameInstanceTypeProvider.SUPPORT_BUNDLE_NAMING_INSTANCE_SPEC_PROPERTY, "");
}
}
}
16 changes: 1 addition & 15 deletions src/main/java/com/cloudbees/jenkins/support/SupportAction.java
Expand Up @@ -48,14 +48,11 @@
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -147,18 +144,7 @@ public void doDownload(StaplerRequest req, StaplerResponse rsp) throws ServletEx
logger.fine("Preparing response...");
rsp.setContentType("application/zip");

String filename = "support"; // default bundle filename
if (supportPlugin != null) {
SupportProvider supportProvider = supportPlugin.getSupportProvider();
if (supportProvider != null) {
// let the provider name it
filename = supportProvider.getName();
}
}

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
rsp.addHeader("Content-Disposition", "inline; filename=" + filename + "_" + dateFormat.format(new Date()) + ".zip;");
rsp.addHeader("Content-Disposition", "inline; filename=" + SupportPlugin.getBundleFileName() + ";");
final ServletOutputStream servletOutputStream = rsp.getOutputStream();
try {
SupportPlugin.setRequesterAuthentication(Jenkins.getAuthentication());
Expand Down
33 changes: 10 additions & 23 deletions src/main/java/com/cloudbees/jenkins/support/SupportCommand.java
Expand Up @@ -25,23 +25,23 @@
package com.cloudbees.jenkins.support;

import com.cloudbees.jenkins.support.api.Component;
import com.cloudbees.jenkins.support.api.SupportProvider;
import hudson.Extension;
import hudson.cli.CLICommand;
import hudson.remoting.Callable;
import hudson.remoting.RemoteOutputStream;
import hudson.security.ACL;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.jenkinsci.remoting.RoleChecker;
import org.kohsuke.args4j.Argument;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -81,15 +81,7 @@ protected int run() throws Exception {
try {
SecurityContext old = ACL.impersonate(ACL.SYSTEM);
try {
String filename = "support";
SupportPlugin supportPlugin = SupportPlugin.getInstance();
if (supportPlugin != null) {
SupportProvider supportProvider = supportPlugin.getSupportProvider();
if (supportProvider != null) {
filename = supportProvider.getName();
}
}
SupportPlugin.writeBundle(checkChannel().call(new SaveBundle(filename)), selected);
SupportPlugin.writeBundle(checkChannel().call(new SaveBundle(SupportPlugin.getBundleFileName())), selected);
} finally {
SecurityContextHolder.setContext(old);
}
Expand All @@ -99,23 +91,18 @@ protected int run() throws Exception {
return 0;
}

private static class SaveBundle implements Callable<OutputStream, IOException> {
private static class SaveBundle extends MasterToSlaveCallable<OutputStream, IOException> {
private final String filename;

SaveBundle(String filename) {
this.filename = filename;
}

public OutputStream call() throws IOException {
File f = File.createTempFile(filename, ".zip");
System.err.println("Creating: " + f);
return new RemoteOutputStream(new FileOutputStream(f));
}

/** {@inheritDoc} */
@Override
public void checkRoles(RoleChecker checker) throws SecurityException {
// TODO: do we have to verify some role?
public OutputStream call() throws IOException {
Path path = Files.createFile(Paths.get(System.getProperty("java.io.tmpdir"), filename));
System.err.println("Creating: " + path);
return new RemoteOutputStream(new FileOutputStream(path.toFile()));
}
}

Expand Down
Expand Up @@ -28,7 +28,7 @@
* DO NOT INCLUDE ANY NON JDK CLASSES IN HERE.
* IT CAN DEADLOCK REMOTING - SEE JENKINS-32622
***********************************************/
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; // Acceptable because RetentionPolicy.CLASS
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
Expand Down
109 changes: 74 additions & 35 deletions src/main/java/com/cloudbees/jenkins/support/SupportPlugin.java
Expand Up @@ -63,7 +63,6 @@
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
Expand All @@ -88,7 +87,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
Expand Down Expand Up @@ -563,6 +561,46 @@ public void run() {
return Collections.emptyList();
}

/**
* Returns the full bundle name.
*
* @return the full bundle name.
*/
@NonNull
public static String getBundleFileName() {
StringBuilder filename = new StringBuilder();
filename.append(getBundlePrefix());

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
filename.append("_").append(dateFormat.format(new Date()));

filename.append(".zip");
return filename.toString();
}

/**
* Returns the prefix of the bundle name.
*
* @return the prefix of the bundle name.
*/
private static String getBundlePrefix() {
String filename = "support"; // default bundle filename
final SupportPlugin instance = getInstance();
if (instance != null) {
SupportProvider supportProvider = instance.getSupportProvider();
if (supportProvider != null) {
// let the provider name it
filename = supportProvider.getName();
}
}
final String instanceType = BundleNameInstanceTypeProvider.getInstance().getInstanceType();
if (StringUtils.isNotBlank(instanceType)) {
filename = filename + "_"+instanceType;
}
return filename;
}

public static class LogHolder {
private static final SupportLogHandler SLAVE_LOG_HANDLER = new SupportLogHandler(256, 2048, 8);
}
Expand Down Expand Up @@ -693,10 +731,6 @@ protected synchronized void doRun() throws Exception {
}
try {
thread = new Thread(new Runnable() {
@edu.umd.cs.findbugs.annotations.SuppressWarnings(
value = {"RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"},
justification = "Best effort"
)
public void run() {
nextBundleWrite.set(System.currentTimeMillis() + TimeUnit2.HOURS.toMillis(AUTO_BUNDLE_PERIOD_HOURS));
thread.setName(String.format("%s periodic bundle generator: since %s",
Expand All @@ -710,12 +744,8 @@ public void run() {
return;
}
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));

final String bundlePrefix = "support";
File file = new File(bundleDir,
bundlePrefix + "_" + dateFormat.format(new Date()) + ".zip");
File file = new File(bundleDir, SupportPlugin.getBundleFileName());
thread.setName(String.format("%s periodic bundle generator: writing %s since %s",
SupportPlugin.class.getSimpleName(), file.getName(), new Date()));
FileOutputStream fos = null;
Expand All @@ -725,30 +755,7 @@ public void run() {
} finally {
IOUtils.closeQuietly(fos);
}
thread.setName(String.format("%s periodic bundle generator: tidying old bundles since %s",
SupportPlugin.class.getSimpleName(), new Date()));
File[] files = bundleDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.startsWith(bundlePrefix) && name.endsWith(".zip");
}
});
long pivot = System.currentTimeMillis();
for (long l = 1; l * 2 > 0; l *= 2) {
boolean seen = false;
for (File f : files) {
if (!f.isFile() || f == file) {
continue;
}
long age = pivot - f.lastModified();
if (l <= age && age < l * 2) {
if (seen) {
f.delete();
} else {
seen = true;
}
}
}
}
cleanupOldBundles(bundleDir, file);
} catch (Throwable t) {
logger.log(Level.WARNING, "Could not save support bundle", t);
} finally {
Expand All @@ -763,6 +770,38 @@ public boolean accept(File dir, String name) {
}
}

@edu.umd.cs.findbugs.annotations.SuppressWarnings(
value = {"RV_RETURN_VALUE_IGNORED_BAD_PRACTICE", "IS2_INCONSISTENT_SYNC"},
justification = "RV_RETURN_VALUE_IGNORED_BAD_PRACTICE=Best effort, " +
"IS2_INCONSISTENT_SYNC=only called from an already synchronized method"
)
private void cleanupOldBundles(File bundleDir, File justGenerated) {
thread.setName(String.format("%s periodic bundle generator: tidying old bundles since %s",
SupportPlugin.class.getSimpleName(), new Date()));
File[] files = bundleDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".zip");
}
});
long pivot = System.currentTimeMillis();
for (long l = 1; l * 2 > 0; l *= 2) {
boolean seen = false;
for (File f : files) {
if (!f.isFile() || f == justGenerated) {
continue;
}
long age = pivot - f.lastModified();
if (l <= age && age < l * 2) {
if (seen) {
f.delete();
} else {
seen = true;
}
}
}
}
}

}

@Extension
Expand Down

0 comments on commit 80b611d

Please sign in to comment.