Răsfoiți Sursa

Create placeholder plugin when loading stable plugins (#90870)

Stable plugins do not extend Plugin class and are not instantiated in PluginService.
Therefore a simple StablePluginPlaceHolder has to be used in order to make pluginService
aware of the plugin and its descriptor. It is needed to return information about loaded
stable plugins in cluster state.

relates #88980
Przemyslaw Gomulka 3 ani în urmă
părinte
comite
2d420ecc68

+ 5 - 0
docs/changelog/90870.yaml

@@ -0,0 +1,5 @@
+pr: 90870
+summary: Create placeholder plugin when loading stable plugins
+area: Infra/Plugins
+type: enhancement
+issues: []

+ 2 - 2
server/src/main/java/org/elasticsearch/indices/analysis/wrappers/StableApiWrappers.java

@@ -82,9 +82,9 @@ public class StableApiWrappers {
             charFilterFactoryClass.getCanonicalName()
         );
 
-        Map<String, AnalysisModule.AnalysisProvider<T>> oldCharFilters = pluginInfosForExtensible.stream()
+        Map<String, AnalysisModule.AnalysisProvider<T>> oldApiComponents = pluginInfosForExtensible.stream()
             .collect(Collectors.toMap(PluginInfo::name, p -> analysisProviderWrapper(p, wrapper)));
-        return oldCharFilters;
+        return oldApiComponents;
     }
 
     @SuppressWarnings("unchecked")

+ 26 - 15
server/src/main/java/org/elasticsearch/plugins/PluginsService.java

@@ -415,7 +415,7 @@ public class PluginsService implements ReportingService<PluginsAndModules> {
         return "constructor for extension [" + extensionClass.getName() + "] of type [" + extensionPointType.getName() + "]";
     }
 
