From 33000ab8aa829598f4813199a3bb85102a1d6ecd Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Tue, 13 Jul 2021 12:33:23 +0200 Subject: [PATCH] Prevent searchable snapshots indices to be shrunk/split (#75227) Today if we try to shrink or to split a searchable snapshot index using the Resize API a new index will be created but can't be assigned, and even if it was assigned it won't work as the number of shards can't be changed and must always match the number of shards from the snapshot. This commit adds some verification to prevent a snapshot backed indices to be resized and if an attempt is made, throw a better error message. Note that cloning is supported since #56595 and in this change we make sure that it is only used to convert the searchable snapshot index back to a regular index. Relates #74977 (comment) --- .../metadata/MetadataCreateIndexService.java | 18 ++- .../SearchableSnapshotsResizeIntegTests.java | 130 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsResizeIntegTests.java diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java index 6202ee0cd3690..a8a114bfaa44e 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java @@ -105,6 +105,8 @@ import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.cluster.metadata.MetadataCreateDataStreamService.validateTimestampFieldMapping; import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.resolveSettings; +import static org.elasticsearch.index.IndexModule.INDEX_RECOVERY_TYPE_SETTING; +import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING; /** * Service responsible for submitting create index requests @@ -1140,6 +1142,9 @@ private static List validateIndexCustomPath(Settings settings, @Nullable */ static List validateShrinkIndex(ClusterState state, String sourceIndex, String targetIndexName, Settings targetIndexSettings) { IndexMetadata sourceMetadata = validateResize(state, sourceIndex, targetIndexName, targetIndexSettings); + if ("snapshot".equals(INDEX_STORE_TYPE_SETTING.get(sourceMetadata.getSettings()))) { + throw new IllegalArgumentException("can't shrink searchable snapshot index [" + sourceIndex + ']'); + } assert INDEX_NUMBER_OF_SHARDS_SETTING.exists(targetIndexSettings); IndexMetadata.selectShrinkShards(0, sourceMetadata, INDEX_NUMBER_OF_SHARDS_SETTING.get(targetIndexSettings)); @@ -1171,6 +1176,9 @@ static List validateShrinkIndex(ClusterState state, String sourceIndex, static void validateSplitIndex(ClusterState state, String sourceIndex, String targetIndexName, Settings targetIndexSettings) { IndexMetadata sourceMetadata = validateResize(state, sourceIndex, targetIndexName, targetIndexSettings); + if ("snapshot".equals(INDEX_STORE_TYPE_SETTING.get(sourceMetadata.getSettings()))) { + throw new IllegalArgumentException("can't split searchable snapshot index [" + sourceIndex + ']'); + } IndexMetadata.selectSplitShard(0, sourceMetadata, IndexMetadata.INDEX_NUMBER_OF_SHARDS_SETTING.get(targetIndexSettings)); if (sourceMetadata.getCreationVersion().before(Version.V_6_0_0_alpha1)) { // ensure we have a single type since this would make the splitting code considerably more complex @@ -1182,6 +1190,15 @@ static void validateSplitIndex(ClusterState state, String sourceIndex, String ta static void validateCloneIndex(ClusterState state, String sourceIndex, String targetIndexName, Settings targetIndexSettings) { IndexMetadata sourceMetadata = validateResize(state, sourceIndex, targetIndexName, targetIndexSettings); + if ("snapshot".equals(INDEX_STORE_TYPE_SETTING.get(sourceMetadata.getSettings()))) { + for (Setting nonCloneableSetting : Arrays.asList(INDEX_STORE_TYPE_SETTING, INDEX_RECOVERY_TYPE_SETTING)) { + if (nonCloneableSetting.exists(targetIndexSettings) == false) { + throw new IllegalArgumentException("can't clone searchable snapshot index [" + sourceIndex + "]; setting [" + + nonCloneableSetting.getKey() + + "] should be overridden"); + } + } + } IndexMetadata.selectCloneShard(0, sourceMetadata, INDEX_NUMBER_OF_SHARDS_SETTING.get(targetIndexSettings)); } @@ -1201,7 +1218,6 @@ static IndexMetadata validateResize(ClusterState state, String sourceIndex, Stri throw new IllegalArgumentException(String.format(Locale.ROOT, "cannot resize the write index [%s] for data stream [%s]", sourceIndex, source.getParentDataStream().getName())); } - // ensure index is read-only if (state.blocks().indexBlocked(ClusterBlockLevel.WRITE, sourceIndex) == false) { throw new IllegalStateException("index " + sourceIndex + " must be read-only to resize index. use \"index.blocks.write=true\""); diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsResizeIntegTests.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsResizeIntegTests.java new file mode 100644 index 0000000000000..907517b35796a --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsResizeIntegTests.java @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.searchablesnapshots; + +import org.elasticsearch.action.admin.indices.shrink.ResizeType; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexModule; +import org.elasticsearch.repositories.fs.FsRepository; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.cluster.routing.allocation.DataTierAllocationDecider; +import org.elasticsearch.xpack.core.DataTier; +import org.junit.After; +import org.junit.Before; + +import static java.util.Collections.singletonList; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_NUMBER_OF_REPLICAS_SETTING; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_NUMBER_OF_SHARDS_SETTING; +import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest.Storage; +import static org.hamcrest.Matchers.equalTo; + +@ESIntegTestCase.ClusterScope(numDataNodes = 1) +public class SearchableSnapshotsResizeIntegTests extends BaseFrozenSearchableSnapshotsIntegTestCase { + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + createRepository("repository", FsRepository.TYPE); + assertAcked( + prepareCreate( + "index", + Settings.builder() + .put(INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0) + .put(INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 2) + .put(INDEX_NUMBER_OF_ROUTING_SHARDS_SETTING.getKey(), 4) + .put(INDEX_SOFT_DELETES_SETTING.getKey(), true) + ) + ); + indexRandomDocs("index", scaledRandomIntBetween(0, 1_000)); + createSnapshot("repository", "snapshot", singletonList("index")); + assertAcked(client().admin().indices().prepareDelete("index")); + mountSnapshot("repository", "snapshot", "index", "mounted-index", Settings.EMPTY, randomFrom(Storage.values())); + ensureGreen("mounted-index"); + } + + @After + @Override + public void tearDown() throws Exception { + assertAcked(client().admin().indices().prepareDelete("mounted-*")); + assertAcked(client().admin().cluster().prepareDeleteSnapshot("repository", "snapshot").get()); + assertAcked(client().admin().cluster().prepareDeleteRepository("repository")); + super.tearDown(); + } + + public void testShrinkSearchableSnapshotIndex() { + final IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> client().admin() + .indices() + .prepareResizeIndex("mounted-index", "shrunk-index") + .setResizeType(ResizeType.SHRINK) + .setSettings(indexSettingsNoReplicas(1).build()) + .get() + ); + assertThat(exception.getMessage(), equalTo("can't shrink searchable snapshot index [mounted-index]")); + } + + public void testSplitSearchableSnapshotIndex() { + final IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> client().admin() + .indices() + .prepareResizeIndex("mounted-index", "split-index") + .setResizeType(ResizeType.SPLIT) + .setSettings(indexSettingsNoReplicas(4).build()) + .get() + ); + assertThat(exception.getMessage(), equalTo("can't split searchable snapshot index [mounted-index]")); + } + + public void testCloneSearchableSnapshotIndex() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> client().admin().indices().prepareResizeIndex("mounted-index", "cloned-index").setResizeType(ResizeType.CLONE).get() + ); + assertThat( + exception.getMessage(), + equalTo("can't clone searchable snapshot index [mounted-index]; setting [index.store.type] should be overridden") + ); + + exception = expectThrows( + IllegalArgumentException.class, + () -> client().admin() + .indices() + .prepareResizeIndex("mounted-index", "cloned-index") + .setResizeType(ResizeType.CLONE) + .setSettings(Settings.builder().putNull(IndexModule.INDEX_STORE_TYPE_SETTING.getKey()).build()) + .get() + ); + assertThat( + exception.getMessage(), + equalTo("can't clone searchable snapshot index [mounted-index]; setting [index.recovery.type] should be overridden") + ); + + assertAcked( + client().admin() + .indices() + .prepareResizeIndex("mounted-index", "cloned-index") + .setResizeType(ResizeType.CLONE) + .setSettings( + Settings.builder() + .putNull(IndexModule.INDEX_STORE_TYPE_SETTING.getKey()) + .putNull(IndexModule.INDEX_RECOVERY_TYPE_SETTING.getKey()) + .put(DataTierAllocationDecider.INDEX_ROUTING_PREFER, DataTier.DATA_HOT) + .put(INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0) + .build() + ) + ); + ensureGreen("cloned-index"); + assertAcked(client().admin().indices().prepareDelete("cloned-index")); + } +}