Skip to content

Commit

Permalink
[JENKINS-34681] Allow custom plugin managers when using war (#2323)
Browse files Browse the repository at this point in the history
* [JENKINS-34681] Allow custom default plugin managers

* [JENKINS-34681] Convert remaining properties to SystemProperties.
  • Loading branch information
andresrc authored and oleg-nenashev committed May 14, 2016
1 parent dbe04b3 commit 454642d
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 12 deletions.
30 changes: 25 additions & 5 deletions core/src/main/java/hudson/LocalPluginManager.java
Expand Up @@ -24,26 +24,46 @@

package hudson;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import jenkins.util.SystemProperties;
import jenkins.model.Jenkins;

import javax.servlet.ServletContext;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.logging.Logger;

/**
* {@link PluginManager}
* Default implementation of {@link PluginManager}.
*
* @author Kohsuke Kawaguchi
*/
public class LocalPluginManager extends PluginManager {
public LocalPluginManager(Jenkins jenkins) {
super(jenkins.servletContext, new File(jenkins.getRootDir(),"plugins"));
/**
* Creates a new LocalPluginManager
* @param context Servlet context. Provided for compatibility as {@code Jenkins.getInstance().servletContext} should be used.
* @param rootDir Jenkins home directory.
*/
public LocalPluginManager(@CheckForNull ServletContext context, @NonNull File rootDir) {
super(context, new File(rootDir,"plugins"));
}

/**
* Creates a new LocalPluginManager
* @param jenkins Jenkins instance that will use the plugin manager.
*/
public LocalPluginManager(@NonNull Jenkins jenkins) {
this(jenkins.servletContext, jenkins.getRootDir());
}

public LocalPluginManager(File rootDir) {
super(null, new File(rootDir,"plugins"));
/**
* Creates a new LocalPluginManager
* @param rootDir Jenkins home directory.
*/
public LocalPluginManager(@NonNull File rootDir) {
this(null, rootDir);
}

/**
Expand Down
102 changes: 96 additions & 6 deletions core/src/main/java/hudson/PluginManager.java
Expand Up @@ -23,6 +23,7 @@
*/
package hudson;

import edu.umd.cs.findbugs.annotations.NonNull;
import jenkins.util.SystemProperties;
import hudson.PluginWrapper.Dependency;
import hudson.init.InitMilestone;
Expand Down Expand Up @@ -136,6 +137,7 @@
import hudson.model.DownloadService;
import hudson.util.FormValidation;

import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
Expand All @@ -145,10 +147,101 @@
/**
* Manages {@link PluginWrapper}s.
*
* <p>
* <b>Setting default Plugin Managers</b>. The default plugin manager in {@code Jenkins} can be replaced by defining a
* System Property (<code>hudson.PluginManager.className</code>). See {@link #createDefault(Jenkins)}.
* This className should be available on early startup, so it cannot come only from a library
* (e.g. Jenkins module or Extra library dependency in the WAR file project).
* Plugins cannot be used for such purpose.
* In order to be correctly instantiated, the class definition must have at least one constructor with the same
* signature as the following ones:
* <ol>
* <li>{@link LocalPluginManager#LocalPluginManager(Jenkins)} </li>
* <li>{@link LocalPluginManager#LocalPluginManager(ServletContext, File)} </li>
* <li>{@link LocalPluginManager#LocalPluginManager(File)} </li>
* </ol>
* Constructors are searched in the order provided above and only the first found suitable constructor is
* tried to build an instance. In the last two cases the {@link File} argument refers to the <i>Jenkins home directory</i>.
*
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public abstract class PluginManager extends AbstractModelObject implements OnMaster, StaplerOverridable {
/** Custom plugin manager system property or context param. */
public static final String CUSTOM_PLUGIN_MANAGER = PluginManager.class.getName() + ".className";

/** Accepted constructors for custom plugin manager, in the order they are tried. */
private enum PMConstructor {
JENKINS {
@Override
@NonNull PluginManager doCreate(@NonNull Class<? extends PluginManager> klass,
@NonNull Jenkins jenkins) throws ReflectiveOperationException {
return klass.getConstructor(Jenkins.class).newInstance(jenkins);
}
},
SC_FILE {
@Override
@NonNull PluginManager doCreate(@NonNull Class<? extends PluginManager> klass,
@NonNull Jenkins jenkins) throws ReflectiveOperationException {
return klass.getConstructor(ServletContext.class, File.class).newInstance(jenkins.servletContext, jenkins.getRootDir());
}
},
FILE {
@Override
@NonNull PluginManager doCreate(@NonNull Class<? extends PluginManager> klass,
@NonNull Jenkins jenkins) throws ReflectiveOperationException {
return klass.getConstructor(File.class).newInstance(jenkins.getRootDir());
}
};

final @CheckForNull PluginManager create(@NonNull Class<? extends PluginManager> klass,
@NonNull Jenkins jenkins) throws ReflectiveOperationException {
try {
return doCreate(klass, jenkins);
} catch(NoSuchMethodException e) {
// Constructor not found. Will try the remaining ones.
return null;
}
}

abstract @NonNull PluginManager doCreate(@NonNull Class<? extends PluginManager> klass,
@NonNull Jenkins jenkins) throws ReflectiveOperationException;
}

/**
* Creates the {@link PluginManager} to use if no one is provided to a {@link Jenkins} object.
* This method will be called after creation of {@link Jenkins} object, but before it is fully initialized.
* @param jenkins Jenkins Instance.
* @return Plugin manager to use. If no custom class is configured or in case of any error, the default
* {@link LocalPluginManager} is returned.
*/
public static @NonNull PluginManager createDefault(@NonNull Jenkins jenkins) {
String pmClassName = SystemProperties.getString(CUSTOM_PLUGIN_MANAGER);
if (!StringUtils.isBlank(pmClassName)) {
LOGGER.log(FINE, String.format("Use of custom plugin manager [%s] requested.", pmClassName));
try {
final Class<? extends PluginManager> klass = Class.forName(pmClassName).asSubclass(PluginManager.class);
// Iteration is in declaration order
for (PMConstructor c : PMConstructor.values()) {
PluginManager pm = c.create(klass, jenkins);
if (pm != null) {
return pm;
}
}
LOGGER.log(WARNING, String.format("Provided custom plugin manager [%s] does not provide any of the suitable constructors. Using default.", pmClassName));
} catch(NullPointerException e) {
// Class.forName and Class.getConstructor are supposed to never return null though a broken ClassLoader
// could break the contract. Just in case we introduce this specific catch to avoid polluting the logs with NPEs.
LOGGER.log(WARNING, String.format("Unable to instantiate custom plugin manager [%s]. Using default.", pmClassName));
} catch(ClassCastException e) {
LOGGER.log(WARNING, String.format("Provided class [%s] does not extend PluginManager. Using default.", pmClassName));
} catch(Exception e) {
LOGGER.log(WARNING, String.format("Unable to instantiate custom plugin manager [%s]. Using default.", pmClassName), e);
}
}
return new LocalPluginManager(jenkins);
}

/**
* All discovered plugins.
*/
Expand Down Expand Up @@ -218,11 +311,8 @@ public PluginManager(ServletContext context, File rootDir) {
this.rootDir = rootDir;
if(!rootDir.exists())
rootDir.mkdirs();
String workDir = System.getProperty(PluginManager.class.getName()+".workDir");
if (workDir == null && context != null) {
workDir = context.getInitParameter(PluginManager.class.getName() + ".workDir");
}
this.workDir = workDir == null ? null : new File(workDir);
String workDir = SystemProperties.getString(PluginManager.class.getName()+".workDir");
this.workDir = StringUtils.isBlank(workDir) ? null : new File(workDir);

strategy = createPluginStrategy();

Expand Down Expand Up @@ -561,7 +651,7 @@ protected static void addDependencies(URL hpiResUrl, String fromPath, Set<URL> d
if (dependencyURL != null) {
// And transitive deps...
addDependencies(dependencyURL, fromPath, dependencySet);
// And then add the current plugin
// And then add the current plugin
dependencySet.add(dependencyURL);
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/jenkins/model/Jenkins.java
Expand Up @@ -865,7 +865,7 @@ protected Jenkins(File root, ServletContext context, PluginManager pluginManager
}

if (pluginManager==null)
pluginManager = new LocalPluginManager(this);
pluginManager = PluginManager.createDefault(this);
this.pluginManager = pluginManager;
// JSON binding needs to be able to see all the classes from all the plugins
WebApp.get(servletContext).setClassLoader(pluginManager.uberClassLoader);
Expand Down
137 changes: 137 additions & 0 deletions test/src/test/java/hudson/CustomPluginManagerTest.java
@@ -0,0 +1,137 @@
/*
* 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 hudson;

import jenkins.model.Jenkins;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRecipe;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.WithPlugin;

import javax.servlet.ServletContext;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
* Tests for the use of a custom plugin manager in custom wars.
*/
public class CustomPluginManagerTest {
@Rule public final JenkinsRule r = new JenkinsRule();

// TODO: Move to jenkins-test-harness
@JenkinsRecipe(WithCustomLocalPluginManager.RuleRunnerImpl.class)
@Target(METHOD)
@Retention(RUNTIME)
public @interface WithCustomLocalPluginManager {
Class<? extends LocalPluginManager> value();

class RuleRunnerImpl extends JenkinsRecipe.Runner<WithCustomLocalPluginManager> {
private String oldValue;

@Override
public void setup(JenkinsRule jenkinsRule, WithCustomLocalPluginManager recipe) throws Exception {
jenkinsRule.useLocalPluginManager = true;
oldValue = System.getProperty(LocalPluginManager.CUSTOM_PLUGIN_MANAGER);
System.setProperty(LocalPluginManager.CUSTOM_PLUGIN_MANAGER, recipe.value().getName());

}

@Override
public void tearDown(JenkinsRule jenkinsRule, WithCustomLocalPluginManager recipe) throws Exception {
System.setProperty(LocalPluginManager.CUSTOM_PLUGIN_MANAGER, oldValue);
}
}
}

private void check(Class<? extends CustomPluginManager> klass) {
assertTrue("Correct plugin manager installed", klass.isAssignableFrom(r.getPluginManager().getClass()));
assertNotNull("Plugin 'tasks' installed", r.jenkins.getPlugin("tasks"));
}

// An interface not to override every constructor.
interface CustomPluginManager {
}

@Issue("JENKINS-34681")
@WithPlugin("tasks.jpi")
@WithCustomLocalPluginManager(CustomPluginManager1.class)
@Test public void customPluginManager1() {
check(CustomPluginManager1.class);
}

public static class CustomPluginManager1 extends LocalPluginManager implements CustomPluginManager {
public CustomPluginManager1(Jenkins jenkins) {
super(jenkins);
}
}

@Issue("JENKINS-34681")
@WithPlugin("tasks.jpi")
@WithCustomLocalPluginManager(CustomPluginManager2.class)
@Test public void customPluginManager2() {
check(CustomPluginManager2.class);
}

public static class CustomPluginManager2 extends LocalPluginManager implements CustomPluginManager {
public CustomPluginManager2(ServletContext ctx, File root) {
super(ctx, root);
}
}

@Issue("JENKINS-34681")
@WithPlugin("tasks.jpi")
@WithCustomLocalPluginManager(CustomPluginManager3.class)
@Test public void customPluginManager3() {
check(CustomPluginManager3.class);
}

public static class CustomPluginManager3 extends LocalPluginManager implements CustomPluginManager {
public CustomPluginManager3(File root) {
super(root);
}
}

@Issue("JENKINS-34681")
@WithPlugin("tasks.jpi")
@WithCustomLocalPluginManager(BadCustomPluginManager.class)
@Test public void badCustomPluginManager() {
assertFalse("Custom plugin manager not installed", r.getPluginManager() instanceof CustomPluginManager);
}

public static class BadCustomPluginManager extends LocalPluginManager implements CustomPluginManager {
public BadCustomPluginManager(File root, ServletContext ctx) {
super(ctx, root);
}
}

}

0 comments on commit 454642d

Please sign in to comment.