From 784089432581c819d2c369e014f40f37072603f6 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 6 Jun 2022 15:08:04 +0200 Subject: [PATCH] SONAR-13822 Make plugin requirements check consistence between CE and WEB processes --- .../ce/container/CePluginRepository.java | 6 +- .../plugins/PluginRequirementsValidator.java | 91 +++++++++++++ .../PluginRequirementsValidatorTest.java | 127 ++++++++++++++++++ .../sonar/server/plugins/PluginJarLoader.java | 51 +------ 4 files changed, 224 insertions(+), 51 deletions(-) create mode 100644 server/sonar-server-common/src/main/java/org/sonar/server/plugins/PluginRequirementsValidator.java create mode 100644 server/sonar-server-common/src/test/java/org/sonar/server/plugins/PluginRequirementsValidatorTest.java diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java index 5e2a0f50a95..319d8efce64 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java @@ -28,14 +28,15 @@ import java.util.Set; 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; @@ -70,6 +71,9 @@ public class CePluginRepository implements PluginRepository, Startable { Loggers.get(getClass()).info("Load plugins"); registerPluginsFromDir(fs.getInstalledBundledPluginsDir()); registerPluginsFromDir(fs.getInstalledExternalPluginsDir()); + + PluginRequirementsValidator.unloadIncompatiblePlugins(pluginInfosByKeys); + Map explodedPluginsByKey = extractPlugins(pluginInfosByKeys); pluginInstancesByKeys.putAll(loader.load(explodedPluginsByKey)); started.set(true); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/plugins/PluginRequirementsValidator.java b/server/sonar-server-common/src/main/java/org/sonar/server/plugins/PluginRequirementsValidator.java new file mode 100644 index 00000000000..43c2e0dd9d1 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/plugins/PluginRequirementsValidator.java @@ -0,0 +1,91 @@ +/* + * 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 + */ +public class PluginRequirementsValidator { + private static final Logger LOG = Loggers.get(PluginRequirementsValidator.class); + + private final Map allPluginsByKeys; + + PluginRequirementsValidator(Map allPluginsByKeys) { + this.allPluginsByKeys = allPluginsByKeys; + } + + /** + * Utility method that removes the plugins that are not compatible with current environment. + */ + public static void unloadIncompatiblePlugins(Map 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 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; + } + +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/plugins/PluginRequirementsValidatorTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/plugins/PluginRequirementsValidatorTest.java new file mode 100644 index 00000000000..dd8cd0b2523 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/plugins/PluginRequirementsValidatorTest.java @@ -0,0 +1,127 @@ +/* + * 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 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 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 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 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"); + } + +} diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java index a7402600556..51ff49a6c2b 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java @@ -19,7 +19,6 @@ */ 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; @@ -42,7 +41,6 @@ import org.sonar.api.utils.log.Loggers; 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; @@ -123,58 +121,11 @@ public class PluginJarLoader { 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 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 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 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(); -- 2.39.5