]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13822 Make plugin requirements check consistence between CE and WEB processes
authorJacek <jacek.poreda@sonarsource.com>
Mon, 6 Jun 2022 13:08:04 +0000 (15:08 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 7 Jun 2022 20:03:09 +0000 (20:03 +0000)
server/sonar-ce/src/main/java/org/sonar/ce/container/CePluginRepository.java
server/sonar-server-common/src/main/java/org/sonar/server/plugins/PluginRequirementsValidator.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/plugins/PluginRequirementsValidatorTest.java [new file with mode: 0644]
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/PluginJarLoader.java

index 5e2a0f50a9536141ccda5b537decea6a585f61fe..319d8efce6458379d972d43300329ceb7223e8f7 100644 (file)
@@ -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<String, ExplodedPlugin> 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 (file)
index 0000000..43c2e0d
--- /dev/null
@@ -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 <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;
+  }
+
+}
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 (file)
index 0000000..dd8cd0b
--- /dev/null
@@ -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<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");
+  }
+
+}
index a7402600556395d5bda526ba230113e6984d9629..51ff49a6c2b6adead8e89446837154ff1cb0b9a5 100644 (file)
@@ -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<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();