diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java index 134dc921c45..51f39407748 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java @@ -28,6 +28,7 @@ import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRe import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse; +import org.elasticsearch.action.admin.cluster.snapshots.clone.CloneSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; @@ -236,6 +237,32 @@ public final class SnapshotClient { CreateSnapshotResponse::fromXContent, listener, emptySet()); } + /** + * Clones a snapshot. + *

+ * See Snapshot and Restore + * API on elastic.co + */ + public AcknowledgedResponse clone(CloneSnapshotRequest cloneSnapshotRequest, RequestOptions options) + throws IOException { + return restHighLevelClient.performRequestAndParseEntity(cloneSnapshotRequest, SnapshotRequestConverters::cloneSnapshot, options, + AcknowledgedResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously clones a snapshot. + *

+ * See Snapshot and Restore + * API on elastic.co + * @return cancellable that may be used to cancel the request + */ + public Cancellable cloneAsync(CloneSnapshotRequest cloneSnapshotRequest, RequestOptions options, + ActionListener listener) { + return restHighLevelClient.performRequestAsyncAndParseEntity(cloneSnapshotRequest, + SnapshotRequestConverters::cloneSnapshot, options, + AcknowledgedResponse::fromXContent, listener, emptySet()); + } + /** * Get snapshots. * See Snapshot and Restore diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java index 796845d3525..e3dec27f97a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotRequestConverters.java @@ -28,6 +28,7 @@ import org.elasticsearch.action.admin.cluster.repositories.delete.DeleteReposito import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRequest; import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest; +import org.elasticsearch.action.admin.cluster.snapshots.clone.CloneSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; @@ -123,6 +124,21 @@ final class SnapshotRequestConverters { return request; } + static Request cloneSnapshot(CloneSnapshotRequest cloneSnapshotRequest) throws IOException { + String endpoint = new RequestConverters.EndpointBuilder().addPathPart("_snapshot") + .addPathPart(cloneSnapshotRequest.repository()) + .addPathPart(cloneSnapshotRequest.source()) + .addPathPart("_clone") + .addPathPart(cloneSnapshotRequest.target()) + .build(); + Request request = new Request(HttpPut.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(); + params.withMasterTimeout(cloneSnapshotRequest.masterNodeTimeout()); + request.addParameters(params.asMap()); + request.setEntity(RequestConverters.createEntity(cloneSnapshotRequest, RequestConverters.REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request getSnapshots(GetSnapshotsRequest getSnapshotsRequest) { RequestConverters.EndpointBuilder endpointBuilder = new RequestConverters.EndpointBuilder().addPathPartAsIs("_snapshot") .addPathPart(getSnapshotsRequest.repository()); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java index 3715011f186..7701d55687e 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java @@ -28,6 +28,7 @@ import org.elasticsearch.action.admin.cluster.repositories.get.GetRepositoriesRe import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse; +import org.elasticsearch.action.admin.cluster.snapshots.clone.CloneSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; @@ -351,6 +352,30 @@ public class SnapshotIT extends ESRestHighLevelClientTestCase { assertTrue(response.isAcknowledged()); } + public void testCloneSnapshot() throws IOException { + String repository = "test_repository"; + String snapshot = "source_snapshot"; + String targetSnapshot = "target_snapshot"; + final String testIndex = "test_idx"; + + createIndex(testIndex, Settings.EMPTY); + assertTrue("index [" + testIndex + "] should have been created", indexExists(testIndex)); + + AcknowledgedResponse putRepositoryResponse = createTestRepository(repository, FsRepository.TYPE, "{\"location\": \".\"}"); + assertTrue(putRepositoryResponse.isAcknowledged()); + + CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(repository, snapshot); + createSnapshotRequest.waitForCompletion(true); + + CreateSnapshotResponse createSnapshotResponse = createTestSnapshot(createSnapshotRequest); + assertEquals(RestStatus.OK, createSnapshotResponse.status()); + + CloneSnapshotRequest request = new CloneSnapshotRequest(repository, snapshot, targetSnapshot, new String[]{testIndex}); + AcknowledgedResponse response = execute(request, highLevelClient().snapshot()::clone, highLevelClient().snapshot()::cloneAsync); + + assertTrue(response.isAcknowledged()); + } + private static Map randomUserMetadata() { if (randomBoolean()) { return null; diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.clone.json b/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.clone.json new file mode 100644 index 00000000000..18122bc209b --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/snapshot.clone.json @@ -0,0 +1,43 @@ +{ + "snapshot.clone":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html", + "description":"Clones indices from one snapshot into another snapshot in the same repository." + }, + "stability":"stable", + "url":{ + "paths":[ + { + "path":"/_snapshot/{repository}/{snapshot}/_clone/{target_snapshot}", + "methods":[ + "PUT" + ], + "parts":{ + "repository":{ + "type":"string", + "description":"A repository name" + }, + "snapshot":{ + "type":"string", + "description":"The name of the snapshot to clone from" + }, + "target_snapshot":{ + "type":"string", + "description":"The name of the cloned snapshot to create" + } + } + } + ] + }, + "params":{ + "master_timeout":{ + "type":"time", + "description":"Explicit operation timeout for connection to master node" + } + }, + "body":{ + "description":"The snapshot clone definition", + "required":true + } + } +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.clone/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.clone/10_basic.yml new file mode 100644 index 00000000000..fb289355e08 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.clone/10_basic.yml @@ -0,0 +1,54 @@ +--- +setup: + + - do: + snapshot.create_repository: + repository: test_repo_create_1 + body: + type: fs + settings: + location: "test_repo_create_1_loc" + + - do: + indices.create: + index: test_index_1 + body: + settings: + number_of_shards: 1 + number_of_replicas: 1 + + - do: + indices.create: + index: test_index_2 + body: + settings: + number_of_shards: 1 + number_of_replicas: 1 + + - do: + snapshot.create: + repository: test_repo_create_1 + snapshot: test_snapshot + wait_for_completion: true + +--- +"Clone a snapshot": + - skip: + version: " - 7.9.99" + reason: "Clone snapshot functionality was introduced in 7.10" + - do: + snapshot.clone: + repository: test_repo_create_1 + snapshot: test_snapshot + target_snapshot: target_snapshot_1 + body: + "indices": test_index_2 + + - match: { acknowledged: true } + + - do: + snapshot.delete: + repository: test_repo_create_1 + snapshot: target_snapshot_1 + + - match: { acknowledged: true } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/clone/CloneSnapshotRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/clone/CloneSnapshotRequest.java index 4d1eb0952e3..c2f4028ffb5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/clone/CloneSnapshotRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/clone/CloneSnapshotRequest.java @@ -23,14 +23,17 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; import static org.elasticsearch.action.ValidateActions.addValidationError; -public class CloneSnapshotRequest extends MasterNodeRequest implements IndicesRequest.Replaceable{ +public class CloneSnapshotRequest extends MasterNodeRequest implements IndicesRequest.Replaceable, ToXContentObject { private final String repository; @@ -139,4 +142,29 @@ public class CloneSnapshotRequest extends MasterNodeRequest