Bootstrap should implement a denylist of Java versions (ranges) (#3164)

* Bootstrap should implement a denylist of Java versions (ranges)

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>

* Addressing code review comments

Signed-off-by: Andriy Redko <andriy.redko@aiven.io>
This commit is contained in:
Andriy Redko 2022-05-11 18:33:48 -04:00 committed by GitHub
parent ad7ce4cd44
commit 677915dc00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 329 additions and 0 deletions

View File

@ -36,6 +36,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.lucene.util.Constants;
import org.opensearch.bootstrap.jvm.DenyJvmVersionsParser;
import org.opensearch.cluster.coordination.ClusterBootstrapService;
import org.opensearch.common.SuppressForbidden;
import org.opensearch.common.io.PathUtils;
@ -224,11 +225,32 @@ final class BootstrapChecks {
checks.add(new OnErrorCheck());
checks.add(new OnOutOfMemoryErrorCheck());
checks.add(new EarlyAccessCheck());
checks.add(new JavaVersionCheck());
checks.add(new AllPermissionCheck());
checks.add(new DiscoveryConfiguredCheck());
return Collections.unmodifiableList(checks);
}
static class JavaVersionCheck implements BootstrapCheck {
@Override
public BootstrapCheckResult check(BootstrapContext context) {
return DenyJvmVersionsParser.getDeniedJvmVersions()
.stream()
.filter(p -> p.test(getVersion()))
.findAny()
.map(
p -> BootstrapCheckResult.failure(
String.format(Locale.ROOT, "The current JVM version %s is not recommended for use: %s", getVersion(), p.getReason())
)
)
.orElseGet(() -> BootstrapCheckResult.success());
}
Runtime.Version getVersion() {
return Runtime.version();
}
}
static class HeapSizeCheck implements BootstrapCheck {
@Override

View File

@ -0,0 +1,169 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
package org.opensearch.bootstrap.jvm;
import org.opensearch.common.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.lang.Runtime.Version;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Parses the list of JVM versions which should be denied to run Opensearch engine with due to discovered
* issues or flaws.
*/
public class DenyJvmVersionsParser {
public interface VersionPredicate extends Predicate<Version> {
String getReason();
}
private static class SingleVersion implements VersionPredicate {
final private Version version;
final private String reason;
public SingleVersion(Version version, String reason) {
this.version = version;
this.reason = reason;
}
@Override
public boolean test(Version v) {
return version.compareTo(v) == 0;
}
@Override
public String getReason() {
return reason;
}
}
private static class VersionRange implements VersionPredicate {
private Version lower;
private boolean lowerIncluded;
private Version upper;
private boolean upperIncluded;
private String reason;
public VersionRange(@Nullable Version lower, boolean lowerIncluded, @Nullable Version upper, boolean upperIncluded, String reason) {
this.lower = lower;
this.lowerIncluded = lowerIncluded;
this.upper = upper;
this.upperIncluded = upperIncluded;
this.reason = reason;
}
@Override
public boolean test(Version v) {
if (lower != null) {
int compare = lower.compareTo(v);
if (compare > 0 || (compare == 0 && lowerIncluded != true)) {
return false;
}
}
if (upper != null) {
int compare = upper.compareTo(v);
if (compare < 0 || (compare == 0 && upperIncluded != true)) {
return false;
}
}
return true;
}
@Override
public String getReason() {
return reason;
}
}
public static Collection<VersionPredicate> getDeniedJvmVersions() {
try (
final InputStreamReader in = new InputStreamReader(
DenyJvmVersionsParser.class.getResourceAsStream("deny-jvm-versions.txt"),
StandardCharsets.UTF_8
)
) {
try (final BufferedReader reader = new BufferedReader(in)) {
return reader.lines()
.map(String::trim)
// filter empty lines
.filter(line -> line.isEmpty() == false)
// filter out all comments
.filter(line -> line.startsWith("//") == false)
.map(DenyJvmVersionsParser::parse)
.collect(Collectors.toList());
}
} catch (final IOException ex) {
throw new UncheckedIOException("Unable to read the list of denied JVM versions", ex);
}
}
/**
* Parse individual line from the list of denied JVM versions. Some version and version range examples are:
* <ul>
* <li>11.0.2: "... reason ..." - version 11.0.2</li>
* <li>[11.0.2, 11.0.14): "... reason ..." - versions 11.0.2.2 (included) to 11.0.14 (not included)</li>
* <li>[11.0.2, 11.0.14]: "... reason ..." - versions 11.0.2 to 11.0.14 (both included)</li>
* <li>[11.0.2,): "... reason ..." - versions 11.0.2 and higher</li>
* </ul>
* @param line line to parse
* @return version or version range predicate
*/
static VersionPredicate parse(String line) {
final String[] parts = Arrays.stream(line.split("[:]", 2)).map(String::trim).toArray(String[]::new);
if (parts.length != 2) {
throw new IllegalArgumentException("Unable to parse JVM version or version range: " + line);
}
final String versionOrRange = parts[0];
final String reason = parts[1];
// dealing with version range here
if (versionOrRange.startsWith("[") == true || versionOrRange.startsWith("(") == true) {
if (versionOrRange.endsWith("]") == false && versionOrRange.endsWith(")") == false) {
throw new IllegalArgumentException("Unable to parse JVM version range: " + versionOrRange);
}
final boolean lowerIncluded = versionOrRange.startsWith("[");
final boolean upperIncluded = versionOrRange.endsWith("]");
final String[] range = Arrays.stream(versionOrRange.substring(1, versionOrRange.length() - 1).split("[,]", 2))
.map(String::trim)
.toArray(String[]::new);
if (range.length != 2) {
throw new IllegalArgumentException("Unable to parse JVM version range: " + versionOrRange);
}
Version lower = null;
if (range[0].isEmpty() == false && range[0].equals("*") == false) {
lower = Version.parse(range[0]);
}
Version upper = null;
if (range[1].isEmpty() == false && range[1].equals("*") == false) {
upper = Version.parse(range[1]);
}
return new VersionRange(lower, lowerIncluded, upper, upperIncluded, reason);
} else {
// this is just a single version
return new SingleVersion(Version.parse(versionOrRange), reason);
}
}
}

View File

@ -0,0 +1,10 @@
// Some version and version range examples are:
// 11.0.2: <... reason ...> version 11.0.2.
// [11.0.2, 11.0.14): <... reason ...> versions 11.0.2.2 (included) to 11.0.14 (not included)
// [11.0.2, 11.0.14]: <... reason ...> versions 11.0.2 to 11.0.14 (both included)
// [11.0.2,): <... reason ...> versions 11.0.2 and higher
// [11.0.2,*): <... reason ...> versions 11.0.2 and higher
// (, 11.0.14]: <... reason ...> versions up to 11.0.14 (included)
// (*, 11.0.14]: <... reason ...> versions up to 11.0.14 (included)
[11.0.2, 11.0.14): for more details, please check https://github.com/opensearch-project/OpenSearch/issues/2791 and https://bugs.openjdk.java.net/browse/JDK-8259541

View File

@ -47,6 +47,7 @@ import org.opensearch.node.NodeValidationException;
import org.opensearch.test.AbstractBootstrapCheckTestCase;
import org.hamcrest.Matcher;
import java.lang.Runtime.Version;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
@ -745,4 +746,32 @@ public class BootstrapChecksTests extends AbstractBootstrapCheckTestCase {
// Validate the deprecated setting is still valid during the node bootstrap.
ensureChecksPass.accept(Settings.builder().putList(ClusterBootstrapService.INITIAL_MASTER_NODES_SETTING.getKey()));
}
public void testJvmVersionCheck() throws NodeValidationException {
final AtomicReference<Version> version = new AtomicReference<>(Version.parse("11.0.13+8"));
final BootstrapCheck check = new BootstrapChecks.JavaVersionCheck() {
@Override
Version getVersion() {
return version.get();
}
};
final NodeValidationException e = expectThrows(
NodeValidationException.class,
() -> BootstrapChecks.check(emptyContext, true, Collections.singletonList(check))
);
assertThat(
e.getMessage(),
containsString(
"The current JVM version 11.0.13+8 is not recommended for use: "
+ "for more details, please check https://github.com/opensearch-project/OpenSearch/issues/2791 and https://bugs.openjdk.java.net/browse/JDK-8259541"
)
);
version.set(Version.parse("11.0.14"));
BootstrapChecks.check(emptyContext, true, Collections.singletonList(check));
version.set(Runtime.version());
BootstrapChecks.check(emptyContext, true, Collections.singletonList(check));
}
}

View File

@ -0,0 +1,99 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
package org.opensearch.bootstrap.jvm;
import org.opensearch.bootstrap.jvm.DenyJvmVersionsParser.VersionPredicate;
import org.opensearch.test.OpenSearchTestCase;
import java.lang.Runtime.Version;
import java.util.Collection;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.collection.IsEmptyCollection.empty;
public class DenyJvmVersionsParserTests extends OpenSearchTestCase {
public void testDefaults() {
final Collection<VersionPredicate> versions = DenyJvmVersionsParser.getDeniedJvmVersions();
assertThat(versions, not(empty()));
}
public void testSingleVersion() {
final VersionPredicate predicate = DenyJvmVersionsParser.parse("11.0.2: know to be flawed version");
assertThat(predicate.test(Version.parse("11.0.2")), is(true));
assertThat(predicate.test(Version.parse("11.0.1")), is(false));
assertThat(predicate.test(Version.parse("11.0.3")), is(false));
assertThat(predicate.getReason(), equalTo("know to be flawed version"));
}
public void testVersionRangeLowerIncluded() {
final VersionPredicate predicate = DenyJvmVersionsParser.parse("[11.0.2, 11.0.14): know to be flawed version");
assertThat(predicate.test(Version.parse("11.0.2")), is(true));
assertThat(predicate.test(Version.parse("11.0.1")), is(false));
assertThat(predicate.test(Version.parse("11.0.13+1")), is(true));
assertThat(predicate.test(Version.parse("11.0.14")), is(false));
assertThat(predicate.getReason(), equalTo("know to be flawed version"));
}
public void testVersionRangeUpperIncluded() {
final VersionPredicate predicate = DenyJvmVersionsParser.parse("[11.0.2,): know to be flawed version");
assertThat(predicate.test(Version.parse("11.0.2")), is(true));
assertThat(predicate.test(Version.parse("11.0.1")), is(false));
assertThat(predicate.test(Version.parse("11.0.13+1")), is(true));
assertThat(predicate.test(Version.parse("11.0.14")), is(true));
assertThat(predicate.test(Version.parse("17.2.1")), is(true));
assertThat(predicate.getReason(), equalTo("know to be flawed version"));
}
public void testVersionRangeLowerAndUpperIncluded() {
final VersionPredicate predicate = DenyJvmVersionsParser.parse("[11.0.2, 11.0.14]: know to be flawed version");
assertThat(predicate.test(Version.parse("11.0.2")), is(true));
assertThat(predicate.test(Version.parse("11.0.1")), is(false));
assertThat(predicate.test(Version.parse("11.0.13+1")), is(true));
assertThat(predicate.test(Version.parse("11.0.14")), is(true));
assertThat(predicate.test(Version.parse("11.0.14.1")), is(false));
assertThat(predicate.test(Version.parse("11.0.15")), is(false));
assertThat(predicate.getReason(), equalTo("know to be flawed version"));
}
public void testAllVersionsRange() {
final VersionPredicate predicate = DenyJvmVersionsParser.parse("(,): know to be flawed version");
assertThat(predicate.test(Version.parse("11.0.2")), is(true));
assertThat(predicate.test(Version.parse("11.0.1")), is(true));
assertThat(predicate.test(Version.parse("11.0.13+1")), is(true));
assertThat(predicate.test(Version.parse("11.0.14")), is(true));
assertThat(predicate.test(Version.parse("11.0.14.1")), is(true));
assertThat(predicate.test(Version.parse("11.0.15")), is(true));
assertThat(predicate.test(Version.parse("17.2.1")), is(true));
assertThat(predicate.getReason(), equalTo("know to be flawed version"));
}
public void testAllVersionsRangeIncluded() {
final VersionPredicate predicate = DenyJvmVersionsParser.parse("[*, *]: know to be flawed version");
assertThat(predicate.test(Version.parse("11.0.2")), is(true));
assertThat(predicate.test(Version.parse("11.0.1")), is(true));
assertThat(predicate.test(Version.parse("11.0.13+1")), is(true));
assertThat(predicate.test(Version.parse("11.0.14")), is(true));
assertThat(predicate.test(Version.parse("11.0.14.1")), is(true));
assertThat(predicate.test(Version.parse("11.0.15")), is(true));
assertThat(predicate.test(Version.parse("17.2.1")), is(true));
assertThat(predicate.getReason(), equalTo("know to be flawed version"));
}
public void testIncorrectVersionRanges() {
assertThrows(IllegalArgumentException.class, () -> DenyJvmVersionsParser.parse("[*, *: know to be flawed version"));
assertThrows(IllegalArgumentException.class, () -> DenyJvmVersionsParser.parse("*, *: know to be flawed version"));
assertThrows(IllegalArgumentException.class, () -> DenyJvmVersionsParser.parse("*, *): know to be flawed version"));
assertThrows(IllegalArgumentException.class, () -> DenyJvmVersionsParser.parse("(): know to be flawed version"));
assertThrows(IllegalArgumentException.class, () -> DenyJvmVersionsParser.parse("[]: know to be flawed version"));
assertThrows(IllegalArgumentException.class, () -> DenyJvmVersionsParser.parse("[,]"));
assertThrows(IllegalArgumentException.class, () -> DenyJvmVersionsParser.parse("11.0.2"));
}
}