import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
-import org.sonar.api.Startable;
import org.sonar.api.Plugin;
+import org.sonar.api.Startable;
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.platform.ExplodedPlugin;
import org.sonar.core.platform.PluginClassLoader;
import org.sonar.core.platform.PluginInfo;
import org.sonar.core.platform.PluginRepository;
import org.sonar.server.platform.ServerFileSystem;
+import org.sonar.server.plugins.PluginRequirementsValidator;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
Loggers.get(getClass()).info("Load plugins");
registerPluginsFromDir(fs.getInstalledBundledPluginsDir());
registerPluginsFromDir(fs.getInstalledExternalPluginsDir());
+
+ PluginRequirementsValidator.unloadIncompatiblePlugins(pluginInfosByKeys);
+
Map<String, ExplodedPlugin> explodedPluginsByKey = extractPlugins(pluginInfosByKeys);
pluginInstancesByKeys.putAll(loader.load(explodedPluginsByKey));
started.set(true);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import com.google.common.base.Strings;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.updatecenter.common.Version;
+
+/**
+ * Checks plugins dependency requirements
+ * @param <T>
+ */
+public class PluginRequirementsValidator<T extends PluginInfo> {
+ private static final Logger LOG = Loggers.get(PluginRequirementsValidator.class);
+
+ private final Map<String, T> allPluginsByKeys;
+
+ PluginRequirementsValidator(Map<String, T> allPluginsByKeys) {
+ this.allPluginsByKeys = allPluginsByKeys;
+ }
+
+ /**
+ * Utility method that removes the plugins that are not compatible with current environment.
+ */
+ public static <T extends PluginInfo> void unloadIncompatiblePlugins(Map<String, T> pluginsByKey) {
+ // loop as long as the previous loop ignored some plugins. That allows to support dependencies
+ // on many levels, for example D extends C, which extends B, which requires A. If A is not installed,
+ // then B, C and D must be ignored. That's not possible to achieve this algorithm with a single iteration over plugins.
+ var validator = new PluginRequirementsValidator<>(pluginsByKey);
+ Set<String> removedKeys = new HashSet<>();
+ do {
+ removedKeys.clear();
+ for (T plugin : pluginsByKey.values()) {
+ if (!validator.isCompatible(plugin)) {
+ removedKeys.add(plugin.getKey());
+ }
+ }
+ for (String removedKey : removedKeys) {
+ pluginsByKey.remove(removedKey);
+ }
+ } while (!removedKeys.isEmpty());
+ }
+
+ boolean isCompatible(T plugin) {
+ if (!Strings.isNullOrEmpty(plugin.getBasePlugin()) && !allPluginsByKeys.containsKey(plugin.getBasePlugin())) {
+ // it extends a plugin that is not installed
+ LOG.warn("Plugin {} [{}] is ignored because its base plugin [{}] is not installed", plugin.getName(), plugin.getKey(), plugin.getBasePlugin());
+ return false;
+ }
+
+ for (PluginInfo.RequiredPlugin requiredPlugin : plugin.getRequiredPlugins()) {
+ PluginInfo installedRequirement = allPluginsByKeys.get(requiredPlugin.getKey());
+ if (installedRequirement == null) {
+ // it requires a plugin that is not installed
+ LOG.warn("Plugin {} [{}] is ignored because the required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), requiredPlugin.getKey());
+ return false;
+ }
+ Version installedRequirementVersion = installedRequirement.getVersion();
+ if (installedRequirementVersion != null && requiredPlugin.getMinimalVersion().compareToIgnoreQualifier(installedRequirementVersion) > 0) {
+ // it requires a more recent version
+ LOG.warn("Plugin {} [{}] is ignored because the version {} of required plugin [{}] is not installed", plugin.getName(), plugin.getKey(),
+ requiredPlugin.getMinimalVersion(), requiredPlugin.getKey());
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.plugins;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.core.platform.PluginInfo.RequiredPlugin;
+import org.sonar.updatecenter.common.Version;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PluginRequirementsValidatorTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Test
+ public void unloadIncompatiblePlugins_removes_incompatible_plugins() {
+ PluginInfo pluginE = new PluginInfo("pluginE");
+
+ PluginInfo pluginD = new PluginInfo("pluginD")
+ .setBasePlugin("pluginC");
+ PluginInfo pluginC = new PluginInfo("pluginC")
+ .setBasePlugin("pluginB");
+ PluginInfo pluginB = new PluginInfo("pluginB")
+ .addRequiredPlugin(RequiredPlugin.parse("pluginA:1.0"));
+ Map<String, PluginInfo> plugins = new HashMap<>();
+ plugins.put(pluginB.getKey(), pluginB);
+ plugins.put(pluginC.getKey(), pluginC);
+ plugins.put(pluginD.getKey(), pluginD);
+ plugins.put(pluginE.getKey(), pluginE);
+
+ PluginRequirementsValidator.unloadIncompatiblePlugins(plugins);
+
+ assertThat(plugins).contains(Map.entry(pluginE.getKey(), pluginE));
+ }
+
+ @Test
+ public void isCompatible_verifies_base_plugin_existence() {
+ PluginInfo pluginWithoutBase = new PluginInfo("plugin-without-base-plugin")
+ .setBasePlugin("not-existing-base-plugin");
+ PluginInfo basePlugin = new PluginInfo("base-plugin");
+ PluginInfo pluginWithBase = new PluginInfo("plugin-with-base-plugin")
+ .setBasePlugin("base-plugin");
+ Map<String, PluginInfo> plugins = new HashMap<>();
+ plugins.put(pluginWithoutBase.getKey(), pluginWithoutBase);
+ plugins.put(basePlugin.getKey(), basePlugin);
+ plugins.put(pluginWithBase.getKey(), pluginWithBase);
+
+ var underTest = new PluginRequirementsValidator<>(plugins);
+
+ assertThat(underTest.isCompatible(pluginWithoutBase)).isFalse();
+ assertThat(underTest.isCompatible(pluginWithBase)).isTrue();
+ assertThat(logTester.logs(LoggerLevel.WARN))
+ .contains("Plugin plugin-without-base-plugin [plugin-without-base-plugin] is ignored"
+ + " because its base plugin [not-existing-base-plugin] is not installed");
+ }
+
+ @Test
+ public void isCompatible_verifies_required_plugin_existence() {
+ PluginInfo requiredPlugin = new PluginInfo("required")
+ .setVersion(Version.create("1.2"));
+ PluginInfo pluginWithRequired = new PluginInfo("plugin-with-required-plugin")
+ .addRequiredPlugin(RequiredPlugin.parse("required:1.2"));
+ PluginInfo pluginWithoutRequired = new PluginInfo("plugin-without-required-plugin")
+ .addRequiredPlugin(RequiredPlugin.parse("notexistingrequired:1.0"));
+
+ Map<String, PluginInfo> plugins = new HashMap<>();
+ plugins.put(requiredPlugin.getKey(), requiredPlugin);
+ plugins.put(pluginWithRequired.getKey(), pluginWithRequired);
+ plugins.put(pluginWithoutRequired.getKey(), pluginWithoutRequired);
+
+ var underTest = new PluginRequirementsValidator<>(plugins);
+
+ assertThat(underTest.isCompatible(pluginWithoutRequired)).isFalse();
+ assertThat(underTest.isCompatible(pluginWithRequired)).isTrue();
+ assertThat(logTester.logs(LoggerLevel.WARN))
+ .contains("Plugin plugin-without-required-plugin [plugin-without-required-plugin] is ignored"
+ + " because the required plugin [notexistingrequired] is not installed");
+ }
+
+ @Test
+ public void isCompatible_verifies_required_plugins_version() {
+ PluginInfo requiredPlugin = new PluginInfo("required")
+ .setVersion(Version.create("1.2"));
+ PluginInfo pluginWithRequired = new PluginInfo("plugin-with-required-plugin")
+ .addRequiredPlugin(RequiredPlugin.parse("required:0.8"));
+ PluginInfo pluginWithoutRequired = new PluginInfo("plugin-without-required-plugin")
+ .addRequiredPlugin(RequiredPlugin.parse("required:1.5"));
+
+ Map<String, PluginInfo> plugins = new HashMap<>();
+ plugins.put(requiredPlugin.getKey(), requiredPlugin);
+ plugins.put(pluginWithRequired.getKey(), pluginWithRequired);
+ plugins.put(pluginWithoutRequired.getKey(), pluginWithoutRequired);
+
+ var underTest = new PluginRequirementsValidator<>(plugins);
+
+ assertThat(underTest.isCompatible(pluginWithoutRequired)).isFalse();
+ assertThat(underTest.isCompatible(pluginWithRequired)).isTrue();
+ assertThat(logTester.logs(LoggerLevel.WARN))
+ .contains("Plugin plugin-without-required-plugin [plugin-without-required-plugin] is ignored"
+ + " because the version 1.5 of required plugin [required] is not installed");
+ }
+
+}
*/
package org.sonar.server.plugins;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import java.io.File;
import java.io.IOException;
import org.sonar.core.platform.PluginInfo;
import org.sonar.core.platform.SonarQubeVersion;
import org.sonar.server.platform.ServerFileSystem;
-import org.sonar.updatecenter.common.Version;
import static java.lang.String.format;
import static org.apache.commons.io.FileUtils.moveFile;
plugins.putAll(externalPluginsByKey);
plugins.putAll(bundledPluginsByKey);
- unloadIncompatiblePlugins(plugins);
+ PluginRequirementsValidator.unloadIncompatiblePlugins(plugins);
return plugins.values();
}
- /**
- * Removes the plugins that are not compatible with current environment.
- */
- private static void unloadIncompatiblePlugins(Map<String, ServerPluginInfo> pluginsByKey) {
- // loop as long as the previous loop ignored some plugins. That allows to support dependencies
- // on many levels, for example D extends C, which extends B, which requires A. If A is not installed,
- // then B, C and D must be ignored. That's not possible to achieve this algorithm with a single iteration over plugins.
- Set<String> removedKeys = new HashSet<>();
- do {
- removedKeys.clear();
- for (ServerPluginInfo plugin : pluginsByKey.values()) {
- if (!isCompatible(plugin, pluginsByKey)) {
- removedKeys.add(plugin.getKey());
- }
- }
- for (String removedKey : removedKeys) {
- pluginsByKey.remove(removedKey);
- }
- } while (!removedKeys.isEmpty());
- }
-
- @VisibleForTesting
- static boolean isCompatible(ServerPluginInfo plugin, Map<String, ServerPluginInfo> allPluginsByKeys) {
- if (!Strings.isNullOrEmpty(plugin.getBasePlugin()) && !allPluginsByKeys.containsKey(plugin.getBasePlugin())) {
- // it extends a plugin that is not installed
- LOG.warn("Plugin {} [{}] is ignored because its base plugin [{}] is not installed", plugin.getName(), plugin.getKey(), plugin.getBasePlugin());
- return false;
- }
-
- for (PluginInfo.RequiredPlugin requiredPlugin : plugin.getRequiredPlugins()) {
- PluginInfo installedRequirement = allPluginsByKeys.get(requiredPlugin.getKey());
- if (installedRequirement == null) {
- // it requires a plugin that is not installed
- LOG.warn("Plugin {} [{}] is ignored because the required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), requiredPlugin.getKey());
- return false;
- }
- Version installedRequirementVersion = installedRequirement.getVersion();
- if (installedRequirementVersion != null && requiredPlugin.getMinimalVersion().compareToIgnoreQualifier(installedRequirementVersion) > 0) {
- // it requires a more recent version
- LOG.warn("Plugin {} [{}] is ignored because the version {} of required plugin [{}] is not installed", plugin.getName(), plugin.getKey(),
- requiredPlugin.getMinimalVersion(), requiredPlugin.getKey());
- return false;
- }
- }
- return true;
- }
-
private static String getRelativeDir(File dir) {
Path parent = dir.toPath().getParent().getParent();
return parent.relativize(dir.toPath()).toString();