Skip to content
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 @@ -28,6 +28,7 @@
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;
Expand Down Expand Up @@ -236,6 +237,32 @@ public Cancellable createAsync(CreateSnapshotRequest createSnapshotRequest, Requ
CreateSnapshotResponse::fromXContent, listener, emptySet());
}

/**
* Clones a snapshot.
* <p>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
* API on elastic.co</a>
*/
public AcknowledgedResponse clone(CloneSnapshotRequest cloneSnapshotRequest, RequestOptions options)
throws IOException {
return restHighLevelClient.performRequestAndParseEntity(cloneSnapshotRequest, SnapshotRequestConverters::cloneSnapshot, options,
AcknowledgedResponse::fromXContent, emptySet());
}

/**
* Asynchronously clones a snapshot.
* <p>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
* API on elastic.co</a>
* @return cancellable that may be used to cancel the request
*/
public Cancellable cloneAsync(CloneSnapshotRequest cloneSnapshotRequest, RequestOptions options,
ActionListener<AcknowledgedResponse> listener) {
return restHighLevelClient.performRequestAsyncAndParseEntity(cloneSnapshotRequest,
SnapshotRequestConverters::cloneSnapshot, options,
AcknowledgedResponse::fromXContent, listener, emptySet());
}

/**
* Get snapshots.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand Down Expand Up @@ -123,6 +124,21 @@ static Request createSnapshot(CreateSnapshotRequest createSnapshotRequest) throw
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")
.addCommaSeparatedPathParts(getSnapshotsRequest.repositories());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand Down Expand Up @@ -350,6 +351,30 @@ public void testDeleteSnapshot() throws IOException {
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<String, Object> randomUserMetadata() {
if (randomBoolean()) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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 }
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@
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<CloneSnapshotRequest> implements IndicesRequest.Replaceable{
public class CloneSnapshotRequest extends MasterNodeRequest<CloneSnapshotRequest> implements IndicesRequest.Replaceable, ToXContentObject {

private final String repository;

Expand Down Expand Up @@ -139,4 +142,29 @@ public String target() {
public String source() {
return this.source;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of meh, we technically only need the indices and indices options fields here for the HLRC client. But then I dug into it and found that we add all these redundant fields for other requests as well so I kept it consistent here for now. I have an idea on how to make this nicer that I'd put in a follow up (could just use a params entry here).

builder.field("repository", repository);
builder.field("source", source);
builder.field("target", target);
if (indices != null) {
builder.startArray("indices");
for (String index : indices) {
builder.value(index);
}
builder.endArray();
}
if (indicesOptions != null) {
indicesOptions.toXContent(builder, params);
}
builder.endObject();
return builder;
}

@Override
public String toString() {
return Strings.toString(this);
}
}