Add 'state' query param to GET snapshots API (#128635)
This change introduces a new optional 'state' query parameter for the Get Snapshots API, allowing users to filter snapshots by state. The parameter accepts comma-separated values for states: SUCCESS, IN_PROGRESS, FAILED, PARTIAL, INCOMPATIBLE (case-insensitive). A new 'snapshots.get.state_parameter' NodeFeature has been added with this change. The new state query parameter will only be supported in clusters where all nodes support this feature. --------- Co-authored-by: Elena Stoeva <elenastoeva99@gmail.com>
This commit is contained in:
parent
217275c229
commit
d43198ea3e
|
@ -0,0 +1,6 @@
|
|||
pr: 128635
|
||||
summary: Add `state` query param to Get snapshots API
|
||||
area: Snapshot/Restore
|
||||
type: enhancement
|
||||
issues:
|
||||
- 97446
|
|
@ -85,6 +85,10 @@
|
|||
"verbose":{
|
||||
"type":"boolean",
|
||||
"description":"Whether to show verbose snapshot info or only show the basic info found in the repository index blob"
|
||||
},
|
||||
"state": {
|
||||
"type": "list",
|
||||
"description": "Filter snapshots by a comma-separated list of states. Valid state values are 'SUCCESS', 'IN_PROGRESS', 'FAILED', 'PARTIAL', or 'INCOMPATIBLE'."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -303,3 +303,72 @@ setup:
|
|||
snapshot.delete:
|
||||
repository: test_repo_get_1
|
||||
snapshot: test_snapshot_no_repo_name
|
||||
|
||||
---
|
||||
"Get snapshot using state parameter":
|
||||
- requires:
|
||||
cluster_features: "snapshots.get.state_parameter"
|
||||
test_runner_features: capabilities
|
||||
capabilities:
|
||||
- method: GET
|
||||
path: /_snapshot/{repository}/{snapshot}
|
||||
parameters: [ state ]
|
||||
reason: "state parameter was introduced in 9.1"
|
||||
|
||||
- do:
|
||||
indices.create:
|
||||
index: test_index
|
||||
body:
|
||||
settings:
|
||||
number_of_shards: 1
|
||||
number_of_replicas: 0
|
||||
|
||||
- do:
|
||||
snapshot.create:
|
||||
repository: test_repo_get_1
|
||||
snapshot: test_snapshot_with_state_param
|
||||
wait_for_completion: true
|
||||
|
||||
- do:
|
||||
snapshot.get:
|
||||
repository: test_repo_get_1
|
||||
snapshot: test_snapshot_with_state_param
|
||||
state: SUCCESS
|
||||
|
||||
- is_true: snapshots
|
||||
- match: { snapshots.0.snapshot: test_snapshot_with_state_param }
|
||||
- match: { snapshots.0.state: SUCCESS }
|
||||
|
||||
- do:
|
||||
snapshot.get:
|
||||
repository: test_repo_get_1
|
||||
snapshot: test_snapshot_with_state_param
|
||||
state: SUCCESS,PARTIAL
|
||||
|
||||
- is_true: snapshots
|
||||
- match: { snapshots.0.snapshot: test_snapshot_with_state_param }
|
||||
- match: { snapshots.0.state: SUCCESS }
|
||||
|
||||
- do:
|
||||
snapshot.get:
|
||||
repository: test_repo_get_1
|
||||
snapshot: test_snapshot_with_state_param
|
||||
state: FAILED
|
||||
|
||||
- is_true: snapshots
|
||||
- length: { snapshots: 0 }
|
||||
|
||||
- do:
|
||||
catch: bad_request
|
||||
snapshot.get:
|
||||
repository: test_repo_get_1
|
||||
snapshot: test_snapshot_with_state_param
|
||||
state: FOO
|
||||
|
||||
- match: { error.type: "illegal_argument_exception" }
|
||||
- match: { error.reason: "No enum constant org.elasticsearch.snapshots.SnapshotState.FOO" }
|
||||
|
||||
- do:
|
||||
snapshot.delete:
|
||||
repository: test_repo_get_1
|
||||
snapshot: test_snapshot_with_state_param
|
||||
|
|
|
@ -55,13 +55,16 @@ import org.elasticsearch.xcontent.json.JsonXContent;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -635,6 +638,63 @@ public class GetSnapshotsIT extends AbstractSnapshotIntegTestCase {
|
|||
expectThrows(RepositoryMissingException.class, multiRepoFuture::actionGet);
|
||||
}
|
||||
|
||||
public void testFilterByState() throws Exception {
|
||||
final String repoName = "test-repo";
|
||||
final Path repoPath = randomRepoPath();
|
||||
createRepository(repoName, "mock", repoPath);
|
||||
|
||||
// Create a successful snapshot
|
||||
createFullSnapshot(repoName, "snapshot-success");
|
||||
|
||||
final Function<EnumSet<SnapshotState>, List<SnapshotInfo>> getSnapshotsForStates = (states) -> {
|
||||
return clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).setStates(states).get().getSnapshots();
|
||||
};
|
||||
|
||||
// Fetch snapshots with state=SUCCESS
|
||||
var snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.SUCCESS));
|
||||
assertThat(snapshots, hasSize(1));
|
||||
assertThat(snapshots.getFirst().state(), is(SnapshotState.SUCCESS));
|
||||
|
||||
// Create a snapshot in progress
|
||||
blockAllDataNodes(repoName);
|
||||
startFullSnapshot(repoName, "snapshot-in-progress");
|
||||
awaitNumberOfSnapshotsInProgress(1);
|
||||
|
||||
// Fetch snapshots with state=IN_PROGRESS
|
||||
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.IN_PROGRESS));
|
||||
assertThat(snapshots, hasSize(1));
|
||||
assertThat(snapshots.getFirst().state(), is(SnapshotState.IN_PROGRESS));
|
||||
|
||||
// Fetch snapshots with multiple states (SUCCESS, IN_PROGRESS)
|
||||
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.SUCCESS, SnapshotState.IN_PROGRESS));
|
||||
assertThat(snapshots, hasSize(2));
|
||||
var states = snapshots.stream().map(SnapshotInfo::state).collect(Collectors.toSet());
|
||||
assertTrue(states.contains(SnapshotState.SUCCESS));
|
||||
assertTrue(states.contains(SnapshotState.IN_PROGRESS));
|
||||
|
||||
// Fetch all snapshots (without state)
|
||||
snapshots = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).get().getSnapshots();
|
||||
assertThat(snapshots, hasSize(2));
|
||||
|
||||
// Fetch snapshots with an invalid state
|
||||
IllegalArgumentException e = expectThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> getSnapshotsForStates.apply(EnumSet.of(SnapshotState.valueOf("FOO")))
|
||||
);
|
||||
assertThat(e.getMessage(), is("No enum constant org.elasticsearch.snapshots.SnapshotState.FOO"));
|
||||
|
||||
// Allow the IN_PROGRESS snapshot to finish, then verify GET using SUCCESS has results and IN_PROGRESS does not.
|
||||
unblockAllDataNodes(repoName);
|
||||
awaitNumberOfSnapshotsInProgress(0);
|
||||
snapshots = clusterAdmin().prepareGetSnapshots(TEST_REQUEST_TIMEOUT, repoName).get().getSnapshots();
|
||||
assertThat(snapshots, hasSize(2));
|
||||
states = snapshots.stream().map(SnapshotInfo::state).collect(Collectors.toSet());
|
||||
assertThat(states, hasSize(1));
|
||||
assertTrue(states.contains(SnapshotState.SUCCESS));
|
||||
snapshots = getSnapshotsForStates.apply(EnumSet.of(SnapshotState.IN_PROGRESS));
|
||||
assertThat(snapshots, hasSize(0));
|
||||
}
|
||||
|
||||
public void testRetrievingSnapshotsWhenRepositoryIsUnreadable() throws Exception {
|
||||
final String repoName = randomIdentifier();
|
||||
final Path repoPath = randomRepoPath();
|
||||
|
@ -956,6 +1016,12 @@ public class GetSnapshotsIT extends AbstractSnapshotIntegTestCase {
|
|||
// INDICES and by SHARDS. The actual sorting behaviour for these cases is tested elsewhere, here we're just checking that sorting
|
||||
// interacts correctly with the other parameters to the API.
|
||||
|
||||
final EnumSet<SnapshotState> states = EnumSet.copyOf(randomNonEmptySubsetOf(Arrays.asList(SnapshotState.values())));
|
||||
// Note: The selected state(s) may not match any existing snapshots.
|
||||
// The actual filtering behaviour for such cases is tested in the dedicated test.
|
||||
// Here we're just checking that states interacts correctly with the other parameters to the API.
|
||||
snapshotInfoPredicate = snapshotInfoPredicate.and(si -> states.contains(si.state()));
|
||||
|
||||
// compute the ordered sequence of snapshots which match the repository/snapshot name filters and SLM policy filter
|
||||
final var selectedSnapshots = snapshotInfos.stream()
|
||||
.filter(snapshotInfoPredicate)
|
||||
|
@ -967,7 +1033,8 @@ public class GetSnapshotsIT extends AbstractSnapshotIntegTestCase {
|
|||
)
|
||||
// apply sorting params
|
||||
.sort(sortKey)
|
||||
.order(order);
|
||||
.order(order)
|
||||
.states(states);
|
||||
|
||||
// sometimes use ?from_sort_value to skip some items; note that snapshots skipped in this way are subtracted from
|
||||
// GetSnapshotsResponse.totalCount whereas snapshots skipped by ?after and ?offset are not
|
||||
|
@ -1054,7 +1121,8 @@ public class GetSnapshotsIT extends AbstractSnapshotIntegTestCase {
|
|||
.sort(sortKey)
|
||||
.order(order)
|
||||
.size(nextSize)
|
||||
.after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter));
|
||||
.after(SnapshotSortKey.decodeAfterQueryParam(nextRequestAfter))
|
||||
.states(states);
|
||||
final GetSnapshotsResponse nextResponse = safeAwait(l -> client().execute(TransportGetSnapshotsAction.TYPE, nextRequest, l));
|
||||
|
||||
assertEquals(
|
||||
|
|
|
@ -425,6 +425,7 @@ module org.elasticsearch.server {
|
|||
org.elasticsearch.action.bulk.BulkFeatures,
|
||||
org.elasticsearch.features.InfrastructureFeatures,
|
||||
org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures,
|
||||
org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures,
|
||||
org.elasticsearch.index.mapper.MapperFeatures,
|
||||
org.elasticsearch.index.IndexFeatures,
|
||||
org.elasticsearch.search.SearchFeatures,
|
||||
|
|
|
@ -299,6 +299,7 @@ public class TransportVersions {
|
|||
public static final TransportVersion NONE_CHUNKING_STRATEGY = def(9_097_0_00);
|
||||
public static final TransportVersion PROJECT_DELETION_GLOBAL_BLOCK = def(9_098_0_00);
|
||||
public static final TransportVersion SECURITY_CLOUD_API_KEY_REALM_AND_TYPE = def(9_099_0_00);
|
||||
public static final TransportVersion STATE_PARAM_GET_SNAPSHOT = def(9_100_0_00);
|
||||
|
||||
/*
|
||||
* STOP! READ THIS FIRST! No, really,
|
||||
|
|
|
@ -864,7 +864,7 @@ public class ActionModule extends AbstractModule {
|
|||
registerHandler.accept(new RestDeleteRepositoryAction());
|
||||
registerHandler.accept(new RestVerifyRepositoryAction());
|
||||
registerHandler.accept(new RestCleanupRepositoryAction());
|
||||
registerHandler.accept(new RestGetSnapshotsAction());
|
||||
registerHandler.accept(new RestGetSnapshotsAction(clusterSupportsFeature));
|
||||
registerHandler.accept(new RestCreateSnapshotAction());
|
||||
registerHandler.accept(new RestCloneSnapshotAction());
|
||||
registerHandler.accept(new RestRestoreSnapshotAction());
|
||||
|
|
|
@ -19,13 +19,16 @@ import org.elasticsearch.common.io.stream.StreamOutput;
|
|||
import org.elasticsearch.core.Nullable;
|
||||
import org.elasticsearch.core.TimeValue;
|
||||
import org.elasticsearch.search.sort.SortOrder;
|
||||
import org.elasticsearch.snapshots.SnapshotState;
|
||||
import org.elasticsearch.tasks.CancellableTask;
|
||||
import org.elasticsearch.tasks.Task;
|
||||
import org.elasticsearch.tasks.TaskId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.elasticsearch.action.ValidateActions.addValidationError;
|
||||
|
||||
|
@ -39,6 +42,7 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
|
|||
public static final boolean DEFAULT_VERBOSE_MODE = true;
|
||||
|
||||
private static final TransportVersion INDICES_FLAG_VERSION = TransportVersions.V_8_3_0;
|
||||
private static final TransportVersion STATE_FLAG_VERSION = TransportVersions.STATE_PARAM_GET_SNAPSHOT;
|
||||
|
||||
public static final int NO_LIMIT = -1;
|
||||
|
||||
|
@ -77,6 +81,8 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
|
|||
|
||||
private boolean includeIndexNames = true;
|
||||
|
||||
private EnumSet<SnapshotState> states = EnumSet.allOf(SnapshotState.class);
|
||||
|
||||
public GetSnapshotsRequest(TimeValue masterNodeTimeout) {
|
||||
super(masterNodeTimeout);
|
||||
}
|
||||
|
@ -118,6 +124,11 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
|
|||
if (in.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) {
|
||||
includeIndexNames = in.readBoolean();
|
||||
}
|
||||
if (in.getTransportVersion().onOrAfter(STATE_FLAG_VERSION)) {
|
||||
states = in.readEnumSet(SnapshotState.class);
|
||||
} else {
|
||||
states = EnumSet.allOf(SnapshotState.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -137,6 +148,13 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
|
|||
if (out.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) {
|
||||
out.writeBoolean(includeIndexNames);
|
||||
}
|
||||
if (out.getTransportVersion().onOrAfter(STATE_FLAG_VERSION)) {
|
||||
out.writeEnumSet(states);
|
||||
} else if (states.equals(EnumSet.allOf(SnapshotState.class)) == false) {
|
||||
final var errorString = "GetSnapshotsRequest [states] field is not supported on all nodes in the cluster";
|
||||
assert false : errorString;
|
||||
throw new IllegalStateException(errorString);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -177,6 +195,9 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
|
|||
} else if (after != null && fromSortValue != null) {
|
||||
validationException = addValidationError("can't use after and from_sort_value simultaneously", validationException);
|
||||
}
|
||||
if (states.isEmpty()) {
|
||||
validationException = addValidationError("states is empty", validationException);
|
||||
}
|
||||
return validationException;
|
||||
}
|
||||
|
||||
|
@ -342,6 +363,15 @@ public class GetSnapshotsRequest extends MasterNodeRequest<GetSnapshotsRequest>
|
|||
return verbose;
|
||||
}
|
||||
|
||||
public EnumSet<SnapshotState> states() {
|
||||
return states;
|
||||
}
|
||||
|
||||
public GetSnapshotsRequest states(EnumSet<SnapshotState> states) {
|
||||
this.states = Objects.requireNonNull(states);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) {
|
||||
return new CancellableTask(id, type, action, getDescription(), parentTaskId, headers);
|
||||
|
|
|
@ -15,6 +15,9 @@ import org.elasticsearch.common.util.ArrayUtils;
|
|||
import org.elasticsearch.core.Nullable;
|
||||
import org.elasticsearch.core.TimeValue;
|
||||
import org.elasticsearch.search.sort.SortOrder;
|
||||
import org.elasticsearch.snapshots.SnapshotState;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Get snapshots request builder
|
||||
|
@ -150,4 +153,8 @@ public class GetSnapshotsRequestBuilder extends MasterNodeOperationRequestBuilde
|
|||
|
||||
}
|
||||
|
||||
public GetSnapshotsRequestBuilder setStates(EnumSet<SnapshotState> states) {
|
||||
request.states(states);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import org.elasticsearch.snapshots.Snapshot;
|
|||
import org.elasticsearch.snapshots.SnapshotId;
|
||||
import org.elasticsearch.snapshots.SnapshotInfo;
|
||||
import org.elasticsearch.snapshots.SnapshotMissingException;
|
||||
import org.elasticsearch.snapshots.SnapshotState;
|
||||
import org.elasticsearch.snapshots.SnapshotsService;
|
||||
import org.elasticsearch.tasks.CancellableTask;
|
||||
import org.elasticsearch.tasks.Task;
|
||||
|
@ -55,6 +56,7 @@ import org.elasticsearch.transport.TransportService;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
|
@ -161,7 +163,8 @@ public class TransportGetSnapshotsAction extends TransportMasterNodeAction<GetSn
|
|||
request.size(),
|
||||
SnapshotsInProgress.get(state),
|
||||
request.verbose(),
|
||||
request.includeIndexNames()
|
||||
request.includeIndexNames(),
|
||||
request.states()
|
||||
).runOperation(listener);
|
||||
}
|
||||
|
||||
|
@ -182,6 +185,7 @@ public class TransportGetSnapshotsAction extends TransportMasterNodeAction<GetSn
|
|||
private final SnapshotNamePredicate snapshotNamePredicate;
|
||||
private final SnapshotPredicates fromSortValuePredicates;
|
||||
private final Predicate<String> slmPolicyPredicate;
|
||||
private final EnumSet<SnapshotState> states;
|
||||
|
||||
// snapshot ordering/pagination
|
||||
private final SnapshotSortKey sortBy;
|
||||
|
@ -225,7 +229,8 @@ public class TransportGetSnapshotsAction extends TransportMasterNodeAction<GetSn
|
|||
int size,
|
||||
SnapshotsInProgress snapshotsInProgress,
|
||||
boolean verbose,
|
||||
boolean indices
|
||||
boolean indices,
|
||||
EnumSet<SnapshotState> states
|
||||
) {
|
||||
this.cancellableTask = cancellableTask;
|
||||
this.repositories = repositories;
|
||||
|
@ -238,6 +243,7 @@ public class TransportGetSnapshotsAction extends TransportMasterNodeAction<GetSn
|
|||
this.snapshotsInProgress = snapshotsInProgress;
|
||||
this.verbose = verbose;
|
||||
this.indices = indices;
|
||||
this.states = states;
|
||||
|
||||
this.snapshotNamePredicate = SnapshotNamePredicate.forSnapshots(ignoreUnavailable, snapshots);
|
||||
this.fromSortValuePredicates = SnapshotPredicates.forFromSortValue(fromSortValue, sortBy, order);
|
||||
|
@ -572,11 +578,16 @@ public class TransportGetSnapshotsAction extends TransportMasterNodeAction<GetSn
|
|||
return false;
|
||||
}
|
||||
|
||||
final var details = repositoryData.getSnapshotDetails(snapshotId);
|
||||
|
||||
if (details != null && details.getSnapshotState() != null && states.contains(details.getSnapshotState()) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slmPolicyPredicate == SlmPolicyPredicate.MATCH_ALL_POLICIES) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final var details = repositoryData.getSnapshotDetails(snapshotId);
|
||||
return details == null || details.getSlmPolicy() == null || slmPolicyPredicate.test(details.getSlmPolicy());
|
||||
}
|
||||
|
||||
|
@ -585,6 +596,10 @@ public class TransportGetSnapshotsAction extends TransportMasterNodeAction<GetSn
|
|||
return false;
|
||||
}
|
||||
|
||||
if (snapshotInfo.state() != null && states.contains(snapshotInfo.state()) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slmPolicyPredicate == SlmPolicyPredicate.MATCH_ALL_POLICIES) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -154,6 +154,8 @@ public abstract class BaseRestHandler implements RestHandler {
|
|||
supportedAndCommon.removeAll(RestRequest.INTERNAL_MARKER_REQUEST_PARAMETERS);
|
||||
final var consumed = new TreeSet<>(request.consumedParams());
|
||||
consumed.removeAll(RestRequest.INTERNAL_MARKER_REQUEST_PARAMETERS);
|
||||
// Response parameters are implicitly consumed since they are made available to response renderings.
|
||||
consumed.addAll(responseParams(request.getRestApiVersion()));
|
||||
assert supportedAndCommon.equals(consumed)
|
||||
: getName() + ": consumed params " + consumed + " while supporting " + supportedAndCommon;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
package org.elasticsearch.rest.action.admin.cluster;
|
||||
|
||||
import org.elasticsearch.features.FeatureSpecification;
|
||||
import org.elasticsearch.features.NodeFeature;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class GetSnapshotsFeatures implements FeatureSpecification {
|
||||
public static final NodeFeature GET_SNAPSHOTS_STATE_PARAMETER = new NodeFeature("snapshots.get.state_parameter");
|
||||
|
||||
@Override
|
||||
public Set<NodeFeature> getFeatures() {
|
||||
return Set.of(GET_SNAPSHOTS_STATE_PARAMETER);
|
||||
}
|
||||
}
|
|
@ -13,17 +13,24 @@ import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
|
|||
import org.elasticsearch.action.admin.cluster.snapshots.get.SnapshotSortKey;
|
||||
import org.elasticsearch.client.internal.node.NodeClient;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.util.set.Sets;
|
||||
import org.elasticsearch.features.NodeFeature;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.RestUtils;
|
||||
import org.elasticsearch.rest.Scope;
|
||||
import org.elasticsearch.rest.ServerlessScope;
|
||||
import org.elasticsearch.rest.action.RestCancellableNodeClient;
|
||||
import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener;
|
||||
import org.elasticsearch.search.sort.SortOrder;
|
||||
import org.elasticsearch.snapshots.SnapshotState;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.GET;
|
||||
import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout;
|
||||
|
@ -37,7 +44,35 @@ import static org.elasticsearch.snapshots.SnapshotInfo.INDEX_NAMES_XCONTENT_PARA
|
|||
@ServerlessScope(Scope.INTERNAL)
|
||||
public class RestGetSnapshotsAction extends BaseRestHandler {
|
||||
|
||||
public RestGetSnapshotsAction() {}
|
||||
private static final Set<String> SUPPORTED_RESPONSE_PARAMETERS = Set.of(
|
||||
INCLUDE_REPOSITORY_XCONTENT_PARAM,
|
||||
INDEX_DETAILS_XCONTENT_PARAM,
|
||||
INDEX_NAMES_XCONTENT_PARAM
|
||||
);
|
||||
|
||||
private static final Set<String> SUPPORTED_QUERY_PARAMETERS = Set.of(
|
||||
RestUtils.REST_MASTER_TIMEOUT_PARAM,
|
||||
"after",
|
||||
"from_sort_value",
|
||||
"ignore_unavailable",
|
||||
"offset",
|
||||
"order",
|
||||
"size",
|
||||
"slm_policy_filter",
|
||||
"sort",
|
||||
"state",
|
||||
"verbose"
|
||||
);
|
||||
|
||||
private static final Set<String> ALL_SUPPORTED_PARAMETERS = Set.copyOf(
|
||||
Sets.union(SUPPORTED_QUERY_PARAMETERS, SUPPORTED_RESPONSE_PARAMETERS, Set.of("repository", "snapshot"))
|
||||
);
|
||||
|
||||
private final Predicate<NodeFeature> clusterSupportsFeature;
|
||||
|
||||
public RestGetSnapshotsAction(Predicate<NodeFeature> clusterSupportsFeature) {
|
||||
this.clusterSupportsFeature = clusterSupportsFeature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Route> routes() {
|
||||
|
@ -51,7 +86,17 @@ public class RestGetSnapshotsAction extends BaseRestHandler {
|
|||
|
||||
@Override
|
||||
protected Set<String> responseParams() {
|
||||
return Set.of(INDEX_DETAILS_XCONTENT_PARAM, INCLUDE_REPOSITORY_XCONTENT_PARAM, INDEX_NAMES_XCONTENT_PARAM);
|
||||
return SUPPORTED_RESPONSE_PARAMETERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedQueryParameters() {
|
||||
return SUPPORTED_QUERY_PARAMETERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> allSupportedParameters() {
|
||||
return ALL_SUPPORTED_PARAMETERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -82,6 +127,18 @@ public class RestGetSnapshotsAction extends BaseRestHandler {
|
|||
final SortOrder order = SortOrder.fromString(request.param("order", getSnapshotsRequest.order().toString()));
|
||||
getSnapshotsRequest.order(order);
|
||||
getSnapshotsRequest.includeIndexNames(request.paramAsBoolean(INDEX_NAMES_XCONTENT_PARAM, getSnapshotsRequest.includeIndexNames()));
|
||||
|
||||
final String stateString = request.param("state");
|
||||
if (stateString == null) {
|
||||
getSnapshotsRequest.states(EnumSet.allOf(SnapshotState.class));
|
||||
} else if (Strings.hasText(stateString) == false) {
|
||||
throw new IllegalArgumentException("[state] parameter must not be empty");
|
||||
} else if (clusterSupportsFeature.test(GetSnapshotsFeatures.GET_SNAPSHOTS_STATE_PARAMETER)) {
|
||||
getSnapshotsRequest.states(EnumSet.copyOf(Arrays.stream(stateString.split(",")).map(SnapshotState::valueOf).toList()));
|
||||
} else {
|
||||
throw new IllegalArgumentException("[state] parameter is not supported on all nodes in the cluster");
|
||||
}
|
||||
|
||||
return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).admin()
|
||||
.cluster()
|
||||
.getSnapshots(getSnapshotsRequest, new RestRefCountedChunkedToXContentListener<>(channel));
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
org.elasticsearch.action.bulk.BulkFeatures
|
||||
org.elasticsearch.features.InfrastructureFeatures
|
||||
org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures
|
||||
org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures
|
||||
org.elasticsearch.index.IndexFeatures
|
||||
org.elasticsearch.index.mapper.MapperFeatures
|
||||
org.elasticsearch.search.SearchFeatures
|
||||
|
|
Loading…
Reference in New Issue