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:

committed by
Marco Miller

parent
b4b7c82382
commit
350fc68114
@@ -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
|
and SSH. If set to true Administrators can install new plugins
|
||||||
remotely, or disable existing plugins. Defaults to false.
|
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::
|
[[plugins.jsLoadTimeout]]plugins.jsLoadTimeout::
|
||||||
+
|
+
|
||||||
Set the timeout value for loading JavaScript plugins in Gerrit UI.
|
Set the timeout value for loading JavaScript plugins in Gerrit UI.
|
||||||
|
@@ -215,7 +215,8 @@ public abstract class AbstractDaemonTest {
|
|||||||
beforeTest(description);
|
beforeTest(description);
|
||||||
ProjectResetter.Config input = requireNonNull(resetProjects());
|
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;
|
AbstractDaemonTest.this.resetter = resetter;
|
||||||
base.evaluate();
|
base.evaluate();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -298,12 +299,16 @@ public abstract class AbstractDaemonTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void clearSender() {
|
public void clearSender() {
|
||||||
sender.clear();
|
if (sender != null) {
|
||||||
|
sender.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void startEventRecorder() {
|
public void startEventRecorder() {
|
||||||
eventRecorder = eventRecorderFactory.create(admin);
|
if (eventRecorderFactory != null) {
|
||||||
|
eventRecorder = eventRecorderFactory.create(admin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -318,7 +323,9 @@ public abstract class AbstractDaemonTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
public void closeEventRecorder() {
|
public void closeEventRecorder() {
|
||||||
eventRecorder.close();
|
if (eventRecorder != null) {
|
||||||
|
eventRecorder.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -20,6 +20,7 @@ import com.google.common.base.MoreObjects;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ComparisonChain;
|
import com.google.common.collect.ComparisonChain;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.LinkedHashMultimap;
|
import com.google.common.collect.LinkedHashMultimap;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
@@ -50,8 +51,10 @@ import java.nio.file.Paths;
|
|||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -87,6 +90,7 @@ public class PluginLoader implements LifecycleListener {
|
|||||||
private final Provider<String> urlProvider;
|
private final Provider<String> urlProvider;
|
||||||
private final PersistentCacheFactory persistentCacheFactory;
|
private final PersistentCacheFactory persistentCacheFactory;
|
||||||
private final boolean remoteAdmin;
|
private final boolean remoteAdmin;
|
||||||
|
private final Set<String> mandatoryPlugins;
|
||||||
private final UniversalServerPluginProvider serverPluginFactory;
|
private final UniversalServerPluginProvider serverPluginFactory;
|
||||||
private final GerritRuntime gerritRuntime;
|
private final GerritRuntime gerritRuntime;
|
||||||
|
|
||||||
@@ -114,6 +118,8 @@ public class PluginLoader implements LifecycleListener {
|
|||||||
serverPluginFactory = pluginFactory;
|
serverPluginFactory = pluginFactory;
|
||||||
|
|
||||||
remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
|
remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
|
||||||
|
mandatoryPlugins =
|
||||||
|
ImmutableSet.copyOf(Arrays.asList(cfg.getStringList("plugins", null, "mandatory")));
|
||||||
this.gerritRuntime = gerritRuntime;
|
this.gerritRuntime = gerritRuntime;
|
||||||
|
|
||||||
long checkFrequency =
|
long checkFrequency =
|
||||||
@@ -381,50 +387,57 @@ public class PluginLoader implements LifecycleListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void rescan() {
|
public synchronized void rescan() {
|
||||||
|
Set<String> loadedPlugins = new HashSet<>();
|
||||||
SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
|
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);
|
Set<String> missingMandatory = Sets.difference(mandatoryPlugins, loadedPlugins);
|
||||||
|
if (!missingMandatory.isEmpty()) {
|
||||||
Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
|
throw new MissingMandatoryPluginsException(missingMandatory);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanInBackground();
|
cleanInBackground();
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user