Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make feature reset API response more informative (#71240) #72341

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,30 @@

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<ResetFeatureStateStatus> features;

private static final ParseField FEATURES = new ParseField("features");

@SuppressWarnings("unchecked")
private static final ConstructingObjectParser<ResetFeaturesResponse, Void> PARSER = new ConstructingObjectParser<>(
"snapshottable_features_response", true,
"features_reset_status_response", true,
(a, ctx) -> new ResetFeaturesResponse((List<ResetFeatureStateStatus>) a[0])
);

Expand All @@ -32,51 +41,93 @@ 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<ResetFeatureStateStatus> features) {
this.features = features;
}

public List<ResetFeatureStateStatus> getFeatures() {
/**
* @return List containing a reset status for each feature that we have tried to reset.
*/
public List<ResetFeatureStateStatus> getFeatureResetStatuses() {
return features;
}

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<ResetFeatureStateStatus, Void> PARSER = new ConstructingObjectParser<>(
"features", true, (a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1])
private static final ConstructingObjectParser<ResetFeatureStateStatus, Void> PARSER = new ConstructingObjectParser<>(
"feature_state_reset_stats", true,
(a, ctx) -> new ResetFeatureStateStatus((String) a[0], (String) a[1], (ElasticsearchException) a[2])
);

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);
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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())));
}
}
Original file line number Diff line number Diff line change
@@ -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<ResetFeatureStateResponse, ResetFeaturesResponse> {

@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<String, String> clientFeatures = clientInstance.getFeatureResetStatuses()
.stream()
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus()));
Map<String, String> serverFeatures = serverTestInstance.getFeatureStateResetStatuses()
.stream()
.collect(Collectors.toMap(f -> f.getFeatureName(), f -> f.getStatus().toString()));

assertThat(clientFeatures.entrySet(), everyItem(is(in(serverFeatures.entrySet()))));
}
}
2 changes: 2 additions & 0 deletions docs/reference/features/apis/features-apis.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ by Elasticsearch and Elasticsearch plugins.
[discrete]
=== Features APIs
* <<get-features-api,Get Features API>>
* <<reset-features-api,Rest Features API>>

include::get-features-api.asciidoc[]
include::reset-features-api.asciidoc[]
54 changes: 54 additions & 0 deletions docs/reference/features/apis/reset-features-api.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[[reset-features-api]]
=== Reset features API
++++
<titleabbrev>Reset features</titleabbrev>
++++

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.

[source,console]
-----------------------------------
POST /_features/_reset
-----------------------------------

[[reset-features-api-request]]
==== {api-request-title}

`POST /_features/_reset`


[[reset-features-api-desc]]
==== {api-description-title}

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.

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.

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 <<get-features-api,get features API>>.

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.

==== {api-examples-title}
Example response:
[source,console-result]
----
{
"features" : [
{
"feature_name" : "security",
"status" : "SUCCESS"
},
{
"feature_name" : "tasks",
"status" : "SUCCESS"
}
]
}
----
// TESTRESPONSE[s/"features" : \[[^\]]*\]/"features": $body.$_path/]
Loading