-    private Plugin loadBundle(PluginBundle bundle, Map<String, LoadedPlugin> loaded) {
+    private void loadBundle(PluginBundle bundle, Map<String, LoadedPlugin> loaded) {
         String name = bundle.plugin.getName();
         logger.debug(() -> "Loading bundle: " + name);
 
@@ -463,25 +463,36 @@ public class PluginsService implements ReportingService<PluginsAndModules> {
             // that have dependencies with their own SPI endpoints have a chance to load
             // and initialize them appropriately.
             privilegedSetContextClassLoader(pluginClassLoader);
+            Plugin plugin;
             if (bundle.pluginDescriptor().isStable()) {
                 stablePluginsRegistry.scanBundleForStablePlugins(bundle, pluginClassLoader);
-            }
+                /*
+                Contrary to old plugins we don't need an instance of the plugin here.
+                Stable plugin register components (like CharFilterFactory) in stable plugin registry, which is then used in AnalysisModule
+                when registering char filter factories and other analysis components.
+                We don't have to support for settings, additional components and other methods
+                that are in org.elasticsearch.plugins.Plugin
+                We need to pass a name though so that we can show that a plugin was loaded (via cluster state api)
+                This might need to be revisited once support for settings is added
+                 */
+                plugin = new StablePluginPlaceHolder(bundle.plugin.getName());
+            } else {
 
-            Class<? extends Plugin> pluginClass = loadPluginClass(bundle.plugin.getClassname(), pluginClassLoader);
-            if (pluginClassLoader != pluginClass.getClassLoader()) {
-                throw new IllegalStateException(
-                    "Plugin ["
-                        + name
-                        + "] must reference a class loader local Plugin class ["
-                        + bundle.plugin.getClassname()
-                        + "] (class loader ["
-                        + pluginClass.getClassLoader()
-                        + "])"
-                );
+                Class<? extends Plugin> pluginClass = loadPluginClass(bundle.plugin.getClassname(), pluginClassLoader);
+                if (pluginClassLoader != pluginClass.getClassLoader()) {
+                    throw new IllegalStateException(
+                        "Plugin ["
+                            + name
+                            + "] must reference a class loader local Plugin class ["
+                            + bundle.plugin.getClassname()
+                            + "] (class loader ["
+                            + pluginClass.getClassLoader()
+                            + "])"
+                    );
+                }
+                plugin = loadPlugin(pluginClass, settings, configPath);
             }
-            Plugin plugin = loadPlugin(pluginClass, settings, configPath);
             loaded.put(name, new LoadedPlugin(bundle.plugin, plugin, spiLayerAndLoader.loader(), spiLayerAndLoader.layer()));
-            return plugin;
         } finally {
             privilegedSetContextClassLoader(cl);
         }

+ 17 - 0
server/src/main/java/org/elasticsearch/plugins/StablePluginPlaceHolder.java

@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.plugins;
+
+class StablePluginPlaceHolder extends Plugin {
+    private final String name;
+
+    StablePluginPlaceHolder(String name) {
+        this.name = name;
+    }
+}

+ 64 - 0
server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java

@@ -11,6 +11,7 @@ package org.elasticsearch.plugins;
 import org.apache.lucene.tests.util.LuceneTestCase;
 import org.apache.lucene.util.Constants;
 import org.elasticsearch.Version;
+import org.elasticsearch.action.admin.cluster.node.info.PluginsAndModules;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.core.Strings;
 import org.elasticsearch.env.Environment;
@@ -46,10 +47,12 @@ import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.everyItem;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.hasToString;
 import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.sameInstance;
 
@@ -765,6 +768,67 @@ public class PluginsServiceTests extends ESTestCase {
         }
     }
 
+    public void testStablePluginLoading() throws Exception {
+        final Path home = createTempDir();
+        final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home).build();
+        final Path plugins = home.resolve("plugins");
+        final Path plugin = plugins.resolve("stable-plugin");
+        Files.createDirectories(plugin);
+        PluginTestUtil.writeStablePluginProperties(
+            plugin,
+            "description",
+            "description",
+            "name",
+            "stable-plugin",
+            "version",
+            "1.0.0",
+            "elasticsearch.version",
+            Version.CURRENT.toString(),
+            "java.version",
+            System.getProperty("java.specification.version")
+        );
+
+        Path jar = plugin.resolve("impl.jar");
+        JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """
+            package p;
+            import java.util.Map;
+            import org.elasticsearch.plugin.analysis.api.CharFilterFactory;
+            import org.elasticsearch.plugin.api.NamedComponent;
+            import java.io.Reader;
+            @NamedComponent(name = "a_name")
+            public class A  implements CharFilterFactory {
+                 @Override
+                public Reader create(Reader reader) {
+                    return reader;
+                }
+            }
+            """)));
+        Path namedComponentFile = plugin.resolve("named_components.json");
+        Files.writeString(namedComponentFile, """
+            {
+              "org.elasticsearch.plugin.analysis.api.CharFilterFactory": {
+                "a_name": "p.A"
+              }
+            }
+            """);
+
+        var pluginService = newPluginsService(settings);
+        try {
+            Map<String, Plugin> stringPluginMap = pluginService.pluginMap();
+            assertThat(stringPluginMap.get("stable-plugin"), instanceOf(StablePluginPlaceHolder.class));
+
+            PluginsAndModules info = pluginService.info();
+            List<PluginRuntimeInfo> pluginInfos = info.getPluginInfos();
+            assertEquals(pluginInfos.size(), 1);
+            assertThat(pluginInfos.get(0).descriptor().getName(), equalTo("stable-plugin"));
+            assertThat(pluginInfos.get(0).descriptor().isStable(), is(true));
+
+            // TODO should we add something to pluginInfos.get(0).pluginApiInfo() ?
+        } finally {
+            closePluginLoaders(pluginService);
+        }
+    }
+
     public void testCanCreateAClassLoader() {
         assertEquals(
             "access denied (\"java.lang.RuntimePermission\" \"createClassLoader\")",