Skip to content

Commit 2a7f61f

Browse files
authored
Scan stable plugins for named components upon install (elastic#92528)
stable plugins not build with ES's gradle plugin will not have named_components.json file. To allow these plugins to expose their named components, a scan can be performed upon install. relates elastic#88980
1 parent d8ac002 commit 2a7f61f

File tree

14 files changed

+199
-29
lines changed

14 files changed

+199
-29
lines changed

distribution/tools/plugin-cli/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,18 @@ apply plugin: 'elasticsearch.build'
1010

1111
archivesBaseName = 'elasticsearch-plugin-cli'
1212

13+
tasks.named("dependencyLicenses").configure {
14+
mapping from: /asm-.*/, to: 'asm'
15+
}
16+
1317
dependencies {
1418
compileOnly project(":server")
1519
compileOnly project(":libs:elasticsearch-cli")
20+
implementation project(":libs:elasticsearch-plugin-api")
21+
implementation project(":libs:elasticsearch-plugin-scanner")
22+
implementation 'org.ow2.asm:asm:9.3'
23+
implementation 'org.ow2.asm:asm-tree:9.3'
24+
1625
api "org.bouncycastle:bcpg-fips:1.0.4"
1726
api "org.bouncycastle:bc-fips:1.0.2"
1827
testImplementation project(":test:framework")
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Copyright (c) 2012 France Télécom
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions
6+
are met:
7+
1. Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
2. Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in the
11+
documentation and/or other materials provided with the distribution.
12+
3. Neither the name of the copyright holders nor the names of its
13+
contributors may be used to endorse or promote products derived from
14+
this software without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
26+
THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@
3838
import org.elasticsearch.core.Tuple;
3939
import org.elasticsearch.env.Environment;
4040
import org.elasticsearch.jdk.JarHell;
41+
import org.elasticsearch.plugin.scanner.ClassReaders;
42+
import org.elasticsearch.plugin.scanner.NamedComponentScanner;
4143
import org.elasticsearch.plugins.Platforms;
4244
import org.elasticsearch.plugins.PluginDescriptor;
4345
import org.elasticsearch.plugins.PluginsUtils;
46+
import org.objectweb.asm.ClassReader;
4447

4548
import java.io.BufferedReader;
4649
import java.io.Closeable;
@@ -82,6 +85,7 @@
8285
import java.util.Timer;
8386
import java.util.TimerTask;
8487
import java.util.stream.Collectors;
88+
import java.util.stream.Stream;
8589
import java.util.zip.ZipEntry;
8690
import java.util.zip.ZipInputStream;
8791

@@ -197,6 +201,7 @@ public class InstallPluginAction implements Closeable {
197201
private Environment env;
198202
private boolean batch;
199203
private Proxy proxy = null;
204+
private NamedComponentScanner scanner = new NamedComponentScanner();
200205

201206
public InstallPluginAction(Terminal terminal, Environment env, boolean batch) {
202207
this.terminal = terminal;
@@ -208,7 +213,6 @@ public void setProxy(Proxy proxy) {
208213
this.proxy = proxy;
209214
}
210215

211-
// pkg private for testing
212216
public void execute(List<InstallablePlugin> plugins) throws Exception {
213217
if (plugins.isEmpty()) {
214218
throw new UserException(ExitCodes.USAGE, "at least one plugin id is required");
@@ -867,9 +871,24 @@ private PluginDescriptor loadPluginInfo(Path pluginRoot) throws Exception {
867871
// check for jar hell before any copying
868872
jarHellCheck(info, pluginRoot, env.pluginsFile(), env.modulesFile());
869873

874+
if (info.isStable() && hasNamedComponentFile(pluginRoot) == false) {
875+
generateNameComponentFile(pluginRoot);
876+
}
870877
return info;
871878
}
872879

880+
private void generateNameComponentFile(Path pluginRoot) throws IOException {
881+
Stream<ClassReader> classPath = ClassReaders.ofClassPath().stream(); // contains plugin-api
882+
List<ClassReader> classReaders = Stream.concat(ClassReaders.ofDirWithJars(pluginRoot).stream(), classPath).toList();
883+
Map<String, Map<String, String>> namedComponentsMap = scanner.scanForNamedClasses(classReaders);
884+
Path outputFile = pluginRoot.resolve(PluginDescriptor.NAMED_COMPONENTS_FILENAME);
885+
scanner.writeToFile(namedComponentsMap, outputFile);
886+
}
887+
888+
private boolean hasNamedComponentFile(Path pluginRoot) {
889+
return Files.exists(pluginRoot.resolve(PluginDescriptor.NAMED_COMPONENTS_FILENAME));
890+
}
891+
873892
private static final String LIB_TOOLS_PLUGIN_CLI_CLASSPATH_JAR;
874893

875894
static {

distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@
4848
import org.elasticsearch.core.Tuple;
4949
import org.elasticsearch.env.Environment;
5050
import org.elasticsearch.env.TestEnvironment;
51+
import org.elasticsearch.plugin.scanner.NamedComponentScanner;
5152
import org.elasticsearch.plugins.Platforms;
5253
import org.elasticsearch.plugins.PluginDescriptor;
5354
import org.elasticsearch.plugins.PluginTestUtil;
5455
import org.elasticsearch.test.ESTestCase;
5556
import org.elasticsearch.test.PosixPermissionsResetter;
57+
import org.elasticsearch.test.compiler.InMemoryJavaCompiler;
58+
import org.elasticsearch.test.jar.JarUtils;
5659
import org.junit.After;
5760
import org.junit.Before;
5861

@@ -86,6 +89,7 @@
8689
import java.util.Arrays;
8790
import java.util.Date;
8891
import java.util.HashSet;
92+
import java.util.LinkedHashMap;
8993
import java.util.List;
9094
import java.util.Locale;
9195
import java.util.Map;
@@ -124,13 +128,16 @@ public class InstallPluginActionTests extends ESTestCase {
124128
private MockTerminal terminal;
125129
private Tuple<Path, Environment> env;
126130
private Path pluginDir;
131+
private NamedComponentScanner namedComponentScanner;
127132

128133
private final boolean isPosix;
129134
private final boolean isReal;
130135
private final String javaIoTmpdir;
131136

132137
@SuppressForbidden(reason = "sets java.io.tmpdir")
133138
public InstallPluginActionTests(FileSystem fs, Function<String, Path> temp) {
139+
assert "false".equals(System.getProperty("tests.security.manager")) : "-Dtests.security.manager=false has to be set";
140+
134141
this.temp = temp;
135142
this.isPosix = fs.supportedFileAttributeViews().contains("posix");
136143
this.isReal = fs == PathUtils.getDefaultFileSystem();
@@ -152,6 +159,7 @@ void jarHellCheck(PluginDescriptor candidateInfo, Path candidate, Path pluginsDi
152159
// no jarhell check
153160
}
154161
};
162+
155163
defaultAction = new InstallPluginAction(terminal, env.v2(), false);
156164
}
157165

@@ -199,7 +207,9 @@ private static Configuration toPosix(Configuration configuration) {
199207
return configuration.toBuilder().setAttributeViews("basic", "owner", "posix", "unix").build();
200208
}
201209

202-
/** Creates a test environment with bin, config and plugins directories. */
210+
/**
211+
* Creates a test environment with bin, config and plugins directories.
212+
*/
203213
static Tuple<Path, Environment> createEnv(Function<String, Path> temp) throws IOException {
204214
Path home = temp.apply("install-plugin-command-tests");
205215
Files.createDirectories(home.resolve("bin"));
@@ -216,7 +226,9 @@ static Path createPluginDir(Function<String, Path> temp) {
216226
return temp.apply("pluginDir");
217227
}
218228

219-
/** creates a fake jar file with empty class files */
229+
/**
230+
* creates a fake jar file with empty class files
231+
*/
220232
static void writeJar(Path jar, String... classes) throws IOException {
221233
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(jar))) {
222234
for (String clazz : classes) {
@@ -237,13 +249,47 @@ static Path writeZip(Path structure, String prefix) throws IOException {
237249
return zip;
238250
}
239251

240-
/** creates a plugin .zip and returns the url for testing */
252+
/**
253+
* creates a plugin .zip and returns the url for testing
254+
*/
241255
static InstallablePlugin createPluginZip(String name, Path structure, String... additionalProps) throws IOException {
242256
return createPlugin(name, structure, additionalProps);
243257
}
244258

259+
static void writeStablePlugin(String name, Path structure, boolean hasNamedComponentFile, String... additionalProps)
260+
throws IOException {
261+
String[] properties = pluginProperties(name, additionalProps, true);
262+
PluginTestUtil.writeStablePluginProperties(structure, properties);
263+
264+
if (hasNamedComponentFile) {
265+
PluginTestUtil.writeNamedComponentsFile(structure, namedComponentsJSON());
266+
}
267+
Path jar = structure.resolve("plugin.jar");
268+
269+
JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """
270+
package p;
271+
import org.elasticsearch.plugin.*;
272+
import org.elasticsearch.plugins.cli.test_model.*;
273+
@NamedComponent("a_component")
274+
public class A implements ExtensibleInterface{}
275+
"""), "p/B.class", InMemoryJavaCompiler.compile("p.B", """
276+
package p;
277+
import org.elasticsearch.plugin.*;
278+
import org.elasticsearch.plugins.cli.test_model.*;
279+
@NamedComponent("b_component")
280+
public class B implements ExtensibleInterface{}
281+
""")));
282+
}
283+
245284
static void writePlugin(String name, Path structure, String... additionalProps) throws IOException {
246-
String[] properties = Stream.concat(
285+
String[] properties = pluginProperties(name, additionalProps, false);
286+
PluginTestUtil.writePluginProperties(structure, properties);
287+
String className = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1) + "Plugin";
288+
writeJar(structure.resolve("plugin.jar"), className);
289+
}
290+
291+
private static String[] pluginProperties(String name, String[] additionalProps, boolean isStable) {
292+
return Stream.of(
247293
Stream.of(
248294
"description",
249295
"fake desc",
@@ -254,15 +300,12 @@ static void writePlugin(String name, Path structure, String... additionalProps)
254300
"elasticsearch.version",
255301
Version.CURRENT.toString(),
256302
"java.version",
257-
System.getProperty("java.specification.version"),
258-
"classname",
259-
"FakePlugin"
303+
System.getProperty("java.specification.version")
304+
260305
),
306+
isStable ? Stream.empty() : Stream.of("classname", "FakePlugin"),
261307
Arrays.stream(additionalProps)
262-
).toArray(String[]::new);
263-
PluginTestUtil.writePluginProperties(structure, properties);
264-
String className = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1) + "Plugin";
265-
writeJar(structure.resolve("plugin.jar"), className);
308+
).flatMap(Function.identity()).toArray(String[]::new);
266309
}
267310

268311
static void writePluginSecurityPolicy(Path pluginDir, String... permissions) throws IOException {
@@ -276,6 +319,12 @@ static void writePluginSecurityPolicy(Path pluginDir, String... permissions) thr
276319
Files.write(pluginDir.resolve("plugin-security.policy"), securityPolicyContent.toString().getBytes(StandardCharsets.UTF_8));
277320
}
278321

322+
static InstallablePlugin createStablePlugin(String name, Path structure, boolean hasNamedComponentFile, String... additionalProps)
323+
throws IOException {
324+
writeStablePlugin(name, structure, hasNamedComponentFile, additionalProps);
325+
return new InstallablePlugin(name, writeZip(structure, null).toUri().toURL().toString());
326+
}
327+
279328
static InstallablePlugin createPlugin(String name, Path structure, String... additionalProps) throws IOException {
280329
writePlugin(name, structure, additionalProps);
281330
return new InstallablePlugin(name, writeZip(structure, null).toUri().toURL().toString());
@@ -310,6 +359,11 @@ void assertPlugin(String name, Path original, Environment environment) throws IO
310359
assertInstallCleaned(environment);
311360
}
312361

362+
void assertNamedComponentFile(String name, Path pluginDir, String expectedContent) throws IOException {
363+
Path namedComponents = pluginDir.resolve(name).resolve(PluginDescriptor.NAMED_COMPONENTS_FILENAME);
364+
assertThat(Files.readString(namedComponents), equalTo(expectedContent));
365+
}
366+
313367
void assertPluginInternal(String name, Path pluginsFile, Path originalPlugin) throws IOException {
314368
Path got = pluginsFile.resolve(name);
315369
assertTrue("dir " + name + " exists", Files.exists(got));
@@ -1507,4 +1561,42 @@ public void testInstallMigratedPlugins() throws Exception {
15071561
assertThat(terminal.getErrorOutput(), containsString("[" + id + "] is no longer a plugin"));
15081562
}
15091563
}
1564+
1565+
public void testStablePluginWithNamedComponentsFile() throws Exception {
1566+
InstallablePlugin stablePluginZip = createStablePlugin("stable1", pluginDir, true);
1567+
installPlugins(List.of(stablePluginZip), env.v1());
1568+
assertPlugin("stable1", pluginDir, env.v2());
1569+
assertNamedComponentFile("stable1", env.v2().pluginsFile(), namedComponentsJSON());
1570+
}
1571+
1572+
@SuppressWarnings("unchecked")
1573+
public void testStablePluginWithoutNamedComponentsFile() throws Exception {
1574+
// named component will have to be generated upon install
1575+
InstallablePlugin stablePluginZip = createStablePlugin("stable1", pluginDir, false);
1576+
1577+
installPlugins(List.of(stablePluginZip), env.v1());
1578+
1579+
assertPlugin("stable1", pluginDir, env.v2());
1580+
assertNamedComponentFile("stable1", env.v2().pluginsFile(), namedComponentsJSON());
1581+
}
1582+
1583+
private Map<String, Map<String, String>> namedComponentsMap() {
1584+
Map<String, Map<String, String>> result = new LinkedHashMap<>();
1585+
Map<String, String> extensibles = new LinkedHashMap<>();
1586+
extensibles.put("a_component", "p.A");
1587+
extensibles.put("b_component", "p.B");
1588+
result.put("org.elasticsearch.plugins.cli.test_model.ExtensibleInterface", extensibles);
1589+
return result;
1590+
}
1591+
1592+
private static String namedComponentsJSON() {
1593+
return """
1594+
{
1595+
"org.elasticsearch.plugins.cli.test_model.ExtensibleInterface": {
1596+
"a_component": "p.A",
1597+
"b_component": "p.B"
1598+
}
1599+
}
1600+
""".replaceAll("[\n\r\s]", "");
1601+
}
15101602
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.plugins.cli.test_model;
10+
11+
import org.elasticsearch.plugin.Extensible;
12+
13+
@Extensible
14+
public interface ExtensibleInterface {}

docs/changelog/92528.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 92528
2+
summary: Scan stable plugins for named components upon install
3+
area: Infra/CLI
4+
type: enhancement
5+
issues: []

libs/plugin-scanner/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ dependencies {
1919
api project(':libs:elasticsearch-plugin-api')
2020
api project(":libs:elasticsearch-x-content")
2121

22-
implementation 'org.ow2.asm:asm:9.3'
23-
implementation 'org.ow2.asm:asm-tree:9.3'
22+
api 'org.ow2.asm:asm:9.3'
23+
api 'org.ow2.asm:asm-tree:9.3'
2424

2525
testImplementation "junit:junit:${versions.junit}"
2626
testImplementation(project(":test:framework")) {

libs/plugin-scanner/src/main/java/org/elasticsearch/plugin/scanner/ClassReaders.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,10 @@ public class ClassReaders {
4040
* This method must be used within a try-with-resources statement or similar
4141
* control structure.
4242
*/
43-
public static List<ClassReader> ofDirWithJars(String path) {
44-
if (path == null) {
43+
public static List<ClassReader> ofDirWithJars(Path dir) {
44+
if (dir == null) {
4545
return Collections.emptyList();
4646
}
47-
Path dir = Paths.get(path);
4847
try (var stream = Files.list(dir)) {
4948
return ofPaths(stream);
5049
} catch (IOException e) {

0 commit comments

Comments
 (0)