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 index 72d004021d6be..a8798842c5ffa 100644 --- 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 @@ -8,13 +8,22 @@ package org.elasticsearch.client.feature; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentParser; import java.util.List; +import java.util.Objects; +/** + * This class represents the response of the Feature State Reset API. It is a + * list containing the response of every feature whose state can be reset. The + * response from each feature will indicate success or failure. In the case of a + * failure, the cause will be returned as well. + */ public class ResetFeaturesResponse { private final List features; @@ -22,7 +31,7 @@ public class ResetFeaturesResponse { @SuppressWarnings("unchecked") private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "snapshottable_features_response", true, + "features_reset_status_response", true, (a, ctx) -> new ResetFeaturesResponse((List) a[0]) ); @@ -32,11 +41,18 @@ public class ResetFeaturesResponse { ResetFeaturesResponse.ResetFeatureStateStatus::parse, FEATURES); } + /** + * Create a new ResetFeaturesResponse + * @param features A full list of status responses from individual feature reset operations. + */ public ResetFeaturesResponse(List features) { this.features = features; } - public List getFeatures() { + /** + * @return List containing a reset status for each feature that we have tried to reset. + */ + public List getFeatureResetStatuses() { return features; } @@ -44,15 +60,24 @@ public static ResetFeaturesResponse parse(XContentParser parser) { return PARSER.apply(parser, null); } + /** + * A class representing the status of an attempt to reset a feature's state. + * The attempt to reset either succeeds and we return the name of the + * feature and a success flag; or it fails and we return the name of the feature, + * a status flag, and the exception thrown during the attempt to reset the feature. + */ public static class ResetFeatureStateStatus { private final String featureName; private final String status; + private final Exception exception; private static final ParseField FEATURE_NAME = new ParseField("feature_name"); private static final ParseField STATUS = new ParseField("status"); + private static final ParseField EXCEPTION = new ParseField("exception"); - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "features", true, (a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1]) + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "feature_state_reset_stats", true, + (a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1], (ElasticsearchException) a[2]) ); static { @@ -60,23 +85,49 @@ public static class ResetFeatureStateStatus { (p, c) -> p.text(), FEATURE_NAME, ObjectParser.ValueType.STRING); PARSER.declareField(ConstructingObjectParser.constructorArg(), (p, c) -> p.text(), STATUS, ObjectParser.ValueType.STRING); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> ElasticsearchException.fromXContent(p), EXCEPTION); } - ResetFeatureStateStatus(String featureName, String status) { + /** + * Create a ResetFeatureStateStatus. + * @param featureName Name of the feature whose status has been reset. + * @param status Whether the reset attempt succeeded or failed. + * @param exception If the reset attempt failed, the exception that caused the + * failure. Must be null when status is "SUCCESS". + */ + ResetFeatureStateStatus(String featureName, String status, @Nullable Exception exception) { this.featureName = featureName; + assert "SUCCESS".equals(status) || "FAILURE".equals(status); this.status = status; + assert "FAILURE".equals(status) ? Objects.nonNull(exception) : Objects.isNull(exception); + this.exception = exception; } public static ResetFeatureStateStatus parse(XContentParser parser, Void ctx) { return PARSER.apply(parser, ctx); } + /** + * @return Name of the feature that we tried to reset + */ public String getFeatureName() { return featureName; } + /** + * @return "SUCCESS" if the reset attempt succeeded, "FAILURE" otherwise. + */ public String getStatus() { return status; } + + /** + * @return The exception that caused the reset attempt to fail. + */ + @Nullable + public Exception getException() { + return exception; + } } } 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 e8c3463762992..d62d032f5d9b7 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 @@ -12,8 +12,11 @@ import org.elasticsearch.client.feature.GetFeaturesResponse; import org.elasticsearch.client.feature.ResetFeaturesRequest; import org.elasticsearch.client.feature.ResetFeaturesResponse; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.search.SearchModule; import java.io.IOException; +import java.util.Collections; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.notNullValue; @@ -31,16 +34,28 @@ public void testGetFeatures() throws IOException { assertTrue(response.getFeatures().stream().anyMatch(feature -> "tasks".equals(feature.getFeatureName()))); } + /** + * This test assumes that at least one of our defined features should reset successfully. + * Since plugins should be testing their own reset operations if they use something + * other than the default, this test tolerates failures in the response from the + * feature reset API. We just need to check that we can reset the "tasks" system index. + */ public void testResetFeatures() throws IOException { ResetFeaturesRequest request = new ResetFeaturesRequest(); + // need superuser privileges to execute the reset + RestHighLevelClient adminHighLevelClient = new RestHighLevelClient( + adminClient(), + (client) -> {}, + new SearchModule(Settings.EMPTY, true, Collections.emptyList()).getNamedXContents()); ResetFeaturesResponse response = execute(request, - highLevelClient().features()::resetFeatures, highLevelClient().features()::resetFeaturesAsync); + adminHighLevelClient.features()::resetFeatures, + adminHighLevelClient.features()::resetFeaturesAsync); assertThat(response, notNullValue()); - assertThat(response.getFeatures(), notNullValue()); - assertThat(response.getFeatures().size(), greaterThan(1)); - assertTrue(response.getFeatures().stream().anyMatch( + assertThat(response.getFeatureResetStatuses(), notNullValue()); + assertThat(response.getFeatureResetStatuses().size(), greaterThan(1)); + assertTrue(response.getFeatureResetStatuses().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/ResetFeaturesResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/ResetFeaturesResponseTests.java new file mode 100644 index 0000000000000..d21f2a469f33d --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/snapshots/ResetFeaturesResponseTests.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.client.snapshots; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse; +import org.elasticsearch.client.AbstractResponseTestCase; +import org.elasticsearch.client.feature.ResetFeaturesResponse; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.is; + +public class ResetFeaturesResponseTests extends AbstractResponseTestCase { + + @Override + protected ResetFeatureStateResponse createServerTestInstance( + XContentType xContentType) { + return new org.elasticsearch.action.admin.cluster.snapshots.features.ResetFeatureStateResponse( + randomList( + 10, + () -> randomBoolean() + ? ResetFeatureStateResponse.ResetFeatureStateStatus.success(randomAlphaOfLengthBetween(6, 10)) + : ResetFeatureStateResponse.ResetFeatureStateStatus.failure( + randomAlphaOfLengthBetween(6, 10), new ElasticsearchException("something went wrong")) + ) + ); + } + + @Override + protected ResetFeaturesResponse doParseToClientInstance(XContentParser parser) throws IOException { + return ResetFeaturesResponse.parse(parser); + } + + @Override + protected void assertInstances(ResetFeatureStateResponse serverTestInstance, ResetFeaturesResponse clientInstance) { + + assertNotNull(serverTestInstance.getFeatureStateResetStatuses()); + assertNotNull(clientInstance.getFeatureResetStatuses()); + + assertThat(clientInstance.getFeatureResetStatuses(), hasSize(serverTestInstance.getFeatureStateResetStatuses().size())); + + Map clientFeatures = clientInstance.getFeatureResetStatuses() + .stream() + .collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus())); + Map serverFeatures = serverTestInstance.getFeatureStateResetStatuses() + .stream() + .collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus().toString())); + + assertThat(clientFeatures.entrySet(), everyItem(is(in(serverFeatures.entrySet())))); + } +} diff --git a/docs/reference/features/apis/reset-features-api.asciidoc b/docs/reference/features/apis/reset-features-api.asciidoc index 2d17825a39d96..300ce166eaa13 100644 --- a/docs/reference/features/apis/reset-features-api.asciidoc +++ b/docs/reference/features/apis/reset-features-api.asciidoc @@ -8,7 +8,7 @@ experimental::[] Clears all of the the state information stored in system indices by {es} features, including the security and machine learning indices. -WARNING: Intended for development and testing use only. Do not reset features on a production cluster. +WARNING: Intended for development and testing use only. Do not reset features on a production cluster. [source,console] ----------------------------------- @@ -26,9 +26,11 @@ POST /_features/_reset Return a cluster to the same state as a new installation by resetting the feature state for all {es} features. This deletes all state information stored in system indices. -Note that select features might provide a way to reset particular system indices. Using this API resets _all_ features, both those that are built-in and implemented as plugins. +The response code is `HTTP 200` if state is successfully reset for all features, `HTTP 207` if there is a mixture of successes and failures, and `HTTP 500` if the reset operation fails for all features. -To list the features that will be affected, use the <>. +Note that select features might provide a way to reset particular system indices. Using this API resets _all_ features, both those that are built-in and implemented as plugins. + +To list the features that will be affected, use the <>. IMPORTANT: The features installed on the node you submit this request to are the features that will be reset. Run on the master node if you have any doubts about which plugins are installed on individual nodes. diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java index 7bbf1267ee8b7..8589f2e0e4fbd 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/FeatureStateResetApiIT.java @@ -8,10 +8,14 @@ package org.elasticsearch.snapshots; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; 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.client.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.indices.SystemIndexDescriptor; @@ -23,10 +27,13 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; public class FeatureStateResetApiIT extends ESIntegTestCase { @@ -35,6 +42,7 @@ protected Collection> nodePlugins() { List> plugins = new ArrayList<>(super.nodePlugins()); plugins.add(SystemIndexTestPlugin.class); plugins.add(SecondSystemIndexTestPlugin.class); + plugins.add(EvilSystemIndexTestPlugin.class); return plugins; } @@ -62,10 +70,11 @@ public void testResetSystemIndices() throws Exception { // 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") + assertThat(apiResponse.getFeatureStateResetStatuses(), containsInAnyOrder( + ResetFeatureStateResponse.ResetFeatureStateStatus.success("SystemIndexTestPlugin"), + ResetFeatureStateResponse.ResetFeatureStateStatus.success("SecondSystemIndexTestPlugin"), + ResetFeatureStateResponse.ResetFeatureStateStatus.success("EvilSystemIndexTestPlugin"), + ResetFeatureStateResponse.ResetFeatureStateStatus.success("tasks") )); // verify that both indices are gone @@ -94,6 +103,31 @@ public void testResetSystemIndices() throws Exception { assertThat(response.getIndices(), arrayContaining("my_index")); } + /** + * Evil test - test that when a feature fails to reset, we get a response object + * indicating the failure + */ + public void testFeatureResetFailure() throws Exception { + try { + EvilSystemIndexTestPlugin.setBeEvil(true); + ResetFeatureStateResponse resetFeatureStateResponse = client().execute(ResetFeatureStateAction.INSTANCE, + new ResetFeatureStateRequest()).get(); + + List failedFeatures = resetFeatureStateResponse.getFeatureStateResetStatuses().stream() + .filter(status -> status.getStatus() == ResetFeatureStateResponse.ResetFeatureStateStatus.Status.FAILURE) + .peek(status -> assertThat(status.getException(), notNullValue())) + .map(status -> { + // all failed statuses should have exceptions + assertThat(status.getException(), notNullValue()); + return status.getFeatureName(); + }) + .collect(Collectors.toList()); + assertThat(failedFeatures, contains("EvilSystemIndexTestPlugin")); + } finally { + EvilSystemIndexTestPlugin.setBeEvil(false); + } + } + /** * A test plugin with patterns for system indices and associated indices. */ @@ -145,4 +179,43 @@ public String getFeatureDescription() { return "A second test plugin"; } } + + /** + * An evil test plugin to test failure cases. + */ + public static class EvilSystemIndexTestPlugin extends Plugin implements SystemIndexPlugin { + + private static boolean beEvil = false; + + @Override + public String getFeatureName() { + return "EvilSystemIndexTestPlugin"; + } + + @Override + public String getFeatureDescription() { + return "a plugin that can be very bad"; + } + + public static synchronized void setBeEvil(boolean evil) { + beEvil = evil; + } + + public static synchronized boolean isEvil() { + return beEvil; + } + + @Override + public void cleanUpFeature( + ClusterService clusterService, + Client client, + ActionListener listener) { + if (isEvil()) { + listener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.failure(getFeatureName(), + new ElasticsearchException("problem!"))); + } else { + listener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.success(getFeatureName())); + } + } + } } 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 index 492b4f934f0d3..0c47c38ab726d 100644 --- 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 @@ -8,7 +8,9 @@ package org.elasticsearch.action.admin.cluster.snapshots.features; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -17,6 +19,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -30,11 +33,11 @@ public class ResetFeatureStateResponse extends ActionResponse implements ToXCont * Create a response showing which features have had state reset and success * or failure status. * - * @param statusList A list of status responses + * @param resetFeatureStateStatuses A list of status responses */ - public ResetFeatureStateResponse(List statusList) { + public ResetFeatureStateResponse(List resetFeatureStateStatuses) { resetFeatureStateStatusList = new ArrayList<>(); - resetFeatureStateStatusList.addAll(statusList); + resetFeatureStateStatusList.addAll(resetFeatureStateStatuses); resetFeatureStateStatusList.sort(Comparator.comparing(ResetFeatureStateStatus::getFeatureName)); } @@ -43,8 +46,11 @@ public ResetFeatureStateResponse(StreamInput in) throws IOException { this.resetFeatureStateStatusList = in.readList(ResetFeatureStateStatus::new); } - public List getItemList() { - return this.resetFeatureStateStatusList; + /** + * @return List of statuses for individual reset operations, one per feature that we tried to reset + */ + public List getFeatureStateResetStatuses() { + return Collections.unmodifiableList(this.resetFeatureStateStatusList); } @Override @@ -92,59 +98,130 @@ public String toString() { */ public static class ResetFeatureStateStatus implements Writeable, ToXContentObject { private final String featureName; - private final String status; + private final Status status; + private final Exception exception; + + /** + * Success or failure enum. Not a boolean so that we can easily display + * "SUCCESS" or "FAILURE" when this object is serialized. + */ + public enum Status { + SUCCESS, + FAILURE + } - public ResetFeatureStateStatus(String featureName, String status) { + /** + * Create a feature status for a successful reset operation + * @param featureName Name of the feature whose state was successfully reset + * @return Success status for a feature + */ + public static ResetFeatureStateStatus success(String featureName) { + return new ResetFeatureStateStatus(featureName, Status.SUCCESS, null); + } + + /** + * Create a feature status for a failed reset operation + * @param featureName Name of the feature that failed + * @param exception The exception that caused or described the failure + * @return Failure status for a feature + */ + public static ResetFeatureStateStatus failure(String featureName, Exception exception) { + return new ResetFeatureStateStatus( + featureName, + Status.FAILURE, + exception); + } + + private ResetFeatureStateStatus(String featureName, Status status, @Nullable Exception exception) { this.featureName = featureName; this.status = status; + assert Status.FAILURE.equals(status) ? Objects.nonNull(exception) : Objects.isNull(exception); + this.exception = exception; } ResetFeatureStateStatus(StreamInput in) throws IOException { this.featureName = in.readString(); - this.status = in.readString(); + this.status = Status.valueOf(in.readString()); + this.exception = in.readBoolean() ? in.readException() : null; } + /** + * @return Name of the feature we tried to reset + */ public String getFeatureName() { return this.featureName; } - public String getStatus() { + /** + * @return Success or failure for the reset operation + */ + public Status getStatus() { return this.status; } + /** + * @return For a failed reset operation, the exception that caused or describes the failure. + */ + @Nullable + public Exception getException() { + return this.exception; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field("feature_name", this.featureName); builder.field("status", this.status); + if (Objects.nonNull(this.exception)) { + builder.field("exception"); + builder.startObject(); + new ElasticsearchException(exception).toXContent(builder, params); + builder.endObject(); + } builder.endObject(); return builder; } - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(this.featureName); - out.writeString(this.status); - } - + /** + * Without a convenient way to compare Exception equality, we consider + * only feature name and success or failure for equality. + * @param o An object to compare for equality + * @return True if the feature name and status are equal, false otherwise + */ @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); + return Objects.equals(featureName, that.featureName) && status == that.status; } + /** + * @return Hash code based only on feature name and status. + */ @Override public int hashCode() { return Objects.hash(featureName, status); } + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(this.featureName); + out.writeString(this.status.toString()); + if (exception != null) { + out.writeBoolean(true); + out.writeException(exception); + } else { + out.writeBoolean(false); + } + } + @Override public String toString() { return "ResetFeatureStateStatus{" + "featureName='" + featureName + '\'' + - ", status='" + status + '\'' + + ", status=" + status + + ", exception='" + exception + '\'' + '}'; } } diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java index a27d88e945399..3e9a5c26d019a 100644 --- a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java +++ b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java @@ -598,7 +598,7 @@ public static void cleanUpFeature( if (allIndices.isEmpty()) { // if no actual indices match the pattern, we can stop here - listener.onResponse(new ResetFeatureStateStatus(name, "SUCCESS")); + listener.onResponse(ResetFeatureStateStatus.success(name)); return; } @@ -607,12 +607,12 @@ public static void cleanUpFeature( client.execute(DeleteIndexAction.INSTANCE, deleteIndexRequest, new ActionListener() { @Override public void onResponse(AcknowledgedResponse acknowledgedResponse) { - listener.onResponse(new ResetFeatureStateStatus(name, "SUCCESS")); + listener.onResponse(ResetFeatureStateStatus.success(name)); } @Override public void onFailure(Exception e) { - listener.onResponse(new ResetFeatureStateStatus(name, "FAILURE: " + e.getMessage())); + listener.onResponse(ResetFeatureStateStatus.failure(name, e)); } }); } 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 index 580e28b536d38..cf63050378159 100644 --- 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 @@ -10,9 +10,11 @@ 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.client.node.NodeClient; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestToXContentListener; import java.io.IOException; @@ -40,6 +42,22 @@ public String getName() { protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { final ResetFeatureStateRequest req = new ResetFeatureStateRequest(); - return restChannel -> client.execute(ResetFeatureStateAction.INSTANCE, req, new RestToXContentListener<>(restChannel)); + return restChannel -> client.execute( + ResetFeatureStateAction.INSTANCE, + req, + new RestToXContentListener(restChannel) { + @Override + protected RestStatus getStatus(ResetFeatureStateResponse response) { + long failures = response.getFeatureStateResetStatuses().stream() + .filter(status -> status.getStatus() == ResetFeatureStateResponse.ResetFeatureStateStatus.Status.FAILURE) + .count(); + if (failures == 0) { + return RestStatus.OK; + } else if (failures == response.getFeatureStateResetStatuses().size()) { + return RestStatus.INTERNAL_SERVER_ERROR; + } + return RestStatus.MULTI_STATUS; + } + }); } } 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 index b326b07e20994..55f63f5b3aca9 100644 --- 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 @@ -8,6 +8,7 @@ package org.elasticsearch.action.admin.cluster.snapshots.features; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.test.AbstractWireSerializingTestCase; @@ -29,25 +30,26 @@ 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"))); + resetStatuses.add(randomFrom( + ResetFeatureStateResponse.ResetFeatureStateStatus.success(feature1), + ResetFeatureStateResponse.ResetFeatureStateStatus.failure(feature1, new ElasticsearchException("bad")))); + resetStatuses.add(randomFrom( + ResetFeatureStateResponse.ResetFeatureStateStatus.success(feature2), + ResetFeatureStateResponse.ResetFeatureStateStatus.failure(feature2, new ElasticsearchException("bad")))); return new ResetFeatureStateResponse(resetStatuses); } @Override protected ResetFeatureStateResponse mutateInstance(ResetFeatureStateResponse instance) throws IOException { int minSize = 0; - if (instance.getItemList().size() == 0) { + if (instance.getFeatureStateResetStatuses().size() == 0) { minSize = 1; } - Set existingFeatureNames = instance.getItemList().stream() + Set existingFeatureNames = instance.getFeatureStateResetStatuses().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)))); + () -> ResetFeatureStateResponse.ResetFeatureStateStatus.success( + randomValueOtherThanMany(existingFeatureNames::contains, () -> randomAlphaOfLengthBetween(4, 10))))); } } 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 c4d502ac70394..a73aeaa87867b 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 @@ -506,7 +506,8 @@ public void cleanUpFeature( + (stopTransformsResponse.getTaskFailures().isEmpty() ? "" : "task failures: " + stopTransformsResponse.getTaskFailures()); - unsetResetModeListener.onResponse(new ResetFeatureStateResponse.ResetFeatureStateStatus(this.getFeatureName(), errMsg)); + unsetResetModeListener.onResponse(ResetFeatureStateResponse.ResetFeatureStateStatus.failure(this.getFeatureName(), + new ElasticsearchException(errMsg))); } }, unsetResetModeListener::onFailure);