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";