You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PluginJarLoader.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.plugins;
  21. import com.google.common.annotations.VisibleForTesting;
  22. import com.google.common.base.Strings;
  23. import com.google.common.collect.ImmutableSet;
  24. import java.io.File;
  25. import java.io.IOException;
  26. import java.nio.file.Path;
  27. import java.util.Collection;
  28. import java.util.Collections;
  29. import java.util.HashMap;
  30. import java.util.HashSet;
  31. import java.util.LinkedHashMap;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Set;
  35. import java.util.function.Function;
  36. import java.util.stream.Collectors;
  37. import org.apache.commons.io.FileUtils;
  38. import org.sonar.api.SonarRuntime;
  39. import org.sonar.api.utils.MessageException;
  40. import org.sonar.api.utils.log.Logger;
  41. import org.sonar.api.utils.log.Loggers;
  42. import org.sonar.core.platform.PluginInfo;
  43. import org.sonar.server.platform.ServerFileSystem;
  44. import org.sonar.updatecenter.common.Version;
  45. import static java.lang.String.format;
  46. import static org.apache.commons.io.FileUtils.moveFile;
  47. import static org.sonar.core.util.FileUtils.deleteQuietly;
  48. import static org.sonar.server.log.ServerProcessLogging.STARTUP_LOGGER_NAME;
  49. import static org.sonar.server.plugins.PluginType.BUNDLED;
  50. import static org.sonar.server.plugins.PluginType.EXTERNAL;
  51. public class PluginJarLoader {
  52. private static final Logger LOG = Loggers.get(PluginJarLoader.class);
  53. // List of plugins that are silently removed if installed
  54. private static final Set<String> DEFAULT_BLACKLISTED_PLUGINS = ImmutableSet.of("scmactivity", "issuesreport", "genericcoverage");
  55. // List of plugins that should prevent the server to finish its startup
  56. private static final Set<String> FORBIDDEN_INCOMPATIBLE_PLUGINS = ImmutableSet
  57. .of("sqale", "report", "views", "authgithub", "authgitlab", "authsaml", "ldap", "scmgit", "scmsvn");
  58. private static final String LOAD_ERROR_GENERIC_MESSAGE = "Startup failed: Plugins can't be loaded. See web logs for more information";
  59. private final ServerFileSystem fs;
  60. private final SonarRuntime runtime;
  61. private final Set<String> blacklistedPluginKeys;
  62. public PluginJarLoader(ServerFileSystem fs, SonarRuntime runtime) {
  63. this(fs, runtime, DEFAULT_BLACKLISTED_PLUGINS);
  64. }
  65. PluginJarLoader(ServerFileSystem fs, SonarRuntime runtime, Set<String> blacklistedPluginKeys) {
  66. this.fs = fs;
  67. this.runtime = runtime;
  68. this.blacklistedPluginKeys = blacklistedPluginKeys;
  69. }
  70. /**
  71. * Load the plugins that are located in lib/extensions and extensions/plugins. Blacklisted plugins are deleted.
  72. */
  73. public Collection<ServerPluginInfo> loadPlugins() {
  74. Map<String, ServerPluginInfo> bundledPluginsByKey = new LinkedHashMap<>();
  75. for (ServerPluginInfo bundled : getBundledPluginsMetadata()) {
  76. failIfContains(bundledPluginsByKey, bundled,
  77. plugin -> MessageException.of(format("Found two versions of the plugin %s [%s] in the directory %s. Please remove one of %s or %s.",
  78. bundled.getName(), bundled.getKey(), getRelativeDir(fs.getInstalledBundledPluginsDir()), bundled.getNonNullJarFile().getName(), plugin.getNonNullJarFile().getName())));
  79. bundledPluginsByKey.put(bundled.getKey(), bundled);
  80. }
  81. Map<String, ServerPluginInfo> externalPluginsByKey = new LinkedHashMap<>();
  82. for (ServerPluginInfo external : getExternalPluginsMetadata()) {
  83. failIfContains(bundledPluginsByKey, external,
  84. plugin -> MessageException.of(format("Found a plugin '%s' in the directory '%s' with the same key [%s] as a built-in feature '%s'. Please remove '%s'.",
  85. external.getName(), getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getKey(), plugin.getName(),
  86. new File(getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getNonNullJarFile().getName()))));
  87. failIfContains(externalPluginsByKey, external,
  88. plugin -> MessageException.of(format("Found two versions of the plugin '%s' [%s] in the directory '%s'. Please remove %s or %s.", external.getName(), external.getKey(),
  89. getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getNonNullJarFile().getName(), plugin.getNonNullJarFile().getName())));
  90. externalPluginsByKey.put(external.getKey(), external);
  91. }
  92. for (PluginInfo downloaded : getDownloadedPluginsMetadata()) {
  93. failIfContains(bundledPluginsByKey, downloaded,
  94. plugin -> MessageException.of(format("Fail to update plugin: %s. Built-in feature with same key already exists: %s. Move or delete plugin from %s directory",
  95. plugin.getName(), plugin.getKey(), getRelativeDir(fs.getDownloadedPluginsDir()))));
  96. ServerPluginInfo installedPlugin;
  97. if (externalPluginsByKey.containsKey(downloaded.getKey())) {
  98. deleteQuietly(externalPluginsByKey.get(downloaded.getKey()).getNonNullJarFile());
  99. installedPlugin = moveDownloadedPluginToExtensions(downloaded);
  100. LOG.info("Plugin {} [{}] updated to version {}", installedPlugin.getName(), installedPlugin.getKey(), installedPlugin.getVersion());
  101. } else {
  102. installedPlugin = moveDownloadedPluginToExtensions(downloaded);
  103. LOG.info("Plugin {} [{}] installed", installedPlugin.getName(), installedPlugin.getKey());
  104. }
  105. externalPluginsByKey.put(downloaded.getKey(), installedPlugin);
  106. }
  107. Map<String, ServerPluginInfo> plugins = new HashMap<>(externalPluginsByKey.size() + bundledPluginsByKey.size());
  108. plugins.putAll(externalPluginsByKey);
  109. plugins.putAll(bundledPluginsByKey);
  110. unloadIncompatiblePlugins(plugins);
  111. return plugins.values();
  112. }
  113. /**
  114. * Removes the plugins that are not compatible with current environment.
  115. */
  116. private static void unloadIncompatiblePlugins(Map<String, ServerPluginInfo> pluginsByKey) {
  117. // loop as long as the previous loop ignored some plugins. That allows to support dependencies
  118. // on many levels, for example D extends C, which extends B, which requires A. If A is not installed,
  119. // then B, C and D must be ignored. That's not possible to achieve this algorithm with a single iteration over plugins.
  120. Set<String> removedKeys = new HashSet<>();
  121. do {
  122. removedKeys.clear();
  123. for (ServerPluginInfo plugin : pluginsByKey.values()) {
  124. if (!isCompatible(plugin, pluginsByKey)) {
  125. removedKeys.add(plugin.getKey());
  126. }
  127. }
  128. for (String removedKey : removedKeys) {
  129. pluginsByKey.remove(removedKey);
  130. }
  131. } while (!removedKeys.isEmpty());
  132. }
  133. @VisibleForTesting
  134. static boolean isCompatible(ServerPluginInfo plugin, Map<String, ServerPluginInfo> allPluginsByKeys) {
  135. if (!Strings.isNullOrEmpty(plugin.getBasePlugin()) && !allPluginsByKeys.containsKey(plugin.getBasePlugin())) {
  136. // it extends a plugin that is not installed
  137. LOG.warn("Plugin {} [{}] is ignored because its base plugin [{}] is not installed", plugin.getName(), plugin.getKey(), plugin.getBasePlugin());
  138. return false;
  139. }
  140. if (plugin.getType() != BUNDLED && !plugin.getRequiredPlugins().isEmpty()) {
  141. LOG.warn("Use of 'Plugin-Dependencies' mechanism is planned for removal. Update the plugin {} [{}] to shade its dependencies instead.",
  142. plugin.getName(), plugin.getKey());
  143. }
  144. for (PluginInfo.RequiredPlugin requiredPlugin : plugin.getRequiredPlugins()) {
  145. PluginInfo installedRequirement = allPluginsByKeys.get(requiredPlugin.getKey());
  146. if (installedRequirement == null) {
  147. // it requires a plugin that is not installed
  148. LOG.warn("Plugin {} [{}] is ignored because the required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), requiredPlugin.getKey());
  149. return false;
  150. }
  151. Version installedRequirementVersion = installedRequirement.getVersion();
  152. if (installedRequirementVersion != null && requiredPlugin.getMinimalVersion().compareToIgnoreQualifier(installedRequirementVersion) > 0) {
  153. // it requires a more recent version
  154. LOG.warn("Plugin {} [{}] is ignored because the version {} of required plugin [{}] is not installed", plugin.getName(), plugin.getKey(),
  155. requiredPlugin.getMinimalVersion(), requiredPlugin.getKey());
  156. return false;
  157. }
  158. }
  159. return true;
  160. }
  161. private static String getRelativeDir(File dir) {
  162. Path parent = dir.toPath().getParent().getParent();
  163. return parent.relativize(dir.toPath()).toString();
  164. }
  165. private static void failIfContains(Map<String, ? extends PluginInfo> map, PluginInfo value, Function<PluginInfo, RuntimeException> msg) {
  166. PluginInfo pluginInfo = map.get(value.getKey());
  167. if (pluginInfo != null) {
  168. RuntimeException exception = msg.apply(pluginInfo);
  169. logGenericPluginLoadErrorLog();
  170. throw exception;
  171. }
  172. }
  173. private static void logGenericPluginLoadErrorLog() {
  174. Logger logger = Loggers.get(STARTUP_LOGGER_NAME);
  175. logger.error(LOAD_ERROR_GENERIC_MESSAGE);
  176. }
  177. private List<ServerPluginInfo> getBundledPluginsMetadata() {
  178. return loadPluginsFromDir(fs.getInstalledBundledPluginsDir(), jar -> ServerPluginInfo.create(jar, BUNDLED));
  179. }
  180. private List<ServerPluginInfo> getExternalPluginsMetadata() {
  181. return loadPluginsFromDir(fs.getInstalledExternalPluginsDir(), jar -> ServerPluginInfo.create(jar, EXTERNAL));
  182. }
  183. private List<PluginInfo> getDownloadedPluginsMetadata() {
  184. return loadPluginsFromDir(fs.getDownloadedPluginsDir(), PluginInfo::create);
  185. }
  186. private ServerPluginInfo moveDownloadedPluginToExtensions(PluginInfo pluginInfo) {
  187. File destDir = fs.getInstalledExternalPluginsDir();
  188. File destFile = new File(destDir, pluginInfo.getNonNullJarFile().getName());
  189. if (destFile.exists()) {
  190. deleteQuietly(destFile);
  191. }
  192. movePlugin(pluginInfo.getNonNullJarFile(), destFile);
  193. return ServerPluginInfo.create(destFile, EXTERNAL);
  194. }
  195. private static void movePlugin(File sourcePluginFile, File destPluginFile) {
  196. try {
  197. moveFile(sourcePluginFile, destPluginFile);
  198. } catch (IOException e) {
  199. throw new IllegalStateException(format("Fail to move plugin: %s to %s", sourcePluginFile.getAbsolutePath(), destPluginFile.getAbsolutePath()), e);
  200. }
  201. }
  202. private <T extends PluginInfo> List<T> loadPluginsFromDir(File pluginsDir, Function<File, T> toPluginInfo) {
  203. List<T> list = listJarFiles(pluginsDir).stream()
  204. .map(toPluginInfo)
  205. .filter(this::checkPluginInfo)
  206. .collect(Collectors.toList());
  207. failIfContainsIncompatiblePlugins(list);
  208. return list;
  209. }
  210. private static void failIfContainsIncompatiblePlugins(List<? extends PluginInfo> plugins) {
  211. List<String> incompatiblePlugins = plugins.stream()
  212. .filter(p -> FORBIDDEN_INCOMPATIBLE_PLUGINS.contains(p.getKey()))
  213. .map(p -> "'" + p.getKey() + "'")
  214. .sorted()
  215. .collect(Collectors.toList());
  216. if (!incompatiblePlugins.isEmpty()) {
  217. logGenericPluginLoadErrorLog();
  218. throw MessageException.of(String.format("The following %s no longer compatible with this version of SonarQube: %s",
  219. incompatiblePlugins.size() > 1 ? "plugins are" : "plugin is", String.join(", ", incompatiblePlugins)));
  220. }
  221. }
  222. private boolean checkPluginInfo(PluginInfo info) {
  223. String pluginKey = info.getKey();
  224. if (blacklistedPluginKeys.contains(pluginKey)) {
  225. LOG.warn("Plugin {} [{}] is blacklisted and is being uninstalled", info.getName(), pluginKey);
  226. deleteQuietly(info.getNonNullJarFile());
  227. return false;
  228. }
  229. if (Strings.isNullOrEmpty(info.getMainClass()) && Strings.isNullOrEmpty(info.getBasePlugin())) {
  230. LOG.warn("Plugin {} [{}] is ignored because entry point class is not defined", info.getName(), info.getKey());
  231. return false;
  232. }
  233. if (!info.isCompatibleWith(runtime.getApiVersion().toString())) {
  234. throw MessageException.of(format("Plugin %s [%s] requires at least SonarQube %s", info.getName(), info.getKey(), info.getMinimalSqVersion()));
  235. }
  236. return true;
  237. }
  238. private static Collection<File> listJarFiles(File dir) {
  239. if (dir.exists()) {
  240. return FileUtils.listFiles(dir, new String[] {"jar"}, false);
  241. }
  242. return Collections.emptyList();
  243. }
  244. }