diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesClient.java
index 4ba0613c8e6cd..a26e1dcc8843d 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesClient.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesClient.java
@@ -9,8 +9,10 @@
package org.elasticsearch.client;
import org.elasticsearch.action.ActionListener;
-import org.elasticsearch.client.snapshots.GetFeaturesRequest;
-import org.elasticsearch.client.snapshots.GetFeaturesResponse;
+import org.elasticsearch.client.feature.GetFeaturesRequest;
+import org.elasticsearch.client.feature.GetFeaturesResponse;
+import org.elasticsearch.client.feature.ResetFeaturesRequest;
+import org.elasticsearch.client.feature.ResetFeaturesResponse;
import java.io.IOException;
@@ -71,4 +73,50 @@ public Cancellable getFeaturesAsync(
emptySet()
);
}
+
+ /**
+ * Reset the state of Elasticsearch features, deleting system indices and performing other
+ * cleanup operations.
+ * See Rest
+ * Features API on elastic.co
+ *
+ * @param resetFeaturesRequest 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 ResetFeaturesResponse resetFeatures(ResetFeaturesRequest resetFeaturesRequest, RequestOptions options)
+ throws IOException {
+ return restHighLevelClient.performRequestAndParseEntity(
+ resetFeaturesRequest,
+ FeaturesRequestConverters::resetFeatures,
+ options,
+ ResetFeaturesResponse::parse,
+ emptySet()
+ );
+ }
+
+ /**
+ * Asynchronously reset the state of Elasticsearch features, deleting system indices and performing other
+ * cleanup operations.
+ * See Get Snapshottable
+ * Features API on elastic.co
+ *
+ * @param resetFeaturesRequest 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 resetFeaturesAsync(
+ ResetFeaturesRequest resetFeaturesRequest, RequestOptions options,
+ ActionListener listener) {
+ return restHighLevelClient.performRequestAsyncAndParseEntity(
+ resetFeaturesRequest,
+ FeaturesRequestConverters::resetFeatures,
+ options,
+ ResetFeaturesResponse::parse,
+ listener,
+ emptySet()
+ );
+ }
}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesRequestConverters.java
index 34dd1e095ba59..bb2b8be43cf3b 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesRequestConverters.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/FeaturesRequestConverters.java
@@ -9,7 +9,9 @@
package org.elasticsearch.client;
import org.apache.http.client.methods.HttpGet;
-import org.elasticsearch.client.snapshots.GetFeaturesRequest;
+import org.apache.http.client.methods.HttpPost;
+import org.elasticsearch.client.feature.GetFeaturesRequest;
+import org.elasticsearch.client.feature.ResetFeaturesRequest;
public class FeaturesRequestConverters {
@@ -23,4 +25,9 @@ static Request getFeatures(GetFeaturesRequest getFeaturesRequest) {
request.addParameters(parameters.asMap());
return request;
}
+
+ static Request resetFeatures(ResetFeaturesRequest resetFeaturesRequest) {
+ String endpoint = "/_features/_reset";
+ return new Request(HttpPost.METHOD_NAME, endpoint);
+ }
}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetFeaturesRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/GetFeaturesRequest.java
similarity index 92%
rename from client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetFeaturesRequest.java
rename to client/rest-high-level/src/main/java/org/elasticsearch/client/feature/GetFeaturesRequest.java
index 65f4826ed7977..71ff178585cf1 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetFeaturesRequest.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/GetFeaturesRequest.java
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-package org.elasticsearch.client.snapshots;
+package org.elasticsearch.client.feature;
import org.elasticsearch.client.TimedRequest;
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetFeaturesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/GetFeaturesResponse.java
similarity index 98%
rename from client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetFeaturesResponse.java
rename to client/rest-high-level/src/main/java/org/elasticsearch/client/feature/GetFeaturesResponse.java
index 03c22e1018928..fb533da2e63bb 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/snapshots/GetFeaturesResponse.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/GetFeaturesResponse.java
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-package org.elasticsearch.client.snapshots;
+package org.elasticsearch.client.feature;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/ResetFeaturesRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/ResetFeaturesRequest.java
new file mode 100644
index 0000000000000..7e49a562c9a4e
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/ResetFeaturesRequest.java
@@ -0,0 +1,14 @@
+/*
+ * 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.feature;
+
+import org.elasticsearch.client.TimedRequest;
+
+public class ResetFeaturesRequest extends TimedRequest {
+}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/ResetFeaturesResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/ResetFeaturesResponse.java
new file mode 100644
index 0000000000000..72d004021d6be
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/feature/ResetFeaturesResponse.java
@@ -0,0 +1,82 @@
+/*
+ * 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.feature;
+
+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;
+
+public class ResetFeaturesResponse {
+ 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 ResetFeaturesResponse((List) a[0])
+ );
+
+ static {
+ PARSER.declareObjectArray(
+ ConstructingObjectParser.constructorArg(),
+ ResetFeaturesResponse.ResetFeatureStateStatus::parse, FEATURES);
+ }
+
+ public ResetFeaturesResponse(List features) {
+ this.features = features;
+ }
+
+ public List getFeatures() {
+ return features;
+ }
+
+ public static ResetFeaturesResponse parse(XContentParser parser) {
+ return PARSER.apply(parser, null);
+ }
+
+ public static class ResetFeatureStateStatus {
+ private final String featureName;
+ private final String status;
+
+ private static final ParseField FEATURE_NAME = new ParseField("feature_name");
+ private static final ParseField STATUS = new ParseField("status");
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "features", true, (a, ctx) -> new ResetFeatureStateStatus((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(), STATUS, ObjectParser.ValueType.STRING);
+ }
+
+ ResetFeatureStateStatus(String featureName, String status) {
+ this.featureName = featureName;
+ this.status = status;
+ }
+
+ public static ResetFeatureStateStatus parse(XContentParser parser, Void ctx) {
+ return PARSER.apply(parser, ctx);
+ }
+
+ public String getFeatureName() {
+ return featureName;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+ }
+}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/FeaturesIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/FeaturesIT.java
index 42757d77db2a4..e8c3463762992 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/FeaturesIT.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/FeaturesIT.java
@@ -8,8 +8,10 @@
package org.elasticsearch.client;
-import org.elasticsearch.client.snapshots.GetFeaturesRequest;
-import org.elasticsearch.client.snapshots.GetFeaturesResponse;
+import org.elasticsearch.client.feature.GetFeaturesRequest;
+import org.elasticsearch.client.feature.GetFeaturesResponse;
+import org.elasticsearch.client.feature.ResetFeaturesRequest;
+import org.elasticsearch.client.feature.ResetFeaturesResponse;
import java.io.IOException;
@@ -28,4 +30,17 @@ public void testGetFeatures() throws IOException {
assertThat(response.getFeatures().size(), greaterThan(1));
assertTrue(response.getFeatures().stream().anyMatch(feature -> "tasks".equals(feature.getFeatureName())));
}
+
+ public void testResetFeatures() throws IOException {
+ ResetFeaturesRequest request = new ResetFeaturesRequest();
+
+ ResetFeaturesResponse response = execute(request,
+ highLevelClient().features()::resetFeatures, highLevelClient().features()::resetFeaturesAsync);
+
+ assertThat(response, notNullValue());
+ assertThat(response.getFeatures(), notNullValue());
+ assertThat(response.getFeatures().size(), greaterThan(1));
+ assertTrue(response.getFeatures().stream().anyMatch(
+ feature -> "tasks".equals(feature.getFeatureName()) && "SUCCESS".equals(feature.getStatus())));
+ }
}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/GetFeaturesResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/GetFeaturesResponseTests.java
index 09d9f508dd418..473587ea3d70b 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/GetFeaturesResponseTests.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/GetFeaturesResponseTests.java
@@ -9,6 +9,7 @@
package org.elasticsearch.client.snapshots;
import org.elasticsearch.client.AbstractResponseTestCase;
+import org.elasticsearch.client.feature.GetFeaturesResponse;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/features.reset_features.json b/rest-api-spec/src/main/resources/rest-api-spec/api/features.reset_features.json
new file mode 100644
index 0000000000000..1a7f944e88079
--- /dev/null
+++ b/rest-api-spec/src/main/resources/rest-api-spec/api/features.reset_features.json
@@ -0,0 +1,23 @@
+{
+ "features.reset_features":{
+ "documentation":{
+ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html",
+ "description":"Resets the internal state of features, usually by deleting system indices"
+ },
+ "stability":"experimental",
+ "visibility":"public",
+ "headers":{
+ "accept": [ "application/json"]
+ },
+ "url":{
+ "paths":[
+ {
+ "path":"/_features/_reset",
+ "methods":[
+ "POST"
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/features.reset_features/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/features.reset_features/10_basic.yml
new file mode 100644
index 0000000000000..5aa33ec5e4255
--- /dev/null
+++ b/rest-api-spec/src/main/resources/rest-api-spec/test/features.reset_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.13.0"
+ - do: { features.get_features: {}}
+ - contains: {'features': {'name': 'tasks'}}
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java
new file mode 100644
index 0000000000000..7bbf1267ee8b7
--- /dev/null
+++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java
@@ -0,0 +1,148 @@
+/*
+ * 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.action.admin.cluster.snapshots.features.ResetFeatureStateAction;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
+import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexNotFoundException;
+import org.elasticsearch.indices.SystemIndexDescriptor;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.plugins.SystemIndexPlugin;
+import org.elasticsearch.test.ESIntegTestCase;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.arrayContaining;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsString;
+
+public class FeatureStateResetApiIT extends ESIntegTestCase {
+
+ @Override
+ protected Collection> nodePlugins() {
+ List> plugins = new ArrayList<>(super.nodePlugins());
+ plugins.add(SystemIndexTestPlugin.class);
+ plugins.add(SecondSystemIndexTestPlugin.class);
+ return plugins;
+ }
+
+ /** Check that the reset method cleans up a feature */
+ public void testResetSystemIndices() throws Exception {
+ String systemIndex1 = ".test-system-idx-1";
+ String systemIndex2 = ".second-test-system-idx-1";
+ String associatedIndex = ".associated-idx-1";
+
+ // put a document in a system index
+ index(systemIndex1, "_doc", "1", "purpose", "system index doc");
+ refresh(systemIndex1);
+
+ // put a document in a second system index
+ index(systemIndex2, "_doc", "1", "purpose", "second system index doc");
+ refresh(systemIndex2);
+
+ // put a document in associated index
+ index(associatedIndex, "_doc", "1", "purpose", "associated index doc");
+ refresh(associatedIndex);
+
+ // put a document in a normal index
+ index("my_index", "_doc", "1", "purpose", "normal index doc");
+ refresh("my_index");
+
+ // call the reset API
+ ResetFeatureStateResponse apiResponse = client().execute(ResetFeatureStateAction.INSTANCE, new ResetFeatureStateRequest()).get();
+ assertThat(apiResponse.getItemList(), containsInAnyOrder(
+ new ResetFeatureStateResponse.ResetFeatureStateStatus("SystemIndexTestPlugin", "SUCCESS"),
+ new ResetFeatureStateResponse.ResetFeatureStateStatus("SecondSystemIndexTestPlugin", "SUCCESS"),
+ new ResetFeatureStateResponse.ResetFeatureStateStatus("tasks", "SUCCESS")
+ ));
+
+ // verify that both indices are gone
+ Exception e1 = expectThrows(IndexNotFoundException.class, () -> client().admin().indices().prepareGetIndex()
+ .addIndices(systemIndex1)
+ .get());
+
+ assertThat(e1.getMessage(), containsString("no such index"));
+
+ Exception e2 = expectThrows(IndexNotFoundException.class, () -> client().admin().indices().prepareGetIndex()
+ .addIndices(associatedIndex)
+ .get());
+
+ assertThat(e2.getMessage(), containsString("no such index"));
+
+ Exception e3 = expectThrows(IndexNotFoundException.class, () -> client().admin().indices().prepareGetIndex()
+ .addIndices(systemIndex2)
+ .get());
+
+ assertThat(e3.getMessage(), containsString("no such index"));
+
+ GetIndexResponse response = client().admin().indices().prepareGetIndex()
+ .addIndices("my_index")
+ .get();
+
+ assertThat(response.getIndices(), arrayContaining("my_index"));
+ }
+
+ /**
+ * A test plugin with patterns for system indices and associated indices.
+ */
+ public static class SystemIndexTestPlugin extends Plugin implements SystemIndexPlugin {
+
+ public static final String SYSTEM_INDEX_PATTERN = ".test-system-idx*";
+ public static final String ASSOCIATED_INDEX_PATTERN = ".associated-idx*";
+
+ @Override
+ public Collection getSystemIndexDescriptors(Settings settings) {
+ return Collections.singletonList(new SystemIndexDescriptor(SYSTEM_INDEX_PATTERN, "System indices for tests"));
+ }
+
+ @Override
+ public Collection getAssociatedIndexPatterns() {
+ return Collections.singletonList(ASSOCIATED_INDEX_PATTERN);
+ }
+
+ @Override
+ public String getFeatureName() {
+ return SystemIndexTestPlugin.class.getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "A simple test plugin";
+ }
+ }
+
+ /**
+ * A second test plugin with a patterns for system indices.
+ */
+ public static class SecondSystemIndexTestPlugin extends Plugin implements SystemIndexPlugin {
+
+ public static final String SYSTEM_INDEX_PATTERN = ".second-test-system-idx*";
+
+ @Override
+ public Collection getSystemIndexDescriptors(Settings settings) {
+ return Collections.singletonList(new SystemIndexDescriptor(SYSTEM_INDEX_PATTERN, "System indices for tests"));
+ }
+
+ @Override
+ public String getFeatureName() {
+ return SecondSystemIndexTestPlugin.class.getSimpleName();
+ }
+
+ @Override
+ public String getFeatureDescription() {
+ return "A second 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 949e51fd9382e..a33acc3871c18 100644
--- a/server/src/main/java/org/elasticsearch/action/ActionModule.java
+++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java
@@ -59,7 +59,9 @@
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.ResetFeatureStateAction;
import org.elasticsearch.action.admin.cluster.snapshots.features.SnapshottableFeaturesAction;
+import org.elasticsearch.action.admin.cluster.snapshots.features.TransportResetFeatureStateAction;
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;
@@ -288,6 +290,7 @@
import org.elasticsearch.rest.action.admin.cluster.RestPutStoredScriptAction;
import org.elasticsearch.rest.action.admin.cluster.RestReloadSecureSettingsAction;
import org.elasticsearch.rest.action.admin.cluster.RestRemoteClusterInfoAction;
+import org.elasticsearch.rest.action.admin.cluster.RestResetFeatureStateAction;
import org.elasticsearch.rest.action.admin.cluster.RestRestoreSnapshotAction;
import org.elasticsearch.rest.action.admin.cluster.RestSnapshotsStatusAction;
import org.elasticsearch.rest.action.admin.cluster.RestSnapshottableFeaturesAction;
@@ -518,6 +521,7 @@ public void reg
actions.register(RestoreSnapshotAction.INSTANCE, TransportRestoreSnapshotAction.class);
actions.register(SnapshotsStatusAction.INSTANCE, TransportSnapshotsStatusAction.class);
actions.register(SnapshottableFeaturesAction.INSTANCE, TransportSnapshottableFeaturesAction.class);
+ actions.register(ResetFeatureStateAction.INSTANCE, TransportResetFeatureStateAction.class);
actions.register(IndicesStatsAction.INSTANCE, TransportIndicesStatsAction.class);
actions.register(IndicesSegmentsAction.INSTANCE, TransportIndicesSegmentsAction.class);
@@ -664,6 +668,7 @@ public void initRestHandlers(Supplier nodesInCluster) {
registerHandler.accept(new RestDeleteSnapshotAction());
registerHandler.accept(new RestSnapshotsStatusAction());
registerHandler.accept(new RestSnapshottableFeaturesAction());
+ registerHandler.accept(new RestResetFeatureStateAction());
registerHandler.accept(new RestGetIndicesAction());
registerHandler.accept(new RestIndicesStatsAction());
registerHandler.accept(new RestIndicesSegmentsAction(threadPool));
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateAction.java
new file mode 100644
index 0000000000000..6953fc00b481a
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateAction.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/** Action for resetting feature states, mostly meaning system indices */
+public class ResetFeatureStateAction extends ActionType {
+
+ public static final ResetFeatureStateAction INSTANCE = new ResetFeatureStateAction();
+ public static final String NAME = "cluster:admin/features/reset";
+
+ private ResetFeatureStateAction() {
+ super(NAME, ResetFeatureStateResponse::new);
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateRequest.java
new file mode 100644
index 0000000000000..62a2b7d78c320
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateRequest.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.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+
+/** Request for resetting feature state */
+public class ResetFeatureStateRequest extends ActionRequest {
+
+ public ResetFeatureStateRequest() {
+ }
+
+ public ResetFeatureStateRequest(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/ResetFeatureStateResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateResponse.java
new file mode 100644
index 0000000000000..492b4f934f0d3
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateResponse.java
@@ -0,0 +1,151 @@
+/*
+ * 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.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/** Response to a feature state reset request. */
+public class ResetFeatureStateResponse extends ActionResponse implements ToXContentObject {
+
+ List resetFeatureStateStatusList;
+
+ /**
+ * Create a response showing which features have had state reset and success
+ * or failure status.
+ *
+ * @param statusList A list of status responses
+ */
+ public ResetFeatureStateResponse(List statusList) {
+ resetFeatureStateStatusList = new ArrayList<>();
+ resetFeatureStateStatusList.addAll(statusList);
+ resetFeatureStateStatusList.sort(Comparator.comparing(ResetFeatureStateStatus::getFeatureName));
+ }
+
+ public ResetFeatureStateResponse(StreamInput in) throws IOException {
+ super(in);
+ this.resetFeatureStateStatusList = in.readList(ResetFeatureStateStatus::new);
+ }
+
+ public List getItemList() {
+ return this.resetFeatureStateStatusList;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ {
+ builder.startArray("features");
+ for (ResetFeatureStateStatus resetFeatureStateStatus : this.resetFeatureStateStatusList) {
+ builder.value(resetFeatureStateStatus);
+ }
+ builder.endArray();
+ }
+ builder.endObject();
+ return builder;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeList(this.resetFeatureStateStatusList);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResetFeatureStateResponse that = (ResetFeatureStateResponse) o;
+ return Objects.equals(resetFeatureStateStatusList, that.resetFeatureStateStatusList);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(resetFeatureStateStatusList);
+ }
+
+ @Override
+ public String toString() {
+ return "ResetFeatureStateResponse{" +
+ "resetFeatureStateStatusList=" + resetFeatureStateStatusList +
+ '}';
+ }
+
+ /**
+ * An object with the name of a feature and a message indicating success or
+ * failure.
+ */
+ public static class ResetFeatureStateStatus implements Writeable, ToXContentObject {
+ private final String featureName;
+ private final String status;
+
+ public ResetFeatureStateStatus(String featureName, String status) {
+ this.featureName = featureName;
+ this.status = status;
+ }
+
+ ResetFeatureStateStatus(StreamInput in) throws IOException {
+ this.featureName = in.readString();
+ this.status = in.readString();
+ }
+
+ public String getFeatureName() {
+ return this.featureName;
+ }
+
+ public String getStatus() {
+ return this.status;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ builder.field("feature_name", this.featureName);
+ builder.field("status", this.status);
+ builder.endObject();
+ return builder;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(this.featureName);
+ out.writeString(this.status);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResetFeatureStateStatus that = (ResetFeatureStateStatus) o;
+ return Objects.equals(featureName, that.featureName) && Objects.equals(status, that.status);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(featureName, status);
+ }
+
+ @Override
+ public String toString() {
+ return "ResetFeatureStateStatus{" +
+ "featureName='" + featureName + '\'' +
+ ", status='" + status + '\'' +
+ '}';
+ }
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportResetFeatureStateAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportResetFeatureStateAction.java
new file mode 100644
index 0000000000000..488c29f339d74
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/features/TransportResetFeatureStateAction.java
@@ -0,0 +1,72 @@
+/*
+ * 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.GroupedActionListener;
+import org.elasticsearch.action.support.HandledTransportAction;
+import org.elasticsearch.client.node.NodeClient;
+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.transport.TransportService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Transport action for cleaning up feature index state.
+ */
+public class TransportResetFeatureStateAction extends HandledTransportAction {
+
+ private final SystemIndices systemIndices;
+ private final NodeClient client;
+ private final ClusterService clusterService;
+
+ @Inject
+ public TransportResetFeatureStateAction(
+ TransportService transportService,
+ ActionFilters actionFilters,
+ SystemIndices systemIndices,
+ NodeClient client,
+ ClusterService clusterService
+ ) {
+ super(ResetFeatureStateAction.NAME, transportService, actionFilters,
+ ResetFeatureStateRequest::new);
+ this.systemIndices = systemIndices;
+ this.client = client;
+ this.clusterService = clusterService;
+ }
+
+ @Override
+ protected void doExecute(
+ Task task,
+ ResetFeatureStateRequest request,
+ ActionListener listener) {
+
+ if (systemIndices.getFeatures().size() == 0) {
+ listener.onResponse(new ResetFeatureStateResponse(Collections.emptyList()));
+ }
+
+ final int features = systemIndices.getFeatures().size();
+ GroupedActionListener groupedActionListener = new GroupedActionListener<>(
+ listener.map(responses -> {
+ assert features == responses.size();
+ return new ResetFeatureStateResponse(new ArrayList<>(responses));
+ }),
+ systemIndices.getFeatures().size()
+ );
+
+ for (SystemIndices.Feature feature : systemIndices.getFeatures().values()) {
+ feature.getCleanUpFunction().apply(clusterService, client, groupedActionListener);
+ }
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java
index 8b848c7e2057e..5cb7ebdd38f14 100644
--- a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java
+++ b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java
@@ -13,8 +13,17 @@
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
import org.apache.lucene.util.automaton.MinimizationOperations;
import org.apache.lucene.util.automaton.Operations;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse.ResetFeatureStateStatus;
+import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction;
+import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.TriConsumer;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.index.Index;
import org.elasticsearch.snapshots.SnapshotsService;
@@ -29,6 +38,7 @@
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
@@ -43,13 +53,18 @@
*/
public class SystemIndices {
private static final Map SERVER_SYSTEM_INDEX_DESCRIPTORS = singletonMap(
- TASKS_FEATURE_NAME, new Feature("Manages task results", singletonList(TASKS_DESCRIPTOR))
+ TASKS_FEATURE_NAME, new Feature(TASKS_FEATURE_NAME, "Manages task results", singletonList(TASKS_DESCRIPTOR))
);
private final CharacterRunAutomaton runAutomaton;
private final Map featureDescriptors;
private final Map productToSystemIndicesMatcher;
+ /**
+ * Initialize the SystemIndices object
+ * @param pluginAndModulesDescriptors A map of this node's feature names to
+ * feature objects.
+ */
public SystemIndices(Map pluginAndModulesDescriptors) {
featureDescriptors = buildSystemIndexDescriptorMap(pluginAndModulesDescriptors);
checkForOverlappingPatterns(featureDescriptors);
@@ -238,6 +253,11 @@ Collection getSystemIndexDescriptors() {
.collect(Collectors.toList());
}
+ /**
+ * Check that a feature name is not reserved
+ * @param name Name of feature
+ * @param plugin Name of plugin providing the feature
+ */
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 +
@@ -245,19 +265,44 @@ public static void validateFeatureName(String name, String plugin) {
}
}
+ /**
+ * Class holding a description of a stateful feature.
+ */
public static class Feature {
private final String description;
private final Collection indexDescriptors;
private final Collection associatedIndexPatterns;
-
- public Feature(String description, Collection indexDescriptors, Collection associatedIndexPatterns) {
+ private final TriConsumer> cleanUpFunction;
+
+ /**
+ * Construct a Feature with a custom cleanup function
+ * @param description Description of the feature
+ * @param indexDescriptors Patterns describing system indices for this feature
+ * @param associatedIndexPatterns Patterns describing associated indices
+ * @param cleanUpFunction A function that will clean up the feature's state
+ */
+ public Feature(
+ String description,
+ Collection indexDescriptors,
+ Collection associatedIndexPatterns,
+ TriConsumer> cleanUpFunction) {
this.description = description;
this.indexDescriptors = indexDescriptors;
this.associatedIndexPatterns = associatedIndexPatterns;
+ this.cleanUpFunction = cleanUpFunction;
}
- public Feature(String description, Collection indexDescriptors) {
- this(description, indexDescriptors, Collections.emptyList());
+ /**
+ * Construct a Feature using the default clean-up function
+ * @param name Name of the feature, used in logging
+ * @param description Description of the feature
+ * @param indexDescriptors Patterns describing system indices for this feature
+ */
+ public Feature(String name, String description, Collection indexDescriptors) {
+ this(description, indexDescriptors, Collections.emptyList(),
+ (clusterService, client, listener) ->
+ cleanUpFeature(indexDescriptors, Collections.emptyList(), name, clusterService, client, listener)
+ );
}
public String getDescription() {
@@ -271,5 +316,53 @@ public Collection getIndexDescriptors() {
public Collection getAssociatedIndexPatterns() {
return associatedIndexPatterns;
}
+
+ public TriConsumer> getCleanUpFunction() {
+ return cleanUpFunction;
+ }
+
+ /**
+ * Clean up the state of a feature
+ * @param indexDescriptors List of descriptors of a feature's system indices
+ * @param associatedIndexPatterns List of patterns of a feature's associated indices
+ * @param name Name of the feature, used in logging
+ * @param clusterService A clusterService, for retrieving cluster metadata
+ * @param client A client, for issuing delete requests
+ * @param listener A listener to return success or failure of cleanup
+ */
+ public static void cleanUpFeature(
+ Collection indexDescriptors,
+ Collection associatedIndexPatterns,
+ String name,
+ ClusterService clusterService,
+ Client client,
+ ActionListener listener) {
+ Stream systemIndices = indexDescriptors.stream()
+ .map(sid -> sid.getMatchingIndices(clusterService.state().getMetadata()))
+ .flatMap(List::stream);
+
+ List allIndices = Stream.concat(systemIndices, associatedIndexPatterns.stream())
+ .collect(Collectors.toList());
+
+ if (allIndices.isEmpty()) {
+ // if no actual indices match the pattern, we can stop here
+ listener.onResponse(new ResetFeatureStateStatus(name, "SUCCESS"));
+ return;
+ }
+
+ DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest();
+ deleteIndexRequest.indices(allIndices.toArray(Strings.EMPTY_ARRAY));
+ client.execute(DeleteIndexAction.INSTANCE, deleteIndexRequest, new ActionListener() {
+ @Override
+ public void onResponse(AcknowledgedResponse acknowledgedResponse) {
+ listener.onResponse(new ResetFeatureStateStatus(name, "SUCCESS"));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ listener.onResponse(new ResetFeatureStateStatus(name, "FAILURE: " + e.getMessage()));
+ }
+ });
+ }
}
}
diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java
index e6354cebe80c3..50e5cee04310f 100644
--- a/server/src/main/java/org/elasticsearch/node/Node.java
+++ b/server/src/main/java/org/elasticsearch/node/Node.java
@@ -424,7 +424,8 @@ protected Node(final Environment initialEnvironment,
plugin -> new SystemIndices.Feature(
plugin.getFeatureDescription(),
plugin.getSystemIndexDescriptors(settings),
- plugin.getAssociatedIndexPatterns()
+ plugin.getAssociatedIndexPatterns(),
+ plugin::cleanUpFeature
))
)
);
diff --git a/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java b/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java
index c3a4a56f24ba1..9724b7eeb182c 100644
--- a/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java
+++ b/server/src/main/java/org/elasticsearch/plugins/SystemIndexPlugin.java
@@ -8,8 +8,13 @@
package org.elasticsearch.plugins;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.indices.SystemIndexDescriptor;
+import org.elasticsearch.indices.SystemIndices;
import java.util.Collection;
import java.util.Collections;
@@ -49,4 +54,25 @@ default Collection getSystemIndexDescriptors(Settings set
default Collection getAssociatedIndexPatterns() {
return Collections.emptyList();
}
+
+ /**
+ * Cleans up the state of the feature by deleting system indices and associated indices.
+ * Override to do more for cleanup (e.g. cancelling tasks).
+ * @param clusterService Cluster service to provide cluster state
+ * @param client A client, for executing actions
+ * @param listener Listener for post-cleanup result
+ */
+ default void cleanUpFeature(
+ ClusterService clusterService, Client client,
+ ActionListener listener) {
+
+ SystemIndices.Feature.cleanUpFeature(
+ getSystemIndexDescriptors(clusterService.getSettings()),
+ getAssociatedIndexPatterns(),
+ getFeatureName(),
+ clusterService,
+ client,
+ listener
+ );
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestResetFeatureStateAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestResetFeatureStateAction.java
new file mode 100644
index 0000000000000..580e28b536d38
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestResetFeatureStateAction.java
@@ -0,0 +1,45 @@
+/*
+ * 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.ResetFeatureStateAction;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateRequest;
+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.Collections;
+import java.util.List;
+
+/** Rest handler for feature state reset requests */
+public class RestResetFeatureStateAction extends BaseRestHandler {
+
+ @Override public boolean allowSystemIndexAccessByDefault() {
+ return true;
+ }
+
+ @Override
+ public List routes() {
+ return Collections.singletonList(new Route(RestRequest.Method.POST, "/_features/_reset"));
+ }
+
+ @Override
+ public String getName() {
+ return "reset_feature_state";
+ }
+
+ @Override
+ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
+ final ResetFeatureStateRequest req = new ResetFeatureStateRequest();
+
+ return restChannel -> client.execute(ResetFeatureStateAction.INSTANCE, req, new RestToXContentListener<>(restChannel));
+ }
+}
diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateResponseTests.java
new file mode 100644
index 0000000000000..b326b07e20994
--- /dev/null
+++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/features/ResetFeatureStateResponseTests.java
@@ -0,0 +1,53 @@
+/*
+ * 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.common.io.stream.Writeable;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ResetFeatureStateResponseTests extends AbstractWireSerializingTestCase {
+
+ @Override
+ protected Writeable.Reader instanceReader() {
+ return ResetFeatureStateResponse::new;
+ }
+
+ @Override
+ protected ResetFeatureStateResponse createTestInstance() {
+ List resetStatuses = new ArrayList<>();
+ String feature1 = randomAlphaOfLengthBetween(4, 10);
+ String feature2 = randomValueOtherThan(feature1, () -> randomAlphaOfLengthBetween(4, 10));
+ resetStatuses.add(new ResetFeatureStateResponse.ResetFeatureStateStatus(
+ feature1, randomFrom("SUCCESS", "FAILURE")));
+ resetStatuses.add(new ResetFeatureStateResponse.ResetFeatureStateStatus(
+ feature2, randomFrom("SUCCESS", "FAILURE")));
+ return new ResetFeatureStateResponse(resetStatuses);
+ }
+
+ @Override
+ protected ResetFeatureStateResponse mutateInstance(ResetFeatureStateResponse instance) throws IOException {
+ int minSize = 0;
+ if (instance.getItemList().size() == 0) {
+ minSize = 1;
+ }
+ Set existingFeatureNames = instance.getItemList().stream()
+ .map(ResetFeatureStateResponse.ResetFeatureStateStatus::getFeatureName)
+ .collect(Collectors.toSet());
+ return new ResetFeatureStateResponse(randomList(minSize, 10,
+ () -> new ResetFeatureStateResponse.ResetFeatureStateStatus(
+ randomValueOtherThanMany(existingFeatureNames::contains, () -> randomAlphaOfLengthBetween(4, 10)),
+ randomAlphaOfLengthBetween(5, 10))));
+ }
+}
diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesActionTests.java
index 83d847275d55e..7630859173d65 100644
--- a/server/src/test/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesActionTests.java
+++ b/server/src/test/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesActionTests.java
@@ -159,7 +159,7 @@ public void testDeprecationWarningEmittedWhenRequestingNonExistingAliasInSystemP
ClusterState state = systemIndexTestClusterState();
SystemIndices systemIndices = new SystemIndices(Collections.singletonMap(
this.getTestName(),
- new SystemIndices.Feature("test feature",
+ new SystemIndices.Feature(this.getTestName(), "test feature",
Collections.singletonList(new SystemIndexDescriptor(".y", "an index that doesn't exist")))));
GetAliasesRequest request = new GetAliasesRequest(".y");
diff --git a/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionTests.java b/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionTests.java
index 800795641d966..2bad0493e26a4 100644
--- a/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionTests.java
+++ b/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionTests.java
@@ -248,6 +248,7 @@ public void testOnlySystem() {
new Index(IndexMetadata.builder(".bar").settings(settings).system(true).numberOfShards(1).numberOfReplicas(0).build()));
SystemIndices systemIndices = new SystemIndices(
Map.of("plugin", new SystemIndices.Feature(
+ "plugin",
"test feature",
org.elasticsearch.common.collect.List.of(new SystemIndexDescriptor(".test", "")))
));
diff --git a/server/src/test/java/org/elasticsearch/action/support/AutoCreateIndexTests.java b/server/src/test/java/org/elasticsearch/action/support/AutoCreateIndexTests.java
index 0f77309d26eb1..846f74d4083b9 100644
--- a/server/src/test/java/org/elasticsearch/action/support/AutoCreateIndexTests.java
+++ b/server/src/test/java/org/elasticsearch/action/support/AutoCreateIndexTests.java
@@ -306,7 +306,8 @@ private static ClusterState buildClusterState(String... indices) {
private AutoCreateIndex newAutoCreateIndex(Settings settings) {
SystemIndices systemIndices = new SystemIndices(org.elasticsearch.common.collect.Map.of(
- "plugin", new SystemIndices.Feature("test feature", singletonList(new SystemIndexDescriptor(TEST_SYSTEM_INDEX_NAME, "")))));
+ "plugin", new SystemIndices.Feature("plugin", "test feature",
+ singletonList(new SystemIndexDescriptor(TEST_SYSTEM_INDEX_NAME, "")))));
return new AutoCreateIndex(settings, new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS),
TestIndexNameExpressionResolver.newInstance(systemIndices), systemIndices);
}
diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java
index a15ff5755935c..a2bca6f711eec 100644
--- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java
+++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolverTests.java
@@ -1922,15 +1922,16 @@ public void testExternalSystemIndexAccess() {
org.elasticsearch.common.collect.Map.of(
"ml",
new Feature(
+ "ml",
"ml indices",
org.elasticsearch.common.collect.List.of(
new SystemIndexDescriptor(".ml-meta", "ml meta"), new SystemIndexDescriptor(".ml-stuff", "other ml"))
),
"watcher",
- new Feature("watcher indices",
+ new Feature("watcher", "watcher indices",
org.elasticsearch.common.collect.List.of(new SystemIndexDescriptor(".watches", "watches index"))),
"stack-component",
- new Feature("stack component",
+ new Feature("stack-component", "stack component",
org.elasticsearch.common.collect.List.of(
new SystemIndexDescriptor(
".external-sys-idx",
@@ -2324,12 +2325,12 @@ private ClusterState systemIndexTestClusterState() {
.put(indexBuilder("some-other-index").state(State.OPEN));
SystemIndices systemIndices = new SystemIndices(
org.elasticsearch.common.collect.Map.of("ml",
- new Feature("ml indices",
+ new Feature("ml", "ml indices",
org.elasticsearch.common.collect.List.of(
new SystemIndexDescriptor(".ml-meta", "ml meta"), new SystemIndexDescriptor(".ml-stuff", "other ml"))
),
"watcher",
- new Feature("watcher indices",
+ new Feature("watcher", "watcher indices",
org.elasticsearch.common.collect.List.of(new SystemIndexDescriptor(".watches", "watches index")))
)
);
diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java
index dba20f6ae8cd5..acf7a7560b344 100644
--- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java
+++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java
@@ -548,7 +548,8 @@ public void testValidateDotIndex() {
null,
threadPool,
null,
- new SystemIndices(Collections.singletonMap("foo", new SystemIndices.Feature("test feature", systemIndexDescriptors))),
+ new SystemIndices(Collections.singletonMap("foo", new SystemIndices.Feature("foo", "test feature",
+ systemIndexDescriptors))),
false
);
// Check deprecations
diff --git a/server/src/test/java/org/elasticsearch/indices/SystemIndexManagerTests.java b/server/src/test/java/org/elasticsearch/indices/SystemIndexManagerTests.java
index 621038931d76d..5972c594a75d3 100644
--- a/server/src/test/java/org/elasticsearch/indices/SystemIndexManagerTests.java
+++ b/server/src/test/java/org/elasticsearch/indices/SystemIndexManagerTests.java
@@ -76,6 +76,7 @@ public class SystemIndexManagerTests extends ESTestCase {
.build();
private static final SystemIndices.Feature FEATURE = new SystemIndices.Feature(
+ "foo",
"a test feature",
org.elasticsearch.common.collect.List.of(DESCRIPTOR)
);
@@ -107,8 +108,8 @@ public void testManagerSkipsDescriptorsThatAreNotManaged() {
.build();
SystemIndices systemIndices = new SystemIndices(org.elasticsearch.common.collect.Map.of(
- "index 1", new SystemIndices.Feature("index 1 feature", org.elasticsearch.common.collect.List.of(d1)),
- "index 2", new SystemIndices.Feature("index 2 feature", org.elasticsearch.common.collect.List.of(d2))));
+ "index 1", new SystemIndices.Feature("index 1", "index 1 feature", org.elasticsearch.common.collect.List.of(d1)),
+ "index 2", new SystemIndices.Feature("index 2", "index 2 feature", org.elasticsearch.common.collect.List.of(d2))));
SystemIndexManager manager = new SystemIndexManager(systemIndices, client);
final List eligibleDescriptors = manager.getEligibleDescriptors(
@@ -145,8 +146,8 @@ public void testManagerSkipsDescriptorsForIndicesThatDoNotExist() {
.build();
SystemIndices systemIndices = new SystemIndices(org.elasticsearch.common.collect.Map.of(
- "index 1", new SystemIndices.Feature("index 1 feature", org.elasticsearch.common.collect.List.of(d1)),
- "index 2", new SystemIndices.Feature("index 2 feature", org.elasticsearch.common.collect.List.of(d2))));;
+ "index 1", new SystemIndices.Feature("index 1", "index 1 feature", org.elasticsearch.common.collect.List.of(d1)),
+ "index 2", new SystemIndices.Feature("index 2", "index 2 feature", org.elasticsearch.common.collect.List.of(d2))));;
SystemIndexManager manager = new SystemIndexManager(systemIndices, client);
final List eligibleDescriptors = manager.getEligibleDescriptors(
diff --git a/server/src/test/java/org/elasticsearch/indices/SystemIndicesTests.java b/server/src/test/java/org/elasticsearch/indices/SystemIndicesTests.java
index 4181a5d2573a7..b2ffc65bc0313 100644
--- a/server/src/test/java/org/elasticsearch/indices/SystemIndicesTests.java
+++ b/server/src/test/java/org/elasticsearch/indices/SystemIndicesTests.java
@@ -37,9 +37,9 @@ public void testBasicOverlappingPatterns() {
String broadPatternSource = "AAA" + randomAlphaOfLength(5);
String otherSource = "ZZZ" + randomAlphaOfLength(6);
Map descriptors = new HashMap<>();
- descriptors.put(broadPatternSource, new SystemIndices.Feature("test feature", List.of(broadPattern)));
+ descriptors.put(broadPatternSource, new SystemIndices.Feature(broadPatternSource, "test feature", List.of(broadPattern)));
descriptors.put(otherSource,
- new SystemIndices.Feature("test 2", List.of(notOverlapping, overlapping1, overlapping2, overlapping3)));
+ new SystemIndices.Feature(otherSource, "test 2", List.of(notOverlapping, overlapping1, overlapping2, overlapping3)));
IllegalStateException exception = expectThrows(IllegalStateException.class,
() -> SystemIndices.checkForOverlappingPatterns(descriptors));
@@ -65,8 +65,8 @@ public void testComplexOverlappingPatterns() {
String source1 = "AAA" + randomAlphaOfLength(5);
String source2 = "ZZZ" + randomAlphaOfLength(6);
Map descriptors = new HashMap<>();
- descriptors.put(source1, new SystemIndices.Feature("test", List.of(pattern1)));
- descriptors.put(source2, new SystemIndices.Feature("test", List.of(pattern2)));
+ descriptors.put(source1, new SystemIndices.Feature(source1, "test", List.of(pattern1)));
+ descriptors.put(source2, new SystemIndices.Feature(source2, "test", List.of(pattern2)));
IllegalStateException exception = expectThrows(IllegalStateException.class,
() -> SystemIndices.checkForOverlappingPatterns(descriptors));
@@ -87,7 +87,9 @@ public void testBuiltInSystemIndices() {
public void testPluginCannotOverrideBuiltInSystemIndex() {
Map pluginMap = singletonMap(
- TASKS_FEATURE_NAME, new SystemIndices.Feature("test", singletonList(new SystemIndexDescriptor(TASK_INDEX, "Task Result Index")))
+ TASKS_FEATURE_NAME, new SystemIndices.Feature(TASKS_FEATURE_NAME, "test", singletonList(new SystemIndexDescriptor(TASK_INDEX,
+ "Task Result " +
+ "Index")))
);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new SystemIndices(pluginMap));
assertThat(e.getMessage(), containsString("plugin or module attempted to define the same source"));
@@ -95,7 +97,9 @@ public void testPluginCannotOverrideBuiltInSystemIndex() {
public void testPatternWithSimpleRange() {
final SystemIndices systemIndices = new SystemIndices(
- singletonMap("test", new SystemIndices.Feature("test feature", singletonList(new SystemIndexDescriptor(".test-[abc]", "")))
+ singletonMap(
+ "test",
+ new SystemIndices.Feature("test", "test feature", singletonList(new SystemIndexDescriptor(".test-[abc]", "")))
));
assertThat(systemIndices.isSystemIndex(".test-a"), equalTo(true));
@@ -110,7 +114,9 @@ public void testPatternWithSimpleRange() {
public void testPatternWithSimpleRangeAndRepeatOperator() {
final SystemIndices systemIndices = new SystemIndices(
- singletonMap("test", new SystemIndices.Feature("test feature", singletonList(new SystemIndexDescriptor(".test-[a]+", "")))
+ singletonMap(
+ "test",
+ new SystemIndices.Feature("test", "test feature", singletonList(new SystemIndexDescriptor(".test-[a]+", "")))
));
assertThat(systemIndices.isSystemIndex(".test-a"), equalTo(true));
@@ -122,7 +128,9 @@ public void testPatternWithSimpleRangeAndRepeatOperator() {
public void testPatternWithComplexRange() {
final SystemIndices systemIndices = new SystemIndices(
- singletonMap("test", new SystemIndices.Feature("test feature", singletonList(new SystemIndexDescriptor(".test-[a-c]", "")))
+ singletonMap(
+ "test",
+ new SystemIndices.Feature("test", "test feature", singletonList(new SystemIndexDescriptor(".test-[a-c]", "")))
));
assertThat(systemIndices.isSystemIndex(".test-a"), equalTo(true));
@@ -143,8 +151,8 @@ public void testOverlappingDescriptorsWithRanges() {
SystemIndexDescriptor pattern2 = new SystemIndexDescriptor(".test-a*", "");
Map descriptors = new HashMap<>();
- descriptors.put(source1, new SystemIndices.Feature("source 1", List.of(pattern1)));
- descriptors.put(source2, new SystemIndices.Feature("source 2", List.of(pattern2)));
+ descriptors.put(source1, new SystemIndices.Feature(source1, "source 1", List.of(pattern1)));
+ descriptors.put(source2, new SystemIndices.Feature(source2, "source 2", List.of(pattern2)));
IllegalStateException exception = expectThrows(IllegalStateException.class,
() -> SystemIndices.checkForOverlappingPatterns(descriptors));
diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java
index 57804d8cf2c16..4b0d7d3701168 100644
--- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java
+++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java
@@ -10,8 +10,10 @@
import org.apache.logging.log4j.Logger;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
import org.elasticsearch.action.support.ActionFilter;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.OriginSettingClient;
@@ -346,8 +348,10 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@@ -1194,6 +1198,62 @@ public String getFeatureDescription() {
return "Provides anomaly detection and forecasting functionality";
}
+ @Override public void cleanUpFeature(
+ ClusterService clusterService,
+ Client client,
+ ActionListener listener) {
+
+ Map results = new ConcurrentHashMap<>();
+
+ ActionListener afterDataframesStopped = ActionListener.wrap(dataFrameStopResponse -> {
+ // Handle the response
+ results.put("data_frame/analytics", dataFrameStopResponse.isStopped());
+
+ if (results.values().stream().allMatch(b -> b)) {
+ // Call into the original listener to clean up the indices
+ SystemIndexPlugin.super.cleanUpFeature(clusterService, client, listener);
+ } else {
+ final List failedComponents = results.entrySet().stream()
+ .filter(result -> result.getValue() == false)
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+ listener.onFailure(new RuntimeException("Some components failed to reset: " + failedComponents));
+ }
+ }, listener::onFailure);
+
+
+ ActionListener afterAnomalyDetectionClosed = ActionListener.wrap(closeJobResponse -> {
+ // Handle the response
+ results.put("anomaly_detectors", closeJobResponse.isClosed());
+
+ // Stop data frame analytics
+ StopDataFrameAnalyticsAction.Request stopDataFramesReq = new StopDataFrameAnalyticsAction.Request("_all");
+ stopDataFramesReq.setForce(true);
+ stopDataFramesReq.setAllowNoMatch(true);
+ client.execute(StopDataFrameAnalyticsAction.INSTANCE, stopDataFramesReq, afterDataframesStopped);
+ }, listener::onFailure);
+
+ // Close anomaly detection jobs
+ ActionListener afterDataFeedsStopped = ActionListener.wrap(datafeedResponse -> {
+ // Handle the response
+ results.put("datafeeds", datafeedResponse.isStopped());
+
+ // Close anomaly detection jobs
+ CloseJobAction.Request closeJobsRequest = new CloseJobAction.Request();
+ closeJobsRequest.setForce(true);
+ closeJobsRequest.setAllowNoMatch(true);
+ closeJobsRequest.setJobId("_all");
+ client.execute(CloseJobAction.INSTANCE, closeJobsRequest, afterAnomalyDetectionClosed);
+ }, listener::onFailure);
+
+ // Stop data feeds
+ StopDatafeedAction.Request stopDatafeedsReq = new StopDatafeedAction.Request("_all");
+ stopDatafeedsReq.setAllowNoMatch(true);
+ stopDatafeedsReq.setForce(true);
+ client.execute(StopDatafeedAction.INSTANCE, stopDatafeedsReq,
+ afterDataFeedsStopped);
+ }
+
@Override
public BreakerSettings getCircuitBreaker(Settings settings) {
return BreakerSettings.updateFromSettings(
diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java
index 94693b3b4c281..c175754c3416b 100644
--- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java
+++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java
@@ -81,6 +81,7 @@ public class Constants {
"cluster:admin/snapshot/restore",
"cluster:admin/snapshot/status",
"cluster:admin/features/get",
+ "cluster:admin/features/reset",
"cluster:admin/tasks/cancel",
"cluster:admin/transform/delete",
"cluster:admin/transform/preview",
diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/Transform.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/Transform.java
index 80313e909f3cb..7669d40fedf92 100644
--- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/Transform.java
+++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/Transform.java
@@ -11,11 +11,14 @@
import org.apache.logging.log4j.Logger;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.metadata.IndexTemplateMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
import org.elasticsearch.cluster.node.DiscoveryNodes;
@@ -408,6 +411,35 @@ public Collection getSystemIndexDescriptors(Settings sett
return org.elasticsearch.common.collect.List.of(AUDIT_INDEX_PATTERN);
}
+ @Override
+ public void cleanUpFeature(
+ ClusterService clusterService,
+ Client client,
+ ActionListener listener
+ ) {
+ ActionListener afterStoppingTransforms = ActionListener.wrap(stopTransformsResponse -> {
+ if (stopTransformsResponse.isAcknowledged()
+ && stopTransformsResponse.getTaskFailures().isEmpty()
+ && stopTransformsResponse.getNodeFailures().isEmpty()) {
+
+ SystemIndexPlugin.super.cleanUpFeature(clusterService, client, listener);
+ } else {
+ String errMsg = "Failed to reset Transform: "
+ + (stopTransformsResponse.isAcknowledged() ? "" : "not acknowledged ")
+ + (stopTransformsResponse.getNodeFailures().isEmpty()
+ ? ""
+ : "node failures: " + stopTransformsResponse.getNodeFailures() + " ")
+ + (stopTransformsResponse.getTaskFailures().isEmpty()
+ ? ""
+ : "task failures: " + stopTransformsResponse.getTaskFailures());
+ listener.onResponse(new ResetFeatureStateResponse.ResetFeatureStateStatus(this.getFeatureName(), errMsg));
+ }
+ }, listener::onFailure);
+
+ StopTransformAction.Request stopTransformsRequest = new StopTransformAction.Request(Metadata.ALL, true, true, null, true, false);
+ client.execute(StopTransformAction.INSTANCE, stopTransformsRequest, afterStoppingTransforms);
+ }
+
@Override
public String getFeatureName() {
return "transform";