Skip to content

Commit

Permalink
Return String from PluginDescriptor#getElasticsearchVersion (elastic#…
Browse files Browse the repository at this point in the history
…100735)

We want plugin descriptors to be able to return opaque strings from 
getElasticsearchVersion. The one problem here is that the stable plugin 
API assumes semantic versioning for determining compatibility: a plugin 
from an earlier release of the same major version can be loaded, but 
not one from a future release. So we check to see if the current build 
version can be parsed as a semantic version, and, if so, we apply this 
logic.

For non-stable plugins on a semantically-versioned build, we check that 
the plugin's version matches the build version.

If the build version is not semantic, we assume that the plugins come 
from the current build, since no one should be installing or modifying 
plugins on serverless..

* getElasticsearchVersion returns string
* Fix utils for snapshot versions
* Remove Version.CURRENT from error messages
* Only check plugin version compatibility for semantically versioned builds
  • Loading branch information
williamrandolph authored Nov 7, 2023
1 parent c7135b6 commit e44eea3
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,8 @@ public String getVersion() {
*
* @return an Elasticsearch version
*/
public Version getElasticsearchVersion() {
return Version.fromString(elasticsearchVersion);
public String getElasticsearchVersion() {
return elasticsearchVersion;
}

/**
Expand Down
120 changes: 95 additions & 25 deletions server/src/main/java/org/elasticsearch/plugins/PluginsUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.jdk.JarHell;

Expand All @@ -30,6 +30,8 @@
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -77,43 +79,111 @@ public static List<Path> findPluginDirs(final Path rootPath) throws IOException
* Verify the given plugin is compatible with the current Elasticsearch installation.
*/
public static void verifyCompatibility(PluginDescriptor info) {
if (info.isStable()) {
if (info.getElasticsearchVersion().major != Version.CURRENT.major) {
throw new IllegalArgumentException(
"Stable Plugin ["
+ info.getName()
+ "] was built for Elasticsearch major version "
+ info.getElasticsearchVersion().major
+ " but version "
+ Version.CURRENT
+ " is running"
final String currentVersion = Build.current().version();
Matcher buildVersionMatcher = SemanticVersion.semanticPattern.matcher(currentVersion);
// If we're not on a semantic version, assume plugins are compatible
if (buildVersionMatcher.matches()) {
SemanticVersion currentElasticsearchSemanticVersion;
try {
currentElasticsearchSemanticVersion = new SemanticVersion(
Integer.parseInt(buildVersionMatcher.group(1)),
Integer.parseInt(buildVersionMatcher.group(2)),
Integer.parseInt(buildVersionMatcher.group(3))
);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Couldn't parse integers from build version [" + currentVersion + "]", e);
}
if (info.getElasticsearchVersion().after(Version.CURRENT)) {
if (info.isStable()) {
Matcher pluginEsVersionMatcher = SemanticVersion.semanticPattern.matcher(info.getElasticsearchVersion());
if (pluginEsVersionMatcher.matches() == false) {
throw new IllegalArgumentException(
"Expected semantic version for plugin [" + info.getName() + "] but was [" + info.getElasticsearchVersion() + "]"
);
}
SemanticVersion pluginElasticsearchSemanticVersion;
try {
pluginElasticsearchSemanticVersion = new SemanticVersion(
Integer.parseInt(pluginEsVersionMatcher.group(1)),
Integer.parseInt(pluginEsVersionMatcher.group(2)),
Integer.parseInt(pluginEsVersionMatcher.group(3))
);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Expected integer version for plugin [" + info.getName() + "] but found [" + info.getElasticsearchVersion() + "]",
e
);
}

// case: Major version mismatch
if (pluginElasticsearchSemanticVersion.major != currentElasticsearchSemanticVersion.major) {
throw new IllegalArgumentException(
"Stable Plugin ["
+ info.getName()
+ "] was built for Elasticsearch major version "
+ pluginElasticsearchSemanticVersion.major
+ " but version "
+ currentVersion
+ " is running"
);
}

// case: stable plugin from the future
if (pluginElasticsearchSemanticVersion.after(currentElasticsearchSemanticVersion)) {
throw new IllegalArgumentException(
"Stable Plugin ["
+ info.getName()
+ "] was built for Elasticsearch version "
+ info.getElasticsearchVersion()
+ " but earlier version "
+ currentVersion
+ " is running"
);
}
} else if (info.getElasticsearchVersion().equals(currentVersion) == false) {
throw new IllegalArgumentException(
"Stable Plugin ["
"Plugin ["
+ info.getName()
+ "] was built for Elasticsearch version "
+ info.getElasticsearchVersion()
+ " but earlier version "
+ Version.CURRENT
+ " but version "
+ currentVersion
+ " is running"
);
}
} else if (info.getElasticsearchVersion().equals(Version.CURRENT) == false) {
throw new IllegalArgumentException(
"Plugin ["
+ info.getName()
+ "] was built for Elasticsearch version "
+ info.getElasticsearchVersion()
+ " but version "
+ Version.CURRENT
+ " is running"
);
}
JarHell.checkJavaVersion(info.getName(), info.getJavaVersion());
}

private record SemanticVersion(int major, int minor, int bugfix) {

static final Pattern semanticPattern = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$");

// does not compare anything after the semantic version
boolean after(SemanticVersion other) {
// major
if (this.major < other.major) {
return false;
}
if (this.major > other.major) {
return true;
}
// minor
if (this.minor < other.minor) {
return false;
}
if (this.minor > other.minor) {
return true;
}
// bugfix
return this.bugfix > other.bugfix;
}

@Override
public String toString() {
return Strings.format("%d.%d.%d", this.major, this.minor, this.bugfix);
}
}

/**
* Check for the existence of a marker file that indicates any plugins are in a garbage state from a failed attempt to remove the
* plugin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ public void testPluginEqualityAndHash() {
descriptor1.getName(),
randomValueOtherThan(descriptor1.getDescription(), () -> randomAlphaOfLengthBetween(4, 12)),
randomValueOtherThan(descriptor1.getVersion(), () -> randomAlphaOfLengthBetween(4, 12)),
descriptor1.getElasticsearchVersion().previousMajor().toString(),
"8.0.0",
randomValueOtherThan(descriptor1.getJavaVersion(), () -> randomAlphaOfLengthBetween(4, 12)),
descriptor1.isStable() ? randomAlphaOfLengthBetween(4, 12) : null,
descriptor1.isStable() ? randomAlphaOfLength(6) : null,
Expand All @@ -352,7 +352,7 @@ public void testPluginEqualityAndHash() {
randomValueOtherThan(descriptor1.getName(), () -> randomAlphaOfLengthBetween(4, 12)),
descriptor1.getDescription(),
descriptor1.getVersion(),
descriptor1.getElasticsearchVersion().toString(),
descriptor1.getElasticsearchVersion(),
descriptor1.getJavaVersion(),
classname,
descriptor1.getModuleName().orElse(null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,18 @@ public void testJarHellSpiConflict() throws Exception {
assertThat(e.getCause().getMessage(), containsString("DummyClass1"));
}

public void testInternalNonSemanticVersions() throws Exception {
PluginDescriptor info = getPluginDescriptorForVersion(randomAlphaOfLengthBetween(6, 32), "1.8", false);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsUtils.verifyCompatibility(info));
assertThat(e.getMessage(), containsString("Plugin [my_plugin] was built for Elasticsearch version"));
}

public void testStableNonSemanticVersions() throws Exception {
PluginDescriptor info = getPluginDescriptorForVersion(randomAlphaOfLengthBetween(6, 32), "1.8", true);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsUtils.verifyCompatibility(info));
assertThat(e.getMessage(), containsString("Expected semantic version for plugin [my_plugin] but was"));
}

public void testStableEarlierElasticsearchVersion() throws Exception {
PluginDescriptor info = getPluginDescriptorForVersion(Version.fromId(Version.CURRENT.id + 1).toString(), "1.8", true);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsUtils.verifyCompatibility(info));
Expand Down

0 comments on commit e44eea3

Please sign in to comment.