Skip to content

Commit c7669b3

Browse files
authored
Include SLM policy name in Snapshot metadata (#43132)
Keep track of which SLM policy in the metadata field of the Snapshots taken by SLM. This allows users to more easily understand where the snapshot came from, and will enable future SLM features such as retention policies.
1 parent 27173fa commit c7669b3

File tree

4 files changed

+118
-2
lines changed

4 files changed

+118
-2
lines changed

server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/create/CreateSnapshotRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public ActionRequestValidationException validate() {
163163
return validationException;
164164
}
165165

166-
private static int metadataSize(Map<String, Object> userMetadata) {
166+
public static int metadataSize(Map<String, Object> userMetadata) {
167167
if (userMetadata == null) {
168168
return 0;
169169
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicy.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.io.IOException;
3131
import java.nio.charset.StandardCharsets;
3232
import java.util.Collections;
33+
import java.util.HashMap;
3334
import java.util.List;
3435
import java.util.Locale;
3536
import java.util.Map;
@@ -57,6 +58,8 @@ public class SnapshotLifecyclePolicy extends AbstractDiffable<SnapshotLifecycleP
5758
private static final ParseField CONFIG = new ParseField("config");
5859
private static final IndexNameExpressionResolver.DateMathExpressionResolver DATE_MATH_RESOLVER =
5960
new IndexNameExpressionResolver.DateMathExpressionResolver();
61+
private static final String POLICY_ID_METADATA_FIELD = "policy";
62+
private static final String METADATA_FIELD_NAME = "metadata";
6063

6164
@SuppressWarnings("unchecked")
6265
private static final ConstructingObjectParser<SnapshotLifecyclePolicy, String> PARSER =
@@ -169,6 +172,30 @@ public ActionRequestValidationException validate() {
169172
}
170173
}
171174

175+
if (configuration.containsKey(METADATA_FIELD_NAME)) {
176+
if (configuration.get(METADATA_FIELD_NAME) instanceof Map == false) {
177+
err.addValidationError("invalid configuration." + METADATA_FIELD_NAME + " [" + configuration.get(METADATA_FIELD_NAME) +
178+
"]: must be an object if present");
179+
} else {
180+
@SuppressWarnings("unchecked")
181+
Map<String, Object> metadata = (Map<String, Object>) configuration.get(METADATA_FIELD_NAME);
182+
if (metadata.containsKey(POLICY_ID_METADATA_FIELD)) {
183+
err.addValidationError("invalid configuration." + METADATA_FIELD_NAME + ": field name [" + POLICY_ID_METADATA_FIELD +
184+
"] is reserved and will be added automatically");
185+
} else {
186+
Map<String, Object> metadataWithPolicyField = addPolicyNameToMetadata(metadata);
187+
int serializedSizeOriginal = CreateSnapshotRequest.metadataSize(metadata);
188+
int serializedSizeWithMetadata = CreateSnapshotRequest.metadataSize(metadataWithPolicyField);
189+
int policyNameAddedBytes = serializedSizeWithMetadata - serializedSizeOriginal;
190+
if (serializedSizeWithMetadata > CreateSnapshotRequest.MAXIMUM_METADATA_BYTES) {
191+
err.addValidationError("invalid configuration." + METADATA_FIELD_NAME + ": must be smaller than [" +
192+
(CreateSnapshotRequest.MAXIMUM_METADATA_BYTES - policyNameAddedBytes) +
193+
"] bytes, but is [" + serializedSizeOriginal + "] bytes");
194+
}
195+
}
196+
}
197+
}
198+
172199
// Repository validation, validation of whether the repository actually exists happens
173200
// elsewhere as it requires cluster state
174201
if (Strings.hasText(repository) == false) {
@@ -178,6 +205,17 @@ public ActionRequestValidationException validate() {
178205
return err.validationErrors().size() == 0 ? null : err;
179206
}
180207

208+
private Map<String, Object> addPolicyNameToMetadata(final Map<String, Object> metadata) {
209+
Map<String, Object> newMetadata;
210+
if (metadata == null) {
211+
newMetadata = new HashMap<>();
212+
} else {
213+
newMetadata = new HashMap<>(metadata);
214+
}
215+
newMetadata.put(POLICY_ID_METADATA_FIELD, this.id);
216+
return newMetadata;
217+
}
218+
181219
/**
182220
* Since snapshots need to be uniquely named, this method will resolve any date math used in
183221
* the provided name, as well as appending a unique identifier so expressions that may overlap
@@ -198,7 +236,12 @@ public String generateSnapshotName(Context context) {
198236
*/
199237
public CreateSnapshotRequest toRequest() {
200238
CreateSnapshotRequest req = new CreateSnapshotRequest(repository, generateSnapshotName(new ResolverContext()));
201-
req.source(configuration);
239+
@SuppressWarnings("unchecked")
240+
Map<String, Object> metadata = (Map<String, Object>) configuration.get("metadata");
241+
Map<String, Object> metadataWithAddedPolicyName = addPolicyNameToMetadata(metadata);
242+
Map<String, Object> mergedConfiguration = new HashMap<>(configuration);
243+
mergedConfiguration.put("metadata", metadataWithAddedPolicyName);
244+
req.source(mergedConfiguration);
202245
req.waitForCompletion(false);
203246
return req;
204247
}

x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleIT.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ public void testFullPolicySnapshot() throws Exception {
8585
Map<String, Object> snapResponse = ((List<Map<String, Object>>) snapshotResponseMap.get("snapshots")).get(0);
8686
assertThat(snapResponse.get("snapshot").toString(), startsWith("snap-"));
8787
assertThat((List<String>)snapResponse.get("indices"), equalTo(Collections.singletonList(indexName)));
88+
Map<String, Object> metadata = (Map<String, Object>) snapResponse.get("metadata");
89+
assertNotNull(metadata);
90+
assertThat(metadata.get("policy"), equalTo(policyName));
91+
assertHistoryIsPresent(policyName, true, repoId);
8892

8993
// Check that the last success date was written to the cluster state
9094
Request getReq = new Request("GET", "/_slm/policy/" + policyName);
@@ -194,6 +198,9 @@ public void testPolicyManualExecution() throws Exception {
194198
snapshotResponseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true);
195199
}
196200
assertThat(snapshotResponseMap.size(), greaterThan(0));
201+
final Map<String, Object> metadata = extractMetadata(snapshotResponseMap, snapshotName);
202+
assertNotNull(metadata);
203+
assertThat(metadata.get("policy"), equalTo(policyName));
197204
assertHistoryIsPresent(policyName, true, repoId);
198205
} catch (ResponseException e) {
199206
fail("expected snapshot to exist but it does not: " + EntityUtils.toString(e.getResponse().getEntity()));
@@ -211,6 +218,16 @@ public void testPolicyManualExecution() throws Exception {
211218
});
212219
}
213220

221+
@SuppressWarnings("unchecked")
222+
private static Map<String, Object> extractMetadata(Map<String, Object> snapshotResponseMap, String snapshotPrefix) {
223+
List<Map<String, Object>> snapshots = ((List<Map<String, Object>>) snapshotResponseMap.get("snapshots"));
224+
return snapshots.stream()
225+
.filter(snapshot -> ((String) snapshot.get("snapshot")).startsWith(snapshotPrefix))
226+
.map(snapshot -> (Map<String, Object>) snapshot.get("metadata"))
227+
.findFirst()
228+
.orElse(null);
229+
}
230+
214231
// This method should be called inside an assertBusy, it has no retry logic of its own
215232
private void assertHistoryIsPresent(String policyName, boolean success, String repository) throws IOException {
216233
final Request historySearchRequest = new Request("GET", ".slm-history*/_search");
@@ -263,6 +280,14 @@ private void createSnapshotPolicy(String policyName, String snapshotNamePattern,
263280
Map<String, Object> snapConfig = new HashMap<>();
264281
snapConfig.put("indices", Collections.singletonList(indexPattern));
265282
snapConfig.put("ignore_unavailable", ignoreUnavailable);
283+
if (randomBoolean()) {
284+
Map<String, Object> metadata = new HashMap<>();
285+
int fieldCount = randomIntBetween(2,5);
286+
for (int i = 0; i < fieldCount; i++) {
287+
metadata.put(randomValueOtherThanMany(key -> "policy".equals(key) || metadata.containsKey(key),
288+
() -> randomAlphaOfLength(5)), randomAlphaOfLength(4));
289+
}
290+
}
266291
SnapshotLifecyclePolicy policy = new SnapshotLifecyclePolicy(policyName, snapshotNamePattern, schedule, repoId, snapConfig);
267292

268293
Request putLifecycle = new Request("PUT", "/_slm/policy/" + policyName);

x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecyclePolicyTests.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.HashMap;
1818
import java.util.Map;
1919

20+
import static org.hamcrest.Matchers.contains;
2021
import static org.hamcrest.Matchers.containsInAnyOrder;
2122
import static org.hamcrest.Matchers.equalTo;
2223
import static org.hamcrest.Matchers.greaterThan;
@@ -71,6 +72,53 @@ public void testValidation() {
7172
"invalid schedule [ ]: must not be empty"));
7273
}
7374

75+
public void testMetadataValidation() {
76+
{
77+
Map<String, Object> configuration = new HashMap<>();
78+
final String metadataString = randomAlphaOfLength(10);
79+
configuration.put("metadata", metadataString);
80+
81+
SnapshotLifecyclePolicy policy = new SnapshotLifecyclePolicy("mypolicy", "<mysnapshot-{now/M}>",
82+
"1 * * * * ?", "myrepo", configuration);
83+
ValidationException e = policy.validate();
84+
assertThat(e.validationErrors(), contains("invalid configuration.metadata [" + metadataString +
85+
"]: must be an object if present"));
86+
}
87+
88+
{
89+
Map<String, Object> metadata = new HashMap<>();
90+
metadata.put("policy", randomAlphaOfLength(5));
91+
Map<String, Object> configuration = new HashMap<>();
92+
configuration.put("metadata", metadata);
93+
94+
SnapshotLifecyclePolicy policy = new SnapshotLifecyclePolicy("mypolicy", "<mysnapshot-{now/M}>",
95+
"1 * * * * ?", "myrepo", configuration);
96+
ValidationException e = policy.validate();
97+
assertThat(e.validationErrors(), contains("invalid configuration.metadata: field name [policy] is reserved and " +
98+
"will be added automatically"));
99+
}
100+
101+
{
102+
Map<String, Object> metadata = new HashMap<>();
103+
final int fieldCount = randomIntBetween(67, 100); // 67 is the smallest field count with these sizes that causes an error
104+
final int keyBytes = 5; // chosen arbitrarily
105+
final int valueBytes = 4; // chosen arbitrarily
106+
int totalBytes = fieldCount * (keyBytes + valueBytes + 6 /* bytes of overhead per key/value pair */) + 1;
107+
for (int i = 0; i < fieldCount; i++) {
108+
metadata.put(randomValueOtherThanMany(key -> "policy".equals(key) || metadata.containsKey(key),
109+
() -> randomAlphaOfLength(keyBytes)), randomAlphaOfLength(valueBytes));
110+
}
111+
Map<String, Object> configuration = new HashMap<>();
112+
configuration.put("metadata", metadata);
113+
114+
SnapshotLifecyclePolicy policy = new SnapshotLifecyclePolicy("mypolicy", "<mysnapshot-{now/M}>",
115+
"1 * * * * ?", "myrepo", configuration);
116+
ValidationException e = policy.validate();
117+
assertThat(e.validationErrors(), contains("invalid configuration.metadata: must be smaller than [1004] bytes, but is [" +
118+
totalBytes + "] bytes"));
119+
}
120+
}
121+
74122
@Override
75123
protected SnapshotLifecyclePolicy doParseInstance(XContentParser parser) throws IOException {
76124
return SnapshotLifecyclePolicy.parse(parser, id);

0 commit comments

Comments
 (0)