Allow to configure mandatory plugins

In gerrit.config we can now define a list of mandatory plugins:

  [plugins]
    mandatory = replication
    mandatory = hooks

If a mandatory plugin does not load, either because it is not present in
the plugins directory or because it fails to load, Gerrit start will
fail.

Change-Id: I26c578051b0f91844bab0a9b46e0fac34d88a15e
This commit is contained in:
Saša Živkov
2019-05-13 14:13:51 +02:00
committed by Marco Miller
parent b4b7c82382
commit 350fc68114
5 changed files with 142 additions and 45 deletions

View File

@@ -3481,6 +3481,11 @@ Enable remote installation, enable and disable of plugins over HTTP
and SSH. If set to true Administrators can install new plugins
remotely, or disable existing plugins. Defaults to false.
[[plugins.mandatory]]plugins.mandatory::
+
List of mandatory plugins. If a plugin from this list does not load,
Gerrit start will fail.
[[plugins.jsLoadTimeout]]plugins.jsLoadTimeout::
+
Set the timeout value for loading JavaScript plugins in Gerrit UI.

View File

@@ -215,7 +215,8 @@ public abstract class AbstractDaemonTest {
beforeTest(description);
ProjectResetter.Config input = requireNonNull(resetProjects());
try (ProjectResetter resetter = projectResetter.builder().build(input)) {
try (ProjectResetter resetter =
projectResetter != null ? projectResetter.builder().build(input) : null) {
AbstractDaemonTest.this.resetter = resetter;
base.evaluate();
} finally {
@@ -298,12 +299,16 @@ public abstract class AbstractDaemonTest {
@Before
public void clearSender() {
sender.clear();
if (sender != null) {
sender.clear();
}
}
@Before
public void startEventRecorder() {
eventRecorder = eventRecorderFactory.create(admin);
if (eventRecorderFactory != null) {
eventRecorder = eventRecorderFactory.create(admin);
}
}
@Before
@@ -318,7 +323,9 @@ public abstract class AbstractDaemonTest {
@After
public void closeEventRecorder() {
eventRecorder.close();
if (eventRecorder != null) {
eventRecorder.close();
}
}
@AfterClass

View File

@@ -0,0 +1,30 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.plugins;
import java.util.Collection;
/** Raised when one or more mandatory plugins are missing. */
public class MissingMandatoryPluginsException extends RuntimeException {
private static final long serialVersionUID = 1L;
public MissingMandatoryPluginsException(Collection<String> pluginNames) {
super(getMessage(pluginNames));
}
private static String getMessage(Collection<String> pluginNames) {
return String.format("Cannot find or load the following mandatory plugins: %s", pluginNames);
}
}

View File

@@ -20,6 +20,7 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
@@ -50,8 +51,10 @@ import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -87,6 +90,7 @@ public class PluginLoader implements LifecycleListener {
private final Provider<String> urlProvider;
private final PersistentCacheFactory persistentCacheFactory;
private final boolean remoteAdmin;
private final Set<String> mandatoryPlugins;
private final UniversalServerPluginProvider serverPluginFactory;
private final GerritRuntime gerritRuntime;
@@ -114,6 +118,8 @@ public class PluginLoader implements LifecycleListener {
serverPluginFactory = pluginFactory;
remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
mandatoryPlugins =
ImmutableSet.copyOf(Arrays.asList(cfg.getStringList("plugins", null, "mandatory")));
this.gerritRuntime = gerritRuntime;
long checkFrequency =
@@ -381,50 +387,57 @@ public class PluginLoader implements LifecycleListener {
}
public synchronized void rescan() {
Set<String> loadedPlugins = new HashSet<>();
SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
if (pluginsFiles.isEmpty()) {
return;
if (!pluginsFiles.isEmpty()) {
syncDisabledPlugins(pluginsFiles);
Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
String name = entry.getKey();
Path path = entry.getValue();
String fileName = path.getFileName().toString();
if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
logger.atWarning().log(
"No Plugin provider was found that handles this file format: %s", fileName);
continue;
}
FileSnapshot brokenTime = broken.get(name);
if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
continue;
}
Plugin active = running.get(name);
if (active != null && !active.isModified(path)) {
loadedPlugins.add(name);
continue;
}
if (active != null) {
logger.atInfo().log("Reloading plugin %s", active.getName());
}
try {
Plugin loadedPlugin = runPlugin(name, path, active);
if (!loadedPlugin.isDisabled()) {
loadedPlugins.add(name);
logger.atInfo().log(
"%s plugin %s, version %s",
active == null ? "Loaded" : "Reloaded",
loadedPlugin.getName(),
loadedPlugin.getVersion());
}
} catch (PluginInstallException e) {
logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
}
}
}
syncDisabledPlugins(pluginsFiles);
Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
String name = entry.getKey();
Path path = entry.getValue();
String fileName = path.getFileName().toString();
if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
logger.atWarning().log(
"No Plugin provider was found that handles this file format: %s", fileName);
continue;
}
FileSnapshot brokenTime = broken.get(name);
if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
continue;
}
Plugin active = running.get(name);
if (active != null && !active.isModified(path)) {
continue;
}
if (active != null) {
logger.atInfo().log("Reloading plugin %s", active.getName());
}
try {
Plugin loadedPlugin = runPlugin(name, path, active);
if (!loadedPlugin.isDisabled()) {
logger.atInfo().log(
"%s plugin %s, version %s",
active == null ? "Loaded" : "Reloaded",
loadedPlugin.getName(),
loadedPlugin.getVersion());
}
} catch (PluginInstallException e) {
logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
}
Set<String> missingMandatory = Sets.difference(mandatoryPlugins, loadedPlugins);
if (!missingMandatory.isEmpty()) {
throw new MissingMandatoryPluginsException(missingMandatory);
}
cleanInBackground();

View File

@@ -0,0 +1,42 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.acceptance.api.plugin;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.server.plugins.MissingMandatoryPluginsException;
import org.junit.Test;
import org.junit.runner.Description;
@NoHttpd
public class PluginLoaderIT extends AbstractDaemonTest {
Description testDescription;
@Override
protected void beforeTest(Description description) throws Exception {
this.testDescription = description;
}
@Override
protected void afterTest() throws Exception {}
@Test(expected = MissingMandatoryPluginsException.class)
@GerritConfig(name = "plugins.mandatory", value = "my-mandatory-plugin")
public void shouldFailToStartGerritWhenMandatoryPluginsAreMissing() throws Exception {
super.beforeTest(testDescription);
}
}