[JENKINS-27177] Add strategies to disable plugins. Move process to core

Move the process to core to be reusable, not only for the cli command.
Add optionalDependants list to PluginWrapper to process them easily.

Replace (depreacate) the disable method without checks by this one with
strategies.

Improve messages and tests
This commit is contained in:
M Ramon Leon authored and Ramon Leon committed 2018-10-15 10:06:21 +02:00
1 parent 18be1960d1
commit fe8d0d5e7c
9 files changed
+686 -182

No files matched your search

+215 -12
View File
@@ -25,7 +25,9 @@
package hudson;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import hudson.PluginManager.PluginInstanceStore;
import hudson.cli.DisablePluginCommand;
import hudson.model.AdministrativeMonitor;
import hudson.model.Api;
import hudson.model.ModelObject;
@@ -56,16 +58,7 @@ import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Predicate;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -200,6 +193,11 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
*/
private Set<String> dependants = Collections.emptySet();
/**
* List of plugins that depend optionally on this plugin.
*/
private Set<String> optionalDependants = Collections.emptySet();
/**
* The core can depend on a plugin if it is bundled. Sometimes it's the only thing that
* depends on the plugin e.g. UI support library bundle plugin.
@@ -214,6 +212,14 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
this.dependants = dependants;
}
/**
* Set the list of components that depend optionally on this plugin.
* @param optionalDependants The list of components that depend optionally on this plugin.
*/
public void setOptionalDependants(@Nonnull Set<String> optionalDependants) {
this.optionalDependants = optionalDependants;
}
/**
* Get the list of components that depend on this plugin.
* @return The list of components that depend on this plugin.
@@ -226,6 +232,14 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
}
}
/**
* Get the list of components that depend optionally on this plugin.
* @return The list of components that depend optionally on this plugin.
*/
public @Nonnull Set<String> getOptionalDependants() {
return optionalDependants;
}
/**
* Does this plugin have anything that depends on it.
* @return {@code true} if something (Jenkins core, or another plugin) depends on this
@@ -234,7 +248,17 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
public boolean hasDependants() {
return (isBundled || !dependants.isEmpty());
}
/**
* Does this plugin have anything that depends optionally on it.
* @return {@code true} if something (Jenkins core, or another plugin) depends optionally on this
* plugin, otherwise {@code false}.
*/
public boolean hasOptionalDependants() {
return !optionalDependants.isEmpty();
}
/**
* Does this plugin depend on any other plugins.
* @return {@code true} if this plugin depends on other plugins, otherwise {@code false}.
@@ -519,9 +543,19 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
}
/**
* Disables this plugin next time Jenkins runs.
* Disables this plugin next time Jenkins runs. As it doesn't check anything, it's recommended to use the method
* {@link #disable(PluginDisableStrategy)}
*/
@Deprecated //see https://issues.jenkins-ci.org/browse/JENKINS-27177
public void disable() throws IOException {
disableWithoutCheck();
}
/**
* Disable a plugin wihout checking any dependency. Only add the disable file.
* @throws IOException
*/
private void disableWithoutCheck() throws IOException {
// creates an empty file
try (OutputStream os = Files.newOutputStream(disableFile.toPath())) {
os.close();
@@ -530,6 +564,95 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
}
}
/**
* Disable this plugin using a strategy.
* @param strategy strategy to use
* @return an object representing the result of the disablement of this plugin and its dependants plugins.
* @throws IOException If this plugin or one dependant plugin raise this exception during its disablement. It could
* be raised because the disable file cannot be created.
*/
@Nonnull
public PluginDisableResult disable(@Nonnull PluginDisableStrategy strategy) throws IOException {
PluginDisableResult result = new PluginDisableResult(shortName);
if (!this.isEnabled()) {
result.setMessage(Messages.PluginWrapper_Already_Disabled(shortName));
result.setStatus(PluginDisableStatus.ALREADY_DISABLED);
return result;
}
// Act as a flag indicating if this plugin, finally, can be disabled. If there is a not-disabled-dependant
// plugin, this one couldn't be disabled.
String aDependantNotDisabled = null;
// List of dependants plugins to 'check'. 'Check' means disable for mandatory or all strategies, or review if
// this dependant-mandatory plugin is enabled in order to return an error for the NONE strategy.
Set<String> dependantsToCheck;
switch (strategy) {
case MANDATORY:
// As of getDependants has all the dependants, we get the difference between them and only the optionals
dependantsToCheck = Sets.difference(this.getDependants(), this.getOptionalDependants());
break;
case ALL:
// getDependants returns all the dependant plugins, mandatory or optional.
dependantsToCheck = this.getDependants();
break;
case NONE:
default:
// with NONE, the process only fail if mandatory dependant plugins exists
dependantsToCheck = Sets.difference(this.getDependants(), this.getOptionalDependants());
}
// Review all the dependants and add to the plugin result what happened with its dependants
for (String dependant : dependantsToCheck) {
PluginWrapper dependantPlugin = parent.getPlugin(dependant);
// The dependant plugin doesn't exist, add an error to the report
if (dependantPlugin == null) {
PluginDisableResult dependantStatus = new PluginDisableResult(dependant);
dependantStatus.setStatus(PluginDisableStatus.NO_SUCH_PLUGIN);
dependantStatus.setMessage(Messages.PluginWrapper_NoSuchPlugin(dependant));
result.addDependantDisableStatus(dependantStatus);
// If the strategy is none and there is some enabled dependant plugin, the plugin cannot be disabled. If
// this dependant plugin is not enabled, continue searching for one enabled.
} else if (strategy.equals(PluginDisableStrategy.NONE)) {
if (dependantPlugin.isEnabled()) {
aDependantNotDisabled = dependant;
break; // in this case, we don't need to continue with the rest of its dependants
}
// If the strategy is not none and this dependant plugin is not enabled, add it as already disabled
} else if (!dependantPlugin.isEnabled()) {
PluginDisableResult dependantStatus = new PluginDisableResult(dependant);
dependantStatus.setStatus(PluginDisableStatus.ALREADY_DISABLED);
dependantStatus.setMessage(Messages.PluginWrapper_Already_Disabled(dependant));
result.addDependantDisableStatus(dependantStatus);
// If the strategy is not none and this dependant plugin is enabled, disable it
} else {
// As there is no cycles in the plugin dependencies, the recursion shouldn't be infinite. The
// strategy used is the same for its dependants plugins
result.addDependantDisableStatus(dependantPlugin.disable(strategy));
}
}
// If there is no enabled-dependant plugin, disable this plugin and add it to the result
if (aDependantNotDisabled == null) {
this.disableWithoutCheck();
result.setMessage(Messages.PluginWrapper_Plugin_Disabled(shortName));
result.setStatus(PluginDisableStatus.DISABLED);
// if there is yet some not disabled dependant plugin (only possible with none strategy), this plugin cannot
// be disabled.
} else {
result.setMessage(Messages.PluginWrapper_Plugin_Has_Dependant(shortName, aDependantNotDisabled, strategy));
result.setStatus(PluginDisableStatus.NOT_DISABLED_DEPENDANTS);
}
return result;
}
/**
* Returns true if this plugin is enabled for this session.
*/
@@ -829,6 +952,86 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
}
}
/**
* The result of the disablement of a plugin and its dependants plugins.
*/
public static class PluginDisableResult {
private String plugin;
private PluginDisableStatus status;
private String message;
private Set<PluginDisableResult> dependantsDisableStatus = new HashSet<>();
public PluginDisableResult(String plugin) {
this.plugin = plugin;
}
public String getPlugin() {
return plugin;
}
public PluginDisableStatus getStatus() {
return status;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginDisableResult that = (PluginDisableResult) o;
return Objects.equals(plugin, that.plugin);
}
@Override
public int hashCode() {
return Objects.hash(plugin);
}
public void setStatus(PluginDisableStatus status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Set<PluginDisableResult> getDependantsDisableStatus() {
return dependantsDisableStatus;
}
public void addDependantDisableStatus(PluginDisableResult dependantDisableStatus) {
dependantsDisableStatus.add(dependantDisableStatus);
}
}
/**
* An enum to hold the status of a disabling action against a plugin. To do it more reader-friendly.
*/
public static enum PluginDisableStatus {
NO_SUCH_PLUGIN,
DISABLED,
ALREADY_DISABLED,
NOT_DISABLED_DEPENDANTS
}
/**
* The strategies defined for disabling a plugin.
*/
public static enum PluginDisableStrategy {
NONE,
MANDATORY,
ALL;
@Override
public String toString() {
return this.name().toLowerCase();
}
}
//
//
// Action methods