diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java
index 730399481f72b..a96b39357ac1d 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java
@@ -28,6 +28,8 @@
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesRequest;
+import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesResponse;
import java.io.IOException;
@@ -378,4 +380,47 @@ public Cancellable deleteAsync(DeleteSnapshotRequest deleteSnapshotRequest, Requ
SnapshotRequestConverters::deleteSnapshot, options,
AcknowledgedResponse::fromXContent, listener, emptySet());
}
+
+ /**
+ * Get a list of features which can be included in a snapshot as feature states.
+ * See Get Snapshottable
+ * Features API on elastic.co
+ *
+ * @param getFeaturesRequest the request
+ * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+ * @return the response
+ * @throws IOException in case there is a problem sending the request or parsing back the response
+ */
+ public GetSnapshottableFeaturesResponse getFeatures(GetSnapshottableFeaturesRequest getFeaturesRequest, RequestOptions options)
+ throws IOException {
+ return restHighLevelClient.performRequestAndParseEntity(
+ getFeaturesRequest,
+ SnapshotRequestConverters::getSnapshottableFeatures,
+ options,
+ GetSnapshottableFeaturesResponse::parse,
+ emptySet()
+ );
+ }
+
+ /**
+ * Asynchronously get a list of features which can be included in a snapshot as feature states.
+ * See Get Snapshottable
+ * Features API on elastic.co
+ *
+ * @param getFeaturesRequest the request
+ * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+ * @param listener the listener to be notified upon request completion
+ * @return cancellable that may be used to cancel the request
+ */
+ public Cancellable getFeaturesAsync(GetSnapshottableFeaturesRequest getFeaturesRequest, RequestOptions options,
+ ActionListener listener) {
+ return restHighLevelClient.performRequestAsyncAndParseEntity(
+ getFeaturesRequest,
+ SnapshotRequestConverters::getSnapshottableFeatures,
+ options,
+ GetSnapshottableFeaturesResponse::parse,
+ listener,
+ emptySet()
+ );
+ }
}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java
index 31383d0c351bc..21dc404036886 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java
@@ -23,6 +23,7 @@
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
+import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesRequest;
import org.elasticsearch.common.Strings;
import java.io.IOException;
@@ -190,4 +191,13 @@ static Request deleteSnapshot(DeleteSnapshotRequest deleteSnapshotRequest) {
request.addParameters(parameters.asMap());
return request;
}
+
+ static Request getSnapshottableFeatures(GetSnapshottableFeaturesRequest getSnapshottableFeaturesRequest) {
+ String endpoint = "/_snapshottable_features";
+ Request request = new Request(HttpGet.METHOD_NAME, endpoint);
+ RequestConverters.Params parameters = new RequestConverters.Params();
+ parameters.withMasterTimeout(getSnapshottableFeaturesRequest.masterNodeTimeout());
+ request.addParameters(parameters.asMap());
+ return request;
+ }
}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesRequest.java
new file mode 100644
index 0000000000000..458c3f5720440
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesRequest.java
@@ -0,0 +1,17 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.client.snapshots;
+
+import org.elasticsearch.client.TimedRequest;
+
+/**
+ * A {@link TimedRequest} to get the list of features available to be included in snapshots in the cluster.
+ */
+public class GetSnapshottableFeaturesRequest extends TimedRequest {
+}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesResponse.java
new file mode 100644
index 0000000000000..049eba6b051b8
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesResponse.java
@@ -0,0 +1,108 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.client.snapshots;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.util.List;
+import java.util.Objects;
+
+public class GetSnapshottableFeaturesResponse {
+
+ private final List features;
+
+ private static final ParseField FEATURES = new ParseField("features");
+
+ @SuppressWarnings("unchecked")
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "snapshottable_features_response", true, (a, ctx) -> new GetSnapshottableFeaturesResponse((List) a[0])
+ );
+
+ static {
+ PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), SnapshottableFeature::parse, FEATURES);
+ }
+
+ public GetSnapshottableFeaturesResponse(List features) {
+ this.features = features;
+ }
+
+ public List getFeatures() {
+ return features;
+ }
+
+ public static GetSnapshottableFeaturesResponse parse(XContentParser parser) {
+ return PARSER.apply(parser, null);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if ((o instanceof GetSnapshottableFeaturesResponse) == false) return false;
+ GetSnapshottableFeaturesResponse that = (GetSnapshottableFeaturesResponse) o;
+ return getFeatures().equals(that.getFeatures());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getFeatures());
+ }
+
+ public static class SnapshottableFeature {
+
+ private final String featureName;
+ private final String description;
+
+ private static final ParseField FEATURE_NAME = new ParseField("name");
+ private static final ParseField DESCRIPTION = new ParseField("description");
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "feature", true, (a, ctx) -> new SnapshottableFeature((String) a[0], (String) a[1])
+ );
+
+ static {
+ PARSER.declareField(ConstructingObjectParser.constructorArg(),
+ (p, c) -> p.text(), FEATURE_NAME, ObjectParser.ValueType.STRING);
+ PARSER.declareField(ConstructingObjectParser.constructorArg(),
+ (p, c) -> p.text(), DESCRIPTION, ObjectParser.ValueType.STRING);
+ }
+
+ public SnapshottableFeature(String featureName, String description) {
+ this.featureName = featureName;
+ this.description = description;
+ }
+
+ public static SnapshottableFeature parse(XContentParser parser, Void ctx) {
+ return PARSER.apply(parser, ctx);
+ }
+
+ public String getFeatureName() {
+ return featureName;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if ((o instanceof SnapshottableFeature) == false) return false;
+ SnapshottableFeature feature = (SnapshottableFeature) o;
+ return Objects.equals(getFeatureName(), feature.getFeatureName());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getFeatureName());
+ }
+ }
+}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java
index fe093c81c4faf..aa7879b43d181 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java
@@ -28,6 +28,8 @@
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest;
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesRequest;
+import org.elasticsearch.client.snapshots.GetSnapshottableFeaturesResponse;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
@@ -39,12 +41,16 @@
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import static org.elasticsearch.snapshots.SnapshotsService.NO_FEATURE_STATES_VALUE;
+import static org.elasticsearch.tasks.TaskResultsService.TASKS_FEATURE_NAME;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
public class SnapshotIT extends ESRestHighLevelClientTestCase {
@@ -150,6 +156,14 @@ public void testCreateSnapshot() throws Exception {
}
request.partial(randomBoolean());
request.includeGlobalState(randomBoolean());
+ final List featureStates = randomFrom(
+ List.of(
+ Collections.emptyList(),
+ Collections.singletonList(TASKS_FEATURE_NAME),
+ Collections.singletonList(NO_FEATURE_STATES_VALUE)
+ )
+ );
+ request.featureStates(featureStates);
CreateSnapshotResponse response = createTestSnapshot(request);
assertEquals(waitForCompletion ? RestStatus.OK : RestStatus.ACCEPTED, response.status());
@@ -262,9 +276,14 @@ public void testRestoreSnapshot() throws IOException {
assertFalse("index [" + testIndex + "] should have been deleted", indexExists(testIndex));
RestoreSnapshotRequest request = new RestoreSnapshotRequest(testRepository, testSnapshot);
+ request.indices(testIndex);
request.waitForCompletion(true);
request.renamePattern(testIndex);
request.renameReplacement(restoredIndex);
+ if (randomBoolean()) {
+ request.includeGlobalState(true);
+ request.featureStates(Collections.singletonList(NO_FEATURE_STATES_VALUE));
+ }
RestoreSnapshotResponse response = execute(request, highLevelClient().snapshot()::restore,
highLevelClient().snapshot()::restoreAsync);
@@ -364,6 +383,18 @@ public void testCloneSnapshot() throws IOException {
assertTrue(response.isAcknowledged());
}
+ public void testGetFeatures() throws IOException {
+ GetSnapshottableFeaturesRequest request = new GetSnapshottableFeaturesRequest();
+
+ GetSnapshottableFeaturesResponse response = execute(request,
+ highLevelClient().snapshot()::getFeatures, highLevelClient().snapshot()::getFeaturesAsync);
+
+ assertThat(response, notNullValue());
+ assertThat(response.getFeatures(), notNullValue());
+ assertThat(response.getFeatures().size(), greaterThan(1));
+ assertTrue(response.getFeatures().stream().anyMatch(feature -> "tasks".equals(feature.getFeatureName())));
+ }
+
private static Map randomUserMetadata() {
if (randomBoolean()) {
return null;
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesResponseTests.java
new file mode 100644
index 0000000000000..0b899af725c7b
--- /dev/null
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/GetSnapshottableFeaturesResponseTests.java
@@ -0,0 +1,67 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.client.snapshots;
+
+import org.elasticsearch.client.AbstractResponseTestCase;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.Matchers.everyItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.in;
+import static org.hamcrest.Matchers.is;
+
+public class GetSnapshottableFeaturesResponseTests extends AbstractResponseTestCase<
+ org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse,
+ GetSnapshottableFeaturesResponse> {
+
+ @Override
+ protected org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse createServerTestInstance(
+ XContentType xContentType
+ ) {
+ return new org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse(
+ randomList(
+ 10,
+ () -> new org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse.SnapshottableFeature(
+ randomAlphaOfLengthBetween(4, 10),
+ randomAlphaOfLengthBetween(5, 10)
+ )
+ )
+ );
+ }
+
+ @Override
+ protected GetSnapshottableFeaturesResponse doParseToClientInstance(XContentParser parser) throws IOException {
+ return GetSnapshottableFeaturesResponse.parse(parser);
+ }
+
+ @Override
+ protected void assertInstances(
+ org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesResponse serverTestInstance,
+ GetSnapshottableFeaturesResponse clientInstance
+ ) {
+ assertNotNull(serverTestInstance.getSnapshottableFeatures());
+ assertNotNull(serverTestInstance.getSnapshottableFeatures());
+
+ assertThat(clientInstance.getFeatures(), hasSize(serverTestInstance.getSnapshottableFeatures().size()));
+
+ Map clientFeatures = clientInstance.getFeatures()
+ .stream()
+ .collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getDescription()));
+ Map serverFeatures = serverTestInstance.getSnapshottableFeatures()
+ .stream()
+ .collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getDescription()));
+
+ assertThat(clientFeatures.entrySet(), everyItem(is(in(serverFeatures.entrySet()))));
+ }
+}
diff --git a/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc b/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc
index a0a80cefd35d6..bcad8e399211b 100644
--- a/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc
+++ b/docs/reference/snapshot-restore/apis/create-snapshot-api.asciidoc
@@ -99,20 +99,35 @@ argument is provided, the snapshot only includes the specified data streams and
+
--
(Optional, Boolean)
-If `true`, the current cluster state is included in the snapshot.
+If `true`, the current global state is included in the snapshot.
Defaults to `true`.
-The cluster state includes:
+The global state includes:
* Persistent cluster settings
* Index templates
* Legacy index templates
* Ingest pipelines
* {ilm-init} lifecycle policies
+* Data stored in system indices, such as Watches and task records (configurable via `feature_states`)
--
+
IMPORTANT: By default, the entire snapshot will fail if one or more indices included in the snapshot do not have all primary shards available. You can change this behavior by setting <> to `true`.
+[[create-snapshot-api-feature-states]]
+`feature_states`::
+(Optional, array of strings)
+A list of feature states to be included in this snapshot. A list of features
+available for inclusion in the snapshot and their descriptions be can be
+retrieved using the <>.
+Each feature state includes one or more system indices containing data necessary
+for the function of that feature. Providing an empty array will include no feature
+states in the snapshot, regardless of the value of `include_global_state`.
++
+By default, all available feature states will be included in the snapshot if
+`include_global_state` is `true`, or no feature states if `include_global_state`
+is `false`.
+
include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=master-timeout]
`metadata`::
@@ -163,6 +178,7 @@ The API returns the following response:
"version": ,
"indices": [],
"data_streams": [],
+ "feature_states": [],
"include_global_state": false,
"metadata": {
"taken_by": "user123",
diff --git a/docs/reference/snapshot-restore/apis/get-snapshot-api.asciidoc b/docs/reference/snapshot-restore/apis/get-snapshot-api.asciidoc
index 241283a29d4e4..35a9a0e8d4611 100644
--- a/docs/reference/snapshot-restore/apis/get-snapshot-api.asciidoc
+++ b/docs/reference/snapshot-restore/apis/get-snapshot-api.asciidoc
@@ -122,6 +122,15 @@ List of <> included in the snapshot.
(Boolean)
Indicates whether the current cluster state is included in the snapshot.
+[[get-snapshot-api-feature-states]]
+`feature_states`::
+(array)
+List of feature states which were included when the snapshot was taken,
+including the list of system indices included as part of the feature state. The
+`feature_name` field of each can be used in the `feature_states` parameter when
+restoring the snapshot to restore a subset of feature states. Only present if
+the snapshot includes one or more feature states.
+
`start_time`::
(string)
Date timestamp of when the snapshot creation process started.
@@ -218,6 +227,7 @@ The API returns the following response:
"version": ,
"indices": [],
"data_streams": [],
+ "feature_states": [],
"include_global_state": true,
"state": "SUCCESS",
"start_time": "2020-07-06T21:55:18.129Z",
diff --git a/docs/reference/snapshot-restore/apis/get-snapshottable-features-api.asciidoc b/docs/reference/snapshot-restore/apis/get-snapshottable-features-api.asciidoc
new file mode 100644
index 0000000000000..6515a03936586
--- /dev/null
+++ b/docs/reference/snapshot-restore/apis/get-snapshottable-features-api.asciidoc
@@ -0,0 +1,56 @@
+[[get-snapshottable-features-api]]
+=== Get Snapshottable Features API
+++++
+Get snapshottable features
+++++
+
+Gets a list of features which can be included in snapshots using the
+<> when creating a
+snapshot.
+
+[source,console]
+-----------------------------------
+GET /_snapshottable_features
+-----------------------------------
+
+[[get-snapshottable-features-api-request]]
+==== {api-request-title}
+
+`GET /_snapshottable_features`
+
+
+[[get-snapshottable-features-api-desc]]
+==== {api-description-title}
+
+You can use the get snapshottable features API to determine which feature states
+to include when <>. By default, all
+feature states are included in a snapshot if that snapshot includes the global
+state, or none if it does not.
+
+A feature state includes one or more system indices necessary for a given
+feature to function. In order to ensure data integrity, all system indices that
+comprise a feature state are snapshotted and restored together.
+
+The features listed by this API are a combination of built-in features and
+features defined by plugins. In order for a feature's state to be listed in this
+API and recognized as a valid feature state by the create snapshot API, the
+plugin which defines that feature must be installed on the master node.
+
+==== {api-examples-title}
+
+[source,console-result]
+----
+{
+ "features": [
+ {
+ "name": "tasks",
+ "description": "Manages task results"
+ },
+ {
+ "name": "kibana",
+ "description": "Manages Kibana configuration and reports"
+ }
+ ]
+}
+----
+// TESTRESPONSE[skip:response differs between default distro and OSS]
diff --git a/docs/reference/snapshot-restore/apis/restore-snapshot-api.asciidoc b/docs/reference/snapshot-restore/apis/restore-snapshot-api.asciidoc
index 0f348148f07ee..af3be7cea1834 100644
--- a/docs/reference/snapshot-restore/apis/restore-snapshot-api.asciidoc
+++ b/docs/reference/snapshot-restore/apis/restore-snapshot-api.asciidoc
@@ -129,21 +129,33 @@ indices.
+
--
(Optional, Boolean)
-If `false`, the cluster state is not restored. Defaults to `false`.
+If `false`, the global state is not restored. Defaults to `false`.
-If `true`, the current cluster state is included in the restore operation.
+If `true`, the current global state is included in the restore operation.
-The cluster state includes:
+The global state includes:
* Persistent cluster settings
* Index templates
* Legacy index templates
* Ingest pipelines
* {ilm-init} lifecycle policies
+* For snapshots taken after 7.12.0, data stored in system indices, such as Watches and task records, replacing any existing configuration (configurable via `feature_states`)
--
+
IMPORTANT: By default, the entire restore operation will fail if one or more indices included in the snapshot do not have all primary shards available. You can change this behavior by setting <> to `true`.
+[[restore-snapshot-api-feature-states]]
+`feature_states`::
+(Optional, array of strings)
+A comma-separated list of feature states you wish to restore. Each feature state contains one or more system indices. The list of feature states
+available in a given snapshot are returned by the <>. Note that feature
+states restored this way will completely replace any existing configuration, rather than returning an error if the system index already exists.
+Providing an empty array will restore no feature states, regardless of the value of `include_global_state`.
++
+By default, all available feature states will be restored if `include_global_state` is `true`, and no feature states will be restored if
+`include_global_state` is `false`.
+
[[restore-snapshot-api-index-settings]]
`index_settings`::
(Optional, string)
diff --git a/docs/reference/snapshot-restore/apis/snapshot-restore-apis.asciidoc b/docs/reference/snapshot-restore/apis/snapshot-restore-apis.asciidoc
index cf70d3bcb2eab..2691f56fc786d 100644
--- a/docs/reference/snapshot-restore/apis/snapshot-restore-apis.asciidoc
+++ b/docs/reference/snapshot-restore/apis/snapshot-restore-apis.asciidoc
@@ -36,3 +36,4 @@ include::get-snapshot-api.asciidoc[]
include::get-snapshot-status-api.asciidoc[]
include::restore-snapshot-api.asciidoc[]
include::delete-snapshot-api.asciidoc[]
+include::get-snapshottable-features-api.asciidoc[]
diff --git a/docs/reference/snapshot-restore/restore-snapshot.asciidoc b/docs/reference/snapshot-restore/restore-snapshot.asciidoc
index 907856f53050a..f889fe5053f8b 100644
--- a/docs/reference/snapshot-restore/restore-snapshot.asciidoc
+++ b/docs/reference/snapshot-restore/restore-snapshot.asciidoc
@@ -32,6 +32,9 @@ By default, all data streams and indices in the snapshot are restored, but the c
supports <>. To include the global cluster state, set
`include_global_state` to `true` in the restore request body.
+Because all indices in the snapshot are restored by default, all system indices will be restored
+by default as well.
+
[WARNING]
====
Each data stream requires a matching
@@ -88,7 +91,7 @@ POST /_snapshot/my_backup/snapshot_1/_restore
// TEST[continued]
<1> By default, `include_global_state` is `false`, meaning the snapshot's
-cluster state is not restored.
+cluster state and feature states are not restored.
+
If `true`, the snapshot's persistent settings, index templates, ingest
pipelines, and {ilm-init} policies are restored into the current cluster. This
diff --git a/docs/reference/snapshot-restore/take-snapshot.asciidoc b/docs/reference/snapshot-restore/take-snapshot.asciidoc
index ddc2812dbe280..5723ffde7ec9f 100644
--- a/docs/reference/snapshot-restore/take-snapshot.asciidoc
+++ b/docs/reference/snapshot-restore/take-snapshot.asciidoc
@@ -77,8 +77,10 @@ The snapshot process starts immediately for the primary shards that have been st
relocation or initialization of shards to complete before snapshotting them.
Besides creating a copy of each data stream and index, the snapshot process can also store global cluster metadata, which includes persistent
-cluster settings and templates. The transient settings and registered snapshot repositories are not stored as part of
-the snapshot.
+cluster settings, templates, and data stored in system indices, such as Watches and task records, regardless of whether those system
+indices are named in the `indices` section of the request. The <> can be used to
+select a subset of system indices to be included in the snapshot. The transient settings and registered snapshot repositories are not stored
+as part of the snapshot.
While a snapshot of a particular shard is being
created, this shard cannot be moved to another node, which can interfere with rebalancing and allocation
@@ -101,7 +103,7 @@ the snapshot.
IMPORTANT: The global cluster state includes the cluster's index
templates, such as those <>. If your snapshot includes data streams, we recommend storing the
-cluster state as part of the snapshot. This lets you later restored any
+global state as part of the snapshot. This lets you later restored any
templates required for a data stream.
By default, the entire snapshot will fail if one or more indices participating in the snapshot do not have
@@ -125,4 +127,4 @@ PUT /_snapshot/my_backup/%3Csnapshot-%7Bnow%2Fd%7D%3E
-----------------------------------
// TEST[continued]
-NOTE: You can also create snapshots that are copies of part of an existing snapshot using the <>.
\ No newline at end of file
+NOTE: You can also create snapshots that are copies of part of an existing snapshot using the <>.
diff --git a/modules/kibana/src/main/java/org/elasticsearch/kibana/KibanaPlugin.java b/modules/kibana/src/main/java/org/elasticsearch/kibana/KibanaPlugin.java
index 2afb2b058ec53..5dc532efbf1a1 100644
--- a/modules/kibana/src/main/java/org/elasticsearch/kibana/KibanaPlugin.java
+++ b/modules/kibana/src/main/java/org/elasticsearch/kibana/KibanaPlugin.java
@@ -64,6 +64,16 @@ public Collection getSystemIndexDescriptors(Settings sett
.collect(Collectors.toUnmodifiableList());
}
+ @Override
+ public String getFeatureName() {
+ return "kibana";
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "Manages Kibana configuration and reports";
+ }
+
@Override
public List getRestHandlers(
Settings settings,
diff --git a/qa/smoke-test-http/src/test/java/org/elasticsearch/http/SystemIndexRestIT.java b/qa/smoke-test-http/src/test/java/org/elasticsearch/http/SystemIndexRestIT.java
index 639b8e93c423d..84fd235d71b1d 100644
--- a/qa/smoke-test-http/src/test/java/org/elasticsearch/http/SystemIndexRestIT.java
+++ b/qa/smoke-test-http/src/test/java/org/elasticsearch/http/SystemIndexRestIT.java
@@ -130,6 +130,16 @@ public Collection getSystemIndexDescriptors(Settings sett
return Collections.singletonList(new SystemIndexDescriptor(SYSTEM_INDEX_NAME, "System indices for tests"));
}
+ @Override
+ public String getFeatureName() {
+ return SystemIndexRestIT.class.getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "test plugin";
+ }
+
public static class AddDocRestHandler extends BaseRestHandler {
@Override
public boolean allowSystemIndexAccessByDefault() {
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get_features.json b/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get_features.json
new file mode 100644
index 0000000000000..76b340d329dd8
--- /dev/null
+++ b/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get_features.json
@@ -0,0 +1,29 @@
+{
+ "snapshot.get_features":{
+ "documentation":{
+ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html",
+ "description":"Returns a list of features which can be snapshotted in this cluster."
+ },
+ "stability":"stable",
+ "visibility":"public",
+ "headers":{
+ "accept": [ "application/json"]
+ },
+ "url":{
+ "paths":[
+ {
+ "path":"/_snapshottable_features",
+ "methods":[
+ "GET"
+ ]
+ }
+ ]
+ },
+ "params":{
+ "master_timeout":{
+ "type":"time",
+ "description":"Explicit operation timeout for connection to master node"
+ }
+ }
+ }
+}
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.features/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.features/10_basic.yml
new file mode 100644
index 0000000000000..6d0567a72e312
--- /dev/null
+++ b/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.features/10_basic.yml
@@ -0,0 +1,8 @@
+---
+"Get Features":
+ - skip:
+ features: contains
+ version: " - 7.99.99" # Adjust this after backport
+ reason: "This API was added in 7.12.0"
+ - do: { snapshot.get_features: {}}
+ - contains: {'features': {'name': 'tasks'}}
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java
index d4979a1f1cbf9..47657f6f336f2 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterInfoServiceIT.java
@@ -84,6 +84,16 @@ public List getActionFilters() {
public Collection getSystemIndexDescriptors(Settings settings) {
return List.of(new SystemIndexDescriptor(TEST_SYSTEM_INDEX_NAME, "System index for [" + getTestClass().getName() + ']'));
}
+
+ @Override
+ public String getFeatureName() {
+ return ClusterInfoServiceIT.class.getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "test plugin";
+ }
}
public static class BlockingActionFilter extends org.elasticsearch.action.support.ActionFilter.Simple {
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java
index a3ebff40d4d8f..059d0f7c5ea6c 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java
@@ -709,8 +709,8 @@ public ClusterState.Custom randomCreate(String name) {
SnapshotsInProgressSerializationTests.randomState(ImmutableOpenMap.of()),
Collections.emptyList(),
Collections.emptyList(),
- Math.abs(randomLong()),
- randomIntBetween(0, 1000),
+ Collections.emptyList(),
+ Math.abs(randomLong()), randomIntBetween(0, 1000),
ImmutableOpenMap.of(),
null,
SnapshotInfoTests.randomUserMetadata(),
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexPlugin.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexPlugin.java
index d4be0d3a2e432..dfcd4a90ee174 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexPlugin.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/TestSystemIndexPlugin.java
@@ -23,4 +23,14 @@ public class TestSystemIndexPlugin extends Plugin implements SystemIndexPlugin {
public Collection getSystemIndexDescriptors(Settings settings) {
return List.of(new TestSystemIndexDescriptor());
}
+
+ @Override
+ public String getFeatureName() {
+ return this.getClass().getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return this.getClass().getCanonicalName();
+ }
}
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SystemIndicesSnapshotIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SystemIndicesSnapshotIT.java
new file mode 100644
index 0000000000000..a617b672a57ca
--- /dev/null
+++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SystemIndicesSnapshotIT.java
@@ -0,0 +1,957 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.snapshots;
+
+import org.apache.logging.log4j.LogManager;
+import org.elasticsearch.action.ActionFuture;
+import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
+import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.common.logging.DeprecationLogger;
+import org.elasticsearch.common.logging.Loggers;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.indices.SystemIndexDescriptor;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.plugins.SystemIndexPlugin;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.test.MockLogAppender;
+import org.junit.Before;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.elasticsearch.snapshots.SnapshotsService.NO_FEATURE_STATES_VALUE;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.in;
+import static org.hamcrest.Matchers.lessThan;
+import static org.hamcrest.Matchers.not;
+
+@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0)
+public class SystemIndicesSnapshotIT extends AbstractSnapshotIntegTestCase {
+
+ public static final String REPO_NAME = "test-repo";
+
+ private List dataNodes = null;
+
+ @Override
+ protected Collection> nodePlugins() {
+ List> plugins = new ArrayList<>(super.nodePlugins());
+ plugins.add(SystemIndexTestPlugin.class);
+ plugins.add(AnotherSystemIndexTestPlugin.class);
+ plugins.add(AssociatedIndicesTestPlugin.class);
+ return plugins;
+ }
+
+ @Before
+ public void setup() {
+ internalCluster().startMasterOnlyNodes(2);
+ dataNodes = internalCluster().startDataOnlyNodes(2);
+ }
+
+ /**
+ * Test that if a snapshot includes system indices and we restore global state,
+ * with no reference to feature state, the system indices are restored too.
+ */
+ public void testRestoreSystemIndicesAsGlobalState() {
+ createRepository(REPO_NAME, "fs");
+ // put a document in a system index
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // run a snapshot including global state
+ createFullSnapshot(REPO_NAME, "test-snap");
+
+ // add another document
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+
+ // restore snapshot with global state, without closing the system index
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(true)
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // verify only the original document is restored
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(1L));
+ }
+
+ /**
+ * If we take a snapshot with includeGlobalState set to false, are system indices included?
+ */
+ public void testSnapshotWithoutGlobalState() {
+ createRepository(REPO_NAME, "fs");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "system index doc");
+ indexDoc("not-a-system-index", "1", "purpose", "non system index doc");
+
+ // run a snapshot without global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(false)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // check snapshot info for for which
+ clusterAdmin().prepareGetRepositories(REPO_NAME).get();
+ Set snapshottedIndices = clusterAdmin().prepareGetSnapshots(REPO_NAME).get()
+ .getSnapshots(REPO_NAME).stream()
+ .map(SnapshotInfo::indices)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+
+ assertThat("not-a-system-index", in(snapshottedIndices));
+ // TODO: without global state the system index shouldn't be snapshotted (8.0 & later only)
+ // assertThat(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, not(in(snapshottedIndices)));
+ }
+
+ /**
+ * Test that we can snapshot feature states by name.
+ */
+ public void testSnapshotByFeature() {
+ createRepository(REPO_NAME, "fs");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ indexDoc(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // snapshot by feature
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setIncludeGlobalState(false)
+ .setWaitForCompletion(true)
+ .setFeatureStates(SystemIndexTestPlugin.class.getSimpleName(), AnotherSystemIndexTestPlugin.class.getSimpleName())
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // add some other documents
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ indexDoc(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+ assertThat(getDocCount(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+
+ // restore indices as global state without closing the index
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(true)
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // verify only the original document is restored
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(1L));
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(1L));
+ }
+
+ /**
+ * Take a snapshot with global state but don't restore system indexes. By
+ * default, snapshot restorations ignore global state. This means that,
+ * for now, the system index is treated as part of the snapshot and must be
+ * handled explicitly. Otherwise, as in this test, there will be an
+ * exception.
+ */
+ public void testDefaultRestoreOnlyRegularIndices() {
+ createRepository(REPO_NAME, "fs");
+ final String regularIndex = "test-idx";
+
+ indexDoc(regularIndex, "1", "purpose", "create an index that can be restored");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(regularIndex, SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // Delete the regular index so we can restore it
+ assertAcked(cluster().client().admin().indices().prepareDelete(regularIndex));
+
+ // restore indices by feature, with only the regular index named explicitly
+ SnapshotRestoreException exception = expectThrows(SnapshotRestoreException.class,
+ () -> clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .get());
+
+ assertThat(exception.getMessage(), containsString(
+ "cannot restore index [" +
+ SystemIndexTestPlugin.SYSTEM_INDEX_NAME
+ + "] because an open index with same name already exists"));
+ }
+
+ /**
+ * Take a snapshot with global state but restore features by state.
+ */
+ public void testRestoreByFeature() {
+ createRepository(REPO_NAME, "fs");
+ final String regularIndex = "test-idx";
+
+ indexDoc(regularIndex, "1", "purpose", "create an index that can be restored");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ indexDoc(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(regularIndex, SystemIndexTestPlugin.SYSTEM_INDEX_NAME, AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // add some other documents
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ indexDoc(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+ assertThat(getDocCount(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+
+ // Delete the regular index so we can restore it
+ assertAcked(cluster().client().admin().indices().prepareDelete(regularIndex));
+
+ // restore indices by feature, with only the regular index named explicitly
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIndices(regularIndex)
+ .setFeatureStates("SystemIndexTestPlugin")
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // verify that the restored system index has only one document
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(1L));
+
+ // but the non-requested feature should still have its new document
+ assertThat(getDocCount(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+ }
+
+ /**
+ * Test that if a feature state has associated indices, they are included in the snapshot
+ * when that feature state is selected.
+ */
+ public void testSnapshotAndRestoreAssociatedIndices() {
+ createRepository(REPO_NAME, "fs");
+ final String regularIndex = "regular-idx";
+
+ // put documents into a regular index as well as the system index and associated index of a feature
+ indexDoc(regularIndex, "1", "purpose", "pre-snapshot doc");
+ indexDoc(AssociatedIndicesTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ indexDoc(AssociatedIndicesTestPlugin.ASSOCIATED_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(regularIndex, AssociatedIndicesTestPlugin.SYSTEM_INDEX_NAME, AssociatedIndicesTestPlugin.ASSOCIATED_INDEX_NAME);
+
+ // snapshot
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setFeatureStates(AssociatedIndicesTestPlugin.class.getSimpleName())
+ .setWaitForCompletion(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // verify the correctness of the snapshot
+ Set snapshottedIndices = clusterAdmin().prepareGetSnapshots(REPO_NAME).get()
+ .getSnapshots(REPO_NAME).stream()
+ .map(SnapshotInfo::indices)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+ assertThat(snapshottedIndices, hasItem(AssociatedIndicesTestPlugin.SYSTEM_INDEX_NAME));
+ assertThat(snapshottedIndices, hasItem(AssociatedIndicesTestPlugin.ASSOCIATED_INDEX_NAME));
+
+ // add some other documents
+ indexDoc(regularIndex, "2", "purpose", "post-snapshot doc");
+ indexDoc(AssociatedIndicesTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(regularIndex, AssociatedIndicesTestPlugin.SYSTEM_INDEX_NAME);
+
+ assertThat(getDocCount(regularIndex), equalTo(2L));
+ assertThat(getDocCount(AssociatedIndicesTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+
+ // And delete the associated index so we can restore it
+ assertAcked(client().admin().indices().prepareDelete(AssociatedIndicesTestPlugin.ASSOCIATED_INDEX_NAME).get());
+
+ // restore the feature state and its associated index
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setIndices(AssociatedIndicesTestPlugin.ASSOCIATED_INDEX_NAME)
+ .setWaitForCompletion(true)
+ .setFeatureStates(AssociatedIndicesTestPlugin.class.getSimpleName())
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // verify only the original document is restored
+ assertThat(getDocCount(AssociatedIndicesTestPlugin.SYSTEM_INDEX_NAME), equalTo(1L));
+ assertThat(getDocCount(AssociatedIndicesTestPlugin.ASSOCIATED_INDEX_NAME), equalTo(1L));
+ }
+
+ /**
+ * Check that if we request a feature not in the snapshot, we get an error.
+ */
+ public void testRestoreFeatureNotInSnapshot() {
+ createRepository(REPO_NAME, "fs");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ final String fakeFeatureStateName = "NonExistentTestPlugin";
+ SnapshotRestoreException exception = expectThrows(
+ SnapshotRestoreException.class,
+ () -> clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setFeatureStates("SystemIndexTestPlugin", fakeFeatureStateName)
+ .get());
+
+ assertThat(exception.getMessage(),
+ containsString("requested feature states [[" + fakeFeatureStateName + "]] are not present in snapshot"));
+ }
+
+ /**
+ * Check that directly requesting a system index in a restore request logs a deprecation warning.
+ * @throws IllegalAccessException if something goes wrong with the mock log appender
+ */
+ public void testRestoringSystemIndexByNameIsDeprecated() throws IllegalAccessException {
+ createRepository(REPO_NAME, "fs");
+ // put a document in system index
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // Delete the index so we can restore it without requesting the feature state
+ assertAcked(client().admin().indices().prepareDelete(SystemIndexTestPlugin.SYSTEM_INDEX_NAME).get());
+
+ // Set up a mock log appender to watch for the log message we expect
+ MockLogAppender mockLogAppender = new MockLogAppender();
+ Loggers.addAppender(LogManager.getLogger("org.elasticsearch.deprecation.snapshots.RestoreService"), mockLogAppender);
+ mockLogAppender.start();
+ mockLogAppender.addExpectation(new MockLogAppender.SeenEventExpectation(
+ "restore-system-index-from-snapshot",
+ "org.elasticsearch.deprecation.snapshots.RestoreService",
+ DeprecationLogger.DEPRECATION,
+ "Restoring system indices by name is deprecated. Use feature states instead. System indices: [.test-system-idx]"));
+
+ // restore system index by name, rather than feature state
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIndices(SystemIndexTestPlugin.SYSTEM_INDEX_NAME)
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // Check that the message was logged and remove log appender
+ mockLogAppender.assertAllExpectationsMatched();
+ mockLogAppender.stop();
+ Loggers.removeAppender(LogManager.getLogger("org.elasticsearch.deprecation.snapshots.RestoreService"), mockLogAppender);
+
+ // verify only the original document is restored
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(1L));
+ }
+
+ /**
+ * Check that if a system index matches a rename pattern in a restore request, it's not renamed
+ */
+ public void testSystemIndicesCannotBeRenamed() {
+ createRepository(REPO_NAME, "fs");
+ final String nonSystemIndex = ".test-non-system-index";
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ indexDoc(nonSystemIndex, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ assertAcked(client().admin().indices().prepareDelete(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, nonSystemIndex).get());
+
+ // Restore using a rename pattern that matches both the regular and the system index
+ clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(true)
+ .setRenamePattern(".test-(.+)")
+ .setRenameReplacement(".test-restored-$1")
+ .get();
+
+ // The original system index and the renamed normal index should exist
+ assertTrue("System index not renamed", indexExists(SystemIndexTestPlugin.SYSTEM_INDEX_NAME));
+ assertTrue("Non-system index was renamed", indexExists(".test-restored-non-system-index"));
+
+ // The original normal index should still be deleted, and there shouldn't be a renamed version of the system index
+ assertFalse("Renamed system index doesn't exist", indexExists(".test-restored-system-index"));
+ assertFalse("Original non-system index doesn't exist", indexExists(nonSystemIndex));
+ }
+
+ /**
+ * If the list of feature states to restore is left unspecified and we are restoring global state,
+ * all feature states should be restored.
+ */
+ public void testRestoreSystemIndicesAsGlobalStateWithDefaultFeatureStateList() {
+ createRepository(REPO_NAME, "fs");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // run a snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // add another document
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+
+ // restore indices as global state a null list of feature states
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(true)
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // verify that the system index is destroyed
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(1L));
+ }
+
+ /**
+ * If the list of feature states to restore contains only "none" and we are restoring global state,
+ * no feature states should be restored.
+ *
+ * In this test, we explicitly request a regular index to avoid any confusion over the meaning of
+ * "all indices."
+ */
+ public void testRestoreSystemIndicesAsGlobalStateWithEmptyListOfFeatureStates() {
+ createRepository(REPO_NAME, "fs");
+ String regularIndex = "my-index";
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ indexDoc(regularIndex, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, regularIndex);
+
+ // run a snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // add another document
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ assertAcked(client().admin().indices().prepareDelete(regularIndex).get());
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+
+ // restore regular index, with global state and an empty list of feature states
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(true)
+ .setFeatureStates(new String[]{ randomFrom("none", "NONE") })
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // verify that the system index still has the updated document, i.e. has not been restored
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+ }
+
+ /**
+ * If the list of feature states to restore contains only "none" and we are restoring global state,
+ * no feature states should be restored. However, for backwards compatibility, if no index is
+ * specified, system indices are included in "all indices." In this edge case, we get an error
+ * saying that the system index must be closed, because here it is included in "all indices."
+ */
+ public void testRestoreSystemIndicesAsGlobalStateWithEmptyListOfFeatureStatesNoIndicesSpecified() {
+ createRepository(REPO_NAME, "fs");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // run a snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // restore indices as global state without closing the index
+ SnapshotRestoreException exception = expectThrows(
+ SnapshotRestoreException.class,
+ () -> clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(true)
+ .setFeatureStates(new String[]{ randomFrom("none", "NONE") })
+ .get());
+
+ assertThat(exception.getMessage(), containsString("cannot restore index [" + SystemIndexTestPlugin.SYSTEM_INDEX_NAME
+ + "] because an open index with same name already exists in the cluster."));
+ }
+
+ /**
+ * When a feature state is restored, all indices that are part of that feature state should be deleted, then the indices in
+ * the snapshot should be restored.
+ *
+ * However, other feature states should be unaffected.
+ */
+ public void testAllSystemIndicesAreRemovedWhenThatFeatureStateIsRestored() {
+ createRepository(REPO_NAME, "fs");
+ // Create a system index we'll snapshot and restore
+ final String systemIndexInSnapshot = SystemIndexTestPlugin.SYSTEM_INDEX_NAME + "-1";
+ indexDoc(systemIndexInSnapshot, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME + "*");
+
+ // And one we'll snapshot but not restore
+ indexDoc(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+
+ // And a regular index so we can avoid matching all indices on the restore
+ final String regularIndex = "regular-index";
+ indexDoc(regularIndex, "1", "purpose", "pre-snapshot doc");
+
+ // run a snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // Now index another doc and create another index in the same pattern as the first index
+ final String systemIndexNotInSnapshot = SystemIndexTestPlugin.SYSTEM_INDEX_NAME + "-2";
+ indexDoc(systemIndexInSnapshot, "2", "purpose", "post-snapshot doc");
+ indexDoc(systemIndexNotInSnapshot, "1", "purpose", "post-snapshot doc");
+
+ // Add another doc to the second system index, so we can be sure it hasn't been touched
+ indexDoc(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(systemIndexInSnapshot, systemIndexNotInSnapshot, AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // Delete the regular index so we can restore it
+ assertAcked(cluster().client().admin().indices().prepareDelete(regularIndex));
+
+ // restore the snapshot
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setFeatureStates("SystemIndexTestPlugin")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(true)
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // The index we created after the snapshot should be gone
+ assertFalse(indexExists(systemIndexNotInSnapshot));
+ // And the first index should have a single doc
+ assertThat(getDocCount(systemIndexInSnapshot), equalTo(1L));
+ // And the system index whose state we didn't restore shouldn't have been touched and still have 2 docs
+ assertThat(getDocCount(AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+ }
+
+ public void testSystemIndexAliasesAreAlwaysRestored() {
+ createRepository(REPO_NAME, "fs");
+ // Create a system index
+ final String systemIndexName = SystemIndexTestPlugin.SYSTEM_INDEX_NAME + "-1";
+ indexDoc(systemIndexName, "1", "purpose", "pre-snapshot doc");
+
+ // And a regular index
+ // And a regular index so we can avoid matching all indices on the restore
+ final String regularIndex = "regular-index";
+ final String regularAlias = "regular-alias";
+ indexDoc(regularIndex, "1", "purpose", "pre-snapshot doc");
+
+ // And make sure they both have aliases
+ final String systemIndexAlias = SystemIndexTestPlugin.SYSTEM_INDEX_NAME + "-alias";
+ assertAcked(client().admin().indices().prepareAliases()
+ .addAlias(systemIndexName, systemIndexAlias)
+ .addAlias(regularIndex, regularAlias).get());
+
+ // run a snapshot including global state
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // And delete both the indices
+ assertAcked(cluster().client().admin().indices().prepareDelete(regularIndex, systemIndexName));
+
+ // Now restore the snapshot with no aliases
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setFeatureStates("SystemIndexTestPlugin")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(false)
+ .setIncludeAliases(false)
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // The regular index should exist
+ assertTrue(indexExists(regularIndex));
+ assertFalse(indexExists(regularAlias));
+ // And the system index, queried by alias, should have a doc
+ assertTrue(indexExists(systemIndexName));
+ assertTrue(indexExists(systemIndexAlias));
+ assertThat(getDocCount(systemIndexAlias), equalTo(1L));
+
+ }
+
+ /**
+ * Tests that the special "none" feature state name cannot be combined with other
+ * feature state names, and an error occurs if it's tried.
+ */
+ public void testNoneFeatureStateMustBeAlone() {
+ createRepository(REPO_NAME, "fs");
+ // put a document in a system index
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // run a snapshot including global state
+ IllegalArgumentException createEx = expectThrows(
+ IllegalArgumentException.class,
+ () -> clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(randomBoolean())
+ .setFeatureStates("SystemIndexTestPlugin", "none", "AnotherSystemIndexTestPlugin")
+ .get()
+ );
+ assertThat(createEx.getMessage(), equalTo("the feature_states value [none] indicates that no feature states should be " +
+ "snapshotted, but other feature states were requested: [SystemIndexTestPlugin, none, AnotherSystemIndexTestPlugin]"));
+
+ // create a successful snapshot with global state/all features
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ SnapshotRestoreException restoreEx = expectThrows(
+ SnapshotRestoreException.class,
+ () -> clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(randomBoolean())
+ .setFeatureStates("SystemIndexTestPlugin", "none")
+ .get()
+ );
+ assertThat(
+ restoreEx.getMessage(),
+ allOf( // the order of the requested feature states is non-deterministic so just check that it includes most of the right stuff
+ containsString(
+ "the feature_states value [none] indicates that no feature states should be restored, but other feature states were "
+ + "requested:"
+ ),
+ containsString("SystemIndexTestPlugin")
+ )
+ );
+ }
+
+ /**
+ * Tests that using the special "none" feature state value creates a snapshot with no feature states included
+ */
+ public void testNoneFeatureStateOnCreation() {
+ createRepository(REPO_NAME, "fs");
+ final String regularIndex = "test-idx";
+
+ indexDoc(regularIndex, "1", "purpose", "create an index that can be restored");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(regularIndex, SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .setFeatureStates(randomFrom("none", "NONE"))
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // Verify that the system index was not included
+ Set snapshottedIndices = clusterAdmin().prepareGetSnapshots(REPO_NAME).get()
+ .getSnapshots(REPO_NAME).stream()
+ .map(SnapshotInfo::indices)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+
+ assertThat(snapshottedIndices, allOf(hasItem(regularIndex), not(hasItem(SystemIndexTestPlugin.SYSTEM_INDEX_NAME))));
+ }
+
+ public void testNoneFeatureStateOnRestore() {
+ createRepository(REPO_NAME, "fs");
+ final String regularIndex = "test-idx";
+
+ indexDoc(regularIndex, "1", "purpose", "create an index that can be restored");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(regularIndex, SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // Create a snapshot
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(true)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // Index another doc into the system index
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "2", "purpose", "post-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+ // And delete the regular index so we can restore it
+ assertAcked(cluster().client().admin().indices().prepareDelete(regularIndex));
+
+ // Restore the snapshot specifying the regular index and "none" for feature states
+ RestoreSnapshotResponse restoreSnapshotResponse = clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setIndices(regularIndex)
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(randomBoolean())
+ .setFeatureStates(randomFrom("none", "NONE"))
+ .get();
+ assertThat(restoreSnapshotResponse.getRestoreInfo().totalShards(), greaterThan(0));
+
+ // The regular index should only have one doc
+ assertThat(getDocCount(regularIndex), equalTo(1L));
+ // But the system index shouldn't have been touched
+ assertThat(getDocCount(SystemIndexTestPlugin.SYSTEM_INDEX_NAME), equalTo(2L));
+ }
+
+ /**
+ * This test checks a piece of BWC logic, and so should be removed when we block restoring system indices by name.
+ *
+ * This test checks whether it's possible to change the name of a system index when it's restored by name (rather than by feature state)
+ */
+ public void testCanRenameSystemIndicesIfRestoredByIndexName() {
+ createRepository(REPO_NAME, "fs");
+ indexDoc(SystemIndexTestPlugin.SYSTEM_INDEX_NAME, "1", "purpose", "pre-snapshot doc");
+ refresh(SystemIndexTestPlugin.SYSTEM_INDEX_NAME);
+
+ // snapshot including our system index
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, "test-snap")
+ .setWaitForCompletion(true)
+ .setIncludeGlobalState(false)
+ .get();
+ assertSnapshotSuccess(createSnapshotResponse);
+
+ // Now restore it with a rename
+ clusterAdmin().prepareRestoreSnapshot(REPO_NAME, "test-snap")
+ .setIndices(SystemIndexTestPlugin.SYSTEM_INDEX_NAME)
+ .setWaitForCompletion(true)
+ .setRestoreGlobalState(false)
+ .setFeatureStates(NO_FEATURE_STATES_VALUE)
+ .setRenamePattern(".test-(.+)")
+ .setRenameReplacement("restored-$1")
+ .get();
+
+ assertTrue("The renamed system index should be present", indexExists("restored-system-idx"));
+ assertTrue("The original index should still be present", indexExists(SystemIndexTestPlugin.SYSTEM_INDEX_NAME));
+ }
+
+ /**
+ * Ensures that if we can only capture a partial snapshot of a system index, then the feature state associated with that index is
+ * not included in the snapshot, because it would not be safe to restore that feature state.
+ */
+ public void testPartialSnapshotsOfSystemIndexRemovesFeatureState() throws Exception {
+ final String partialIndexName = SystemIndexTestPlugin.SYSTEM_INDEX_NAME;
+ final String fullIndexName = AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME;
+
+ createRepositoryNoVerify(REPO_NAME, "mock");
+
+ // Creating the index that we'll get a partial snapshot of with a bunch of shards
+ assertAcked(prepareCreate(partialIndexName, 0, indexSettingsNoReplicas(6)));
+ indexDoc(partialIndexName, "1", "purpose", "pre-snapshot doc");
+ // And another one with the default
+ indexDoc(fullIndexName, "1", "purpose", "pre-snapshot doc");
+ ensureGreen();
+
+ // Stop a random data node so we lose a shard from the partial index
+ internalCluster().stopRandomDataNode();
+ assertBusy(() -> assertEquals(ClusterHealthStatus.RED, client().admin().cluster().prepareHealth().get().getStatus()),
+ 30, TimeUnit.SECONDS);
+
+ // Get ready to block
+ blockMasterFromFinalizingSnapshotOnIndexFile(REPO_NAME);
+
+ // Start a snapshot and wait for it to hit the block, then kill the master to force a failover
+ final String partialSnapName = "test-partial-snap";
+ CreateSnapshotResponse createSnapshotResponse = clusterAdmin().prepareCreateSnapshot(REPO_NAME, partialSnapName)
+ .setIncludeGlobalState(true)
+ .setWaitForCompletion(false)
+ .setPartial(true)
+ .get();
+ assertThat(createSnapshotResponse.status(), equalTo(RestStatus.ACCEPTED));
+ waitForBlock(internalCluster().getMasterName(), REPO_NAME);
+ internalCluster().stopCurrentMasterNode();
+
+ // Now get the snapshot and do our checks
+ assertBusy(() -> {
+ GetSnapshotsResponse snapshotsStatusResponse = client().admin().cluster()
+ .prepareGetSnapshots(REPO_NAME).setSnapshots(partialSnapName).get();
+ SnapshotInfo snapshotInfo = snapshotsStatusResponse.getSnapshots(REPO_NAME).get(0);
+ assertNotNull(snapshotInfo);
+ assertThat(snapshotInfo.failedShards(), lessThan(snapshotInfo.totalShards()));
+ List statesInSnapshot = snapshotInfo.featureStates().stream()
+ .map(SnapshotFeatureInfo::getPluginName)
+ .collect(Collectors.toList());
+ assertThat(statesInSnapshot, not(hasItem((new SystemIndexTestPlugin()).getFeatureName())));
+ assertThat(statesInSnapshot, hasItem((new AnotherSystemIndexTestPlugin()).getFeatureName()));
+ });
+ }
+
+ public void testParallelIndexDeleteRemovesFeatureState() throws Exception {
+ final String indexToBeDeleted = SystemIndexTestPlugin.SYSTEM_INDEX_NAME;
+ final String fullIndexName = AnotherSystemIndexTestPlugin.SYSTEM_INDEX_NAME;
+ final String nonsystemIndex = "nonsystem-idx";
+
+ // Stop one data node so we only have one data node to start with
+ internalCluster().stopNode(dataNodes.get(1));
+ dataNodes.remove(1);
+
+ createRepositoryNoVerify(REPO_NAME, "mock");
+
+ // Creating the index that we'll get a partial snapshot of with a bunch of shards
+ assertAcked(prepareCreate(indexToBeDeleted, 0, indexSettingsNoReplicas(6)));
+ indexDoc(indexToBeDeleted, "1", "purpose", "pre-snapshot doc");
+ // And another one with the default
+ indexDoc(fullIndexName, "1", "purpose", "pre-snapshot doc");
+
+ // Now start up a new node and create an index that should get allocated to it
+ dataNodes.add(internalCluster().startDataOnlyNode());
+ createIndexWithContent(
+ nonsystemIndex,
+ indexSettingsNoReplicas(2).put("index.routing.allocation.require._name", dataNodes.get(1)).build()
+ );
+ refresh();
+ ensureGreen();
+
+ logger.info("--> Created indices, blocking repo on new data node...");
+ blockDataNode(REPO_NAME, dataNodes.get(1));
+
+ // Start a snapshot - need to do this async because some blocks will block this call
+ logger.info("--> Blocked repo, starting snapshot...");
+ final String partialSnapName = "test-partial-snap";
+ ActionFuture createSnapshotFuture = clusterAdmin().prepareCreateSnapshot(REPO_NAME, partialSnapName)
+ .setIndices(nonsystemIndex)
+ .setIncludeGlobalState(true)
+ .setWaitForCompletion(true)
+ .setPartial(true)
+ .execute();
+
+ logger.info("--> Started snapshot, waiting for block...");
+ waitForBlock(dataNodes.get(1), REPO_NAME);
+
+ logger.info("--> Repo hit block, deleting the index...");
+ assertAcked(cluster().client().admin().indices().prepareDelete(indexToBeDeleted));
+
+ logger.info("--> Index deleted, unblocking repo...");
+ unblockNode(REPO_NAME, dataNodes.get(1));
+
+ logger.info("--> Repo unblocked, checking that snapshot finished...");
+ CreateSnapshotResponse createSnapshotResponse = createSnapshotFuture.actionGet();
+ logger.info(createSnapshotResponse.toString());
+ assertThat(createSnapshotResponse.status(), equalTo(RestStatus.OK));
+
+ logger.info("--> All operations complete, running assertions");
+ SnapshotInfo snapshotInfo = createSnapshotResponse.getSnapshotInfo();
+ assertNotNull(snapshotInfo);
+ assertThat(snapshotInfo.indices(), not(hasItem(indexToBeDeleted)));
+ List statesInSnapshot = snapshotInfo.featureStates().stream()
+ .map(SnapshotFeatureInfo::getPluginName)
+ .collect(Collectors.toList());
+ assertThat(statesInSnapshot, not(hasItem((new SystemIndexTestPlugin()).getFeatureName())));
+ assertThat(statesInSnapshot, hasItem((new AnotherSystemIndexTestPlugin()).getFeatureName()));
+ }
+
+ private void assertSnapshotSuccess(CreateSnapshotResponse createSnapshotResponse) {
+ assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(), greaterThan(0));
+ assertThat(createSnapshotResponse.getSnapshotInfo().successfulShards(),
+ equalTo(createSnapshotResponse.getSnapshotInfo().totalShards()));
+ }
+
+ private long getDocCount(String indexName) {
+ return client().admin().indices().prepareStats(indexName).get().getPrimaries().getDocs().getCount();
+ }
+
+ public static class SystemIndexTestPlugin extends Plugin implements SystemIndexPlugin {
+
+ public static final String SYSTEM_INDEX_NAME = ".test-system-idx";
+
+ @Override
+ public Collection getSystemIndexDescriptors(Settings settings) {
+ return Collections.singletonList(new SystemIndexDescriptor(SYSTEM_INDEX_NAME + "*", "System indices for tests"));
+ }
+
+ @Override
+ public String getFeatureName() {
+ return SystemIndexTestPlugin.class.getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "A simple test plugin";
+ }
+ }
+
+ public static class AnotherSystemIndexTestPlugin extends Plugin implements SystemIndexPlugin {
+
+ public static final String SYSTEM_INDEX_NAME = ".another-test-system-idx";
+
+ @Override
+ public Collection getSystemIndexDescriptors(Settings settings) {
+ return Collections.singletonList(new SystemIndexDescriptor(SYSTEM_INDEX_NAME, "System indices for tests"));
+ }
+
+ @Override
+ public String getFeatureName() {
+ return AnotherSystemIndexTestPlugin.class.getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "Another simple test plugin";
+ }
+ }
+
+ public static class AssociatedIndicesTestPlugin extends Plugin implements SystemIndexPlugin {
+
+ public static final String SYSTEM_INDEX_NAME = ".third-test-system-idx";
+ public static final String ASSOCIATED_INDEX_NAME = ".associated-idx";
+
+ @Override
+ public Collection getSystemIndexDescriptors(Settings settings) {
+ return Collections.singletonList(new SystemIndexDescriptor(SYSTEM_INDEX_NAME, "System & associated indices for tests"));
+ }
+
+ @Override
+ public Collection getAssociatedIndexPatterns() {
+ return Collections.singletonList(ASSOCIATED_INDEX_NAME);
+ }
+
+ @Override
+ public String getFeatureName() {
+ return AssociatedIndicesTestPlugin.class.getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "Another simple test plugin";
+ }
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java
index cdb56efe022d9..d4ff96be8db45 100644
--- a/server/src/main/java/org/elasticsearch/action/ActionModule.java
+++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java
@@ -58,6 +58,8 @@
import org.elasticsearch.action.admin.cluster.snapshots.create.TransportCreateSnapshotAction;
import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotAction;
import org.elasticsearch.action.admin.cluster.snapshots.delete.TransportDeleteSnapshotAction;
+import org.elasticsearch.action.admin.cluster.snapshots.features.SnapshottableFeaturesAction;
+import org.elasticsearch.action.admin.cluster.snapshots.features.TransportSnapshottableFeaturesAction;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsAction;
import org.elasticsearch.action.admin.cluster.snapshots.get.TransportGetSnapshotsAction;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotAction;
@@ -282,6 +284,7 @@
import org.elasticsearch.rest.action.admin.cluster.RestRemoteClusterInfoAction;
import org.elasticsearch.rest.action.admin.cluster.RestRestoreSnapshotAction;
import org.elasticsearch.rest.action.admin.cluster.RestSnapshotsStatusAction;
+import org.elasticsearch.rest.action.admin.cluster.RestSnapshottableFeaturesAction;
import org.elasticsearch.rest.action.admin.cluster.RestVerifyRepositoryAction;
import org.elasticsearch.rest.action.admin.cluster.dangling.RestDeleteDanglingIndexAction;
import org.elasticsearch.rest.action.admin.cluster.dangling.RestImportDanglingIndexAction;
@@ -497,6 +500,7 @@ public void reg
actions.register(CloneSnapshotAction.INSTANCE, TransportCloneSnapshotAction.class);
actions.register(RestoreSnapshotAction.INSTANCE, TransportRestoreSnapshotAction.class);
actions.register(SnapshotsStatusAction.INSTANCE, TransportSnapshotsStatusAction.class);
+ actions.register(SnapshottableFeaturesAction.INSTANCE, TransportSnapshottableFeaturesAction.class);
actions.register(IndicesStatsAction.INSTANCE, TransportIndicesStatsAction.class);
actions.register(IndicesSegmentsAction.INSTANCE, TransportIndicesSegmentsAction.class);
@@ -646,6 +650,7 @@ public void initRestHandlers(Supplier nodesInCluster) {
registerHandler.accept(new RestRestoreSnapshotAction());
registerHandler.accept(new RestDeleteSnapshotAction());
registerHandler.accept(new RestSnapshotsStatusAction());
+ registerHandler.accept(new RestSnapshottableFeaturesAction());
registerHandler.accept(new RestGetIndicesAction());
registerHandler.accept(new RestIndicesStatsAction());
registerHandler.accept(new RestIndicesSegmentsAction());
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequest.java
index a86ba4f88bf2c..ceb5111528455 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequest.java
@@ -22,6 +22,7 @@
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.snapshots.SnapshotsService;
import java.io.IOException;
import java.util.Arrays;
@@ -64,6 +65,8 @@ public class CreateSnapshotRequest extends MasterNodeRequest MAXIMUM_METADATA_BYTES) {
validationException = addValidationError("metadata must be smaller than 1024 bytes, but was [" + metadataSize + "]",
@@ -337,6 +349,28 @@ public CreateSnapshotRequest userMetadata(Map userMetadata) {
return this;
}
+ /**
+ * @return Which plugin states should be included in the snapshot
+ */
+ public String[] featureStates() {
+ return featureStates;
+ }
+
+ /**
+ * @param featureStates The plugin states to be included in the snapshot
+ */
+ public CreateSnapshotRequest featureStates(String[] featureStates) {
+ this.featureStates = featureStates;
+ return this;
+ }
+
+ /**
+ * @param featureStates The plugin states to be included in the snapshot
+ */
+ public CreateSnapshotRequest featureStates(List featureStates) {
+ return featureStates(featureStates.toArray(EMPTY_ARRAY));
+ }
+
/**
* Parses snapshot definition.
*
@@ -355,6 +389,12 @@ public CreateSnapshotRequest source(Map source) {
} else {
throw new IllegalArgumentException("malformed indices section, should be an array of strings");
}
+ } else if (name.equals("feature_states")) {
+ if (entry.getValue() instanceof List) {
+ featureStates((List) entry.getValue());
+ } else {
+ throw new IllegalArgumentException("malformed feature_states section, should be an array of strings");
+ }
} else if (name.equals("partial")) {
partial(nodeBooleanValue(entry.getValue(), "partial"));
} else if (name.equals("include_global_state")) {
@@ -380,6 +420,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.value(index);
}
builder.endArray();
+ if (featureStates != null) {
+ builder.startArray("feature_states");
+ for (String plugin : featureStates) {
+ builder.value(plugin);
+ }
+ builder.endArray();
+ }
builder.field("partial", partial);
builder.field("include_global_state", includeGlobalState);
if (indicesOptions != null) {
@@ -407,6 +454,7 @@ public boolean equals(Object o) {
Objects.equals(repository, that.repository) &&
Arrays.equals(indices, that.indices) &&
Objects.equals(indicesOptions, that.indicesOptions) &&
+ Arrays.equals(featureStates, that.featureStates) &&
Objects.equals(masterNodeTimeout, that.masterNodeTimeout) &&
Objects.equals(userMetadata, that.userMetadata);
}
@@ -416,6 +464,7 @@ public int hashCode() {
int result = Objects.hash(snapshot, repository, indicesOptions, partial, includeGlobalState,
waitForCompletion, userMetadata);
result = 31 * result + Arrays.hashCode(indices);
+ result = 31 * result + Arrays.hashCode(featureStates);
return result;
}
@@ -426,6 +475,7 @@ public String toString() {
", repository='" + repository + '\'' +
", indices=" + (indices == null ? null : Arrays.asList(indices)) +
", indicesOptions=" + indicesOptions +
+ ", featureStates=" + Arrays.asList(featureStates) +
", partial=" + partial +
", includeGlobalState=" + includeGlobalState +
", waitForCompletion=" + waitForCompletion +
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequestBuilder.java
index 2fe7033c85ab2..355060834d8f3 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequestBuilder.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequestBuilder.java
@@ -111,4 +111,15 @@ public CreateSnapshotRequestBuilder setIncludeGlobalState(boolean includeGlobalS
request.includeGlobalState(includeGlobalState);
return this;
}
+
+ /**
+ * Provide a list of features whose state indices should be included in the snapshot
+ *
+ * @param featureStates A list of feature names
+ * @return this builder
+ */
+ public CreateSnapshotRequestBuilder setFeatureStates(String... featureStates) {
+ request.featureStates(featureStates);
+ return this;
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/GetSnapshottableFeaturesRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/GetSnapshottableFeaturesRequest.java
new file mode 100644
index 0000000000000..545f5c7fbdd7a
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/GetSnapshottableFeaturesRequest.java
@@ -0,0 +1,37 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.cluster.snapshots.features;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.master.MasterNodeRequest;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+
+public class GetSnapshottableFeaturesRequest extends MasterNodeRequest {
+
+ public GetSnapshottableFeaturesRequest() {
+
+ }
+
+ public GetSnapshottableFeaturesRequest(StreamInput in) throws IOException {
+ super(in);
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ return null;
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/GetSnapshottableFeaturesResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/GetSnapshottableFeaturesResponse.java
new file mode 100644
index 0000000000000..a2048bab29c58
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/GetSnapshottableFeaturesResponse.java
@@ -0,0 +1,123 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.cluster.snapshots.features;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class GetSnapshottableFeaturesResponse extends ActionResponse implements ToXContentObject {
+
+ private final List snapshottableFeatures;
+
+ public GetSnapshottableFeaturesResponse(List features) {
+ this.snapshottableFeatures = Collections.unmodifiableList(features);
+ }
+
+ public GetSnapshottableFeaturesResponse(StreamInput in) throws IOException {
+ super(in);
+ snapshottableFeatures = in.readList(SnapshottableFeature::new);
+ }
+
+ public List getSnapshottableFeatures() {
+ return snapshottableFeatures;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeList(snapshottableFeatures);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ {
+ builder.startArray("features");
+ for (SnapshottableFeature feature : snapshottableFeatures) {
+ builder.value(feature);
+ }
+ builder.endArray();
+ }
+ builder.endObject();
+ return builder;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if ((o instanceof GetSnapshottableFeaturesResponse) == false) return false;
+ GetSnapshottableFeaturesResponse that = (GetSnapshottableFeaturesResponse) o;
+ return snapshottableFeatures.equals(that.snapshottableFeatures);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(snapshottableFeatures);
+ }
+
+ public static class SnapshottableFeature implements Writeable, ToXContentObject {
+
+ private final String featureName;
+ private final String description;
+
+ public SnapshottableFeature(String featureName, String description) {
+ this.featureName = featureName;
+ this.description = description;
+ }
+
+ public SnapshottableFeature(StreamInput in) throws IOException {
+ featureName = in.readString();
+ description = in.readString();
+ }
+
+ public String getFeatureName() {
+ return featureName;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(featureName);
+ out.writeString(description);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ builder.field("name", featureName);
+ builder.field("description", description);
+ builder.endObject();
+ return builder;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if ((o instanceof SnapshottableFeature) == false) return false;
+ SnapshottableFeature feature = (SnapshottableFeature) o;
+ return Objects.equals(getFeatureName(), feature.getFeatureName());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getFeatureName());
+ }
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/SnapshottableFeaturesAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/SnapshottableFeaturesAction.java
new file mode 100644
index 0000000000000..38bf7afd6b505
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/SnapshottableFeaturesAction.java
@@ -0,0 +1,21 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.cluster.snapshots.features;
+
+import org.elasticsearch.action.ActionType;
+
+public class SnapshottableFeaturesAction extends ActionType {
+
+ public static final SnapshottableFeaturesAction INSTANCE = new SnapshottableFeaturesAction();
+ public static final String NAME = "cluster:admin/snapshot/features/get";
+
+ private SnapshottableFeaturesAction() {
+ super(NAME, GetSnapshottableFeaturesResponse::new);
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportSnapshottableFeaturesAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportSnapshottableFeaturesAction.java
new file mode 100644
index 0000000000000..62f32bc460f0e
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportSnapshottableFeaturesAction.java
@@ -0,0 +1,56 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.action.admin.cluster.snapshots.features;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.master.TransportMasterNodeAction;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.block.ClusterBlockLevel;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.indices.SystemIndices;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+
+import java.util.stream.Collectors;
+
+public class TransportSnapshottableFeaturesAction extends TransportMasterNodeAction {
+
+ private final SystemIndices systemIndices;
+
+ @Inject
+ public TransportSnapshottableFeaturesAction(TransportService transportService, ClusterService clusterService, ThreadPool threadPool,
+ ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
+ SystemIndices systemIndices) {
+ super(SnapshottableFeaturesAction.NAME, transportService, clusterService, threadPool, actionFilters,
+ GetSnapshottableFeaturesRequest::new, indexNameExpressionResolver, GetSnapshottableFeaturesResponse::new,
+ ThreadPool.Names.SAME);
+ this.systemIndices = systemIndices;
+ }
+
+ @Override
+ protected void masterOperation(Task task, GetSnapshottableFeaturesRequest request, ClusterState state,
+ ActionListener listener) throws Exception {
+ listener.onResponse(new GetSnapshottableFeaturesResponse(systemIndices.getFeatures().entrySet().stream()
+ .map(featureEntry -> new GetSnapshottableFeaturesResponse.SnapshottableFeature(
+ featureEntry.getKey(),
+ featureEntry.getValue().getDescription()))
+ .collect(Collectors.toList())));
+ }
+
+ @Override
+ protected ClusterBlockException checkBlock(GetSnapshottableFeaturesRequest request, ClusterState state) {
+ return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java
index fedccc0854450..ea62ee86a9fb1 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java
@@ -279,7 +279,8 @@ private static List buildSimpleSnapshotInfos(final Set
for (SnapshotId snapshotId : toResolve) {
final List indices = snapshotsToIndices.getOrDefault(snapshotId, Collections.emptyList());
CollectionUtil.timSort(indices);
- snapshotInfos.add(new SnapshotInfo(snapshotId, indices, Collections.emptyList(), repositoryData.getSnapshotState(snapshotId)));
+ snapshotInfos.add(new SnapshotInfo(snapshotId, indices, Collections.emptyList(), Collections.emptyList(),
+ repositoryData.getSnapshotState(snapshotId)));
}
CollectionUtil.timSort(snapshotInfos);
return Collections.unmodifiableList(snapshotInfos);
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java
index f2d412d7baab9..0498472dd8d91 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java
@@ -28,6 +28,7 @@
import java.util.Objects;
import static org.elasticsearch.action.ValidateActions.addValidationError;
+import static org.elasticsearch.snapshots.SnapshotsService.FEATURE_STATES_VERSION;
import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS;
import static org.elasticsearch.common.settings.Settings.readSettingsFromStream;
import static org.elasticsearch.common.settings.Settings.writeSettingsToStream;
@@ -42,6 +43,7 @@ public class RestoreSnapshotRequest extends MasterNodeRequest featureStates) {
+ return featureStates(featureStates.toArray(Strings.EMPTY_ARRAY));
+ }
+
/**
* Parses restore definition
*
@@ -466,6 +500,12 @@ public RestoreSnapshotRequest source(Map source) {
} else {
throw new IllegalArgumentException("malformed indices section, should be an array of strings");
}
+ } else if (name.equals("feature_states")) {
+ if (entry.getValue() instanceof List) {
+ featureStates((List) entry.getValue());
+ } else {
+ throw new IllegalArgumentException("malformed feature_states section, should be an array of strings");
+ }
} else if (name.equals("partial")) {
partial(nodeBooleanValue(entry.getValue(), "partial"));
} else if (name.equals("include_global_state")) {
@@ -530,6 +570,13 @@ private void toXContentFragment(XContentBuilder builder, Params params) throws I
if (renameReplacement != null) {
builder.field("rename_replacement", renameReplacement);
}
+ if (featureStates != null && featureStates.length > 0) {
+ builder.startArray("feature_states");
+ for (String plugin : featureStates) {
+ builder.value(plugin);
+ }
+ builder.endArray();
+ }
builder.field("include_global_state", includeGlobalState);
builder.field("partial", partial);
builder.field("include_aliases", includeAliases);
@@ -565,6 +612,7 @@ public boolean equals(Object o) {
Objects.equals(repository, that.repository) &&
Arrays.equals(indices, that.indices) &&
Objects.equals(indicesOptions, that.indicesOptions) &&
+ Arrays.equals(featureStates, that.featureStates) &&
Objects.equals(renamePattern, that.renamePattern) &&
Objects.equals(renameReplacement, that.renameReplacement) &&
Objects.equals(indexSettings, that.indexSettings) &&
@@ -579,6 +627,7 @@ public int hashCode() {
includeGlobalState, partial, includeAliases, indexSettings, snapshotUuid, skipOperatorOnlyState);
result = 31 * result + Arrays.hashCode(indices);
result = 31 * result + Arrays.hashCode(ignoreIndexSettings);
+ result = 31 * result + Arrays.hashCode(featureStates);
return result;
}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestBuilder.java
index b3dd357f12ae0..ceab46e73fc63 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestBuilder.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestBuilder.java
@@ -223,4 +223,12 @@ public RestoreSnapshotRequestBuilder setIgnoreIndexSettings(List ignoreI
request.ignoreIndexSettings(ignoreIndexSettings);
return this;
}
+
+ /**
+ * Sets the list of features whose states should be restored as part of this snapshot
+ */
+ public RestoreSnapshotRequestBuilder setFeatureStates(String... featureStates) {
+ request.featureStates(featureStates);
+ return this;
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java b/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java
index 090b6797fe500..54fb1d20a80a0 100644
--- a/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java
+++ b/server/src/main/java/org/elasticsearch/cluster/ClusterModule.java
@@ -95,6 +95,7 @@ public class ClusterModule extends AbstractModule {
private final AllocationDeciders allocationDeciders;
private final AllocationService allocationService;
private final List clusterPlugins;
+ private final MetadataDeleteIndexService metadataDeleteIndexService;
// pkg private for tests
final Collection deciderList;
final ShardsAllocator shardsAllocator;
@@ -108,6 +109,7 @@ public ClusterModule(Settings settings, ClusterService clusterService, List getNamedWriteables() {
@@ -248,13 +250,17 @@ public AllocationService getAllocationService() {
return allocationService;
}
+ public MetadataDeleteIndexService getMetadataDeleteIndexService() {
+ return metadataDeleteIndexService;
+ }
+
@Override
protected void configure() {
bind(GatewayAllocator.class).asEagerSingleton();
bind(AllocationService.class).toInstance(allocationService);
bind(ClusterService.class).toInstance(clusterService);
bind(NodeConnectionsService.class).asEagerSingleton();
- bind(MetadataDeleteIndexService.class).asEagerSingleton();
+ bind(MetadataDeleteIndexService.class).toInstance(metadataDeleteIndexService);
bind(MetadataIndexStateService.class).asEagerSingleton();
bind(MetadataMappingService.class).asEagerSingleton();
bind(MetadataIndexAliasesService.class).asEagerSingleton();
diff --git a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java
index 3bfb26e1aae13..12906d755666c 100644
--- a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java
+++ b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java
@@ -25,10 +25,11 @@
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.repositories.IndexId;
-import org.elasticsearch.repositories.RepositoryShardId;
import org.elasticsearch.repositories.RepositoryOperation;
+import org.elasticsearch.repositories.RepositoryShardId;
import org.elasticsearch.snapshots.InFlightShardSnapshotStates;
import org.elasticsearch.snapshots.Snapshot;
+import org.elasticsearch.snapshots.SnapshotFeatureInfo;
import org.elasticsearch.snapshots.SnapshotId;
import org.elasticsearch.snapshots.SnapshotsService;
@@ -42,6 +43,8 @@
import java.util.Set;
import java.util.stream.Collectors;
+import static org.elasticsearch.snapshots.SnapshotsService.FEATURE_STATES_VERSION;
+
/**
* Meta data about snapshots that are currently executing
*/
@@ -82,12 +85,11 @@ public String toString() {
* will be in state {@link State#SUCCESS} right away otherwise it will be in state {@link State#STARTED}.
*/
public static Entry startedEntry(Snapshot snapshot, boolean includeGlobalState, boolean partial, List indices,
- List dataStreams, long startTime, long repositoryStateId,
- ImmutableOpenMap shards, Map userMetadata,
- Version version) {
+ List dataStreams, long startTime, long repositoryStateId, ImmutableOpenMap shards,
+ Map userMetadata, Version version, List featureStates) {
return new SnapshotsInProgress.Entry(snapshot, includeGlobalState, partial,
completed(shards.values()) ? State.SUCCESS : State.STARTED,
- indices, dataStreams, startTime, repositoryStateId, shards, null, userMetadata, version);
+ indices, dataStreams, featureStates, startTime, repositoryStateId, shards, null, userMetadata, version);
}
/**
@@ -104,8 +106,8 @@ public static Entry startedEntry(Snapshot snapshot, boolean includeGlobalState,
public static Entry startClone(Snapshot snapshot, SnapshotId source, List indices, long startTime,
long repositoryStateId, Version version) {
return new SnapshotsInProgress.Entry(snapshot, true, false, State.STARTED, indices, Collections.emptyList(),
- startTime, repositoryStateId, ImmutableOpenMap.of(), null, Collections.emptyMap(), version, source,
- ImmutableOpenMap.of());
+ Collections.emptyList(), startTime, repositoryStateId, ImmutableOpenMap.of(), null, Collections.emptyMap(), version, source,
+ ImmutableOpenMap.of());
}
public static class Entry implements Writeable, ToXContent, RepositoryOperation {
@@ -119,6 +121,7 @@ public static class Entry implements Writeable, ToXContent, RepositoryOperation
private final ImmutableOpenMap shards;
private final List indices;
private final List dataStreams;
+ private final List featureStates;
private final long startTime;
private final long repositoryStateId;
// see #useShardGenerations
@@ -141,24 +144,25 @@ public static class Entry implements Writeable, ToXContent, RepositoryOperation
// visible for testing, use #startedEntry and copy constructors in production code
public Entry(Snapshot snapshot, boolean includeGlobalState, boolean partial, State state, List indices,
- List dataStreams, long startTime, long repositoryStateId,
+ List dataStreams, List featureStates, long startTime, long repositoryStateId,
ImmutableOpenMap shards, String failure, Map userMetadata,
Version version) {
- this(snapshot, includeGlobalState, partial, state, indices, dataStreams, startTime, repositoryStateId, shards, failure,
- userMetadata, version, null, ImmutableOpenMap.of());
+ this(snapshot, includeGlobalState, partial, state, indices, dataStreams, featureStates, startTime, repositoryStateId, shards,
+ failure, userMetadata, version, null, ImmutableOpenMap.of());
}
private Entry(Snapshot snapshot, boolean includeGlobalState, boolean partial, State state, List indices,
- List dataStreams, long startTime, long repositoryStateId,
- ImmutableOpenMap shards, String failure, Map userMetadata,
- Version version, @Nullable SnapshotId source,
- @Nullable ImmutableOpenMap clones) {
+ List dataStreams, List featureStates, long startTime, long repositoryStateId,
+ ImmutableOpenMap shards, String failure, Map userMetadata,
+ Version version, @Nullable SnapshotId source,
+ @Nullable ImmutableOpenMap clones) {
this.state = state;
this.snapshot = snapshot;
this.includeGlobalState = includeGlobalState;
this.partial = partial;
this.indices = indices;
this.dataStreams = dataStreams;
+ this.featureStates = Collections.unmodifiableList(featureStates);
this.startTime = startTime;
this.shards = shards;
this.repositoryStateId = repositoryStateId;
@@ -195,6 +199,11 @@ private Entry(StreamInput in) throws IOException {
source = null;
clones = ImmutableOpenMap.of();
}
+ if (in.getVersion().onOrAfter(FEATURE_STATES_VERSION)) {
+ featureStates = Collections.unmodifiableList(in.readList(SnapshotFeatureInfo::new));
+ } else {
+ featureStates = Collections.emptyList();
+ }
}
private static boolean assertShardsConsistent(SnapshotId source, State state, List indices,
@@ -229,8 +238,8 @@ assert hasFailures(clones) == false || state == State.FAILED
public Entry withRepoGen(long newRepoGen) {
assert newRepoGen > repositoryStateId : "Updated repository generation [" + newRepoGen
+ "] must be higher than current generation [" + repositoryStateId + "]";
- return new Entry(snapshot, includeGlobalState, partial, state, indices, dataStreams, startTime, newRepoGen, shards, failure,
- userMetadata, version, source, clones);
+ return new Entry(snapshot, includeGlobalState, partial, state, indices, dataStreams, featureStates, startTime, newRepoGen,
+ shards, failure, userMetadata, version, source, clones);
}
public Entry withClones(ImmutableOpenMap updatedClones) {
@@ -239,8 +248,8 @@ public Entry withClones(ImmutableOpenMap
}
return new Entry(snapshot, includeGlobalState, partial,
completed(updatedClones.values()) ? (hasFailures(updatedClones) ? State.FAILED : State.SUCCESS) :
- state, indices, dataStreams, startTime, repositoryStateId, shards, failure, userMetadata, version, source,
- updatedClones);
+ state, indices, dataStreams, featureStates, startTime, repositoryStateId, shards, failure, userMetadata,
+ version, source, updatedClones);
}
/**
@@ -276,8 +285,8 @@ public Entry abort() {
}
public Entry fail(ImmutableOpenMap shards, State state, String failure) {
- return new Entry(snapshot, includeGlobalState, partial, state, indices, dataStreams, startTime, repositoryStateId, shards,
- failure, userMetadata, version, source, clones);
+ return new Entry(snapshot, includeGlobalState, partial, state, indices, dataStreams, featureStates, startTime,
+ repositoryStateId, shards, failure, userMetadata, version, source, clones);
}
/**
@@ -290,8 +299,8 @@ public Entry fail(ImmutableOpenMap shards, State s
*/
public Entry withShardStates(ImmutableOpenMap shards) {
if (completed(shards.values())) {
- return new Entry(snapshot, includeGlobalState, partial, State.SUCCESS, indices, dataStreams, startTime, repositoryStateId,
- shards, failure, userMetadata, version);
+ return new Entry(snapshot, includeGlobalState, partial, State.SUCCESS, indices, dataStreams, featureStates,
+ startTime, repositoryStateId, shards, failure, userMetadata, version);
}
return withStartedShards(shards);
}
@@ -302,7 +311,7 @@ public Entry withShardStates(ImmutableOpenMap shar
*/
public Entry withStartedShards(ImmutableOpenMap shards) {
final SnapshotsInProgress.Entry updated = new Entry(snapshot, includeGlobalState, partial, state, indices, dataStreams,
- startTime, repositoryStateId, shards, failure, userMetadata, version);
+ featureStates, startTime, repositoryStateId, shards, failure, userMetadata, version);
assert updated.state().completed() == false && completed(updated.shards().values()) == false
: "Only running snapshots allowed but saw [" + updated + "]";
return updated;
@@ -349,6 +358,10 @@ public List dataStreams() {
return dataStreams;
}
+ public List featureStates() {
+ return featureStates;
+ }
+
@Override
public long repositoryStateId() {
return repositoryStateId;
@@ -399,6 +412,7 @@ public boolean equals(Object o) {
if (version.equals(entry.version) == false) return false;
if (Objects.equals(source, ((Entry) o).source) == false) return false;
if (clones.equals(((Entry) o).clones) == false) return false;
+ if (featureStates.equals(entry.featureStates) == false) return false;
return true;
}
@@ -419,6 +433,7 @@ public int hashCode() {
result = 31 * result + version.hashCode();
result = 31 * result + (source == null ? 0 : source.hashCode());
result = 31 * result + clones.hashCode();
+ result = 31 * result + featureStates.hashCode();
return result;
}
@@ -461,6 +476,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
}
}
builder.endArray();
+ builder.startArray(FEATURE_STATES);
+ {
+ for (SnapshotFeatureInfo featureState : featureStates) {
+ featureState.toXContent(builder, params);
+ }
+ }
+ builder.endArray();
if (isClone()) {
builder.field(SOURCE, source);
builder.startArray(CLONES);
@@ -503,6 +525,9 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeOptionalWriteable(source);
out.writeMap(clones);
}
+ if (out.getVersion().onOrAfter(FEATURE_STATES_VERSION)) {
+ out.writeList(featureStates);
+ }
}
@Override
@@ -804,6 +829,7 @@ public void writeTo(StreamOutput out) throws IOException {
private static final String INDEX = "index";
private static final String SHARD = "shard";
private static final String NODE = "node";
+ private static final String FEATURE_STATES = "feature_states";
@Override
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java
index 6bc2cf1d5d438..1a40e915e39c2 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java
@@ -125,6 +125,11 @@ public String[] concreteIndexNames(ClusterState state, IndicesOptions options, I
return concreteIndexNames(context, request.indices());
}
+ public String[] concreteIndexNamesWithSystemIndexAccess(ClusterState state, IndicesOptions options, String... indexExpressions) {
+ Context context = new Context(state, options, true);
+ return concreteIndexNames(context, indexExpressions);
+ }
+
public List dataStreamNames(ClusterState state, IndicesOptions options, String... indexExpressions) {
// Allow system index access - they'll be filtered out below as there's no such thing (yet) as system data streams
Context context = new Context(state, options, false, false, true, true, true);
diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java
index 3fddd19f7d158..4f79f8a5a212e 100644
--- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java
+++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java
@@ -866,19 +866,20 @@ public void afterIndexShardClosed(ShardId shardId, IndexShard indexShard, Settin
* but does not deal with in-memory structures. For those call {@link #removeIndex(Index, IndexRemovalReason, String)}
*/
@Override
- public void deleteUnassignedIndex(String reason, IndexMetadata metadata, ClusterState clusterState) {
+ public void deleteUnassignedIndex(String reason, IndexMetadata oldIndexMetadata, ClusterState clusterState) {
if (nodeEnv.hasNodeFile()) {
- String indexName = metadata.getIndex().getName();
+ Index index = oldIndexMetadata.getIndex();
try {
- if (clusterState.metadata().hasIndex(indexName)) {
- final IndexMetadata index = clusterState.metadata().index(indexName);
- throw new IllegalStateException("Can't delete unassigned index store for [" + indexName + "] - it's still part of " +
- "the cluster state [" + index.getIndexUUID() + "] [" + metadata.getIndexUUID() + "]");
+ if (clusterState.metadata().hasIndex(index)) {
+ final IndexMetadata currentMetadata = clusterState.metadata().index(index);
+ throw new IllegalStateException("Can't delete unassigned index store for [" + index.getName() + "] - it's still part " +
+ "of the cluster state [" + currentMetadata.getIndexUUID() + "] [" +
+ oldIndexMetadata.getIndexUUID() + "]");
}
- deleteIndexStore(reason, metadata);
+ deleteIndexStore(reason, oldIndexMetadata);
} catch (Exception e) {
logger.warn(() -> new ParameterizedMessage("[{}] failed to delete unassigned index (reason [{}])",
- metadata.getIndex(), reason), e);
+ oldIndexMetadata.getIndex(), reason), e);
}
}
}
diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndexDescriptor.java b/server/src/main/java/org/elasticsearch/indices/SystemIndexDescriptor.java
index 728d110a59899..d880416868ba0 100644
--- a/server/src/main/java/org/elasticsearch/indices/SystemIndexDescriptor.java
+++ b/server/src/main/java/org/elasticsearch/indices/SystemIndexDescriptor.java
@@ -14,10 +14,14 @@
import org.apache.lucene.util.automaton.RegExp;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -188,6 +192,26 @@ public boolean matchesIndexPattern(String index) {
return indexPatternAutomaton.run(index);
}
+ /**
+ * Retrieves a list of all indices which match this descriptor's pattern.
+ *
+ * This cannot be done via {@link org.elasticsearch.cluster.metadata.IndexNameExpressionResolver} because that class can only handle
+ * simple wildcard expressions, but system index name patterns may use full Lucene regular expression syntax,
+ *
+ * @param metadata The current metadata to get the list of matching indices from
+ * @return A list of index names that match this descriptor
+ */
+ public List getMatchingIndices(Metadata metadata) {
+ ArrayList matchingIndices = new ArrayList<>();
+ metadata.indices().keysIt().forEachRemaining(indexName -> {
+ if (matchesIndexPattern(indexName)) {
+ matchingIndices.add(indexName);
+ }
+ });
+
+ return Collections.unmodifiableList(matchingIndices);
+ }
+
/**
* @return A short description of the purpose of this system index.
*/
diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java
index cd97756b16429..9eda85e963db5 100644
--- a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java
+++ b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java
@@ -16,9 +16,10 @@
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.index.Index;
-import org.elasticsearch.tasks.TaskResultsService;
+import org.elasticsearch.snapshots.SnapshotsService;
import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -28,6 +29,7 @@
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.elasticsearch.tasks.TaskResultsService.TASKS_DESCRIPTOR;
+import static org.elasticsearch.tasks.TaskResultsService.TASKS_FEATURE_NAME;
/**
* This class holds the {@link SystemIndexDescriptor} objects that represent system indices the
@@ -35,19 +37,18 @@
* to reduce the locations within the code that need to deal with {@link SystemIndexDescriptor}s.
*/
public class SystemIndices {
- private static final Map> SERVER_SYSTEM_INDEX_DESCRIPTORS = Map.of(
- TaskResultsService.class.getName(), List.of(TASKS_DESCRIPTOR)
+ private static final Map SERVER_SYSTEM_INDEX_DESCRIPTORS = Map.of(
+ TASKS_FEATURE_NAME, new Feature("Manages task results", List.of(TASKS_DESCRIPTOR))
);
private final CharacterRunAutomaton runAutomaton;
- private final Collection systemIndexDescriptors;
-
- public SystemIndices(Map> pluginAndModulesDescriptors) {
- final Map> descriptorsMap = buildSystemIndexDescriptorMap(pluginAndModulesDescriptors);
- checkForOverlappingPatterns(descriptorsMap);
- this.systemIndexDescriptors = descriptorsMap.values().stream().flatMap(Collection::stream).collect(Collectors.toUnmodifiableList());
- checkForDuplicateAliases(this.systemIndexDescriptors);
- this.runAutomaton = buildCharacterRunAutomaton(systemIndexDescriptors);
+ private final Map featureDescriptors;
+
+ public SystemIndices(Map pluginAndModulesDescriptors) {
+ featureDescriptors = buildSystemIndexDescriptorMap(pluginAndModulesDescriptors);
+ checkForOverlappingPatterns(featureDescriptors);
+ checkForDuplicateAliases(this.getSystemIndexDescriptors());
+ this.runAutomaton = buildCharacterRunAutomaton(featureDescriptors);
}
private void checkForDuplicateAliases(Collection descriptors) {
@@ -97,7 +98,8 @@ public boolean isSystemIndex(String indexName) {
* @throws IllegalStateException if multiple descriptors match the name
*/
public @Nullable SystemIndexDescriptor findMatchingDescriptor(String name) {
- final List matchingDescriptors = systemIndexDescriptors.stream()
+ final List matchingDescriptors = featureDescriptors.values().stream()
+ .flatMap(feature -> feature.getIndexDescriptors().stream())
.filter(descriptor -> descriptor.matchesIndexPattern(name))
.collect(toUnmodifiableList());
@@ -120,8 +122,13 @@ public boolean isSystemIndex(String indexName) {
}
}
- private static CharacterRunAutomaton buildCharacterRunAutomaton(Collection descriptors) {
- Optional automaton = descriptors.stream()
+ public Map getFeatures() {
+ return featureDescriptors;
+ }
+
+ private static CharacterRunAutomaton buildCharacterRunAutomaton(Map descriptors) {
+ Optional automaton = descriptors.values().stream()
+ .flatMap(feature -> feature.getIndexDescriptors().stream())
.map(descriptor -> SystemIndexDescriptor.buildAutomaton(descriptor.getIndexPattern(), descriptor.getAliasName()))
.reduce(Operations::union);
return new CharacterRunAutomaton(MinimizationOperations.minimize(automaton.orElse(Automata.makeEmpty()), Integer.MAX_VALUE));
@@ -134,9 +141,9 @@ private static CharacterRunAutomaton buildCharacterRunAutomaton(Collection> sourceToDescriptors) {
+ static void checkForOverlappingPatterns(Map sourceToDescriptors) {
List> sourceDescriptorPair = sourceToDescriptors.entrySet().stream()
- .flatMap(entry -> entry.getValue().stream().map(descriptor -> new Tuple<>(entry.getKey(), descriptor)))
+ .flatMap(entry -> entry.getValue().getIndexDescriptors().stream().map(descriptor -> new Tuple<>(entry.getKey(), descriptor)))
.sorted(Comparator.comparing(d -> d.v1() + ":" + d.v2().getIndexPattern())) // Consistent ordering -> consistent error message
.collect(Collectors.toUnmodifiableList());
@@ -165,14 +172,12 @@ private static boolean overlaps(SystemIndexDescriptor a1, SystemIndexDescriptor
return Operations.isEmpty(Operations.intersection(a1Automaton, a2Automaton)) == false;
}
- private static Map> buildSystemIndexDescriptorMap(
- Map> pluginAndModulesMap) {
- final Map> map =
- new HashMap<>(pluginAndModulesMap.size() + SERVER_SYSTEM_INDEX_DESCRIPTORS.size());
- map.putAll(pluginAndModulesMap);
+ private static Map buildSystemIndexDescriptorMap(Map featuresMap) {
+ final Map map = new HashMap<>(featuresMap.size() + SERVER_SYSTEM_INDEX_DESCRIPTORS.size());
+ map.putAll(featuresMap);
// put the server items last since we expect less of them
- SERVER_SYSTEM_INDEX_DESCRIPTORS.forEach((source, descriptors) -> {
- if (map.putIfAbsent(source, descriptors) != null) {
+ SERVER_SYSTEM_INDEX_DESCRIPTORS.forEach((source, feature) -> {
+ if (map.putIfAbsent(source, feature) != null) {
throw new IllegalArgumentException("plugin or module attempted to define the same source [" + source +
"] as a built-in system index");
}
@@ -181,6 +186,43 @@ private static Map> buildSystemIndexDe
}
Collection getSystemIndexDescriptors() {
- return this.systemIndexDescriptors;
+ return this.featureDescriptors.values().stream()
+ .flatMap(f -> f.getIndexDescriptors().stream())
+ .collect(Collectors.toList());
+ }
+
+ public static void validateFeatureName(String name, String plugin) {
+ if (SnapshotsService.NO_FEATURE_STATES_VALUE.equalsIgnoreCase(name)) {
+ throw new IllegalArgumentException("feature name cannot be reserved name [\"" + SnapshotsService.NO_FEATURE_STATES_VALUE +
+ "\"], but was for plugin [" + plugin + "]");
+ }
+ }
+
+ public static class Feature {
+ private final String description;
+ private final Collection indexDescriptors;
+ private final Collection associatedIndexPatterns;
+
+ public Feature(String description, Collection indexDescriptors, Collection associatedIndexPatterns) {
+ this.description = description;
+ this.indexDescriptors = indexDescriptors;
+ this.associatedIndexPatterns = associatedIndexPatterns;
+ }
+
+ public Feature(String description, Collection indexDescriptors) {
+ this(description, indexDescriptors, Collections.emptyList());
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Collection getIndexDescriptors() {
+ return indexDescriptors;
+ }
+
+ public Collection getAssociatedIndexPatterns() {
+ return associatedIndexPatterns;
+ }
}
}
diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java
index f0ecdf761a8d6..cb51b0d7ccae8 100644
--- a/server/src/main/java/org/elasticsearch/node/Node.java
+++ b/server/src/main/java/org/elasticsearch/node/Node.java
@@ -99,7 +99,6 @@
import org.elasticsearch.indices.IndicesModule;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.indices.ShardLimitValidator;
-import org.elasticsearch.indices.SystemIndexDescriptor;
import org.elasticsearch.indices.SystemIndexManager;
import org.elasticsearch.indices.SystemIndices;
import org.elasticsearch.indices.analysis.AnalysisModule;
@@ -496,13 +495,19 @@ protected Node(final Environment initialEnvironment,
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
- final Map> systemIndexDescriptorMap = pluginsService
+ final Map featuresMap = pluginsService
.filterPlugins(SystemIndexPlugin.class)
.stream()
+ .peek(plugin -> SystemIndices.validateFeatureName(plugin.getFeatureName(), plugin.getClass().getCanonicalName()))
.collect(Collectors.toUnmodifiableMap(
- plugin -> plugin.getClass().getSimpleName(),
- plugin -> plugin.getSystemIndexDescriptors(settings)));
- final SystemIndices systemIndices = new SystemIndices(systemIndexDescriptorMap);
+ plugin -> plugin.getFeatureName(),
+ plugin -> new SystemIndices.Feature(
+ plugin.getFeatureDescription(),
+ plugin.getSystemIndexDescriptors(settings),
+ plugin.getAssociatedIndexPatterns()
+ ))
+ );
+ final SystemIndices systemIndices = new SystemIndices(featuresMap);
final SystemIndexManager systemIndexManager = new SystemIndexManager(systemIndices, client);
clusterService.addListener(systemIndexManager);
@@ -592,11 +597,13 @@ protected Node(final Environment initialEnvironment,
RepositoriesService repositoryService = repositoriesModule.getRepositoryService();
repositoriesServiceReference.set(repositoryService);
SnapshotsService snapshotsService = new SnapshotsService(settings, clusterService,
- clusterModule.getIndexNameExpressionResolver(), repositoryService, transportService, actionModule.getActionFilters());
+ clusterModule.getIndexNameExpressionResolver(), repositoryService, transportService, actionModule.getActionFilters(),
+ systemIndices.getFeatures());
SnapshotShardsService snapshotShardsService = new SnapshotShardsService(settings, clusterService, repositoryService,
transportService, indicesService);
RestoreService restoreService = new RestoreService(clusterService, repositoryService, clusterModule.getAllocationService(),
- metadataCreateIndexService, indexMetadataVerifier, shardLimitValidator);
+ metadataCreateIndexService, clusterModule.getMetadataDeleteIndexService(), indexMetadataVerifier,
+ shardLimitValidator, systemIndices);
final DiskThresholdMonitor diskThresholdMonitor = new DiskThresholdMonitor(settings, clusterService::state,
clusterService.getClusterSettings(), client, threadPool::relativeTimeInMillis, rerouteService);
clusterInfoService.addListener(diskThresholdMonitor::onNewInfo);
diff --git a/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java b/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java
index 861677e61e58f..c3a4a56f24ba1 100644
--- a/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java
+++ b/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java
@@ -29,4 +29,24 @@ public interface SystemIndexPlugin extends ActionPlugin {
default Collection getSystemIndexDescriptors(Settings settings) {
return Collections.emptyList();
}
+
+ /**
+ * @return The name of the feature, as used for specifying feature states in snapshot creation and restoration.
+ */
+ String getFeatureName();
+
+ /**
+ * @return A description of the feature, as used for the Get Snapshottable Features API.
+ */
+ String getFeatureDescription();
+
+ /**
+ * Returns a list of index patterns for "associated indices": indices which depend on this plugin's system indices, but are not
+ * themselves system indices.
+ *
+ * @return A list of index patterns which depend on the contents of this plugin's system indices, but are not themselves system indices
+ */
+ default Collection getAssociatedIndexPatterns() {
+ return Collections.emptyList();
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestSnapshottableFeaturesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestSnapshottableFeaturesAction.java
new file mode 100644
index 0000000000000..50092106dd32b
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestSnapshottableFeaturesAction.java
@@ -0,0 +1,41 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.rest.action.admin.cluster;
+
+import org.elasticsearch.action.admin.cluster.snapshots.features.GetSnapshottableFeaturesRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.features.SnapshottableFeaturesAction;
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.rest.BaseRestHandler;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.RestToXContentListener;
+
+import java.io.IOException;
+import java.util.List;
+
+public class RestSnapshottableFeaturesAction extends BaseRestHandler {
+ @Override
+ public List routes() {
+ return List.of(new Route(RestRequest.Method.GET, "/_snapshottable_features"));
+ }
+
+ @Override
+ public String getName() {
+ return "get_snapshottable_features";
+ }
+
+ @Override
+ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
+ final GetSnapshottableFeaturesRequest req = new GetSnapshottableFeaturesRequest();
+ req.masterNodeTimeout(request.paramAsTime("master_timeout", req.masterNodeTimeout()));
+
+ return restChannel -> {
+ client.execute(SnapshottableFeaturesAction.INSTANCE, req, new RestToXContentListener<>(restChannel));
+ };
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java
index b7d924da2310d..926dc140b7251 100644
--- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java
+++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java
@@ -40,6 +40,7 @@
import org.elasticsearch.cluster.metadata.IndexTemplateMetadata;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.metadata.MetadataCreateIndexService;
+import org.elasticsearch.cluster.metadata.MetadataDeleteIndexService;
import org.elasticsearch.cluster.metadata.MetadataIndexStateService;
import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
import org.elasticsearch.cluster.node.DiscoveryNode;
@@ -55,6 +56,8 @@
import org.elasticsearch.common.Priority;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.collect.ImmutableOpenMap;
+import org.elasticsearch.common.logging.DeprecationCategory;
+import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.lucene.Lucene;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.settings.ClusterSettings;
@@ -66,6 +69,7 @@
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.indices.ShardLimitValidator;
+import org.elasticsearch.indices.SystemIndices;
import org.elasticsearch.repositories.IndexId;
import org.elasticsearch.repositories.RepositoriesService;
import org.elasticsearch.repositories.Repository;
@@ -81,6 +85,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
@@ -99,6 +104,8 @@
import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_VERSION_CREATED;
import static org.elasticsearch.common.util.set.Sets.newHashSet;
import static org.elasticsearch.snapshots.SnapshotUtils.filterIndices;
+import static org.elasticsearch.snapshots.SnapshotsService.FEATURE_STATES_VERSION;
+import static org.elasticsearch.snapshots.SnapshotsService.NO_FEATURE_STATES_VALUE;
/**
* Service responsible for restoring snapshots
@@ -123,6 +130,7 @@
public class RestoreService implements ClusterStateApplier {
private static final Logger logger = LogManager.getLogger(RestoreService.class);
+ private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestoreService.class);
public static final Setting REFRESH_REPO_UUID_ON_RESTORE_SETTING = Setting.boolSetting(
"snapshot.refresh_repo_uuid_on_restore",
@@ -158,27 +166,40 @@ public class RestoreService implements ClusterStateApplier {
private final IndexMetadataVerifier indexMetadataVerifier;
+ private final MetadataDeleteIndexService metadataDeleteIndexService;
+
private final ShardLimitValidator shardLimitValidator;
private final ClusterSettings clusterSettings;
+ private final SystemIndices systemIndices;
+
private volatile boolean refreshRepositoryUuidOnRestore;
private static final CleanRestoreStateTaskExecutor cleanRestoreStateTaskExecutor = new CleanRestoreStateTaskExecutor();
- public RestoreService(ClusterService clusterService, RepositoriesService repositoriesService,
- AllocationService allocationService, MetadataCreateIndexService createIndexService,
- IndexMetadataVerifier indexMetadataVerifier, ShardLimitValidator shardLimitValidator) {
+ public RestoreService(
+ ClusterService clusterService,
+ RepositoriesService repositoriesService,
+ AllocationService allocationService,
+ MetadataCreateIndexService createIndexService,
+ MetadataDeleteIndexService metadataDeleteIndexService,
+ IndexMetadataVerifier indexMetadataVerifier,
+ ShardLimitValidator shardLimitValidator,
+ SystemIndices systemIndices
+ ) {
this.clusterService = clusterService;
this.repositoriesService = repositoriesService;
this.allocationService = allocationService;
this.createIndexService = createIndexService;
this.indexMetadataVerifier = indexMetadataVerifier;
+ this.metadataDeleteIndexService = metadataDeleteIndexService;
if (DiscoveryNode.isMasterNode(clusterService.getSettings())) {
clusterService.addStateApplier(this);
}
this.clusterSettings = clusterService.getClusterSettings();
this.shardLimitValidator = shardLimitValidator;
+ this.systemIndices = systemIndices;
this.refreshRepositoryUuidOnRestore = REFRESH_REPO_UUID_ON_RESTORE_SETTING.get(clusterService.getSettings());
clusterService.getClusterSettings().addSettingsUpdateConsumer(
REFRESH_REPO_UUID_ON_RESTORE_SETTING,
@@ -238,55 +259,74 @@ public void restoreSnapshot(final RestoreSnapshotRequest request,
// Make sure that we can restore from this snapshot
validateSnapshotRestorable(repositoryName, snapshotInfo);
+ // Get the global state if necessary
Metadata globalMetadata = null;
- // Resolve the indices from the snapshot that need to be restored
- Map dataStreams;
- List requestIndices = new ArrayList<>(Arrays.asList(request.indices()));
-
- List requestedDataStreams = filterIndices(snapshotInfo.dataStreams(), requestIndices.toArray(String[]::new),
- IndicesOptions.fromOptions(true, true, true, true));
- if (requestedDataStreams.isEmpty()) {
- dataStreams = new HashMap<>();
- } else {
+ final Metadata.Builder metadataBuilder;
+ if (request.includeGlobalState()) {
globalMetadata = repository.getSnapshotGlobalMetadata(snapshotId);
- final Map dataStreamsInSnapshot = globalMetadata.dataStreams();
- dataStreams = new HashMap<>(requestedDataStreams.size());
- for (String requestedDataStream : requestedDataStreams) {
- final DataStream dataStreamInSnapshot = dataStreamsInSnapshot.get(requestedDataStream);
- assert dataStreamInSnapshot != null : "DataStream [" + requestedDataStream + "] not found in snapshot";
- dataStreams.put(requestedDataStream, dataStreamInSnapshot);
- }
+ metadataBuilder = Metadata.builder(globalMetadata);
+ } else {
+ metadataBuilder = Metadata.builder();
}
- requestIndices.removeAll(dataStreams.keySet());
- Set dataStreamIndices = dataStreams.values().stream()
+
+ List requestIndices = new ArrayList<>(Arrays.asList(request.indices()));
+
+ // Get data stream metadata for requested data streams
+ Map dataStreamsToRestore = getDataStreamsToRestore(repository, snapshotId, snapshotInfo, globalMetadata,
+ requestIndices);
+
+ // Remove the data streams from the list of requested indices
+ requestIndices.removeAll(dataStreamsToRestore.keySet());
+
+ // And add the backing indices
+ Set dataStreamIndices = dataStreamsToRestore.values().stream()
.flatMap(ds -> ds.getIndices().stream())
.map(Index::getName)
.collect(Collectors.toSet());
requestIndices.addAll(dataStreamIndices);
- final List indicesInSnapshot = filterIndices(snapshotInfo.indices(), requestIndices.toArray(String[]::new),
+ // Determine system indices to restore from requested feature states
+ final Map> featureStatesToRestore = getFeatureStatesToRestore(request, snapshotInfo, snapshot);
+ final Set featureStateIndices = featureStatesToRestore.values().stream()
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+
+ // Resolve the indices that were directly requested
+ final List requestedIndicesInSnapshot = filterIndices(snapshotInfo.indices(), requestIndices.toArray(String[]::new),
request.indicesOptions());
- final Metadata.Builder metadataBuilder;
- if (request.includeGlobalState()) {
- if (globalMetadata == null) {
- globalMetadata = repository.getSnapshotGlobalMetadata(snapshotId);
+ // Combine into the final list of indices to be restored
+ final List requestedIndicesIncludingSystem = Stream.concat(
+ requestedIndicesInSnapshot.stream(),
+ featureStateIndices.stream()
+ ).distinct().collect(Collectors.toList());
+
+ final Set explicitlyRequestedSystemIndices = new HashSet<>();
+ final List indexIdsInSnapshot = repositoryData.resolveIndices(requestedIndicesIncludingSystem);
+ for (IndexId indexId : indexIdsInSnapshot) {
+ IndexMetadata snapshotIndexMetaData = repository.getSnapshotIndexMetaData(repositoryData, snapshotId, indexId);
+ if (snapshotIndexMetaData.isSystem()) {
+ if (requestedIndicesInSnapshot.contains(indexId.getName())) {
+ explicitlyRequestedSystemIndices.add(indexId.getName());
+ }
}
- metadataBuilder = Metadata.builder(globalMetadata);
- } else {
- metadataBuilder = Metadata.builder();
+ metadataBuilder.put(snapshotIndexMetaData, false);
}
- final List indexIdsInSnapshot = repositoryData.resolveIndices(indicesInSnapshot);
- for (IndexId indexId : indexIdsInSnapshot) {
- metadataBuilder.put(repository.getSnapshotIndexMetaData(repositoryData, snapshotId, indexId), false);
+ // log a deprecation warning if the any of the indexes to delete were included in the request and the snapshot
+ // is from a version that should have feature states
+ if (snapshotInfo.version().onOrAfter(FEATURE_STATES_VERSION) && explicitlyRequestedSystemIndices.isEmpty() == false) {
+ deprecationLogger.deprecate(DeprecationCategory.API, "restore-system-index-from-snapshot",
+ "Restoring system indices by name is deprecated. Use feature states instead. System indices: "
+ + explicitlyRequestedSystemIndices);
}
- final Metadata metadata = metadataBuilder.dataStreams(dataStreams).build();
+ final Metadata metadata = metadataBuilder.dataStreams(dataStreamsToRestore).build();
// Apply renaming on index names, returning a map of names where
// the key is the renamed index and the value is the original name
- final Map indices = renamedIndices(request, indicesInSnapshot, dataStreamIndices);
+ final Map indices = renamedIndices(request, requestedIndicesIncludingSystem, dataStreamIndices,
+ featureStateIndices);
// Now we can start the actual restore process by adding shards to be recovered in the cluster state
// and updating cluster metadata (global and index) as needed
@@ -306,6 +346,13 @@ public ClusterState execute(ClusterState currentState) {
deletionsInProgress.getEntries().get(0) + "]");
}
+ // Clear out all existing indices which fall within a system index pattern being restored
+ final Set systemIndicesToDelete = resolveSystemIndicesToDelete(
+ currentState,
+ featureStatesToRestore.keySet()
+ );
+ currentState = metadataDeleteIndexService.deleteIndices(currentState, systemIndicesToDelete);
+
// Updating cluster state
ClusterState.Builder builder = ClusterState.builder(currentState);
Metadata.Builder mdBuilder = Metadata.builder(currentState.metadata());
@@ -355,7 +402,8 @@ public ClusterState execute(ClusterState currentState) {
.put(IndexMetadata.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()))
.timestampRange(IndexLongFieldRange.NO_SHARDS);
shardLimitValidator.validateShardLimit(snapshotIndexMetadata.getSettings(), currentState);
- if (request.includeAliases() == false && snapshotIndexMetadata.getAliases().isEmpty() == false) {
+ if (request.includeAliases() == false && snapshotIndexMetadata.getAliases().isEmpty() == false
+ && isSystemIndex(snapshotIndexMetadata) == false) {
// Remove all aliases - they shouldn't be restored
indexMdBuilder.removeAllAliases();
} else {
@@ -393,7 +441,7 @@ public ClusterState execute(ClusterState currentState) {
Math.max(snapshotIndexMetadata.primaryTerm(shard), currentIndexMetadata.primaryTerm(shard)));
}
- if (request.includeAliases() == false) {
+ if (request.includeAliases() == false && isSystemIndex(snapshotIndexMetadata) == false) {
// Remove all snapshot aliases
if (snapshotIndexMetadata.getAliases().isEmpty() == false) {
indexMdBuilder.removeAllAliases();
@@ -445,7 +493,7 @@ restoreUUID, snapshot, overallState(RestoreInProgress.State.INIT, shards),
checkAliasNameConflicts(indices, aliases);
Map updatedDataStreams = new HashMap<>(currentState.metadata().dataStreams());
- updatedDataStreams.putAll(dataStreams.values().stream()
+ updatedDataStreams.putAll(dataStreamsToRestore.values().stream()
.map(ds -> updateDataStream(ds, mdBuilder, request))
.collect(Collectors.toMap(DataStream::getName, Function.identity())));
mdBuilder.dataStreams(updatedDataStreams);
@@ -706,6 +754,105 @@ public void onFailure(Exception e) {
}
+ private boolean isSystemIndex(IndexMetadata indexMetadata) {
+ return indexMetadata.isSystem() || systemIndices.isSystemIndex(indexMetadata.getIndex());
+ }
+
+ private Map getDataStreamsToRestore(Repository repository, SnapshotId snapshotId, SnapshotInfo snapshotInfo,
+ Metadata globalMetadata, List requestIndices) {
+ Map dataStreams;
+ List requestedDataStreams = filterIndices(snapshotInfo.dataStreams(), requestIndices.toArray(String[]::new),
+ IndicesOptions.fromOptions(true, true, true, true));
+ if (requestedDataStreams.isEmpty()) {
+ dataStreams = Collections.emptyMap();
+ } else {
+ if (globalMetadata == null) {
+ globalMetadata = repository.getSnapshotGlobalMetadata(snapshotId);
+ }
+ final Map dataStreamsInSnapshot = globalMetadata.dataStreams();
+ dataStreams = new HashMap<>(requestedDataStreams.size());
+ for (String requestedDataStream : requestedDataStreams) {
+ final DataStream dataStreamInSnapshot = dataStreamsInSnapshot.get(requestedDataStream);
+ assert dataStreamInSnapshot != null : "DataStream [" + requestedDataStream + "] not found in snapshot";
+ dataStreams.put(requestedDataStream, dataStreamInSnapshot);
+ }
+ }
+ return dataStreams;
+ }
+
+ private Map> getFeatureStatesToRestore(RestoreSnapshotRequest request, SnapshotInfo snapshotInfo,
+ Snapshot snapshot) {
+ if (snapshotInfo.featureStates() == null) {
+ return Collections.emptyMap();
+ }
+ final Map> snapshotFeatureStates = snapshotInfo.featureStates().stream()
+ .collect(Collectors.toMap(SnapshotFeatureInfo::getPluginName, SnapshotFeatureInfo::getIndices));
+
+ final Map> featureStatesToRestore;
+ final String[] requestedFeatureStates = request.featureStates();
+
+ if (requestedFeatureStates == null || requestedFeatureStates.length == 0) {
+ // Handle the default cases - defer to the global state value
+ if (request.includeGlobalState()) {
+ featureStatesToRestore = new HashMap<>(snapshotFeatureStates);
+ } else {
+ featureStatesToRestore = Collections.emptyMap();
+ }
+ } else if (requestedFeatureStates.length == 1 && NO_FEATURE_STATES_VALUE.equalsIgnoreCase(requestedFeatureStates[0])) {
+ // If there's exactly one value and it's "none", include no states
+ featureStatesToRestore = Collections.emptyMap();
+ } else {
+ // Otherwise, handle the list of requested feature states
+ final Set requestedStates = Set.of(requestedFeatureStates);
+ if (requestedStates.contains(NO_FEATURE_STATES_VALUE)) {
+ throw new SnapshotRestoreException(snapshot, "the feature_states value [" + NO_FEATURE_STATES_VALUE +
+ "] indicates that no feature states should be restored, but other feature states were requested: " + requestedStates);
+ }
+ if (snapshotFeatureStates.keySet().containsAll(requestedStates) == false) {
+ Set nonExistingRequestedStates = new HashSet<>(requestedStates);
+ nonExistingRequestedStates.removeAll(snapshotFeatureStates.keySet());
+ throw new SnapshotRestoreException(snapshot, "requested feature states [" + nonExistingRequestedStates +
+ "] are not present in snapshot");
+ }
+ featureStatesToRestore = new HashMap<>(snapshotFeatureStates);
+ featureStatesToRestore.keySet().retainAll(requestedStates);
+ }
+
+ final List featuresNotOnThisNode = featureStatesToRestore.keySet().stream()
+ .filter(featureName -> systemIndices.getFeatures().containsKey(featureName) == false)
+ .collect(Collectors.toList());
+ if (featuresNotOnThisNode.isEmpty() == false) {
+ throw new SnapshotRestoreException(snapshot, "requested feature states " + featuresNotOnThisNode + " are present in " +
+ "snapshot but those features are not installed on the current master node");
+ }
+ return featureStatesToRestore;
+ }
+
+ /**
+ * Resolves a set of index names that currently exist in the cluster that are part of a feature state which is about to be restored,
+ * and should therefore be removed prior to restoring those feature states from the snapshot.
+ *
+ * @param currentState The current cluster state
+ * @param featureStatesToRestore A set of feature state names that are about to be restored
+ * @return A set of index names that should be removed based on the feature states being restored
+ */
+ private Set resolveSystemIndicesToDelete(ClusterState currentState, Set featureStatesToRestore) {
+ if (featureStatesToRestore == null) {
+ return Collections.emptySet();
+ }
+
+ return featureStatesToRestore.stream()
+ .map(featureName -> systemIndices.getFeatures().get(featureName))
+ .filter(Objects::nonNull) // Features that aren't present on this node will be warned about in `getFeatureStatesToRestore`
+ .flatMap(feature -> feature.getIndexDescriptors().stream())
+ .flatMap(descriptor -> descriptor.getMatchingIndices(currentState.metadata()).stream())
+ .map(indexName -> {
+ assert currentState.metadata().hasIndex(indexName) : "index [" + indexName + "] not found in metadata but must be present";
+ return currentState.metadata().getIndices().get(indexName).getIndex();
+ })
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
//visible for testing
static DataStream updateDataStream(DataStream dataStream, Metadata.Builder metadata, RestoreSnapshotRequest request) {
String dataStreamName = dataStream.getName();
@@ -979,10 +1126,16 @@ public static int failedShards(ImmutableOpenMap renamedIndices(RestoreSnapshotRequest request, List filteredIndices,
- Set dataStreamIndices) {
+ Set dataStreamIndices, Set featureIndices) {
Map renamedIndices = new HashMap<>();
for (String index : filteredIndices) {
- String renamedIndex = renameIndex(index, request, dataStreamIndices.contains(index));
+ String renamedIndex;
+ if (featureIndices.contains(index)) {
+ // Don't rename system indices
+ renamedIndex = index;
+ } else {
+ renamedIndex = renameIndex(index, request, dataStreamIndices.contains(index));
+ }
String previousIndex = renamedIndices.put(renamedIndex, index);
if (previousIndex != null) {
throw new SnapshotRestoreException(request.repository(), request.snapshot(),
diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotFeatureInfo.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotFeatureInfo.java
new file mode 100644
index 0000000000000..419d75a7226a5
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotFeatureInfo.java
@@ -0,0 +1,102 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.snapshots;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+public class SnapshotFeatureInfo implements Writeable, ToXContentObject {
+ final String pluginName;
+ final List indices;
+
+ static final ConstructingObjectParser SNAPSHOT_FEATURE_INFO_PARSER =
+ new ConstructingObjectParser<>("feature_info", true, (a, name) -> {
+ String pluginName = (String) a[0];
+ List indices = (List) a[1];
+ return new SnapshotFeatureInfo(pluginName, indices);
+ });
+
+ static {
+ SNAPSHOT_FEATURE_INFO_PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("feature_name"));
+ SNAPSHOT_FEATURE_INFO_PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), new ParseField("indices"));
+ }
+
+ public SnapshotFeatureInfo(String pluginName, List indices) {
+ this.pluginName = pluginName;
+ this.indices = indices;
+ }
+
+ public SnapshotFeatureInfo(final StreamInput in) throws IOException {
+ this.pluginName = in.readString();
+ this.indices = in.readStringList();
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(pluginName);
+ out.writeStringCollection(indices);
+ }
+
+ public static SnapshotFeatureInfo fromXContent(XContentParser parser) throws IOException {
+ return SNAPSHOT_FEATURE_INFO_PARSER.parse(parser, null);
+ }
+
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ public List getIndices() {
+ return indices;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ {
+ builder.field("feature_name", pluginName);
+ builder.startArray("indices");
+ for (String index : indices) {
+ builder.value(index);
+ }
+ builder.endArray();
+ }
+ builder.endObject();
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ return Strings.toString(this);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if ((o instanceof SnapshotFeatureInfo) == false) return false;
+ SnapshotFeatureInfo that = (SnapshotFeatureInfo) o;
+ return getPluginName().equals(that.getPluginName()) &&
+ getIndices().equals(that.getIndices());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getPluginName(), getIndices());
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java
index bdb1954efa22b..d6f6ba03bd642 100644
--- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java
+++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java
@@ -37,6 +37,8 @@
import java.util.Objects;
import java.util.stream.Collectors;
+import static org.elasticsearch.snapshots.SnapshotsService.FEATURE_STATES_VERSION;
+
/**
* Information about a snapshot
*/
@@ -69,6 +71,7 @@ public final class SnapshotInfo implements Comparable, ToXContent,
private static final String SUCCESSFUL_SHARDS = "successful_shards";
private static final String INCLUDE_GLOBAL_STATE = "include_global_state";
private static final String USER_METADATA = "metadata";
+ private static final String FEATURE_STATES = "feature_states";
private static final Comparator COMPARATOR =
Comparator.comparing(SnapshotInfo::startTime).thenComparing(SnapshotInfo::snapshotId);
@@ -80,6 +83,7 @@ public static final class SnapshotInfoBuilder {
private String reason = null;
private List indices = null;
private List dataStreams = null;
+ private List featureStates = null;
private long startTime = 0L;
private long endTime = 0L;
private ShardStatsBuilder shardStatsBuilder = null;
@@ -112,6 +116,10 @@ private void setDataStreams(List dataStreams) {
this.dataStreams = dataStreams;
}
+ private void setFeatureStates(List featureStates) {
+ this.featureStates = featureStates;
+ }
+
private void setStartTime(long startTime) {
this.startTime = startTime;
}
@@ -151,6 +159,10 @@ public SnapshotInfo build() {
dataStreams = Collections.emptyList();
}
+ if (featureStates == null) {
+ featureStates = Collections.emptyList();
+ }
+
SnapshotState snapshotState = state == null ? null : SnapshotState.valueOf(state);
Version version = this.version == -1 ? Version.CURRENT : Version.fromId(this.version);
@@ -161,8 +173,9 @@ public SnapshotInfo build() {
shardFailures = new ArrayList<>();
}
- return new SnapshotInfo(snapshotId, indices, dataStreams, snapshotState, reason, version, startTime, endTime,
- totalShards, successfulShards, shardFailures, includeGlobalState, userMetadata);
+ return new SnapshotInfo(snapshotId, indices, dataStreams, featureStates, reason, version, startTime, endTime, totalShards,
+ successfulShards, shardFailures, includeGlobalState, userMetadata, snapshotState
+ );
}
}
@@ -200,6 +213,8 @@ int getSuccessfulShards() {
SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::setReason, new ParseField(REASON));
SNAPSHOT_INFO_PARSER.declareStringArray(SnapshotInfoBuilder::setIndices, new ParseField(INDICES));
SNAPSHOT_INFO_PARSER.declareStringArray(SnapshotInfoBuilder::setDataStreams, new ParseField(DATA_STREAMS));
+ SNAPSHOT_INFO_PARSER.declareObjectArray(SnapshotInfoBuilder::setFeatureStates, SnapshotFeatureInfo.SNAPSHOT_FEATURE_INFO_PARSER,
+ new ParseField(FEATURE_STATES));
SNAPSHOT_INFO_PARSER.declareLong(SnapshotInfoBuilder::setStartTime, new ParseField(START_TIME_IN_MILLIS));
SNAPSHOT_INFO_PARSER.declareLong(SnapshotInfoBuilder::setEndTime, new ParseField(END_TIME_IN_MILLIS));
SNAPSHOT_INFO_PARSER.declareObject(SnapshotInfoBuilder::setShardStatsBuilder, SHARD_STATS_PARSER, new ParseField(SHARDS));
@@ -225,6 +240,8 @@ int getSuccessfulShards() {
private final List dataStreams;
+ private final List featureStates;
+
private final long startTime;
private final long endTime;
@@ -244,33 +261,40 @@ int getSuccessfulShards() {
private final List shardFailures;
- public SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, SnapshotState state) {
- this(snapshotId, indices, dataStreams, state, null, null, 0L, 0L, 0, 0, Collections.emptyList(), null, null);
+ public SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, List featureStates,
+ SnapshotState state) {
+ this(snapshotId, indices, dataStreams, featureStates, null, null, 0L, 0L, 0, 0, Collections.emptyList(), null, null, state);
}
- public SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, SnapshotState state, Version version) {
- this(snapshotId, indices, dataStreams, state, null, version, 0L, 0L, 0, 0, Collections.emptyList(), null, null);
+ public SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, List featureStates,
+ Version version, SnapshotState state) {
+ this(snapshotId, indices, dataStreams, featureStates, null, version, 0L, 0L, 0, 0, Collections.emptyList(), null, null, state);
}
public SnapshotInfo(SnapshotsInProgress.Entry entry) {
this(entry.snapshot().getSnapshotId(),
- entry.indices().stream().map(IndexId::getName).collect(Collectors.toList()), entry.dataStreams(), SnapshotState.IN_PROGRESS,
- null, Version.CURRENT, entry.startTime(), 0L, 0, 0, Collections.emptyList(), entry.includeGlobalState(), entry.userMetadata());
+ entry.indices().stream().map(IndexId::getName).collect(Collectors.toList()), entry.dataStreams(), entry.featureStates(),
+ null, Version.CURRENT, entry.startTime(), 0L, 0, 0, Collections.emptyList(), entry.includeGlobalState(), entry.userMetadata(),
+ SnapshotState.IN_PROGRESS
+ );
}
- public SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, long startTime, String reason,
- long endTime, int totalShards, List shardFailures, Boolean includeGlobalState,
- Map userMetadata) {
- this(snapshotId, indices, dataStreams, snapshotState(reason, shardFailures), reason, Version.CURRENT,
- startTime, endTime, totalShards, totalShards - shardFailures.size(), shardFailures, includeGlobalState, userMetadata);
+ public SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, List featureStates,
+ String reason, long endTime, int totalShards, List shardFailures, Boolean includeGlobalState,
+ Map userMetadata, long startTime) {
+ this(snapshotId, indices, dataStreams, featureStates, reason, Version.CURRENT, startTime, endTime, totalShards,
+ totalShards - shardFailures.size(), shardFailures, includeGlobalState, userMetadata, snapshotState(reason, shardFailures)
+ );
}
- SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, SnapshotState state, String reason,
- Version version, long startTime, long endTime, int totalShards, int successfulShards,
- List shardFailures, Boolean includeGlobalState, Map userMetadata) {
+ SnapshotInfo(SnapshotId snapshotId, List indices, List dataStreams, List featureStates,
+ String reason, Version version, long startTime, long endTime, int totalShards, int successfulShards,
+ List shardFailures, Boolean includeGlobalState, Map userMetadata,
+ SnapshotState state) {
this.snapshotId = Objects.requireNonNull(snapshotId);
this.indices = Collections.unmodifiableList(Objects.requireNonNull(indices));
this.dataStreams = Collections.unmodifiableList(Objects.requireNonNull(dataStreams));
+ this.featureStates = Collections.unmodifiableList(Objects.requireNonNull(featureStates));
this.state = state;
this.reason = reason;
this.version = version;
@@ -300,6 +324,11 @@ public SnapshotInfo(final StreamInput in) throws IOException {
includeGlobalState = in.readOptionalBoolean();
userMetadata = in.readMap();
dataStreams = in.readStringList();
+ if (in.getVersion().before(FEATURE_STATES_VERSION)) {
+ featureStates = Collections.emptyList();
+ } else {
+ featureStates = Collections.unmodifiableList(in.readList(SnapshotFeatureInfo::new));
+ }
}
/**
@@ -307,7 +336,7 @@ public SnapshotInfo(final StreamInput in) throws IOException {
* all information stripped out except the snapshot id, state, and indices.
*/
public SnapshotInfo basic() {
- return new SnapshotInfo(snapshotId, indices, Collections.emptyList(), state);
+ return new SnapshotInfo(snapshotId, indices, Collections.emptyList(), featureStates, state);
}
/**
@@ -439,6 +468,10 @@ public Map userMetadata() {
return userMetadata;
}
+ public List featureStates() {
+ return featureStates;
+ }
+
/**
* Compares two snapshots by their start time; if the start times are the same, then
* compares the two snapshots by their snapshot ids.
@@ -462,6 +495,7 @@ public String toString() {
", includeGlobalState=" + includeGlobalState +
", version=" + version +
", shardFailures=" + shardFailures +
+ ", featureStates=" + featureStates +
'}';
}
@@ -540,6 +574,14 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa
builder.field(SUCCESSFUL, successfulShards);
builder.endObject();
}
+ if (verbose || featureStates.isEmpty() == false) {
+ builder.startArray(FEATURE_STATES);
+ for (SnapshotFeatureInfo snapshotFeatureInfo : featureStates) {
+ builder.value(snapshotFeatureInfo);
+ }
+ builder.endArray();
+
+ }
builder.endObject();
return builder;
}
@@ -577,6 +619,12 @@ private XContentBuilder toXContentInternal(final XContentBuilder builder, final
shardFailure.toXContent(builder, params);
}
builder.endArray();
+ builder.startArray(FEATURE_STATES);
+ for (SnapshotFeatureInfo snapshotFeatureInfo : featureStates) {
+ builder.value(snapshotFeatureInfo);
+ }
+ builder.endArray();
+
builder.endObject();
return builder;
}
@@ -601,6 +649,7 @@ public static SnapshotInfo fromXContentInternal(final XContentParser parser) thr
Boolean includeGlobalState = null;
Map userMetadata = null;
List shardFailures = Collections.emptyList();
+ List featureStates = Collections.emptyList();
if (parser.currentToken() == null) { // fresh parser? move to the first token
parser.nextToken();
}
@@ -655,6 +704,12 @@ public static SnapshotInfo fromXContentInternal(final XContentParser parser) thr
shardFailureArrayList.add(SnapshotShardFailure.fromXContent(parser));
}
shardFailures = Collections.unmodifiableList(shardFailureArrayList);
+ } else if (FEATURE_STATES.equals(currentFieldName)) {
+ ArrayList snapshotFeatureInfoArrayList = new ArrayList<>();
+ while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
+ snapshotFeatureInfoArrayList.add(SnapshotFeatureInfo.fromXContent(parser));
+ }
+ featureStates = Collections.unmodifiableList(snapshotFeatureInfoArrayList);
} else {
// It was probably created by newer version - ignoring
parser.skipChildren();
@@ -677,7 +732,7 @@ public static SnapshotInfo fromXContentInternal(final XContentParser parser) thr
return new SnapshotInfo(new SnapshotId(name, uuid),
indices,
dataStreams,
- state,
+ featureStates,
reason,
version,
startTime,
@@ -686,7 +741,9 @@ public static SnapshotInfo fromXContentInternal(final XContentParser parser) thr
successfulShards,
shardFailures,
includeGlobalState,
- userMetadata);
+ userMetadata,
+ state
+ );
}
@Override
@@ -714,6 +771,9 @@ public void writeTo(final StreamOutput out) throws IOException {
out.writeOptionalBoolean(includeGlobalState);
out.writeMap(userMetadata);
out.writeStringCollection(dataStreams);
+ if (out.getVersion().onOrAfter(FEATURE_STATES_VERSION)) {
+ out.writeList(featureStates);
+ }
}
private static SnapshotState snapshotState(final String reason, final List shardFailures) {
@@ -745,12 +805,15 @@ public boolean equals(Object o) {
Objects.equals(includeGlobalState, that.includeGlobalState) &&
Objects.equals(version, that.version) &&
Objects.equals(shardFailures, that.shardFailures) &&
- Objects.equals(userMetadata, that.userMetadata);
+ Objects.equals(userMetadata, that.userMetadata) &&
+ Objects.equals(featureStates, that.featureStates);
}
@Override
public int hashCode() {
return Objects.hash(snapshotId, state, reason, indices, dataStreams, startTime, endTime,
- totalShards, successfulShards, includeGlobalState, version, shardFailures, userMetadata);
+ totalShards, successfulShards, includeGlobalState, version, shardFailures, userMetadata,
+ featureStates);
}
+
}
diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
index 913e99736323e..7f849ba68c043 100644
--- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
+++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java
@@ -69,6 +69,7 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.indices.SystemIndices;
import org.elasticsearch.repositories.IndexId;
import org.elasticsearch.repositories.RepositoriesService;
import org.elasticsearch.repositories.Repository;
@@ -107,6 +108,7 @@
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableList;
+import static org.elasticsearch.action.support.IndicesOptions.LENIENT_EXPAND_OPEN_CLOSED_HIDDEN;
import static org.elasticsearch.cluster.SnapshotsInProgress.completed;
/**
@@ -129,6 +131,8 @@ public class SnapshotsService extends AbstractLifecycleComponent implements Clus
public static final Version OLD_SNAPSHOT_FORMAT = Version.V_7_5_0;
+ public static final Version FEATURE_STATES_VERSION = Version.V_8_0_0;
+
private static final Logger logger = LogManager.getLogger(SnapshotsService.class);
public static final String UPDATE_SNAPSHOT_STATUS_ACTION_NAME = "internal:cluster/snapshot/update_snapshot_status";
@@ -153,6 +157,8 @@ public class SnapshotsService extends AbstractLifecycleComponent implements Clus
public static final String CACHE_FILE_NAME = "shared_snapshot_cache";
+ public static final String NO_FEATURE_STATES_VALUE = "none";
+
private final ClusterService clusterService;
private final IndexNameExpressionResolver indexNameExpressionResolver;
@@ -184,6 +190,8 @@ public class SnapshotsService extends AbstractLifecycleComponent implements Clus
private final OngoingRepositoryOperations repositoryOperations = new OngoingRepositoryOperations();
+ private final Map systemIndexDescriptorMap;
+
/**
* Setting that specifies the maximum number of allowed concurrent snapshot create and delete operations in the
* cluster state. The number of concurrent operations in a cluster state is defined as the sum of the sizes of
@@ -195,7 +203,8 @@ public class SnapshotsService extends AbstractLifecycleComponent implements Clus
private volatile int maxConcurrentOperations;
public SnapshotsService(Settings settings, ClusterService clusterService, IndexNameExpressionResolver indexNameExpressionResolver,
- RepositoriesService repositoriesService, TransportService transportService, ActionFilters actionFilters) {
+ RepositoriesService repositoriesService, TransportService transportService, ActionFilters actionFilters,
+ Map systemIndexDescriptorMap) {
this.clusterService = clusterService;
this.indexNameExpressionResolver = indexNameExpressionResolver;
this.repositoriesService = repositoriesService;
@@ -212,6 +221,7 @@ public SnapshotsService(Settings settings, ClusterService clusterService, IndexN
clusterService.getClusterSettings().addSettingsUpdateConsumer(MAX_CONCURRENT_SNAPSHOT_OPERATIONS_SETTING,
i -> maxConcurrentOperations = i);
}
+ this.systemIndexDescriptorMap = systemIndexDescriptorMap;
}
/**
@@ -267,6 +277,59 @@ public ClusterState execute(ClusterState currentState) {
// Store newSnapshot here to be processed in clusterStateProcessed
List indices = Arrays.asList(indexNameExpressionResolver.concreteIndexNames(currentState, request));
+ List featureStates = Collections.emptyList();
+ final List requestedStates = Arrays.asList(request.featureStates());
+
+ // We should only use the feature states logic if we're sure we'll be able to finish the snapshot without a lower-version
+ // node taking over and causing problems. Therefore, if we're in a mixed cluster with versions that don't know how to handle
+ // feature states, skip all feature states logic, and if `feature_states` is explicitly configured, throw an exception.
+ if (currentState.nodes().getMinNodeVersion().onOrAfter(FEATURE_STATES_VERSION)) {
+ if (request.includeGlobalState() || requestedStates.isEmpty() == false) {
+ final Set featureStatesSet;
+ if (request.includeGlobalState() && requestedStates.isEmpty()) {
+ // If we're including global state and feature states aren't specified, include all of them
+ featureStatesSet = new HashSet<>(systemIndexDescriptorMap.keySet());
+ } else if (requestedStates.size() == 1 && NO_FEATURE_STATES_VALUE.equalsIgnoreCase(requestedStates.get(0))) {
+ // If there's exactly one value and it's "none", include no states
+ featureStatesSet = Collections.emptySet();
+ } else {
+ // Otherwise, check for "none" then use the list of requested states
+ if (requestedStates.contains(NO_FEATURE_STATES_VALUE)) {
+ throw new IllegalArgumentException("the feature_states value [" + SnapshotsService.NO_FEATURE_STATES_VALUE +
+ "] indicates that no feature states should be snapshotted, but other feature states were requested: " +
+ requestedStates);
+ }
+ featureStatesSet = new HashSet<>(requestedStates);
+ }
+
+ featureStates = systemIndexDescriptorMap.keySet().stream()
+ .filter(feature -> featureStatesSet.contains(feature))
+ .map(feature -> new SnapshotFeatureInfo(feature, resolveFeatureIndexNames(currentState, feature)))
+ .filter(featureInfo -> featureInfo.getIndices().isEmpty() == false) // Omit any empty featureStates
+ .collect(Collectors.toList());
+ final Stream featureStateIndices = featureStates.stream().flatMap(feature -> feature.getIndices().stream());
+
+ final Stream associatedIndices = systemIndexDescriptorMap.keySet().stream()
+ .filter(feature -> featureStatesSet.contains(feature))
+ .flatMap(feature -> resolveAssociatedIndices(currentState, feature).stream());
+
+ // Add all resolved indices from the feature states to the list of indices
+ indices = Stream.of(indices.stream(), featureStateIndices, associatedIndices)
+ .flatMap(s -> s)
+ .distinct()
+ .collect(Collectors.toList());
+ }
+ } else if (requestedStates.isEmpty() == false) {
+ throw new SnapshotException(
+ new Snapshot(repositoryName, snapshotId),
+ "feature_states can only be used when all nodes in cluster are version ["
+ + FEATURE_STATES_VERSION
+ + "] or higher, but at least one node in this cluster is on version ["
+ + currentState.nodes().getMinNodeVersion()
+ + "]"
+ );
+ }
+
final List dataStreams =
indexNameExpressionResolver.dataStreamNames(currentState, request.indicesOptions(), request.indices());
@@ -291,7 +354,8 @@ public ClusterState execute(ClusterState currentState) {
}
newEntry = SnapshotsInProgress.startedEntry(
new Snapshot(repositoryName, snapshotId), request.includeGlobalState(), request.partial(),
- indexIds, dataStreams, threadPool.absoluteTimeInMillis(), repositoryData.getGenId(), shards, userMeta, version);
+ indexIds, dataStreams, threadPool.absoluteTimeInMillis(), repositoryData.getGenId(), shards,
+ userMeta, version, featureStates);
return ClusterState.builder(currentState).putCustom(SnapshotsInProgress.TYPE,
SnapshotsInProgress.of(CollectionUtils.appendToCopy(runningSnapshots, newEntry))).build();
}
@@ -316,6 +380,29 @@ public void clusterStateProcessed(String source, ClusterState oldState, final Cl
}, "create_snapshot [" + snapshotName + ']', listener::onFailure);
}
+ private List resolveFeatureIndexNames(ClusterState currentState, String featureName) {
+ if (systemIndexDescriptorMap.containsKey(featureName) == false) {
+ throw new IllegalArgumentException("requested snapshot of feature state for unknown feature [" + featureName + "]");
+ }
+
+ final SystemIndices.Feature feature = systemIndexDescriptorMap.get(featureName);
+ return feature.getIndexDescriptors().stream()
+ .flatMap(descriptor -> descriptor.getMatchingIndices(currentState.metadata()).stream())
+ .collect(Collectors.toList());
+ }
+
+ private List resolveAssociatedIndices(ClusterState currentState, String featureName) {
+ if (systemIndexDescriptorMap.containsKey(featureName) == false) {
+ throw new IllegalArgumentException("requested associated indices for feature state for unknown feature [" + featureName + "]");
+ }
+
+ final SystemIndices.Feature feature = systemIndexDescriptorMap.get(featureName);
+ return feature.getAssociatedIndexPatterns().stream()
+ .flatMap(pattern -> Arrays.stream(indexNameExpressionResolver.concreteIndexNamesWithSystemIndexAccess(currentState,
+ LENIENT_EXPAND_OPEN_CLOSED_HIDDEN, pattern)))
+ .collect(Collectors.toList());
+ }
+
private static void ensureSnapshotNameNotRunning(List runningSnapshots, String repositoryName,
String snapshotName) {
if (runningSnapshots.stream().anyMatch(s -> {
@@ -1210,14 +1297,18 @@ private void finalizeSnapshotEntry(SnapshotsInProgress.Entry entry, Metadata met
}
metadataListener.whenComplete(meta -> {
final Metadata metaForSnapshot = metadataForSnapshot(entry, meta);
+ final List finalIndices = shardGenerations.indices().stream()
+ .map(IndexId::getName)
+ .collect(Collectors.toList());
final SnapshotInfo snapshotInfo = new SnapshotInfo(snapshot.getSnapshotId(),
- shardGenerations.indices().stream().map(IndexId::getName).collect(Collectors.toList()),
+ finalIndices,
entry.partial() ? entry.dataStreams().stream()
.filter(metaForSnapshot.dataStreams()::containsKey)
.collect(Collectors.toList()) : entry.dataStreams(),
- entry.startTime(), failure, threadPool.absoluteTimeInMillis(),
+ entry.partial() ? onlySuccessfulFeatureStates(entry, finalIndices) : entry.featureStates(),
+ failure, threadPool.absoluteTimeInMillis(),
entry.partial() ? shardGenerations.totalShards() : entry.shards().size(), shardFailures,
- entry.includeGlobalState(), entry.userMetadata());
+ entry.includeGlobalState(), entry.userMetadata(), entry.startTime());
repo.finalizeSnapshot(
shardGenerations,
repositoryData.getGenId(),
@@ -1239,6 +1330,31 @@ private void finalizeSnapshotEntry(SnapshotsInProgress.Entry entry, Metadata met
}
}
+ /**
+ * Removes all feature states which have missing or failed shards, as they are no longer safely restorable.
+ * @param entry The "in progress" entry with a list of feature states and one or more failed shards.
+ * @param finalIndices The final list of indices in the snapshot, after any indices that were concurrently deleted are removed.
+ * @return The list of feature states which were completed successfully in the given entry.
+ */
+ private List onlySuccessfulFeatureStates(SnapshotsInProgress.Entry entry, List finalIndices) {
+ assert entry.partial() : "should not try to filter feature states from a non-partial entry";
+
+ // Figure out which indices have unsuccessful shards
+ Set