diff --git a/modules/rest-root/src/test/java/org/elasticsearch/rest/root/MainResponseTests.java b/modules/rest-root/src/test/java/org/elasticsearch/rest/root/MainResponseTests.java index 3dfcac3cdecae..dc61abf33e286 100644 --- a/modules/rest-root/src/test/java/org/elasticsearch/rest/root/MainResponseTests.java +++ b/modules/rest-root/src/test/java/org/elasticsearch/rest/root/MainResponseTests.java @@ -66,7 +66,7 @@ public void testToXContent() throws IOException { build.date(), build.isSnapshot(), indexVersion.luceneVersion().toString(), - version.minimumCompatibilityVersion().toString(), + build.minWireCompatVersion(), Build.minimumCompatString(IndexVersion.MINIMUM_COMPATIBLE) ) ), diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexDescriptor.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexDescriptor.java index 79c6dca764250..41be7f2dea98e 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexDescriptor.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexDescriptor.java @@ -8,6 +8,7 @@ package org.elasticsearch.indices; +import org.elasticsearch.Build; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.Strings; @@ -51,7 +52,7 @@ public class TestSystemIndexDescriptor extends SystemIndexDescriptor { 0, "version", "stack", - Version.CURRENT.minimumCompatibilityVersion(), + Version.fromString(Build.current().minWireCompatVersion()), Type.INTERNAL_MANAGED, List.of(), List.of(), @@ -72,7 +73,7 @@ public class TestSystemIndexDescriptor extends SystemIndexDescriptor { 0, "version", "stack", - Version.CURRENT.minimumCompatibilityVersion(), + Version.fromString(Build.current().minWireCompatVersion()), Type.INTERNAL_MANAGED, List.of(), List.of(), diff --git a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java index 673484ac94c77..943cb06e9c6b5 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java +++ b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java @@ -87,6 +87,8 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -525,19 +527,21 @@ static void checkForIndexCompatibility(Logger logger, DataPath... dataPaths) thr logger.info("oldest index version recorded in NodeMetadata {}", metadata.oldestIndexVersion()); if (metadata.oldestIndexVersion().isLegacyIndexVersion()) { + + String bestDowngradeVersion = getBestDowngradeVersion(metadata.previousNodeVersion().toString()); throw new IllegalStateException( "Cannot start this node because it holds metadata for indices with version [" + metadata.oldestIndexVersion() + "] with which this node of version [" + Build.current().version() + "] is incompatible. Revert this node to version [" - + Version.max(Version.CURRENT.minimumCompatibilityVersion(), metadata.previousNodeVersion()) + + bestDowngradeVersion + "] and delete any indices with versions earlier than [" + IndexVersion.MINIMUM_COMPATIBLE + "] before upgrading to version [" + Build.current().version() + "]. If all such indices have already been deleted, revert this node to version [" - + Version.max(Version.CURRENT.minimumCompatibilityVersion(), metadata.previousNodeVersion()) + + bestDowngradeVersion + "] and wait for it to join the cluster to clean up any older indices from its metadata." ); } @@ -1505,4 +1509,32 @@ private static void tryWriteTempFile(Path path) throws IOException { } } } + + /** + * Get a useful version string to direct a user's downgrade operation + * + *

If a user is trying to install 8.0 but has incompatible indices, the user should + * downgrade to 7.17.x. We return 7.17.0, unless the user is trying to upgrade from + * a 7.17.x release, in which case we return the last installed version. + * @return Version to downgrade to + */ + // visible for testing + static String getBestDowngradeVersion(String previousNodeVersion) { + // this method should only be called in the context of an upgrade to 8.x + assert Build.current().version().startsWith("9.") == false; + Pattern pattern = Pattern.compile("^7\\.(\\d+)\\.\\d+$"); + Matcher matcher = pattern.matcher(previousNodeVersion); + if (matcher.matches()) { + try { + int minorVersion = Integer.parseInt(matcher.group(1)); + if (minorVersion >= 17) { + return previousNodeVersion; + } + } catch (NumberFormatException e) { + // continue and return default + } + } + return "7.17.0"; + } + } diff --git a/server/src/main/java/org/elasticsearch/env/NodeMetadata.java b/server/src/main/java/org/elasticsearch/env/NodeMetadata.java index ecc29e793f331..37dd01733e664 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeMetadata.java +++ b/server/src/main/java/org/elasticsearch/env/NodeMetadata.java @@ -113,7 +113,7 @@ public void verifyUpgradeToCurrentVersion() { assert (nodeVersion.equals(Version.V_EMPTY) == false) || (Version.CURRENT.major <= Version.V_7_0_0.major + 1) : "version is required in the node metadata from v9 onwards"; - if (nodeVersion.before(Version.CURRENT.minimumCompatibilityVersion())) { + if (NodeMetadata.isNodeVersionWireCompatible(nodeVersion.toString()) == false) { throw new IllegalStateException( "cannot upgrade a node from version [" + nodeVersion @@ -220,4 +220,21 @@ public NodeMetadata fromXContent(XContentParser parser) throws IOException { } public static final MetadataStateFormat FORMAT = new NodeMetadataStateFormat(false); + + /** + * Check whether a node version is compatible with the current minimum transport version. + * @param version A version identifier as a string + * @throws IllegalArgumentException if version is not a valid transport version identifier + * @return true if the version is compatible, false otherwise + */ + // visible for testing + static boolean isNodeVersionWireCompatible(String version) { + try { + Version esVersion = Version.fromString(version); + return esVersion.onOrAfter(Version.CURRENT.minimumCompatibilityVersion()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Cannot parse [" + version + "] as a transport version identifier", e); + } + } + } diff --git a/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java b/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java index f6c2655d74023..c33819911b99e 100644 --- a/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java +++ b/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java @@ -40,6 +40,7 @@ import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.test.NodeRoles; import org.elasticsearch.test.junit.annotations.TestLogging; +import org.hamcrest.Matchers; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -636,6 +637,20 @@ public void testSymlinkDataDirectory() throws Exception { env.close(); } + public void testGetBestDowngradeVersion() { + assertThat(NodeEnvironment.getBestDowngradeVersion("7.17.0"), Matchers.equalTo("7.17.0")); + assertThat(NodeEnvironment.getBestDowngradeVersion("7.17.5"), Matchers.equalTo("7.17.5")); + assertThat(NodeEnvironment.getBestDowngradeVersion("7.17.1234"), Matchers.equalTo("7.17.1234")); + assertThat(NodeEnvironment.getBestDowngradeVersion("7.18.0"), Matchers.equalTo("7.18.0")); + assertThat(NodeEnvironment.getBestDowngradeVersion("7.17.x"), Matchers.equalTo("7.17.0")); + assertThat(NodeEnvironment.getBestDowngradeVersion("7.17.5-SNAPSHOT"), Matchers.equalTo("7.17.0")); + assertThat(NodeEnvironment.getBestDowngradeVersion("7.17.6b"), Matchers.equalTo("7.17.0")); + assertThat(NodeEnvironment.getBestDowngradeVersion("7.16.0"), Matchers.equalTo("7.17.0")); + // when we get to version 7.2147483648.0 we will have to rethink our approach, but for now we return 7.17.0 with an integer overflow + assertThat(NodeEnvironment.getBestDowngradeVersion("7." + Integer.MAX_VALUE + "0.0"), Matchers.equalTo("7.17.0")); + assertThat(NodeEnvironment.getBestDowngradeVersion("foo"), Matchers.equalTo("7.17.0")); + } + private void verifyFailsOnShardData(Settings settings, Path indexPath, String shardDataDirName) { IllegalStateException ex = expectThrows( IllegalStateException.class, diff --git a/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java b/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java index 44ef31cfbd9d7..6a309767878c4 100644 --- a/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/env/NodeMetadataTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.index.IndexVersion; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; +import org.elasticsearch.test.TransportVersionUtils; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.index.IndexVersionUtils; @@ -158,6 +159,23 @@ public void testUpgradeMarksPreviousVersion() { assertThat(nodeMetadata.previousNodeVersion(), equalTo(version)); } + public void testIsNodeVersionWireCompatible() { + String nodeVersion = VersionUtils.randomCompatibleVersion(random(), Version.CURRENT).toString(); + assertTrue(NodeMetadata.isNodeVersionWireCompatible(nodeVersion)); + nodeVersion = VersionUtils.getPreviousVersion(Version.CURRENT.minimumCompatibilityVersion()).toString(); + assertFalse(NodeMetadata.isNodeVersionWireCompatible(nodeVersion)); + + String transportVersion = TransportVersionUtils.randomCompatibleVersion(random()).toString(); + IllegalArgumentException e1 = expectThrows( + IllegalArgumentException.class, + () -> NodeMetadata.isNodeVersionWireCompatible(transportVersion) + ); + assertThat(e1.getMessage(), equalTo("Cannot parse [" + transportVersion + "] as a transport version identifier")); + + IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, () -> NodeMetadata.isNodeVersionWireCompatible("x.y.z")); + assertThat(e2.getMessage(), equalTo("Cannot parse [x.y.z] as a transport version identifier")); + } + public static Version tooNewVersion() { return Version.fromId(between(Version.CURRENT.id + 1, 99999999)); }