Skip to content

Commit

Permalink
Add custom metadata to snapshots (#41281)
Browse files Browse the repository at this point in the history
Adds a metadata field to snapshots which can be used to store arbitrary
key-value information. This may be useful for attaching a description of
why a snapshot was taken, tagging snapshots to make categorization
easier, or identifying the source of automatically-created snapshots.
  • Loading branch information
gwbrown authored Jun 5, 2019
1 parent 1f4ff97 commit 6eb4600
Show file tree
Hide file tree
Showing 24 changed files with 518 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@
import org.elasticsearch.repositories.fs.FsRepository;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.snapshots.RestoreInfo;
import org.elasticsearch.snapshots.SnapshotInfo;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.contains;
Expand Down Expand Up @@ -139,6 +143,9 @@ public void testCreateSnapshot() throws IOException {
CreateSnapshotRequest request = new CreateSnapshotRequest(repository, snapshot);
boolean waitForCompletion = randomBoolean();
request.waitForCompletion(waitForCompletion);
if (randomBoolean()) {
request.userMetadata(randomUserMetadata());
}
request.partial(randomBoolean());
request.includeGlobalState(randomBoolean());

Expand Down Expand Up @@ -167,6 +174,8 @@ public void testGetSnapshots() throws IOException {
CreateSnapshotResponse putSnapshotResponse1 = createTestSnapshot(createSnapshotRequest1);
CreateSnapshotRequest createSnapshotRequest2 = new CreateSnapshotRequest(repository, snapshot2);
createSnapshotRequest2.waitForCompletion(true);
Map<String, Object> originalMetadata = randomUserMetadata();
createSnapshotRequest2.userMetadata(originalMetadata);
CreateSnapshotResponse putSnapshotResponse2 = createTestSnapshot(createSnapshotRequest2);
// check that the request went ok without parsing JSON here. When using the high level client, check acknowledgement instead.
assertEquals(RestStatus.OK, putSnapshotResponse1.status());
Expand All @@ -186,6 +195,15 @@ public void testGetSnapshots() throws IOException {
assertEquals(2, response.getSnapshots().size());
assertThat(response.getSnapshots().stream().map((s) -> s.snapshotId().getName()).collect(Collectors.toList()),
contains("test_snapshot1", "test_snapshot2"));
Optional<Map<String, Object>> returnedMetadata = response.getSnapshots().stream()
.filter(s -> s.snapshotId().getName().equals("test_snapshot2"))
.findFirst()
.map(SnapshotInfo::userMetadata);
if (returnedMetadata.isPresent()) {
assertEquals(originalMetadata, returnedMetadata.get());
} else {
assertNull("retrieved metadata is null, expected non-null metadata", originalMetadata);
}
}

public void testSnapshotsStatus() throws IOException {
Expand Down Expand Up @@ -231,6 +249,9 @@ public void testRestoreSnapshot() throws IOException {
CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(testRepository, testSnapshot);
createSnapshotRequest.indices(testIndex);
createSnapshotRequest.waitForCompletion(true);
if (randomBoolean()) {
createSnapshotRequest.userMetadata(randomUserMetadata());
}
CreateSnapshotResponse createSnapshotResponse = createTestSnapshot(createSnapshotRequest);
assertEquals(RestStatus.OK, createSnapshotResponse.status());

Expand Down Expand Up @@ -261,6 +282,9 @@ public void testDeleteSnapshot() throws IOException {

CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(repository, snapshot);
createSnapshotRequest.waitForCompletion(true);
if (randomBoolean()) {
createSnapshotRequest.userMetadata(randomUserMetadata());
}
CreateSnapshotResponse createSnapshotResponse = createTestSnapshot(createSnapshotRequest);
// check that the request went ok without parsing JSON here. When using the high level client, check acknowledgement instead.
assertEquals(RestStatus.OK, createSnapshotResponse.status());
Expand All @@ -270,4 +294,28 @@ public void testDeleteSnapshot() throws IOException {

assertTrue(response.isAcknowledged());
}

private static Map<String, Object> randomUserMetadata() {
if (randomBoolean()) {
return null;
}

Map<String, Object> metadata = new HashMap<>();
long fields = randomLongBetween(0, 4);
for (int i = 0; i < fields; i++) {
if (randomBoolean()) {
metadata.put(randomValueOtherThanMany(metadata::containsKey, () -> randomAlphaOfLengthBetween(2,10)),
randomAlphaOfLengthBetween(5, 5));
} else {
Map<String, Object> nested = new HashMap<>();
long nestedFields = randomLongBetween(0, 4);
for (int j = 0; j < nestedFields; j++) {
nested.put(randomValueOtherThanMany(nested::containsKey, () -> randomAlphaOfLengthBetween(2,10)),
randomAlphaOfLengthBetween(5, 5));
}
metadata.put(randomValueOtherThanMany(metadata::containsKey, () -> randomAlphaOfLengthBetween(2,10)), nested);
}
}
return metadata;
}
}
9 changes: 8 additions & 1 deletion docs/reference/modules/snapshots.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,11 @@ PUT /_snapshot/my_backup/snapshot_2?wait_for_completion=true
{
"indices": "index_1,index_2",
"ignore_unavailable": true,
"include_global_state": false
"include_global_state": false,
"_meta": {
"taken_by": "kimchy",
"taken_because": "backup before upgrading"
}
}
-----------------------------------
// CONSOLE
Expand All @@ -363,6 +367,9 @@ By setting `include_global_state` to false it's possible to prevent the cluster
the snapshot. By default, the entire snapshot will fail if one or more indices participating in the snapshot don't have
all primary shards available. This behaviour can be changed by setting `partial` to `true`.

The `_meta` field can be used to attach arbitrary metadata to the snapshot. This may be a record of who took the snapshot,
why it was taken, or any other data that might be useful.

Snapshot names can be automatically derived using <<date-math-index-names,date math expressions>>, similarly as when creating
new indices. Note that special characters need to be URI encoded.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ setup:
- is_false: snapshots.0.failures
- is_false: snapshots.0.shards
- is_false: snapshots.0.version
- is_false: snapshots.0._meta

- do:
snapshot.delete:
Expand Down Expand Up @@ -152,3 +153,41 @@ setup:
snapshot.delete:
repository: test_repo_get_1
snapshot: test_snapshot_without_include_global_state

---
"Get snapshot info with metadata":
- skip:
version: " - 7.9.99"
reason: "https://github.com/elastic/elasticsearch/pull/41281 not yet backported to 7.x"

- 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_metadata
wait_for_completion: true
body: |
{ "metadata": {"taken_by": "test", "foo": {"bar": "baz"}} }
- do:
snapshot.get:
repository: test_repo_get_1
snapshot: test_snapshot_with_metadata

- is_true: snapshots
- match: { snapshots.0.snapshot: test_snapshot_with_metadata }
- match: { snapshots.0.state: SUCCESS }
- match: { snapshots.0.metadata.taken_by: test }
- match: { snapshots.0.metadata.foo.bar: baz }

- do:
snapshot.delete:
repository: test_repo_get_1
snapshot: test_snapshot_with_metadata
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

package org.elasticsearch.action.admin.cluster.snapshots.create;

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchGenerationException;
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.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
Expand All @@ -46,6 +48,7 @@
import static org.elasticsearch.common.settings.Settings.readSettingsFromStream;
import static org.elasticsearch.common.settings.Settings.writeSettingsToStream;
import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeBooleanValue;
import static org.elasticsearch.snapshots.SnapshotInfo.METADATA_FIELD_INTRODUCED;

/**
* Create snapshot request
Expand All @@ -63,6 +66,7 @@
*/
public class CreateSnapshotRequest extends MasterNodeRequest<CreateSnapshotRequest>
implements IndicesRequest.Replaceable, ToXContentObject {
public static int MAXIMUM_METADATA_BYTES = 1024; // chosen arbitrarily

private String snapshot;

Expand All @@ -80,6 +84,8 @@ public class CreateSnapshotRequest extends MasterNodeRequest<CreateSnapshotReque

private boolean waitForCompletion;

private Map<String, Object> userMetadata;

public CreateSnapshotRequest() {
}

Expand All @@ -104,6 +110,9 @@ public CreateSnapshotRequest(StreamInput in) throws IOException {
includeGlobalState = in.readBoolean();
waitForCompletion = in.readBoolean();
partial = in.readBoolean();
if (in.getVersion().onOrAfter(METADATA_FIELD_INTRODUCED)) {
userMetadata = in.readMap();
}
}

@Override
Expand All @@ -117,6 +126,9 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeBoolean(includeGlobalState);
out.writeBoolean(waitForCompletion);
out.writeBoolean(partial);
if (out.getVersion().onOrAfter(METADATA_FIELD_INTRODUCED)) {
out.writeMap(userMetadata);
}
}

@Override
Expand Down Expand Up @@ -144,9 +156,28 @@ public ActionRequestValidationException validate() {
if (settings == null) {
validationException = addValidationError("settings is null", validationException);
}
final int metadataSize = metadataSize(userMetadata);
if (metadataSize > MAXIMUM_METADATA_BYTES) {
validationException = addValidationError("metadata must be smaller than 1024 bytes, but was [" + metadataSize + "]",
validationException);
}
return validationException;
}

private static int metadataSize(Map<String, Object> userMetadata) {
if (userMetadata == null) {
return 0;
}
try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
builder.value(userMetadata);
int size = BytesReference.bytes(builder).length();
return size;
} catch (IOException e) {
// This should not be possible as we are just rendering the xcontent in memory
throw new ElasticsearchException(e);
}
}

/**
* Sets the snapshot name
*
Expand Down Expand Up @@ -378,6 +409,15 @@ public boolean includeGlobalState() {
return includeGlobalState;
}

public Map<String, Object> userMetadata() {
return userMetadata;
}

public CreateSnapshotRequest userMetadata(Map<String, Object> userMetadata) {
this.userMetadata = userMetadata;
return this;
}

/**
* Parses snapshot definition.
*
Expand Down Expand Up @@ -405,6 +445,11 @@ public CreateSnapshotRequest source(Map<String, Object> source) {
settings((Map<String, Object>) entry.getValue());
} else if (name.equals("include_global_state")) {
includeGlobalState = nodeBooleanValue(entry.getValue(), "include_global_state");
} else if (name.equals("metadata")) {
if (entry.getValue() != null && (entry.getValue() instanceof Map == false)) {
throw new IllegalArgumentException("malformed metadata, should be an object");
}
userMetadata((Map<String, Object>) entry.getValue());
}
}
indicesOptions(IndicesOptions.fromMap(source, indicesOptions));
Expand Down Expand Up @@ -433,6 +478,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
if (indicesOptions != null) {
indicesOptions.toXContent(builder, params);
}
builder.field("metadata", userMetadata);
builder.endObject();
return builder;
}
Expand Down Expand Up @@ -460,12 +506,14 @@ public boolean equals(Object o) {
Arrays.equals(indices, that.indices) &&
Objects.equals(indicesOptions, that.indicesOptions) &&
Objects.equals(settings, that.settings) &&
Objects.equals(masterNodeTimeout, that.masterNodeTimeout);
Objects.equals(masterNodeTimeout, that.masterNodeTimeout) &&
Objects.equals(userMetadata, that.userMetadata);
}

@Override
public int hashCode() {
int result = Objects.hash(snapshot, repository, indicesOptions, partial, settings, includeGlobalState, waitForCompletion);
int result = Objects.hash(snapshot, repository, indicesOptions, partial, settings, includeGlobalState,
waitForCompletion, userMetadata);
result = 31 * result + Arrays.hashCode(indices);
return result;
}
Expand All @@ -482,6 +530,7 @@ public String toString() {
", includeGlobalState=" + includeGlobalState +
", waitForCompletion=" + waitForCompletion +
", masterNodeTimeout=" + masterNodeTimeout +
", metadata=" + userMetadata +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
Expand Down Expand Up @@ -117,4 +118,9 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(snapshots);
}

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

0 comments on commit 6eb4600

Please sign in to comment.