diff --git a/docs/changelog/128635.yaml b/docs/changelog/128635.yaml new file mode 100644 index 0000000000000..19a1dd0404ce1 --- /dev/null +++ b/docs/changelog/128635.yaml @@ -0,0 +1,6 @@ +pr: 128635 +summary: Add `state` query param to Get snapshots API +area: Snapshot/Restore +type: enhancement +issues: + - 97446 diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get.json b/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get.json index 23f5f737995d0..f40042c1b0dac 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.get.json @@ -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'." } } } diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/snapshot.get/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/snapshot.get/10_basic.yml index ca79a4419b813..e37939f71b1de 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/snapshot.get/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/snapshot.get/10_basic.yml @@ -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 diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/GetSnapshotsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/GetSnapshotsIT.java index 0923b4a3bfda9..a3132cff5dc95 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/GetSnapshotsIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/GetSnapshotsIT.java @@ -55,13 +55,16 @@ 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 void testRetrievingSnapshotsWhenRepositoryIsMissing() throws Exception { 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, List> 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 void testAllFeatures() { // 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 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 void testAllFeatures() { ) // 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 void testAllFeatures() { .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( diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index cd418d21e05c8..7b2b6d91fdc83 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -425,6 +425,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.mapper.MapperFeatures, org.elasticsearch.index.IndexFeatures, org.elasticsearch.search.SearchFeatures, diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index 1bbea421fe7c8..b9f0fbc755b8a 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -299,6 +299,7 @@ static TransportVersion def(int id) { 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, diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index d7cb57bad4812..d865b09acd887 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -864,7 +864,7 @@ public void initRestHandlers(Supplier nodesInCluster, Predicate< 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()); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java index ac3da1db420a5..628e48ab1ceb3 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java @@ -19,13 +19,16 @@ 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 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 private boolean includeIndexNames = true; + private EnumSet states = EnumSet.allOf(SnapshotState.class); + public GetSnapshotsRequest(TimeValue masterNodeTimeout) { super(masterNodeTimeout); } @@ -118,6 +124,11 @@ public GetSnapshotsRequest(StreamInput in) throws IOException { 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 void writeTo(StreamOutput out) throws IOException { 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 ActionRequestValidationException validate() { } 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 boolean verbose() { return verbose; } + public EnumSet states() { + return states; + } + + public GetSnapshotsRequest states(EnumSet states) { + this.states = Objects.requireNonNull(states); + return this; + } + @Override public Task createTask(long id, String type, String action, TaskId parentTaskId, Map headers) { return new CancellableTask(id, type, action, getDescription(), parentTaskId, headers); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequestBuilder.java index 4ace7536a86e1..176523a4725f0 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequestBuilder.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequestBuilder.java @@ -15,6 +15,9 @@ 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 GetSnapshotsRequestBuilder setIncludeIndexNames(boolean indices) { } + public GetSnapshotsRequestBuilder setStates(EnumSet states) { + request.states(states); + return this; + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index 09ea80a5bff88..eab763165a74c 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -46,6 +46,7 @@ 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 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 @@ protected void masterOperation( request.size(), SnapshotsInProgress.get(state), request.verbose(), - request.includeIndexNames() + request.includeIndexNames(), + request.states() ).runOperation(listener); } @@ -182,6 +185,7 @@ private class GetSnapshotsOperation { private final SnapshotNamePredicate snapshotNamePredicate; private final SnapshotPredicates fromSortValuePredicates; private final Predicate slmPolicyPredicate; + private final EnumSet states; // snapshot ordering/pagination private final SnapshotSortKey sortBy; @@ -225,7 +229,8 @@ private class GetSnapshotsOperation { int size, SnapshotsInProgress snapshotsInProgress, boolean verbose, - boolean indices + boolean indices, + EnumSet states ) { this.cancellableTask = cancellableTask; this.repositories = repositories; @@ -238,6 +243,7 @@ private class GetSnapshotsOperation { 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 @@ private boolean matchesPredicates(SnapshotId snapshotId, RepositoryData reposito 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 @@ private boolean matchesPredicates(SnapshotInfo snapshotInfo) { return false; } + if (snapshotInfo.state() != null && states.contains(snapshotInfo.state()) == false) { + return false; + } + if (slmPolicyPredicate == SlmPolicyPredicate.MATCH_ALL_POLICIES) { return true; } diff --git a/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java b/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java index 509086b982319..6834c01d2d0e9 100644 --- a/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java +++ b/server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java @@ -154,6 +154,8 @@ private boolean assertConsumesSupportedParams(@Nullable Set supported, R 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; } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/GetSnapshotsFeatures.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/GetSnapshotsFeatures.java new file mode 100644 index 0000000000000..050d77b62db86 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/GetSnapshotsFeatures.java @@ -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 getFeatures() { + return Set.of(GET_SNAPSHOTS_STATE_PARAMETER); + } +} diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetSnapshotsAction.java index b3a6430822020..4b58fae1741c7 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestGetSnapshotsAction.java @@ -13,17 +13,24 @@ 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 @@ @ServerlessScope(Scope.INTERNAL) public class RestGetSnapshotsAction extends BaseRestHandler { - public RestGetSnapshotsAction() {} + private static final Set SUPPORTED_RESPONSE_PARAMETERS = Set.of( + INCLUDE_REPOSITORY_XCONTENT_PARAM, + INDEX_DETAILS_XCONTENT_PARAM, + INDEX_NAMES_XCONTENT_PARAM + ); + + private static final Set 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 ALL_SUPPORTED_PARAMETERS = Set.copyOf( + Sets.union(SUPPORTED_QUERY_PARAMETERS, SUPPORTED_RESPONSE_PARAMETERS, Set.of("repository", "snapshot")) + ); + + private final Predicate clusterSupportsFeature; + + public RestGetSnapshotsAction(Predicate clusterSupportsFeature) { + this.clusterSupportsFeature = clusterSupportsFeature; + } @Override public List routes() { @@ -51,7 +86,17 @@ public String getName() { @Override protected Set responseParams() { - return Set.of(INDEX_DETAILS_XCONTENT_PARAM, INCLUDE_REPOSITORY_XCONTENT_PARAM, INDEX_NAMES_XCONTENT_PARAM); + return SUPPORTED_RESPONSE_PARAMETERS; + } + + @Override + public Set supportedQueryParameters() { + return SUPPORTED_QUERY_PARAMETERS; + } + + @Override + public Set allSupportedParameters() { + return ALL_SUPPORTED_PARAMETERS; } @Override @@ -82,6 +127,18 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC 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)); diff --git a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification index 82c971f9144d8..14749330f09da 100644 --- a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification +++ b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification @@ -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