diff --git a/docs/reference/data-streams/promote-data-stream-api.asciidoc b/docs/reference/data-streams/promote-data-stream-api.asciidoc index 111c7a2256f8a..5ba9c4d9fad0e 100644 --- a/docs/reference/data-streams/promote-data-stream-api.asciidoc +++ b/docs/reference/data-streams/promote-data-stream-api.asciidoc @@ -18,6 +18,10 @@ available, the data stream in the local cluster can be promoted to a regular data stream, which allows these data streams to be rolled over in the local cluster. +NOTE: When promoting a data stream, ensure the local cluster has a data stream enabled index template that matches the data stream. +If this is missing, the data stream will not be able to roll over until a matching index template is created. +This will affect the lifecycle management of the data stream and interfere with the data stream size and retention. + [source,console] ---- POST /_data_stream/_promote/my-data-stream diff --git a/docs/reference/snapshot-restore/index.asciidoc b/docs/reference/snapshot-restore/index.asciidoc index 390f6664391bd..33645034c30a1 100644 --- a/docs/reference/snapshot-restore/index.asciidoc +++ b/docs/reference/snapshot-restore/index.asciidoc @@ -48,6 +48,9 @@ Snapshots don't contain or back up: * Node configuration files * <> +NOTE: When restoring a data stream, if the target cluster does not have an index template that matches the data stream, the data stream will not be able to roll over until a matching index template is created. +This will affect the lifecycle management of the data stream and interfere with the data stream size and retention. + [discrete] [[feature-state]] === Feature states diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java index 36fb02dcff0d8..638e4d813a79a 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/DataStreamsSnapshotsIT.java @@ -15,6 +15,7 @@ import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequestBuilder; import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse; import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; @@ -45,6 +46,7 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.snapshots.AbstractSnapshotIntegTestCase; import org.elasticsearch.snapshots.RestoreInfo; +import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotInProgressException; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.snapshots.SnapshotRestoreException; @@ -62,7 +64,9 @@ import java.util.stream.Collectors; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoWarningHeaderOnResponse; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertResponse; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertWarningHeaderOnResponse; import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -80,6 +84,8 @@ public class DataStreamsSnapshotsIT extends AbstractSnapshotIntegTestCase { private static final Map DOCUMENT_SOURCE = Collections.singletonMap("@timestamp", 123); public static final String REPO = "repo"; public static final String SNAPSHOT = "snap"; + public static final String TEMPLATE_1_ID = "t1"; + public static final String TEMPLATE_2_ID = "t2"; private Client client; private String dsBackingIndexName; @@ -103,8 +109,8 @@ public void setup() throws Exception { Path location = randomRepoPath(); createRepository(REPO, "fs", location); - DataStreamIT.putComposableIndexTemplate("t1", List.of("ds", "other-ds")); - DataStreamIT.putComposableIndexTemplate("t2", """ + DataStreamIT.putComposableIndexTemplate(TEMPLATE_1_ID, List.of("ds", "other-ds")); + DataStreamIT.putComposableIndexTemplate(TEMPLATE_2_ID, """ { "properties": { "@timestamp": { @@ -1335,4 +1341,149 @@ public void testRestoreDataStreamAliasWithConflictingIndicesAlias() throws Excep ); assertThat(e.getMessage(), containsString("data stream alias and indices alias have the same name (my-alias)")); } + + public void testWarningHeaderOnRestoreWithoutTemplates() throws Exception { + String datastreamName = "ds"; + + CreateSnapshotResponse createSnapshotResponse = client.admin() + .cluster() + .prepareCreateSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setIndices(datastreamName) + .setIncludeGlobalState(false) + .get(); + + RestStatus status = createSnapshotResponse.getSnapshotInfo().status(); + SnapshotId snapshotId = createSnapshotResponse.getSnapshotInfo().snapshotId(); + assertEquals(RestStatus.OK, status); + + assertEquals(Collections.singletonList(dsBackingIndexName), getSnapshot(REPO, SNAPSHOT).indices()); + + assertAcked( + client.execute( + DeleteDataStreamAction.INSTANCE, + new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, datastreamName, "other-ds") + ) + ); + + assertAcked( + client.execute( + TransportDeleteComposableIndexTemplateAction.TYPE, + new TransportDeleteComposableIndexTemplateAction.Request(TEMPLATE_1_ID) + ).get() + ); + + RestoreSnapshotRequestBuilder request = client.admin() + .cluster() + .prepareRestoreSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setIndices(datastreamName); + + assertWarningHeaderOnResponse( + client, + request, + "Snapshot [" + + snapshotId + + "] contains data stream [" + + datastreamName + + "] but custer does not have a matching index " + + "template. This will cause rollover to fail until a matching index template is created" + ); + + } + + public void testWarningHeaderAbsentOnRestoreWithTemplates() throws Exception { + String datastreamName = "ds"; + + CreateSnapshotResponse createSnapshotResponse = client.admin() + .cluster() + .prepareCreateSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setIndices(datastreamName) + .setIncludeGlobalState(false) + .get(); + + RestStatus status = createSnapshotResponse.getSnapshotInfo().status(); + SnapshotId snapshotId = createSnapshotResponse.getSnapshotInfo().snapshotId(); + assertEquals(RestStatus.OK, status); + + assertEquals(Collections.singletonList(dsBackingIndexName), getSnapshot(REPO, SNAPSHOT).indices()); + + assertAcked( + client.execute( + DeleteDataStreamAction.INSTANCE, + new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, datastreamName, "other-ds", "with-fs") + ) + ); + + RestoreSnapshotRequestBuilder request = client.admin() + .cluster() + .prepareRestoreSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setIndices(datastreamName); + + assertNoWarningHeaderOnResponse( + client, + request, + "but custer does not have a matching index template. This will cause rollover to fail until a matching index " + + "template is created" + ); + + } + + /** + * This test is muted as it's awaiting the same fix as {@link #testPartialRestoreSnapshotThatIncludesDataStreamWithGlobalState()} + */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/107515") + public void testWarningHeaderOnRestoreTemplateFromSnapshot() throws Exception { + String datastreamName = "ds"; + + CreateSnapshotResponse createSnapshotResponse = client.admin() + .cluster() + .prepareCreateSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setIndices(datastreamName) + .setIncludeGlobalState(true) + .get(); + + RestStatus status = createSnapshotResponse.getSnapshotInfo().status(); + SnapshotId snapshotId = createSnapshotResponse.getSnapshotInfo().snapshotId(); + assertEquals(RestStatus.OK, status); + + assertEquals(Collections.singletonList(dsBackingIndexName), getSnapshot(REPO, SNAPSHOT).indices()); + + assertAcked( + client.execute( + DeleteDataStreamAction.INSTANCE, + new DeleteDataStreamAction.Request(TEST_REQUEST_TIMEOUT, datastreamName, "other-ds") + ) + ); + + assertAcked( + client.execute( + TransportDeleteComposableIndexTemplateAction.TYPE, + new TransportDeleteComposableIndexTemplateAction.Request(TEMPLATE_1_ID) + ).get() + ); + + RestoreSnapshotRequestBuilder request = client.admin() + .cluster() + .prepareRestoreSnapshot(TEST_REQUEST_TIMEOUT, REPO, SNAPSHOT) + .setWaitForCompletion(true) + .setRestoreGlobalState(true) + .setIndices(datastreamName); + + assertNoWarningHeaderOnResponse( + client, + request, + "Snapshot [" + + snapshotId + + "] contains data stream [" + + datastreamName + + "] but custer does not have a matching index " + + "template. This will cause rollover to fail until a matching index template is created" + ); + + } + } diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/PromoteDataStreamTransportAction.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/PromoteDataStreamTransportAction.java index b9f5bdea9c90e..edc17433ab746 100644 --- a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/PromoteDataStreamTransportAction.java +++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/action/PromoteDataStreamTransportAction.java @@ -8,6 +8,8 @@ */ package org.elasticsearch.datastreams.action; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.datastreams.PromoteDataStreamAction; @@ -23,6 +25,8 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Priority; +import org.elasticsearch.common.logging.HeaderWarning; +import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.indices.SystemIndices; @@ -31,8 +35,12 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import static org.elasticsearch.core.Strings.format; + public class PromoteDataStreamTransportAction extends AcknowledgedTransportMasterNodeAction { + private static final Logger logger = LogManager.getLogger(PromoteDataStreamTransportAction.class); + private final SystemIndices systemIndices; @Inject @@ -94,16 +102,41 @@ private void submitUnbatchedTask(@SuppressWarnings("SameParameterValue") String static ClusterState promoteDataStream(ClusterState currentState, PromoteDataStreamAction.Request request) { DataStream dataStream = currentState.getMetadata().dataStreams().get(request.getName()); + if (dataStream == null) { throw new ResourceNotFoundException("data stream [" + request.getName() + "] does not exist"); } + warnIfTemplateMissingForDatastream(dataStream, currentState); + DataStream promotedDataStream = dataStream.promoteDataStream(); Metadata.Builder metadata = Metadata.builder(currentState.metadata()); metadata.put(promotedDataStream); return ClusterState.builder(currentState).metadata(metadata).build(); } + private static void warnIfTemplateMissingForDatastream(DataStream dataStream, ClusterState currentState) { + var datastreamName = dataStream.getName(); + + var matchingIndex = currentState.metadata() + .templatesV2() + .values() + .stream() + .filter(cit -> cit.getDataStreamTemplate() != null) + .flatMap(cit -> cit.indexPatterns().stream()) + .anyMatch(pattern -> Regex.simpleMatch(pattern, datastreamName)); + + if (matchingIndex == false) { + String warningMessage = format( + "Data stream [%s] does not have a matching index template. This will cause rollover to fail until a matching index " + + "template is created", + datastreamName + ); + logger.warn(() -> warningMessage); + HeaderWarning.addWarning(warningMessage); + } + } + @Override protected ClusterBlockException checkBlock(PromoteDataStreamAction.Request request, ClusterState state) { return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE); diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java index e901dc28ea541..cf023b0e629c6 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java @@ -26,6 +26,7 @@ import org.elasticsearch.cluster.SnapshotDeletionsInProgress; import org.elasticsearch.cluster.block.ClusterBlocks; import org.elasticsearch.cluster.metadata.AliasMetadata; +import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.DataStreamAlias; import org.elasticsearch.cluster.metadata.IndexMetadata; @@ -52,6 +53,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.ClusterSettings; @@ -398,6 +400,8 @@ private void startRestore( Map dataStreamsToRestore = result.v1(); Map dataStreamAliasesToRestore = result.v2(); + validateDataStreamTemplatesExistAndWarnIfMissing(dataStreamsToRestore, snapshotInfo, globalMetadata); + // Remove the data streams from the list of requested indices requestIndices.removeAll(dataStreamsToRestore.keySet()); @@ -510,6 +514,35 @@ private void startRestore( ); } + private void validateDataStreamTemplatesExistAndWarnIfMissing( + Map dataStreamsToRestore, + SnapshotInfo snapshotInfo, + Metadata globalMetadata + ) { + + Stream streams = Stream.concat( + clusterService.state().metadata().templatesV2().values().stream(), + globalMetadata == null ? Stream.empty() : globalMetadata.templatesV2().values().stream() + ); + + Set templatePatterns = streams.filter(cit -> cit.getDataStreamTemplate() != null) + .flatMap(cit -> cit.indexPatterns().stream()) + .collect(Collectors.toSet()); + + for (String name : dataStreamsToRestore.keySet()) { + if (templatePatterns.stream().noneMatch(pattern -> Regex.simpleMatch(pattern, name))) { + String warningMessage = format( + "Snapshot [%s] contains data stream [%s] but custer does not have a matching index template. This will cause" + + " rollover to fail until a matching index template is created", + snapshotInfo.snapshotId(), + name + ); + logger.warn(() -> warningMessage); + HeaderWarning.addWarning(warningMessage); + } + } + } + @SuppressForbidden(reason = "legacy usage of unbatched task") // TODO add support for batching here private void submitUnbatchedTask(@SuppressWarnings("SameParameterValue") String source, ClusterStateUpdateTask task) { clusterService.submitUnbatchedStateUpdateTask(source, task); diff --git a/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java b/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java index ad61a63f7c46e..552e301650d9d 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java +++ b/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java @@ -14,7 +14,9 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.RequestBuilder; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequestBuilder; @@ -873,6 +875,73 @@ public static void awaitLatch(CountDownLatch latch, long timeout, TimeUnit unit) assertThat(message, isCountedDown, is(true)); } + /** + * Check the response of a client request to ensure it has a warning header that contains the provided string + * + * Currently, this allows a fixed 10 seconds for response to be received + * @param client Client making the request - Required to access the response headers + * @param requestBuilder Request to be made + * @param toMatch String to match in the warning headers + * @param Type of the response + * @throws InterruptedException If the request times out + */ + public static void assertWarningHeaderOnResponse( + Client client, + ActionRequestBuilder requestBuilder, + String toMatch + ) throws InterruptedException { + assertWarningHeaderMatchOnResponse(client, requestBuilder, hasItem(containsString(toMatch))); + } + + /** + * Check the response of a client request to ensure it does not have a warning header that contains the provided string + * + * Currently, this allows a fixed 10 seconds for response to be received + * @param client Client making the request - Required to access the response headers + * @param requestBuilder Request to be made + * @param toMatch String to not match in the warning headers + * @param Type of the response + * @throws InterruptedException If the request times out + */ + public static void assertNoWarningHeaderOnResponse( + Client client, + ActionRequestBuilder requestBuilder, + String toMatch + ) throws InterruptedException { + assertWarningHeaderMatchOnResponse(client, requestBuilder, not(hasItem(containsString(toMatch)))); + } + + private static void assertWarningHeaderMatchOnResponse( + Client client, + ActionRequestBuilder requestBuilder, + Matcher> matcher + ) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + requestBuilder.execute(new ActionListener<>() { + @Override + public void onResponse(T response) { + try { + final var warningHeaders = client.threadPool().getThreadContext().getResponseHeaders().get("Warning"); + assertThat(warningHeaders, matcher); + } finally { + latch.countDown(); + } + } + + @Override + public void onFailure(Exception e) { + try { + throw new AssertionError("Failed to execute request", e); + } finally { + latch.countDown(); + } + } + }); + if (latch.await(10, TimeUnit.SECONDS) == false) { + fail("Did not receive request response before timeout"); + } + } + /** * Compares two maps recursively, using arrays comparisons for byte[] through Arrays.equals(byte[], byte[]) */ diff --git a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowIT.java b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowIT.java index 6f66e7e386066..9303191fcf75f 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowIT.java @@ -13,6 +13,7 @@ import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.WarningFailureException; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; @@ -1120,6 +1121,117 @@ public void testAutoFollowSearchableSnapshotsFails() throws Exception { } } + public void testNoWarningOnPromoteDatastreamWhenTemplateExistsOnFollower() throws Exception { + if ("follow".equals(targetCluster) == false) { + return; + } + testDataStreamPromotionWarnings(true); + } + + public void testWarningOnPromoteDatastreamWhenTemplateDoesNotExistsOnFollower() { + if ("follow".equals(targetCluster) == false) { + return; + } + WarningFailureException exception = assertThrows(WarningFailureException.class, () -> testDataStreamPromotionWarnings(false)); + assertThat( + exception.getMessage(), + containsString( + "does not have a matching index template. This will cause rollover to fail until a matching index template is created]" + ) + ); + } + + private void testDataStreamPromotionWarnings(Boolean createFollowerTemplate) throws Exception { + final int numDocs = 64; + final String dataStreamName = getTestName().toLowerCase(Locale.ROOT) + "-dopromo"; + final String autoFollowPatternName = getTestName().toLowerCase(Locale.ROOT); + + int initialNumberOfSuccessfulFollowedIndices = getNumberOfSuccessfulFollowedIndices(); + List backingIndexNames = null; + try { + // Create index template + Request putComposableIndexTemplateRequest = new Request("POST", "/_index_template/" + getTestName().toLowerCase(Locale.ROOT)); + putComposableIndexTemplateRequest.setJsonEntity("{\"index_patterns\":[\"" + dataStreamName + "*\"],\"data_stream\":{}}"); + + if (createFollowerTemplate) { + assertOK(client().performRequest(putComposableIndexTemplateRequest)); + } + + // Create auto follow pattern + createAutoFollowPattern(client(), autoFollowPatternName, dataStreamName + "*", "leader_cluster", null); + + // Create data stream and ensure that it is auto followed + try (var leaderClient = buildLeaderClient()) { + assertOK(leaderClient.performRequest(putComposableIndexTemplateRequest)); + + for (int i = 0; i < numDocs; i++) { + var indexRequest = new Request("POST", "/" + dataStreamName + "/_doc"); + indexRequest.addParameter("refresh", "true"); + indexRequest.setJsonEntity("{\"@timestamp\": \"" + DATE_FORMAT.format(new Date()) + "\",\"message\":\"abc\"}"); + assertOK(leaderClient.performRequest(indexRequest)); + } + verifyDataStream(leaderClient, dataStreamName, backingIndexName(dataStreamName, 1)); + verifyDocuments(leaderClient, dataStreamName, numDocs); + } + assertBusy(() -> { + assertThat(getNumberOfSuccessfulFollowedIndices(), equalTo(initialNumberOfSuccessfulFollowedIndices + 1)); + verifyDataStream(client(), dataStreamName, backingIndexName(dataStreamName, 1)); + ensureYellow(dataStreamName); + verifyDocuments(client(), dataStreamName, numDocs); + }); + + // Rollover in leader cluster and ensure second backing index is replicated: + try (var leaderClient = buildLeaderClient()) { + var rolloverRequest = new Request("POST", "/" + dataStreamName + "/_rollover"); + assertOK(leaderClient.performRequest(rolloverRequest)); + verifyDataStream(leaderClient, dataStreamName, backingIndexName(dataStreamName, 1), backingIndexName(dataStreamName, 2)); + + var indexRequest = new Request("POST", "/" + dataStreamName + "/_doc"); + indexRequest.addParameter("refresh", "true"); + indexRequest.setJsonEntity("{\"@timestamp\": \"" + DATE_FORMAT.format(new Date()) + "\",\"message\":\"abc\"}"); + assertOK(leaderClient.performRequest(indexRequest)); + verifyDocuments(leaderClient, dataStreamName, numDocs + 1); + } + assertBusy(() -> { + assertThat(getNumberOfSuccessfulFollowedIndices(), equalTo(initialNumberOfSuccessfulFollowedIndices + 2)); + verifyDataStream(client(), dataStreamName, backingIndexName(dataStreamName, 1), backingIndexName(dataStreamName, 2)); + ensureYellow(dataStreamName); + verifyDocuments(client(), dataStreamName, numDocs + 1); + }); + + backingIndexNames = verifyDataStream( + client(), + dataStreamName, + backingIndexName(dataStreamName, 1), + backingIndexName(dataStreamName, 2) + ); + + // Promote local data stream + var promoteRequest = new Request("POST", "/_data_stream/_promote/" + dataStreamName); + Response response = client().performRequest(promoteRequest); + assertOK(response); + } finally { + if (backingIndexNames == null) { + // we failed to compute the actual backing index names in the test because we failed earlier on, guessing them on a + // best-effort basis + backingIndexNames = List.of(backingIndexName(dataStreamName, 1), backingIndexName(dataStreamName, 2)); + } + + // These cleanup methods are copied from the finally block of other Data Stream tests in this class however + // they may no longer be required but have been included for completeness + cleanUpFollower(backingIndexNames, List.of(dataStreamName), List.of(autoFollowPatternName)); + cleanUpLeader(backingIndexNames.subList(0, 1), List.of(dataStreamName), List.of()); + Request deleteTemplateRequest = new Request("DELETE", "/_index_template/" + getTestName().toLowerCase(Locale.ROOT)); + if (createFollowerTemplate) { + assertOK(client().performRequest(deleteTemplateRequest)); + } + try (var leaderClient = buildLeaderClient()) { + assertOK(leaderClient.performRequest(deleteTemplateRequest)); + } + } + + } + private int getNumberOfSuccessfulFollowedIndices() throws IOException { return getNumberOfSuccessfulFollowedIndices(client()); }