浏览代码

[8.x] Allow archive and searchable snapshots indices in N-2 version (#118923)

This is the backport of #118941 for 8.18.

This change relaxes the index compatibility version checks 
to allow archive and searchable snapshot indices in version 
N-2 to exist on a 9.x cluster. It uses the min. read-only index 
compatible version added in #118884 to accept join requests 
from 9.x nodes that can read indices created in version 7.x
 (N-2) as long as they have a write block and are either archive 
or searchable snapshot indices.

Relates ES-10274
Tanguy Leroux 10 月之前
父节点
当前提交
a716ede23e

+ 31 - 12
server/src/main/java/org/elasticsearch/cluster/coordination/NodeJoinExecutor.java

@@ -49,6 +49,7 @@ import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
+import static org.elasticsearch.cluster.metadata.IndexMetadataVerifier.isReadOnlySupportedVersion;
 import static org.elasticsearch.gateway.GatewayService.STATE_NOT_RECOVERED_BLOCK;
 import static org.elasticsearch.gateway.GatewayService.STATE_NOT_RECOVERED_BLOCK;
 
 
 public class NodeJoinExecutor implements ClusterStateTaskExecutor<JoinTask> {
 public class NodeJoinExecutor implements ClusterStateTaskExecutor<JoinTask> {
@@ -179,8 +180,12 @@ public class NodeJoinExecutor implements ClusterStateTaskExecutor<JoinTask> {
                         Set<String> newNodeEffectiveFeatures = enforceNodeFeatureBarrier(node, effectiveClusterFeatures, features);
                         Set<String> newNodeEffectiveFeatures = enforceNodeFeatureBarrier(node, effectiveClusterFeatures, features);
                         // we do this validation quite late to prevent race conditions between nodes joining and importing dangling indices
                         // we do this validation quite late to prevent race conditions between nodes joining and importing dangling indices
                         // we have to reject nodes that don't support all indices we have in this cluster
                         // we have to reject nodes that don't support all indices we have in this cluster
-                        ensureIndexCompatibility(node.getMinIndexVersion(), node.getMaxIndexVersion(), initialState.getMetadata());
-
+                        ensureIndexCompatibility(
+                            node.getMinIndexVersion(),
+                            node.getMinReadOnlyIndexVersion(),
+                            node.getMaxIndexVersion(),
+                            initialState.getMetadata()
+                        );
                         nodesBuilder.add(node);
                         nodesBuilder.add(node);
                         compatibilityVersionsMap.put(node.getId(), compatibilityVersions);
                         compatibilityVersionsMap.put(node.getId(), compatibilityVersions);
                         // store the actual node features here, not including assumed features, as this is persisted in cluster state
                         // store the actual node features here, not including assumed features, as this is persisted in cluster state
@@ -394,9 +399,15 @@ public class NodeJoinExecutor implements ClusterStateTaskExecutor<JoinTask> {
      * will not be created with a newer version of elasticsearch as well as that all indices are newer or equal to the minimum index
      * will not be created with a newer version of elasticsearch as well as that all indices are newer or equal to the minimum index
      * compatibility version.
      * compatibility version.
      * @see IndexVersions#MINIMUM_COMPATIBLE
      * @see IndexVersions#MINIMUM_COMPATIBLE
+     * @see IndexVersions#MINIMUM_READONLY_COMPATIBLE
      * @throws IllegalStateException if any index is incompatible with the given version
      * @throws IllegalStateException if any index is incompatible with the given version
      */
      */
-    public static void ensureIndexCompatibility(IndexVersion minSupportedVersion, IndexVersion maxSupportedVersion, Metadata metadata) {
+    public static void ensureIndexCompatibility(
+        IndexVersion minSupportedVersion,
+        IndexVersion minReadOnlySupportedVersion,
+        IndexVersion maxSupportedVersion,
+        Metadata metadata
+    ) {
         // we ensure that all indices in the cluster we join are compatible with us no matter if they are
         // we ensure that all indices in the cluster we join are compatible with us no matter if they are
         // closed or not we can't read mappings of these indices so we need to reject the join...
         // closed or not we can't read mappings of these indices so we need to reject the join...
         for (IndexMetadata idxMetadata : metadata) {
         for (IndexMetadata idxMetadata : metadata) {
@@ -411,14 +422,17 @@ public class NodeJoinExecutor implements ClusterStateTaskExecutor<JoinTask> {
                 );
                 );
             }
             }
             if (idxMetadata.getCompatibilityVersion().before(minSupportedVersion)) {
             if (idxMetadata.getCompatibilityVersion().before(minSupportedVersion)) {
-                throw new IllegalStateException(
-                    "index "
-                        + idxMetadata.getIndex()
-                        + " version not supported: "
-                        + idxMetadata.getCompatibilityVersion().toReleaseVersion()
-                        + " minimum compatible index version is: "
-                        + minSupportedVersion.toReleaseVersion()
-                );
+                boolean isReadOnlySupported = isReadOnlySupportedVersion(idxMetadata, minSupportedVersion, minReadOnlySupportedVersion);
+                if (isReadOnlySupported == false) {
+                    throw new IllegalStateException(
+                        "index "
+                            + idxMetadata.getIndex()
+                            + " version not supported: "
+                            + idxMetadata.getCompatibilityVersion().toReleaseVersion()
+                            + " minimum compatible index version is: "
+                            + minSupportedVersion.toReleaseVersion()
+                    );
+                }
             }
             }
         }
         }
     }
     }
@@ -542,7 +556,12 @@ public class NodeJoinExecutor implements ClusterStateTaskExecutor<JoinTask> {
         final Collection<BiConsumer<DiscoveryNode, ClusterState>> validators = new ArrayList<>();
         final Collection<BiConsumer<DiscoveryNode, ClusterState>> validators = new ArrayList<>();
         validators.add((node, state) -> {
         validators.add((node, state) -> {
             ensureNodesCompatibility(node.getVersion(), state.getNodes());
             ensureNodesCompatibility(node.getVersion(), state.getNodes());
-            ensureIndexCompatibility(node.getMinIndexVersion(), node.getMaxIndexVersion(), state.getMetadata());
+            ensureIndexCompatibility(
+                node.getMinIndexVersion(),
+                node.getMinReadOnlyIndexVersion(),
+                node.getMaxIndexVersion(),
+                state.getMetadata()
+            );
         });
         });
         validators.addAll(onJoinValidators);
         validators.addAll(onJoinValidators);
         return Collections.unmodifiableCollection(validators);
         return Collections.unmodifiableCollection(validators);

+ 79 - 20
server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadataVerifier.java

@@ -88,8 +88,12 @@ public class IndexMetadataVerifier {
      * If the index does not need upgrade it returns the index metadata unchanged, otherwise it returns a modified index metadata. If index
      * If the index does not need upgrade it returns the index metadata unchanged, otherwise it returns a modified index metadata. If index
      * cannot be updated the method throws an exception.
      * cannot be updated the method throws an exception.
      */
      */
-    public IndexMetadata verifyIndexMetadata(IndexMetadata indexMetadata, IndexVersion minimumIndexCompatibilityVersion) {
-        checkSupportedVersion(indexMetadata, minimumIndexCompatibilityVersion);
+    public IndexMetadata verifyIndexMetadata(
+        IndexMetadata indexMetadata,
+        IndexVersion minimumIndexCompatibilityVersion,
+        IndexVersion minimumReadOnlyIndexCompatibilityVersion
+    ) {
+        checkSupportedVersion(indexMetadata, minimumIndexCompatibilityVersion, minimumReadOnlyIndexCompatibilityVersion);
 
 
         // First convert any shared_cache searchable snapshot indices to only use _tier_preference: data_frozen
         // First convert any shared_cache searchable snapshot indices to only use _tier_preference: data_frozen
         IndexMetadata newMetadata = convertSharedCacheTierPreference(indexMetadata);
         IndexMetadata newMetadata = convertSharedCacheTierPreference(indexMetadata);
@@ -105,26 +109,81 @@ public class IndexMetadataVerifier {
     }
     }
 
 
     /**
     /**
-     * Check that the index version is compatible. Elasticsearch does not support indices created before the
-     * previous major version.
+     * Check that the index version is compatible. Elasticsearch supports reading and writing indices created in the current version ("N")
+     + as well as the previous major version ("N-1"). Elasticsearch only supports reading indices created down to the penultimate version
+     + ("N-2") and does not support reading nor writing any version below that.
      */
      */
-    private static void checkSupportedVersion(IndexMetadata indexMetadata, IndexVersion minimumIndexCompatibilityVersion) {
-        boolean isSupportedVersion = indexMetadata.getCompatibilityVersion().onOrAfter(minimumIndexCompatibilityVersion);
-        if (isSupportedVersion == false) {
-            throw new IllegalStateException(
-                "The index "
-                    + indexMetadata.getIndex()
-                    + " has current compatibility version ["
-                    + indexMetadata.getCompatibilityVersion().toReleaseVersion()
-                    + "] but the minimum compatible version is ["
-                    + minimumIndexCompatibilityVersion.toReleaseVersion()
-                    + "]. It should be re-indexed in Elasticsearch "
-                    + (Version.CURRENT.major - 1)
-                    + ".x before upgrading to "
-                    + Build.current().version()
-                    + "."
-            );
+    private static void checkSupportedVersion(
+        IndexMetadata indexMetadata,
+        IndexVersion minimumIndexCompatibilityVersion,
+        IndexVersion minimumReadOnlyIndexCompatibilityVersion
+    ) {
+        if (isFullySupportedVersion(indexMetadata, minimumIndexCompatibilityVersion)) {
+            return;
+        }
+        if (isReadOnlySupportedVersion(indexMetadata, minimumIndexCompatibilityVersion, minimumReadOnlyIndexCompatibilityVersion)) {
+            return;
+        }
+        throw new IllegalStateException(
+            "The index "
+                + indexMetadata.getIndex()
+                + " has current compatibility version ["
+                + indexMetadata.getCompatibilityVersion().toReleaseVersion()
+                + "] but the minimum compatible version is ["
+                + minimumIndexCompatibilityVersion.toReleaseVersion()
+                + "]. It should be re-indexed in Elasticsearch "
+                + (Version.CURRENT.major - 1)
+                + ".x before upgrading to "
+                + Build.current().version()
+                + "."
+        );
+    }
+
+    private static boolean isFullySupportedVersion(IndexMetadata indexMetadata, IndexVersion minimumIndexCompatibilityVersion) {
+        return indexMetadata.getCompatibilityVersion().onOrAfter(minimumIndexCompatibilityVersion);
+    }
+
+    /**
+     * Returns {@code true} if the index version is compatible in read-only mode. As of today, only searchable snapshots and archive indices
+     * in version N-2 with a write block are read-only compatible. This method throws an {@link IllegalStateException} if the index is
+     * either a searchable snapshot or an archive index with a read-only compatible version but is missing the write block.
+     *
+     * @param indexMetadata                         the index metadata
+     * @param minimumIndexCompatibilityVersion      the min. index compatible version for reading and writing indices (used in assertion)
+     * @param minReadOnlyIndexCompatibilityVersion  the min. index compatible version for only reading indices
+     *
+     * @return {@code true} if the index version is compatible in read-only mode, {@code false} otherwise.
+     * @throws IllegalStateException if the index is read-only compatible but has no write block in place.
+     */
+    public static boolean isReadOnlySupportedVersion(
+        IndexMetadata indexMetadata,
+        IndexVersion minimumIndexCompatibilityVersion,
+        IndexVersion minReadOnlyIndexCompatibilityVersion
+    ) {
+        boolean isReadOnlySupportedVersion = indexMetadata.getCompatibilityVersion().onOrAfter(minReadOnlyIndexCompatibilityVersion);
+        assert isFullySupportedVersion(indexMetadata, minimumIndexCompatibilityVersion) == false;
+
+        if (isReadOnlySupportedVersion
+            && (indexMetadata.isSearchableSnapshot() || indexMetadata.getCreationVersion().isLegacyIndexVersion())) {
+            boolean isReadOnly = IndexMetadata.INDEX_BLOCKS_WRITE_SETTING.get(indexMetadata.getSettings());
+            if (isReadOnly == false) {
+                throw new IllegalStateException(
+                    "The index "
+                        + indexMetadata.getIndex()
+                        + " created in version ["
+                        + indexMetadata.getCreationVersion()
+                        + "] with current compatibility version ["
+                        + indexMetadata.getCompatibilityVersion().toReleaseVersion()
+                        + "] must be marked as read-only using the setting ["
+                        + IndexMetadata.SETTING_BLOCKS_WRITE
+                        + "] set to [true] before upgrading to "
+                        + Build.current().version()
+                        + '.'
+                );
+            }
+            return true;
         }
         }
+        return false;
     }
     }
 
 
     /**
     /**

+ 6 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java

@@ -1120,6 +1120,7 @@ public class MetadataIndexStateService {
             final Metadata.Builder metadata = Metadata.builder(currentState.metadata());
             final Metadata.Builder metadata = Metadata.builder(currentState.metadata());
             final ClusterBlocks.Builder blocks = ClusterBlocks.builder(currentState.blocks());
             final ClusterBlocks.Builder blocks = ClusterBlocks.builder(currentState.blocks());
             final IndexVersion minIndexCompatibilityVersion = currentState.getNodes().getMinSupportedIndexVersion();
             final IndexVersion minIndexCompatibilityVersion = currentState.getNodes().getMinSupportedIndexVersion();
+            final IndexVersion minReadOnlyIndexCompatibilityVersion = currentState.getNodes().getMinReadOnlySupportedIndexVersion();
 
 
             for (IndexMetadata indexMetadata : indicesToOpen) {
             for (IndexMetadata indexMetadata : indicesToOpen) {
                 final Index index = indexMetadata.getIndex();
                 final Index index = indexMetadata.getIndex();
@@ -1137,7 +1138,11 @@ public class MetadataIndexStateService {
 
 
                     // The index might be closed because we couldn't import it due to an old incompatible
                     // The index might be closed because we couldn't import it due to an old incompatible
                     // version, so we need to verify its compatibility.
                     // version, so we need to verify its compatibility.
-                    newIndexMetadata = indexMetadataVerifier.verifyIndexMetadata(newIndexMetadata, minIndexCompatibilityVersion);
+                    newIndexMetadata = indexMetadataVerifier.verifyIndexMetadata(
+                        newIndexMetadata,
+                        minIndexCompatibilityVersion,
+                        minReadOnlyIndexCompatibilityVersion
+                    );
                     try {
                     try {
                         indicesService.verifyIndexMetadata(newIndexMetadata, newIndexMetadata);
                         indicesService.verifyIndexMetadata(newIndexMetadata, newIndexMetadata);
                     } catch (Exception e) {
                     } catch (Exception e) {

+ 5 - 1
server/src/main/java/org/elasticsearch/gateway/GatewayMetaState.java

@@ -308,7 +308,11 @@ public class GatewayMetaState implements Closeable {
         boolean changed = false;
         boolean changed = false;
         final Metadata.Builder upgradedMetadata = Metadata.builder(metadata);
         final Metadata.Builder upgradedMetadata = Metadata.builder(metadata);
         for (IndexMetadata indexMetadata : metadata) {
         for (IndexMetadata indexMetadata : metadata) {
-            IndexMetadata newMetadata = indexMetadataVerifier.verifyIndexMetadata(indexMetadata, IndexVersions.MINIMUM_COMPATIBLE);
+            IndexMetadata newMetadata = indexMetadataVerifier.verifyIndexMetadata(
+                indexMetadata,
+                IndexVersions.MINIMUM_COMPATIBLE,
+                IndexVersions.MINIMUM_READONLY_COMPATIBLE
+            );
             changed |= indexMetadata != newMetadata;
             changed |= indexMetadata != newMetadata;
             upgradedMetadata.put(newMetadata, false);
             upgradedMetadata.put(newMetadata, false);
         }
         }

+ 6 - 1
server/src/main/java/org/elasticsearch/gateway/LocalAllocateDangledIndices.java

@@ -125,6 +125,7 @@ public class LocalAllocateDangledIndices {
                         currentState.routingTable()
                         currentState.routingTable()
                     );
                     );
                     IndexVersion minIndexCompatibilityVersion = currentState.nodes().getMinSupportedIndexVersion();
                     IndexVersion minIndexCompatibilityVersion = currentState.nodes().getMinSupportedIndexVersion();
+                    IndexVersion minReadOnlyIndexCompatibilityVersion = currentState.nodes().getMinReadOnlySupportedIndexVersion();
                     IndexVersion maxIndexCompatibilityVersion = currentState.nodes().getMaxDataNodeCompatibleIndexVersion();
                     IndexVersion maxIndexCompatibilityVersion = currentState.nodes().getMaxDataNodeCompatibleIndexVersion();
                     boolean importNeeded = false;
                     boolean importNeeded = false;
                     StringBuilder sb = new StringBuilder();
                     StringBuilder sb = new StringBuilder();
@@ -176,7 +177,11 @@ public class LocalAllocateDangledIndices {
                         try {
                         try {
                             // The dangled index might be from an older version, we need to make sure it's compatible
                             // The dangled index might be from an older version, we need to make sure it's compatible
                             // with the current version.
                             // with the current version.
-                            newIndexMetadata = indexMetadataVerifier.verifyIndexMetadata(indexMetadata, minIndexCompatibilityVersion);
+                            newIndexMetadata = indexMetadataVerifier.verifyIndexMetadata(
+                                indexMetadata,
+                                minIndexCompatibilityVersion,
+                                minReadOnlyIndexCompatibilityVersion
+                            );
                             newIndexMetadata = IndexMetadata.builder(newIndexMetadata)
                             newIndexMetadata = IndexMetadata.builder(newIndexMetadata)
                                 .settings(
                                 .settings(
                                     Settings.builder()
                                     Settings.builder()

+ 1 - 1
server/src/main/java/org/elasticsearch/index/IndexVersion.java

@@ -124,7 +124,7 @@ public record IndexVersion(int id, Version luceneVersion) implements VersionId<I
     }
     }
 
 
     public boolean isLegacyIndexVersion() {
     public boolean isLegacyIndexVersion() {
-        return before(IndexVersions.MINIMUM_COMPATIBLE);
+        return before(IndexVersions.MINIMUM_READONLY_COMPATIBLE);
     }
     }
 
 
     public static IndexVersion getMinimumCompatibleIndexVersion(int versionId) {
     public static IndexVersion getMinimumCompatibleIndexVersion(int versionId) {

+ 7 - 2
server/src/main/java/org/elasticsearch/snapshots/RestoreService.java

@@ -1315,6 +1315,7 @@ public final class RestoreService implements ClusterStateApplier {
             final Map<ShardId, ShardRestoreStatus> shards = new HashMap<>();
             final Map<ShardId, ShardRestoreStatus> shards = new HashMap<>();
 
 
             final IndexVersion minIndexCompatibilityVersion = currentState.getNodes().getMinSupportedIndexVersion();
             final IndexVersion minIndexCompatibilityVersion = currentState.getNodes().getMinSupportedIndexVersion();
+            final IndexVersion minReadOnlyIndexCompatibilityVersion = currentState.getNodes().getMinReadOnlySupportedIndexVersion();
             final String localNodeId = clusterService.state().nodes().getLocalNodeId();
             final String localNodeId = clusterService.state().nodes().getLocalNodeId();
             for (Map.Entry<String, IndexId> indexEntry : indicesToRestore.entrySet()) {
             for (Map.Entry<String, IndexId> indexEntry : indicesToRestore.entrySet()) {
                 final IndexId index = indexEntry.getValue();
                 final IndexId index = indexEntry.getValue();
@@ -1327,12 +1328,16 @@ public final class RestoreService implements ClusterStateApplier {
                     request.indexSettings(),
                     request.indexSettings(),
                     request.ignoreIndexSettings()
                     request.ignoreIndexSettings()
                 );
                 );
-                if (snapshotIndexMetadata.getCompatibilityVersion().before(minIndexCompatibilityVersion)) {
+                if (snapshotIndexMetadata.getCompatibilityVersion().isLegacyIndexVersion()) {
                     // adapt index metadata so that it can be understood by current version
                     // adapt index metadata so that it can be understood by current version
                     snapshotIndexMetadata = convertLegacyIndex(snapshotIndexMetadata, currentState, indicesService);
                     snapshotIndexMetadata = convertLegacyIndex(snapshotIndexMetadata, currentState, indicesService);
                 }
                 }
                 try {
                 try {
-                    snapshotIndexMetadata = indexMetadataVerifier.verifyIndexMetadata(snapshotIndexMetadata, minIndexCompatibilityVersion);
+                    snapshotIndexMetadata = indexMetadataVerifier.verifyIndexMetadata(
+                        snapshotIndexMetadata,
+                        minIndexCompatibilityVersion,
+                        minReadOnlyIndexCompatibilityVersion
+                    );
                 } catch (Exception ex) {
                 } catch (Exception ex) {
                     throw new SnapshotRestoreException(snapshot, "cannot restore index [" + index + "] because it cannot be upgraded", ex);
                     throw new SnapshotRestoreException(snapshot, "cannot restore index [" + index + "] because it cannot be upgraded", ex);
                 }
                 }

+ 84 - 3
server/src/test/java/org/elasticsearch/cluster/coordination/NodeJoinExecutorTests.java

@@ -39,6 +39,7 @@ import org.elasticsearch.features.FeatureSpecification;
 import org.elasticsearch.features.NodeFeature;
 import org.elasticsearch.features.NodeFeature;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.IndexVersion;
 import org.elasticsearch.index.IndexVersions;
 import org.elasticsearch.index.IndexVersions;
+import org.elasticsearch.snapshots.SearchableSnapshotsSettings;
 import org.elasticsearch.test.ClusterServiceUtils;
 import org.elasticsearch.test.ClusterServiceUtils;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.test.MockLog;
 import org.elasticsearch.test.MockLog;
@@ -58,6 +59,7 @@ import java.util.stream.Stream;
 
 
 import static org.elasticsearch.cluster.metadata.DesiredNodesTestCase.assertDesiredNodesStatusIsCorrect;
 import static org.elasticsearch.cluster.metadata.DesiredNodesTestCase.assertDesiredNodesStatusIsCorrect;
 import static org.elasticsearch.cluster.metadata.DesiredNodesTestCase.randomDesiredNode;
 import static org.elasticsearch.cluster.metadata.DesiredNodesTestCase.randomDesiredNode;
+import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING;
 import static org.elasticsearch.test.VersionUtils.maxCompatibleVersion;
 import static org.elasticsearch.test.VersionUtils.maxCompatibleVersion;
 import static org.elasticsearch.test.VersionUtils.randomCompatibleVersion;
 import static org.elasticsearch.test.VersionUtils.randomCompatibleVersion;
 import static org.elasticsearch.test.VersionUtils.randomVersion;
 import static org.elasticsearch.test.VersionUtils.randomVersion;
@@ -89,12 +91,18 @@ public class NodeJoinExecutorTests extends ESTestCase {
             .build();
             .build();
         metaBuilder.put(indexMetadata, false);
         metaBuilder.put(indexMetadata, false);
         Metadata metadata = metaBuilder.build();
         Metadata metadata = metaBuilder.build();
-        NodeJoinExecutor.ensureIndexCompatibility(IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current(), metadata);
+        NodeJoinExecutor.ensureIndexCompatibility(
+            IndexVersions.MINIMUM_COMPATIBLE,
+            IndexVersions.MINIMUM_READONLY_COMPATIBLE,
+            IndexVersion.current(),
+            metadata
+        );
 
 
         expectThrows(
         expectThrows(
             IllegalStateException.class,
             IllegalStateException.class,
             () -> NodeJoinExecutor.ensureIndexCompatibility(
             () -> NodeJoinExecutor.ensureIndexCompatibility(
                 IndexVersions.MINIMUM_COMPATIBLE,
                 IndexVersions.MINIMUM_COMPATIBLE,
+                IndexVersions.MINIMUM_READONLY_COMPATIBLE,
                 IndexVersionUtils.getPreviousVersion(IndexVersion.current()),
                 IndexVersionUtils.getPreviousVersion(IndexVersion.current()),
                 metadata
                 metadata
             )
             )
@@ -113,10 +121,78 @@ public class NodeJoinExecutorTests extends ESTestCase {
         Metadata metadata = metaBuilder.build();
         Metadata metadata = metaBuilder.build();
         expectThrows(
         expectThrows(
             IllegalStateException.class,
             IllegalStateException.class,
-            () -> NodeJoinExecutor.ensureIndexCompatibility(IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current(), metadata)
+            () -> NodeJoinExecutor.ensureIndexCompatibility(
+                IndexVersions.MINIMUM_COMPATIBLE,
+                IndexVersions.MINIMUM_READONLY_COMPATIBLE,
+                IndexVersion.current(),
+                metadata
+            )
         );
         );
     }
     }
 
 
+    public void testJoinClusterWithReadOnlyCompatibleIndices() {
+        assertThat(
+            "8.x has no N-2 support so read-only compatibility is the same as regular read/write compatibility",
+            IndexVersions.MINIMUM_READONLY_COMPATIBLE,
+            equalTo(IndexVersions.MINIMUM_COMPATIBLE)
+        );
+        {
+            var indexMetadata = IndexMetadata.builder("searchable-snapshot")
+                .settings(
+                    Settings.builder()
+                        .put(INDEX_STORE_TYPE_SETTING.getKey(), SearchableSnapshotsSettings.SEARCHABLE_SNAPSHOT_STORE_TYPE)
+                        .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersions.MINIMUM_READONLY_COMPATIBLE)
+                        .put(IndexMetadata.SETTING_BLOCKS_WRITE, randomBoolean())
+                )
+                .numberOfShards(1)
+                .numberOfReplicas(1)
+                .build();
+
+            NodeJoinExecutor.ensureIndexCompatibility(
+                IndexVersions.MINIMUM_COMPATIBLE,
+                IndexVersions.MINIMUM_READONLY_COMPATIBLE,
+                IndexVersion.current(),
+                Metadata.builder().put(indexMetadata, false).build()
+            );
+        }
+        {
+            var indexMetadata = IndexMetadata.builder("archive")
+                .settings(
+                    Settings.builder()
+                        .put(IndexMetadata.SETTING_VERSION_CREATED, Version.fromId(randomFrom(5000099, 6000099)))
+                        .put(IndexMetadata.SETTING_VERSION_COMPATIBILITY, IndexVersions.MINIMUM_READONLY_COMPATIBLE)
+                        .put(IndexMetadata.SETTING_BLOCKS_WRITE, randomBoolean())
+                )
+                .numberOfShards(1)
+                .numberOfReplicas(1)
+                .build();
+
+            NodeJoinExecutor.ensureIndexCompatibility(
+                IndexVersions.MINIMUM_COMPATIBLE,
+                IndexVersions.MINIMUM_READONLY_COMPATIBLE,
+                IndexVersion.current(),
+                Metadata.builder().put(indexMetadata, false).build()
+            );
+        }
+        {
+            var indexMetadata = IndexMetadata.builder("legacy")
+                .settings(Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, Version.fromId(randomFrom(5000099, 6000099))))
+                .numberOfShards(1)
+                .numberOfReplicas(1)
+                .build();
+
+            expectThrows(
+                IllegalStateException.class,
+                () -> NodeJoinExecutor.ensureIndexCompatibility(
+                    IndexVersions.MINIMUM_COMPATIBLE,
+                    IndexVersions.MINIMUM_READONLY_COMPATIBLE,
+                    IndexVersion.current(),
+                    Metadata.builder().put(indexMetadata, false).build()
+                )
+            );
+        }
+    }
+
     public void testPreventJoinClusterWithUnsupportedNodeVersions() {
     public void testPreventJoinClusterWithUnsupportedNodeVersions() {
         DiscoveryNodes.Builder builder = DiscoveryNodes.builder();
         DiscoveryNodes.Builder builder = DiscoveryNodes.builder();
         final Version version = randomCompatibleVersion(random(), Version.CURRENT);
         final Version version = randomCompatibleVersion(random(), Version.CURRENT);
@@ -467,7 +543,12 @@ public class NodeJoinExecutorTests extends ESTestCase {
             .build();
             .build();
         metaBuilder.put(indexMetadata, false);
         metaBuilder.put(indexMetadata, false);
         Metadata metadata = metaBuilder.build();
         Metadata metadata = metaBuilder.build();
-        NodeJoinExecutor.ensureIndexCompatibility(IndexVersions.MINIMUM_COMPATIBLE, IndexVersion.current(), metadata);
+        NodeJoinExecutor.ensureIndexCompatibility(
+            IndexVersions.MINIMUM_COMPATIBLE,
+            IndexVersions.MINIMUM_READONLY_COMPATIBLE,
+            IndexVersion.current(),
+            metadata
+        );
     }
     }
 
 
     public static Settings.Builder randomCompatibleVersionSettings() {
     public static Settings.Builder randomCompatibleVersionSettings() {

+ 3 - 3
server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataVerifierTests.java

@@ -97,7 +97,7 @@ public class IndexMetadataVerifierTests extends ESTestCase {
                 .put("index.similarity.my_similarity.after_effect", "l")
                 .put("index.similarity.my_similarity.after_effect", "l")
                 .build()
                 .build()
         );
         );
-        service.verifyIndexMetadata(src, IndexVersions.MINIMUM_COMPATIBLE);
+        service.verifyIndexMetadata(src, IndexVersions.MINIMUM_COMPATIBLE, IndexVersions.MINIMUM_READONLY_COMPATIBLE);
     }
     }
 
 
     public void testIncompatibleVersion() {
     public void testIncompatibleVersion() {
@@ -110,7 +110,7 @@ public class IndexMetadataVerifierTests extends ESTestCase {
         );
         );
         String message = expectThrows(
         String message = expectThrows(
             IllegalStateException.class,
             IllegalStateException.class,
-            () -> service.verifyIndexMetadata(metadata, IndexVersions.MINIMUM_COMPATIBLE)
+            () -> service.verifyIndexMetadata(metadata, IndexVersions.MINIMUM_COMPATIBLE, IndexVersions.MINIMUM_READONLY_COMPATIBLE)
         ).getMessage();
         ).getMessage();
         assertThat(
         assertThat(
             message,
             message,
@@ -132,7 +132,7 @@ public class IndexMetadataVerifierTests extends ESTestCase {
 
 
         indexCreated = IndexVersionUtils.randomVersionBetween(random(), minCompat, IndexVersion.current());
         indexCreated = IndexVersionUtils.randomVersionBetween(random(), minCompat, IndexVersion.current());
         IndexMetadata goodMeta = newIndexMeta("foo", Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, indexCreated).build());
         IndexMetadata goodMeta = newIndexMeta("foo", Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, indexCreated).build());
-        service.verifyIndexMetadata(goodMeta, IndexVersions.MINIMUM_COMPATIBLE);
+        service.verifyIndexMetadata(goodMeta, IndexVersions.MINIMUM_COMPATIBLE, IndexVersions.MINIMUM_READONLY_COMPATIBLE);
     }
     }
 
 
     private IndexMetadataVerifier getIndexMetadataVerifier() {
     private IndexMetadataVerifier getIndexMetadataVerifier() {

+ 5 - 1
server/src/test/java/org/elasticsearch/gateway/GatewayMetaStateTests.java

@@ -200,7 +200,11 @@ public class GatewayMetaStateTests extends ESTestCase {
         }
         }
 
 
         @Override
         @Override
-        public IndexMetadata verifyIndexMetadata(IndexMetadata indexMetadata, IndexVersion minimumIndexCompatibilityVersion) {
+        public IndexMetadata verifyIndexMetadata(
+            IndexMetadata indexMetadata,
+            IndexVersion minimumIndexCompatibilityVersion,
+            IndexVersion minimumReadOnlyIndexCompatibilityVersion
+        ) {
             return upgrade ? IndexMetadata.builder(indexMetadata).build() : indexMetadata;
             return upgrade ? IndexMetadata.builder(indexMetadata).build() : indexMetadata;
         }
         }
     }
     }

+ 5 - 1
server/src/test/java/org/elasticsearch/indices/cluster/ClusterStateChanges.java

@@ -252,7 +252,11 @@ public class ClusterStateChanges {
         ) {
         ) {
             // metadata upgrader should do nothing
             // metadata upgrader should do nothing
             @Override
             @Override
-            public IndexMetadata verifyIndexMetadata(IndexMetadata indexMetadata, IndexVersion minimumIndexCompatibilityVersion) {
+            public IndexMetadata verifyIndexMetadata(
+                IndexMetadata indexMetadata,
+                IndexVersion minimumIndexCompatibilityVersion,
+                IndexVersion minimumReadOnlyIndexCompatibilityVersion
+            ) {
                 return indexMetadata;
                 return indexMetadata;
             }
             }
         };
         };