Skip to content

Commit ff232d4

Browse files
authored
Add support for dependencies in plugin descriptor properties with semver range (#11441)
* Add support for dependencies in plugin descriptor properties with semver range (#1707) Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Remove unused gson licenses Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Maintain bwc in PluginInfo with addition of semver range Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Added support for list of ranges Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Add bwc tests and restrict range list size to 1 Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Update SemverRange javadoc Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Minor change to trigger jenkins re-run Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Use jackson instead of gson * Remove jackson databind and annotations dependency from server Signed-off-by: Abhilasha Seth <abseth@amazon.com> * nit fixes Signed-off-by: Abhilasha Seth <abseth@amazon.com> * Minor change to re-run jenkins workflow Signed-off-by: Abhilasha Seth <abseth@amazon.com> --------- Signed-off-by: Abhilasha Seth <abseth@amazon.com>
1 parent a0b5198 commit ff232d4

File tree

25 files changed

+1256
-27
lines changed

25 files changed

+1256
-27
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1515
- GHA to verify checklist items completion in PR descriptions ([#10800](https://github.com/opensearch-project/OpenSearch/pull/10800))
1616
- Allow to pass the list settings through environment variables (like [], ["a", "b", "c"], ...) ([#10625](https://github.com/opensearch-project/OpenSearch/pull/10625))
1717
- [Admission Control] Integrate CPU AC with ResourceUsageCollector and add CPU AC stats to nodes/stats ([#10887](https://github.com/opensearch-project/OpenSearch/pull/10887))
18+
- Add support for dependencies in plugin descriptor properties with semver range ([#11441](https://github.com/opensearch-project/OpenSearch/pull/11441))
1819
- [S3 Repository] Add setting to control connection count for sync client ([#12028](https://github.com/opensearch-project/OpenSearch/pull/12028))
1920

2021
### Dependencies

distribution/tools/plugin-cli/src/main/java/org/opensearch/plugins/ListPluginsCommand.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,14 @@ private void printPlugin(Environment env, Terminal terminal, Path plugin, String
7878
PluginInfo info = PluginInfo.readFromProperties(env.pluginsDir().resolve(plugin));
7979
terminal.println(Terminal.Verbosity.SILENT, prefix + info.getName());
8080
terminal.println(Terminal.Verbosity.VERBOSE, info.toString(prefix));
81-
if (info.getOpenSearchVersion().equals(Version.CURRENT) == false) {
81+
if (!PluginsService.isPluginVersionCompatible(info, Version.CURRENT)) {
8282
terminal.errorPrintln(
8383
"WARNING: plugin ["
8484
+ info.getName()
8585
+ "] was built for OpenSearch version "
86-
+ info.getVersion()
87-
+ " but version "
86+
+ info.getOpenSearchVersionRangesString()
87+
+ " and is not compatible with "
8888
+ Version.CURRENT
89-
+ " is required"
9089
);
9190
}
9291
}

distribution/tools/plugin-cli/src/test/java/org/opensearch/plugins/InstallPluginCommandTests.java

+57
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@
7070
import org.opensearch.core.util.FileSystemUtils;
7171
import org.opensearch.env.Environment;
7272
import org.opensearch.env.TestEnvironment;
73+
import org.opensearch.semver.SemverRange;
7374
import org.opensearch.test.OpenSearchTestCase;
7475
import org.opensearch.test.PosixPermissionsResetter;
76+
import org.opensearch.test.VersionUtils;
7577
import org.junit.After;
7678
import org.junit.Before;
7779

@@ -284,6 +286,35 @@ static void writePlugin(String name, Path structure, String... additionalProps)
284286
writeJar(structure.resolve("plugin.jar"), className);
285287
}
286288

289+
static void writePlugin(String name, Path structure, SemverRange opensearchVersionRange, String... additionalProps) throws IOException {
290+
String[] properties = Stream.concat(
291+
Stream.of(
292+
"description",
293+
"fake desc",
294+
"name",
295+
name,
296+
"version",
297+
"1.0",
298+
"dependencies",
299+
"{opensearch:\"" + opensearchVersionRange + "\"}",
300+
"java.version",
301+
System.getProperty("java.specification.version"),
302+
"classname",
303+
"FakePlugin"
304+
),
305+
Arrays.stream(additionalProps)
306+
).toArray(String[]::new);
307+
PluginTestUtil.writePluginProperties(structure, properties);
308+
String className = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1) + "Plugin";
309+
writeJar(structure.resolve("plugin.jar"), className);
310+
}
311+
312+
static Path createPlugin(String name, Path structure, SemverRange opensearchVersionRange, String... additionalProps)
313+
throws IOException {
314+
writePlugin(name, structure, opensearchVersionRange, additionalProps);
315+
return writeZip(structure, null);
316+
}
317+
287318
static void writePluginSecurityPolicy(Path pluginDir, String... permissions) throws IOException {
288319
StringBuilder securityPolicyContent = new StringBuilder("grant {\n ");
289320
for (String permission : permissions) {
@@ -867,6 +898,32 @@ public void testInstallMisspelledOfficialPlugins() throws Exception {
867898
assertThat(e.getMessage(), containsString("Unknown plugin unknown_plugin"));
868899
}
869900

901+
public void testInstallPluginWithCompatibleDependencies() throws Exception {
902+
Tuple<Path, Environment> env = createEnv(fs, temp);
903+
Path pluginDir = createPluginDir(temp);
904+
String pluginZip = createPlugin("fake", pluginDir, SemverRange.fromString("~" + Version.CURRENT.toString())).toUri()
905+
.toURL()
906+
.toString();
907+
skipJarHellCommand.execute(terminal, Collections.singletonList(pluginZip), false, env.v2());
908+
assertThat(terminal.getOutput(), containsString("100%"));
909+
}
910+
911+
public void testInstallPluginWithIncompatibleDependencies() throws Exception {
912+
Tuple<Path, Environment> env = createEnv(fs, temp);
913+
Path pluginDir = createPluginDir(temp);
914+
// Core version is behind plugin version by one w.r.t patch, hence incompatible
915+
Version coreVersion = Version.CURRENT;
916+
Version pluginVersion = VersionUtils.getVersion(coreVersion.major, coreVersion.minor, (byte) (coreVersion.revision + 1));
917+
String pluginZip = createPlugin("fake", pluginDir, SemverRange.fromString("~" + pluginVersion.toString())).toUri()
918+
.toURL()
919+
.toString();
920+
IllegalArgumentException e = expectThrows(
921+
IllegalArgumentException.class,
922+
() -> skipJarHellCommand.execute(terminal, Collections.singletonList(pluginZip), false, env.v2())
923+
);
924+
assertThat(e.getMessage(), containsString("Plugin [fake] was built for OpenSearch version ~" + pluginVersion));
925+
}
926+
870927
public void testBatchFlag() throws Exception {
871928
MockTerminal terminal = new MockTerminal();
872929
installPlugin(terminal, true);

distribution/tools/plugin-cli/src/test/java/org/opensearch/plugins/ListPluginsCommandTests.java

+38-1
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,49 @@ public void testExistingIncompatiblePlugin() throws Exception {
278278
buildFakePlugin(env, "fake desc 2", "fake_plugin2", "org.fake2");
279279

280280
MockTerminal terminal = listPlugins(home);
281-
String message = "plugin [fake_plugin1] was built for OpenSearch version 1.0 but version " + Version.CURRENT + " is required";
281+
String message = "plugin [fake_plugin1] was built for OpenSearch version 5.0.0 and is not compatible with " + Version.CURRENT;
282282
assertEquals("fake_plugin1\nfake_plugin2\n", terminal.getOutput());
283283
assertEquals("WARNING: " + message + "\n", terminal.getErrorOutput());
284284

285285
String[] params = { "-s" };
286286
terminal = listPlugins(home, params);
287287
assertEquals("fake_plugin1\nfake_plugin2\n", terminal.getOutput());
288288
}
289+
290+
public void testPluginWithDependencies() throws Exception {
291+
PluginTestUtil.writePluginProperties(
292+
env.pluginsDir().resolve("fake_plugin1"),
293+
"description",
294+
"fake desc 1",
295+
"name",
296+
"fake_plugin1",
297+
"version",
298+
"1.0",
299+
"dependencies",
300+
"{opensearch:\"" + Version.CURRENT + "\"}",
301+
"java.version",
302+
System.getProperty("java.specification.version"),
303+
"classname",
304+
"org.fake1"
305+
);
306+
String[] params = { "-v" };
307+
MockTerminal terminal = listPlugins(home, params);
308+
assertEquals(
309+
buildMultiline(
310+
"Plugins directory: " + env.pluginsDir(),
311+
"fake_plugin1",
312+
"- Plugin information:",
313+
"Name: fake_plugin1",
314+
"Description: fake desc 1",
315+
"Version: 1.0",
316+
"OpenSearch Version: " + Version.CURRENT.toString(),
317+
"Java Version: " + System.getProperty("java.specification.version"),
318+
"Native Controller: false",
319+
"Extended Plugins: []",
320+
" * Classname: org.fake1",
321+
"Folder name: null"
322+
),
323+
terminal.getOutput()
324+
);
325+
}
289326
}

libs/core/src/main/java/org/opensearch/core/common/io/stream/StreamInput.java

+7
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.opensearch.core.concurrency.OpenSearchRejectedExecutionException;
5757
import org.opensearch.core.xcontent.MediaType;
5858
import org.opensearch.core.xcontent.MediaTypeRegistry;
59+
import org.opensearch.semver.SemverRange;
5960

6061
import java.io.ByteArrayInputStream;
6162
import java.io.EOFException;
@@ -750,6 +751,8 @@ public Object readGenericValue() throws IOException {
750751
return readCollection(StreamInput::readGenericValue, HashSet::new, Collections.emptySet());
751752
case 26:
752753
return readBigInteger();
754+
case 27:
755+
return readSemverRange();
753756
default:
754757
throw new IOException("Can't read unknown type [" + type + "]");
755758
}
@@ -1090,6 +1093,10 @@ public Version readVersion() throws IOException {
10901093
return Version.fromId(readVInt());
10911094
}
10921095

1096+
public SemverRange readSemverRange() throws IOException {
1097+
return SemverRange.fromString(readString());
1098+
}
1099+
10931100
/** Reads the {@link Version} from the input stream */
10941101
public Build readBuild() throws IOException {
10951102
// the following is new for opensearch: we write the distribution to support any "forks"

libs/core/src/main/java/org/opensearch/core/common/io/stream/StreamOutput.java

+9
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.opensearch.core.common.settings.SecureString;
5555
import org.opensearch.core.common.text.Text;
5656
import org.opensearch.core.concurrency.OpenSearchRejectedExecutionException;
57+
import org.opensearch.semver.SemverRange;
5758

5859
import java.io.EOFException;
5960
import java.io.FileNotFoundException;
@@ -784,6 +785,10 @@ public final void writeOptionalInstant(@Nullable Instant instant) throws IOExcep
784785
o.writeByte((byte) 26);
785786
o.writeString(v.toString());
786787
});
788+
writers.put(SemverRange.class, (o, v) -> {
789+
o.writeByte((byte) 27);
790+
o.writeSemverRange((SemverRange) v);
791+
});
787792
WRITERS = Collections.unmodifiableMap(writers);
788793
}
789794

@@ -1101,6 +1106,10 @@ public void writeVersion(final Version version) throws IOException {
11011106
writeVInt(version.id);
11021107
}
11031108

1109+
public void writeSemverRange(final SemverRange range) throws IOException {
1110+
writeString(range.toString());
1111+
}
1112+
11041113
/** Writes the OpenSearch {@link Build} informn to the output stream */
11051114
public void writeBuild(final Build build) throws IOException {
11061115
// the following is new for opensearch: we write the distribution name to support any "forks" of the code
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.semver;
10+
11+
import org.opensearch.Version;
12+
import org.opensearch.common.Nullable;
13+
import org.opensearch.core.xcontent.ToXContentFragment;
14+
import org.opensearch.core.xcontent.XContentBuilder;
15+
import org.opensearch.semver.expr.Caret;
16+
import org.opensearch.semver.expr.Equal;
17+
import org.opensearch.semver.expr.Expression;
18+
import org.opensearch.semver.expr.Tilde;
19+
20+
import java.io.IOException;
21+
import java.util.Objects;
22+
import java.util.Optional;
23+
24+
import static java.util.Arrays.stream;
25+
26+
/**
27+
* Represents a single semver range that allows for specifying which {@code org.opensearch.Version}s satisfy the range.
28+
* It is composed of a range version and a range operator. Following are the supported operators:
29+
* <ul>
30+
* <li>'=' Requires exact match with the range version. For example, =1.2.3 range would match only 1.2.3</li>
31+
* <li>'~' Allows for patch version variability starting from the range version. For example, ~1.2.3 range would match versions greater than or equal to 1.2.3 but less than 1.3.0</li>
32+
* <li>'^' Allows for patch and minor version variability starting from the range version. For example, ^1.2.3 range would match versions greater than or equal to 1.2.3 but less than 2.0.0</li>
33+
* </ul>
34+
*/
35+
public class SemverRange implements ToXContentFragment {
36+
37+
private final Version rangeVersion;
38+
private final RangeOperator rangeOperator;
39+
40+
public SemverRange(final Version rangeVersion, final RangeOperator rangeOperator) {
41+
this.rangeVersion = rangeVersion;
42+
this.rangeOperator = rangeOperator;
43+
}
44+
45+
/**
46+
* Constructs a {@code SemverRange} from its string representation.
47+
* @param range given range
48+
* @return a {@code SemverRange}
49+
*/
50+
public static SemverRange fromString(final String range) {
51+
RangeOperator rangeOperator = RangeOperator.fromRange(range);
52+
String version = range.replaceFirst(rangeOperator.asEscapedString(), "");
53+
if (!Version.stringHasLength(version)) {
54+
throw new IllegalArgumentException("Version cannot be empty");
55+
}
56+
return new SemverRange(Version.fromString(version), rangeOperator);
57+
}
58+
59+
/**
60+
* Return the range operator for this range.
61+
* @return range operator
62+
*/
63+
public RangeOperator getRangeOperator() {
64+
return rangeOperator;
65+
}
66+
67+
/**
68+
* Return the version for this range.
69+
* @return the range version
70+
*/
71+
public Version getRangeVersion() {
72+
return rangeVersion;
73+
}
74+
75+
/**
76+
* Check if range is satisfied by given version string.
77+
*
78+
* @param versionToEvaluate version to check
79+
* @return {@code true} if range is satisfied by version, {@code false} otherwise
80+
*/
81+
public boolean isSatisfiedBy(final String versionToEvaluate) {
82+
return isSatisfiedBy(Version.fromString(versionToEvaluate));
83+
}
84+
85+
/**
86+
* Check if range is satisfied by given version.
87+
*
88+
* @param versionToEvaluate version to check
89+
* @return {@code true} if range is satisfied by version, {@code false} otherwise
90+
* @see #isSatisfiedBy(String)
91+
*/
92+
public boolean isSatisfiedBy(final Version versionToEvaluate) {
93+
return this.rangeOperator.expression.evaluate(this.rangeVersion, versionToEvaluate);
94+
}
95+
96+
@Override
97+
public boolean equals(@Nullable final Object o) {
98+
if (this == o) {
99+
return true;
100+
}
101+
if (o == null || getClass() != o.getClass()) {
102+
return false;
103+
}
104+
SemverRange range = (SemverRange) o;
105+
return Objects.equals(rangeVersion, range.rangeVersion) && rangeOperator == range.rangeOperator;
106+
}
107+
108+
@Override
109+
public int hashCode() {
110+
return Objects.hash(rangeVersion, rangeOperator);
111+
}
112+
113+
@Override
114+
public String toString() {
115+
return rangeOperator.asString() + rangeVersion;
116+
}
117+
118+
@Override
119+
public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
120+
return builder.value(toString());
121+
}
122+
123+
/**
124+
* A range operator.
125+
*/
126+
public enum RangeOperator {
127+
128+
EQ("=", new Equal()),
129+
TILDE("~", new Tilde()),
130+
CARET("^", new Caret()),
131+
DEFAULT("", new Equal());
132+
133+
private final String operator;
134+
private final Expression expression;
135+
136+
RangeOperator(final String operator, final Expression expression) {
137+
this.operator = operator;
138+
this.expression = expression;
139+
}
140+
141+
/**
142+
* String representation of the range operator.
143+
*
144+
* @return range operator as string
145+
*/
146+
public String asString() {
147+
return operator;
148+
}
149+
150+
/**
151+
* Escaped string representation of the range operator,
152+
* if operator is a regex character.
153+
*
154+
* @return range operator as escaped string, if operator is a regex character
155+
*/
156+
public String asEscapedString() {
157+
if (Objects.equals(operator, "^")) {
158+
return "\\^";
159+
}
160+
return operator;
161+
}
162+
163+
public static RangeOperator fromRange(final String range) {
164+
Optional<RangeOperator> rangeOperator = stream(values()).filter(
165+
operator -> operator != DEFAULT && range.startsWith(operator.asString())
166+
).findFirst();
167+
return rangeOperator.orElse(DEFAULT);
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)