diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotInvocationRecord.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotInvocationRecord.java new file mode 100644 index 0000000000000..a39153f991664 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotInvocationRecord.java @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.snapshotlifecycle; + +import org.elasticsearch.cluster.AbstractDiffable; +import org.elasticsearch.cluster.Diffable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +/** + * Holds information about Snapshots kicked off by Snapshot Lifecycle Management in the cluster state, so that this information can be + * presented to the user. This class is used for both successes and failures as the structure of the data is very similar. + */ +public class SnapshotInvocationRecord extends AbstractDiffable + implements Writeable, ToXContentObject, Diffable { + + static final ParseField SNAPSHOT_NAME = new ParseField("snapshot_name"); + static final ParseField TIMESTAMP = new ParseField("time"); + static final ParseField DETAILS = new ParseField("details"); + + private String snapshotName; + private long timestamp; + private String details; + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("snapshot_policy_invocation_record", true, + a -> new SnapshotInvocationRecord((String) a[0], (long) a[1], (String) a[2])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), SNAPSHOT_NAME); + PARSER.declareLong(ConstructingObjectParser.constructorArg(), TIMESTAMP); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), DETAILS); + } + + public static SnapshotInvocationRecord parse(XContentParser parser, String name) { + return PARSER.apply(parser, name); + } + + public SnapshotInvocationRecord(String snapshotName, long timestamp, String details) { + this.snapshotName = Objects.requireNonNull(snapshotName, "snapshot name must be provided"); + this.timestamp = timestamp; + this.details = details; + } + + public SnapshotInvocationRecord(StreamInput in) throws IOException { + this.snapshotName = in.readString(); + this.timestamp = in.readVLong(); + this.details = in.readOptionalString(); + } + + public String getSnapshotName() { + return snapshotName; + } + + public long getTimestamp() { + return timestamp; + } + + public String getDetails() { + return details; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(snapshotName); + out.writeVLong(timestamp); + out.writeOptionalString(details); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.field(SNAPSHOT_NAME.getPreferredName(), snapshotName); + builder.timeField(TIMESTAMP.getPreferredName(), "time_string", timestamp); + if (Objects.nonNull(details)) { + builder.field(DETAILS.getPreferredName(), details); + } + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SnapshotInvocationRecord that = (SnapshotInvocationRecord) o; + return getTimestamp() == that.getTimestamp() && + Objects.equals(getSnapshotName(), that.getSnapshotName()) && + Objects.equals(getDetails(), that.getDetails()); + } + + @Override + public int hashCode() { + return Objects.hash(getSnapshotName(), getTimestamp(), getDetails()); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicy.java index e1837978541a2..ad52e4256591b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicy.java @@ -73,7 +73,7 @@ public class SnapshotLifecyclePolicy extends AbstractDiffable configuration) { - this.id = id; + this.id = Objects.requireNonNull(id); this.name = name; this.schedule = schedule; this.repository = repository; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicyMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicyMetadata.java index 1c2e1956707f2..b2b5db865d95d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicyMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicyMetadata.java @@ -8,6 +8,7 @@ import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.Diffable; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -21,8 +22,10 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; /** * {@code SnapshotLifecyclePolicyMetadata} encapsulates a {@link SnapshotLifecyclePolicy} as well as @@ -37,18 +40,34 @@ public class SnapshotLifecyclePolicyMetadata extends AbstractDiffable headers; private final long version; private final long modifiedDate; + @Nullable + private final SnapshotInvocationRecord lastSuccess; + @Nullable + private final SnapshotInvocationRecord lastFailure; @SuppressWarnings("unchecked") public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("snapshot_policy_metadata", a -> { SnapshotLifecyclePolicy policy = (SnapshotLifecyclePolicy) a[0]; - return new SnapshotLifecyclePolicyMetadata(policy, (Map) a[1], (long) a[2], (long) a[3]); + SnapshotInvocationRecord lastSuccess = (SnapshotInvocationRecord) a[5]; + SnapshotInvocationRecord lastFailure = (SnapshotInvocationRecord) a[6]; + + return builder() + .setPolicy(policy) + .setHeaders((Map) a[1]) + .setVersion((long) a[2]) + .setModifiedDate((long) a[3]) + .setLastSuccess(lastSuccess) + .setLastFailure(lastFailure) + .build(); }); static { @@ -57,17 +76,22 @@ public class SnapshotLifecyclePolicyMetadata extends AbstractDiffable headers, long version, long modifiedDate) { + SnapshotLifecyclePolicyMetadata(SnapshotLifecyclePolicy policy, Map headers, long version, long modifiedDate, + SnapshotInvocationRecord lastSuccess, SnapshotInvocationRecord lastFailure) { this.policy = policy; this.headers = headers; this.version = version; this.modifiedDate = modifiedDate; + this.lastSuccess = lastSuccess; + this.lastFailure = lastFailure; } @SuppressWarnings("unchecked") @@ -76,6 +100,8 @@ public SnapshotLifecyclePolicyMetadata(SnapshotLifecyclePolicy policy, Map) in.readGenericValue(); this.version = in.readVLong(); this.modifiedDate = in.readVLong(); + this.lastSuccess = in.readOptionalWriteable(SnapshotInvocationRecord::new); + this.lastFailure = in.readOptionalWriteable(SnapshotInvocationRecord::new); } @Override @@ -84,6 +110,25 @@ public void writeTo(StreamOutput out) throws IOException { out.writeGenericValue(this.headers); out.writeVLong(this.version); out.writeVLong(this.modifiedDate); + out.writeOptionalWriteable(this.lastSuccess); + out.writeOptionalWriteable(this.lastFailure); + } + + public static Builder builder() { + return new Builder(); + } + + public static Builder builder(SnapshotLifecyclePolicyMetadata metadata) { + if (metadata == null) { + return builder(); + } + return new Builder() + .setHeaders(metadata.getHeaders()) + .setPolicy(metadata.getPolicy()) + .setVersion(metadata.getVersion()) + .setModifiedDate(metadata.getModifiedDate()) + .setLastSuccess(metadata.getLastSuccess()) + .setLastFailure(metadata.getLastFailure()); } public Map getHeaders() { @@ -106,9 +151,24 @@ public long getModifiedDate() { return modifiedDate; } + private String dateToDateString(Long date) { + if (Objects.isNull(date)) { + return null; + } + ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(date), ZoneOffset.UTC); + return dateTime.toString(); + } + public String getModifiedDateString() { - ZonedDateTime modifiedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(modifiedDate), ZoneOffset.UTC); - return modifiedDateTime.toString(); + return dateToDateString(modifiedDate); + } + + public SnapshotInvocationRecord getLastSuccess() { + return lastSuccess; + } + + public SnapshotInvocationRecord getLastFailure() { + return lastFailure; } @Override @@ -119,13 +179,19 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(VERSION.getPreferredName(), version); builder.field(MODIFIED_DATE.getPreferredName(), modifiedDate); builder.field(MODIFIED_DATE_STRING.getPreferredName(), getModifiedDateString()); + if (Objects.nonNull(lastSuccess)) { + builder.field(LAST_SUCCESS.getPreferredName(), lastSuccess); + } + if (Objects.nonNull(lastFailure)) { + builder.field(LAST_FAILURE.getPreferredName(), lastFailure); + } builder.endObject(); return builder; } @Override public int hashCode() { - return Objects.hash(policy, headers, version, modifiedDate); + return Objects.hash(policy, headers, version, modifiedDate, lastSuccess, lastFailure); } @Override @@ -140,7 +206,9 @@ public boolean equals(Object obj) { return Objects.equals(policy, other.policy) && Objects.equals(headers, other.headers) && Objects.equals(version, other.version) && - Objects.equals(modifiedDate, other.modifiedDate); + Objects.equals(modifiedDate, other.modifiedDate) && + Objects.equals(lastSuccess, other.lastSuccess) && + Objects.equals(lastFailure, other.lastFailure); } @Override @@ -150,4 +218,58 @@ public String toString() { // should not emit them in case it accidentally gets logged. return super.toString(); } + + public static class Builder { + + private Builder() { + } + + private SnapshotLifecyclePolicy policy; + private Map headers; + private long version = 1L; + private Long modifiedDate; + private SnapshotInvocationRecord lastSuccessDate; + private SnapshotInvocationRecord lastFailureDate; + + public Builder setPolicy(SnapshotLifecyclePolicy policy) { + this.policy = policy; + return this; + } + + public Builder setHeaders(Map headers) { + this.headers = headers; + return this; + } + + public Builder setVersion(long version) { + this.version = version; + return this; + } + + public Builder setModifiedDate(long modifiedDate) { + this.modifiedDate = modifiedDate; + return this; + } + + public Builder setLastSuccess(SnapshotInvocationRecord lastSuccessDate) { + this.lastSuccessDate = lastSuccessDate; + return this; + } + + public Builder setLastFailure(SnapshotInvocationRecord lastFailureDate) { + this.lastFailureDate = lastFailureDate; + return this; + } + + public SnapshotLifecyclePolicyMetadata build() { + return new SnapshotLifecyclePolicyMetadata( + Objects.requireNonNull(policy), + Optional.ofNullable(headers).orElse(new HashMap<>()), + version, + Objects.requireNonNull(modifiedDate, "modifiedDate must be set"), + lastSuccessDate, + lastFailureDate); + } + } + } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/action/GetSnapshotLifecycleAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/action/GetSnapshotLifecycleAction.java index 5606bf837e2c4..10992a58210cc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/action/GetSnapshotLifecycleAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/snapshotlifecycle/action/GetSnapshotLifecycleAction.java @@ -10,6 +10,7 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.support.master.AcknowledgedRequest; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -17,6 +18,7 @@ import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotInvocationRecord; import org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotLifecyclePolicy; import org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotLifecyclePolicyMetadata; @@ -123,6 +125,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeList(lifecycles); + } + @Override public int hashCode() { return Objects.hash(lifecycles); @@ -146,22 +153,30 @@ public boolean equals(Object obj) { * {@link SnapshotLifecyclePolicyMetadata}, however, it elides the headers to ensure that they * are not leaked to the user since they may contain sensitive information. */ - public static class SnapshotLifecyclePolicyItem implements ToXContentFragment { + public static class SnapshotLifecyclePolicyItem implements ToXContentFragment, Writeable { private final SnapshotLifecyclePolicy policy; private final long version; private final long modifiedDate; + @Nullable + private final SnapshotInvocationRecord lastSuccess; + @Nullable + private final SnapshotInvocationRecord lastFailure; - public SnapshotLifecyclePolicyItem(SnapshotLifecyclePolicy policy, long version, long modifiedDate) { - this.policy = policy; - this.version = version; - this.modifiedDate = modifiedDate; + public SnapshotLifecyclePolicyItem(SnapshotLifecyclePolicyMetadata policyMetadata) { + this.policy = policyMetadata.getPolicy(); + this.version = policyMetadata.getVersion(); + this.modifiedDate = policyMetadata.getModifiedDate(); + this.lastSuccess = policyMetadata.getLastSuccess(); + this.lastFailure = policyMetadata.getLastFailure(); } public SnapshotLifecyclePolicyItem(StreamInput in) throws IOException { this.policy = new SnapshotLifecyclePolicy(in); this.version = in.readVLong(); this.modifiedDate = in.readVLong(); + this.lastSuccess = in.readOptionalWriteable(SnapshotInvocationRecord::new); + this.lastFailure = in.readOptionalWriteable(SnapshotInvocationRecord::new); } public SnapshotLifecyclePolicy getPolicy() { @@ -176,6 +191,15 @@ public long getModifiedDate() { return modifiedDate; } + @Override + public void writeTo(StreamOutput out) throws IOException { + policy.writeTo(out); + out.writeVLong(version); + out.writeVLong(modifiedDate); + out.writeOptionalWriteable(lastSuccess); + out.writeOptionalWriteable(lastFailure); + } + @Override public int hashCode() { return Objects.hash(policy, version, modifiedDate); @@ -201,6 +225,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("version", version); builder.field("modified_date", modifiedDate); builder.field("policy", policy); + if (lastSuccess != null) { + builder.field("last_success", lastSuccess); + } + if (lastFailure != null) { + builder.field("last_failure", lastFailure); + } builder.endObject(); return builder; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotInvocationRecordTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotInvocationRecordTests.java new file mode 100644 index 0000000000000..af9511b183e9e --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotInvocationRecordTests.java @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.snapshotlifecycle; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +public class SnapshotInvocationRecordTests extends AbstractSerializingTestCase { + + @Override + protected SnapshotInvocationRecord doParseInstance(XContentParser parser) throws IOException { + return SnapshotInvocationRecord.parse(parser, null); + } + + @Override + protected SnapshotInvocationRecord createTestInstance() { + return randomSnapshotInvocationRecord(); + } + + @Override + protected Writeable.Reader instanceReader() { + return SnapshotInvocationRecord::new; + } + + @Override + protected SnapshotInvocationRecord mutateInstance(SnapshotInvocationRecord instance) { + switch (between(0, 2)) { + case 0: + return new SnapshotInvocationRecord( + randomValueOtherThan(instance.getSnapshotName(), () -> randomAlphaOfLengthBetween(2,10)), + instance.getTimestamp(), + instance.getDetails()); + case 1: + return new SnapshotInvocationRecord(instance.getSnapshotName(), + randomValueOtherThan(instance.getTimestamp(), ESTestCase::randomNonNegativeLong), + instance.getDetails()); + case 2: + return new SnapshotInvocationRecord(instance.getSnapshotName(), + instance.getTimestamp(), + randomValueOtherThan(instance.getDetails(), () -> randomAlphaOfLengthBetween(2,10))); + default: + throw new AssertionError("failure, got illegal switch case"); + } + } + + public static SnapshotInvocationRecord randomSnapshotInvocationRecord() { + return new SnapshotInvocationRecord( + randomAlphaOfLengthBetween(5,10), + randomNonNegativeLong(), + randomBoolean() ? null : randomAlphaOfLengthBetween(5, 10)); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicyMetadataTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicyMetadataTests.java new file mode 100644 index 0000000000000..f63cf7f2b241d --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/snapshotlifecycle/SnapshotLifecyclePolicyMetadataTests.java @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.snapshotlifecycle; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotInvocationRecordTests.randomSnapshotInvocationRecord; + +public class SnapshotLifecyclePolicyMetadataTests extends AbstractSerializingTestCase { + private String policyId; + + @Override + protected SnapshotLifecyclePolicyMetadata doParseInstance(XContentParser parser) throws IOException { + return SnapshotLifecyclePolicyMetadata.PARSER.apply(parser, policyId); + } + + @Override + protected SnapshotLifecyclePolicyMetadata createTestInstance() { + policyId = randomAlphaOfLength(5); + SnapshotLifecyclePolicyMetadata.Builder builder = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(createRandomPolicy(policyId)) + .setVersion(randomNonNegativeLong()) + .setModifiedDate(randomNonNegativeLong()); + if (randomBoolean()) { + builder.setHeaders(randomHeaders()); + } + if (randomBoolean()) { + builder.setLastSuccess(randomSnapshotInvocationRecord()); + } + if (randomBoolean()) { + builder.setLastFailure(randomSnapshotInvocationRecord()); + } + return builder.build(); + } + + private Map randomHeaders() { + Map headers = new HashMap<>(); + int headerCount = randomIntBetween(1,10); + for (int i = 0; i < headerCount; i++) { + headers.put(randomAlphaOfLengthBetween(5,10), randomAlphaOfLengthBetween(5,10)); + } + return headers; + } + + @Override + protected Writeable.Reader instanceReader() { + return SnapshotLifecyclePolicyMetadata::new; + } + + @Override + protected SnapshotLifecyclePolicyMetadata mutateInstance(SnapshotLifecyclePolicyMetadata instance) throws IOException { + switch (between(0, 4)) { + case 0: + return SnapshotLifecyclePolicyMetadata.builder(instance) + .setPolicy(randomValueOtherThan(instance.getPolicy(), () -> createRandomPolicy(randomAlphaOfLength(10)))) + .build(); + case 1: + return SnapshotLifecyclePolicyMetadata.builder(instance) + .setVersion(randomValueOtherThan(instance.getVersion(), ESTestCase::randomNonNegativeLong)) + .build(); + case 2: + return SnapshotLifecyclePolicyMetadata.builder(instance) + .setHeaders(randomValueOtherThan(instance.getHeaders(), this::randomHeaders)) + .build(); + case 3: + return SnapshotLifecyclePolicyMetadata.builder(instance) + .setLastSuccess(randomValueOtherThan(instance.getLastSuccess(), + SnapshotInvocationRecordTests::randomSnapshotInvocationRecord)) + .build(); + case 4: + return SnapshotLifecyclePolicyMetadata.builder(instance) + .setLastFailure(randomValueOtherThan(instance.getLastFailure(), + SnapshotInvocationRecordTests::randomSnapshotInvocationRecord)) + .build(); + default: + throw new AssertionError("failure, got illegal switch case"); + } + } + + public static SnapshotLifecyclePolicy createRandomPolicy(String policyId) { + Map config = new HashMap<>(); + for (int i = 0; i < randomIntBetween(2, 5); i++) { + config.put(randomAlphaOfLength(4), randomAlphaOfLength(4)); + } + return new SnapshotLifecyclePolicy(policyId, + randomAlphaOfLength(4), + randomSchedule(), + randomAlphaOfLength(4), + config); + } + + private static String randomSchedule() { + return randomIntBetween(0, 59) + " " + + randomIntBetween(0, 59) + " " + + randomIntBetween(0, 12) + " * * ?"; + } +} diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleIT.java index e08b4cfdc41b5..60cd8944037ac 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleIT.java @@ -28,6 +28,7 @@ import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.startsWith; @@ -36,52 +37,54 @@ public class SnapshotLifecycleIT extends ESRestTestCase { @SuppressWarnings("unchecked") public void testFullPolicySnapshot() throws Exception { - final String IDX = "test"; + final String indexName = "test"; + final String policyName = "test-policy"; + final String repoId = "my-repo"; int docCount = randomIntBetween(10, 50); List indexReqs = new ArrayList<>(); for (int i = 0; i < docCount; i++) { - index(client(), IDX, "" + i, "foo", "bar"); + index(client(), indexName, "" + i, "foo", "bar"); } // Create a snapshot repo - Request request = new Request("PUT", "/_snapshot/my-repo"); - request.setJsonEntity(Strings - .toString(JsonXContent.contentBuilder() - .startObject() - .field("type", "fs") - .startObject("settings") - .field("compress", randomBoolean()) - .field("location", System.getProperty("tests.path.repo")) - .field("max_snapshot_bytes_per_sec", "256b") - .endObject() - .endObject())); - assertOK(client().performRequest(request)); + inializeRepo(repoId); - Map snapConfig = new HashMap<>(); - snapConfig.put("indices", Collections.singletonList(IDX)); - SnapshotLifecyclePolicy policy = new SnapshotLifecyclePolicy("test-policy", "snap", "*/1 * * * * ?", "my-repo", snapConfig); - - Request putLifecycle = new Request("PUT", "/_ilm/snapshot/test-policy"); - XContentBuilder lifecycleBuilder = JsonXContent.contentBuilder(); - policy.toXContent(lifecycleBuilder, ToXContent.EMPTY_PARAMS); - putLifecycle.setJsonEntity(Strings.toString(lifecycleBuilder)); - assertOK(client().performRequest(putLifecycle)); + createSnapshotPolicy(policyName, "snap", "*/1 * * * * ?", repoId, indexName, true); // Check that the snapshot was actually taken assertBusy(() -> { - Response response = client().performRequest(new Request("GET", "/_snapshot/my-repo/_all")); - Map responseMap; + Response response = client().performRequest(new Request("GET", "/_snapshot/" + repoId + "/_all")); + Map snapshotResponseMap; try (InputStream is = response.getEntity().getContent()) { - responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + snapshotResponseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); } - assertThat(responseMap.size(), greaterThan(0)); - assertThat(((List>) responseMap.get("snapshots")).size(), greaterThan(0)); - Map snapResponse = ((List>) responseMap.get("snapshots")).get(0); + assertThat(snapshotResponseMap.size(), greaterThan(0)); + assertThat(((List>) snapshotResponseMap.get("snapshots")).size(), greaterThan(0)); + Map snapResponse = ((List>) snapshotResponseMap.get("snapshots")).get(0); assertThat(snapResponse.get("snapshot").toString(), startsWith("snap-")); - assertThat((List)snapResponse.get("indices"), equalTo(Collections.singletonList(IDX))); + assertThat((List)snapResponse.get("indices"), equalTo(Collections.singletonList(indexName))); + + // Check that the last success date was written to the cluster state + Request getReq = new Request("GET", "/_ilm/snapshot/" + policyName); + Response policyMetadata = client().performRequest(getReq); + Map policyResponseMap; + try (InputStream is = policyMetadata.getEntity().getContent()) { + policyResponseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + } + Map policyMetadataMap = (Map) policyResponseMap.get(policyName); + Map lastSuccessObject = (Map) policyMetadataMap.get("last_success"); + assertNotNull(lastSuccessObject); + Long lastSuccess = (Long) lastSuccessObject.get("time"); + Long modifiedDate = (Long) policyMetadataMap.get("modified_date"); + assertNotNull(lastSuccess); + assertNotNull(modifiedDate); + assertThat(lastSuccess, greaterThan(modifiedDate)); + + String lastSnapshotName = (String) lastSuccessObject.get("snapshot_name"); + assertThat(lastSnapshotName, startsWith("snap-")); }); - Request delReq = new Request("DELETE", "/_ilm/snapshot/test-policy"); + Request delReq = new Request("DELETE", "/_ilm/snapshot/" + policyName); assertOK(client().performRequest(delReq)); // It's possible there could have been a snapshot in progress when the @@ -91,6 +94,75 @@ public void testFullPolicySnapshot() throws Exception { }); } + @SuppressWarnings("unchecked") + public void testPolicyFailure() throws Exception { + final String policyName = "test-policy"; + final String repoName = "test-repo"; + final String indexPattern = "index-doesnt-exist"; + inializeRepo(repoName); + + // Create a policy with ignore_unvailable: false and an index that doesn't exist + createSnapshotPolicy(policyName, "snap", "*/1 * * * * ?", repoName, indexPattern, false); + + assertBusy(() -> { + // Check that the failure is written to the cluster state + Request getReq = new Request("GET", "/_ilm/snapshot/" + policyName); + Response policyMetadata = client().performRequest(getReq); + try (InputStream is = policyMetadata.getEntity().getContent()) { + Map responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + Map policyMetadataMap = (Map) responseMap.get(policyName); + Map lastFailureObject = (Map) policyMetadataMap.get("last_failure"); + assertNotNull(lastFailureObject); + + Long lastFailure = (Long) lastFailureObject.get("time"); + Long modifiedDate = (Long) policyMetadataMap.get("modified_date"); + assertNotNull(lastFailure); + assertNotNull(modifiedDate); + assertThat(lastFailure, greaterThan(modifiedDate)); + + String lastFailureInfo = (String) lastFailureObject.get("details"); + assertNotNull(lastFailureInfo); + assertThat(lastFailureInfo, containsString("no such index [index-doesnt-exist]")); + + String snapshotName = (String) lastFailureObject.get("snapshot_name"); + assertNotNull(snapshotName); + assertThat(snapshotName, startsWith("snap-")); + } + }); + + Request delReq = new Request("DELETE", "/_ilm/snapshot/" + policyName); + assertOK(client().performRequest(delReq)); + } + + private void createSnapshotPolicy(String policyName, String snapshotNamePattern, String schedule, String repoId, + String indexPattern, boolean ignoreUnavailable) throws IOException { + Map snapConfig = new HashMap<>(); + snapConfig.put("indices", Collections.singletonList(indexPattern)); + snapConfig.put("ignore_unavailable", ignoreUnavailable); + SnapshotLifecyclePolicy policy = new SnapshotLifecyclePolicy(policyName, snapshotNamePattern, schedule, repoId, snapConfig); + + Request putLifecycle = new Request("PUT", "/_ilm/snapshot/" + policyName); + XContentBuilder lifecycleBuilder = JsonXContent.contentBuilder(); + policy.toXContent(lifecycleBuilder, ToXContent.EMPTY_PARAMS); + putLifecycle.setJsonEntity(Strings.toString(lifecycleBuilder)); + assertOK(client().performRequest(putLifecycle)); + } + + private void inializeRepo(String repoName) throws IOException { + Request request = new Request("PUT", "/_snapshot/" + repoName); + request.setJsonEntity(Strings + .toString(JsonXContent.contentBuilder() + .startObject() + .field("type", "fs") + .startObject("settings") + .field("compress", randomBoolean()) + .field("location", System.getProperty("tests.path.repo")) + .field("max_snapshot_bytes_per_sec", "256b") + .endObject() + .endObject())); + assertOK(client().performRequest(request)); + } + private static void index(RestClient client, String index, String id, Object... fields) throws IOException { XContentBuilder document = jsonBuilder().startObject(); for (int i = 0; i < fields.length; i += 2) { diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTask.java index 78e143a7e60bb..73d44a16ddbb9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTask.java @@ -8,21 +8,36 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.scheduler.SchedulerEngine; +import org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotInvocationRecord; import org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotLifecycleMetadata; import org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotLifecyclePolicyMetadata; import org.elasticsearch.xpack.indexlifecycle.LifecyclePolicySecurityClient; +import java.io.IOException; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; + public class SnapshotLifecycleTask implements SchedulerEngine.Listener { private static Logger logger = LogManager.getLogger(SnapshotLifecycleTask.class); @@ -48,16 +63,18 @@ public void triggered(SchedulerEngine.Event event) { clientWithHeaders.admin().cluster().createSnapshot(request, new ActionListener() { @Override public void onResponse(CreateSnapshotResponse createSnapshotResponse) { - // TODO: persist this information in cluster state somewhere logger.info("snapshot response for [{}]: {}", policyMetadata.getPolicy().getId(), Strings.toString(createSnapshotResponse)); + clusterService.submitStateUpdateTask("slm-record-success-" + policyMetadata.getPolicy().getId(), + WriteJobStatus.success(policyMetadata.getPolicy().getId(), request.snapshot(), Instant.now().toEpochMilli())); } @Override public void onFailure(Exception e) { - // TODO: persist the failure information in cluster state somewhere - logger.error("failed to issue create snapshot request for snapshot lifecycle policy " + + logger.error("failed to issue create snapshot request for snapshot lifecycle policy [{}]: {}", policyMetadata.getPolicy().getId(), e); + clusterService.submitStateUpdateTask("slm-record-failure-" + policyMetadata.getPolicy().getId(), + WriteJobStatus.failure(policyMetadata.getPolicy().getId(), request.snapshot(), Instant.now().toEpochMilli(), e)); } }); return true; @@ -78,4 +95,86 @@ static Optional getSnapPolicyMetadata(final Str .filter(policyMeta -> jobId.equals(SnapshotLifecycleService.getJobId(policyMeta))) .findFirst()); } + + /** + * A cluster state update task to write the result of a snapshot job to the cluster metadata for the associated policy. + */ + private static class WriteJobStatus extends ClusterStateUpdateTask { + private static final ToXContent.Params STACKTRACE_PARAMS = + new ToXContent.MapParams(Collections.singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false")); + + private final String policyName; + private final String snapshotName; + private final long timestamp; + private final Optional exception; + + private WriteJobStatus(String policyName, String snapshotName, long timestamp, Optional exception) { + this.policyName = policyName; + this.snapshotName = snapshotName; + this.exception = exception; + this.timestamp = timestamp; + } + + static WriteJobStatus success(String policyId, String snapshotName, long timestamp) { + return new WriteJobStatus(policyId, snapshotName, timestamp, Optional.empty()); + } + + static WriteJobStatus failure(String policyId, String snapshotName, long timestamp, Exception exception) { + return new WriteJobStatus(policyId, snapshotName, timestamp, Optional.of(exception)); + } + + private String exceptionToString() throws IOException { + if (exception.isPresent()) { + try (XContentBuilder causeXContentBuilder = JsonXContent.contentBuilder()) { + causeXContentBuilder.startObject(); + ElasticsearchException.generateThrowableXContent(causeXContentBuilder, STACKTRACE_PARAMS, exception.get()); + causeXContentBuilder.endObject(); + return BytesReference.bytes(causeXContentBuilder).utf8ToString(); + } + } + return null; + } + + @Override + public ClusterState execute(ClusterState currentState) throws Exception { + SnapshotLifecycleMetadata snapMeta = currentState.metaData().custom(SnapshotLifecycleMetadata.TYPE); + + assert snapMeta != null : "this should never be called while the snapshot lifecycle cluster metadata is null"; + if (snapMeta == null) { + logger.error("failed to record snapshot [{}] for snapshot [{}] in policy [{}]: snapshot lifecycle metadata is null", + exception.isPresent() ? "failure" : "success", snapshotName, policyName); + return currentState; + } + + Map snapLifecycles = new HashMap<>(snapMeta.getSnapshotConfigurations()); + SnapshotLifecyclePolicyMetadata policyMetadata = snapLifecycles.get(policyName); + if (policyMetadata == null) { + logger.warn("failed to record snapshot [{}] for snapshot [{}] in policy [{}]: policy not found", + exception.isPresent() ? "failure" : "success", snapshotName, policyName); + return currentState; + } + + SnapshotLifecyclePolicyMetadata.Builder newPolicyMetadata = SnapshotLifecyclePolicyMetadata.builder(policyMetadata); + + if (exception.isPresent()) { + newPolicyMetadata.setLastFailure(new SnapshotInvocationRecord(snapshotName, timestamp, exceptionToString())); + } else { + newPolicyMetadata.setLastSuccess(new SnapshotInvocationRecord(snapshotName, timestamp, null)); + } + + snapLifecycles.put(policyName, newPolicyMetadata.build()); + SnapshotLifecycleMetadata lifecycleMetadata = new SnapshotLifecycleMetadata(snapLifecycles); + MetaData currentMeta = currentState.metaData(); + return ClusterState.builder(currentState) + .metaData(MetaData.builder(currentMeta) + .putCustom(SnapshotLifecycleMetadata.TYPE, lifecycleMetadata)) + .build(); + } + + @Override + public void onFailure(String source, Exception e) { + logger.error("failed to record snapshot policy execution status for snapshot [{}] in policy [{}], (source: [{}]): {}", + snapshotName, policyName, source, e); + } + } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportGetSnapshotLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportGetSnapshotLifecycleAction.java index 30cee15128b19..f5ecdfaac34e1 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportGetSnapshotLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportGetSnapshotLifecycleAction.java @@ -76,9 +76,7 @@ protected void masterOperation(final GetSnapshotLifecycleAction.Request request, return ids.contains(meta.getPolicy().getId()); } }) - .map(meta -> - new GetSnapshotLifecycleAction.SnapshotLifecyclePolicyItem(meta.getPolicy(), - meta.getVersion(), meta.getModifiedDate())) + .map(GetSnapshotLifecycleAction.SnapshotLifecyclePolicyItem::new) .collect(Collectors.toList()); listener.onResponse(new GetSnapshotLifecycleAction.Response(lifecycles)); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportPutSnapshotLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportPutSnapshotLifecycleAction.java index 01fd69ae9abbd..ec55d616b66f9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportPutSnapshotLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/snapshotlifecycle/action/TransportPutSnapshotLifecycleAction.java @@ -82,15 +82,22 @@ public ClusterState execute(ClusterState currentState) { String id = request.getLifecycleId(); final SnapshotLifecycleMetadata lifecycleMetadata; if (snapMeta == null) { - SnapshotLifecyclePolicyMetadata meta = new SnapshotLifecyclePolicyMetadata(request.getLifecycle(), filteredHeaders, - 0, Instant.now().toEpochMilli()); + SnapshotLifecyclePolicyMetadata meta = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(request.getLifecycle()) + .setHeaders(filteredHeaders) + .setModifiedDate(Instant.now().toEpochMilli()) + .build(); lifecycleMetadata = new SnapshotLifecycleMetadata(Collections.singletonMap(id, meta)); logger.info("adding new snapshot lifecycle [{}]", id); } else { Map snapLifecycles = new HashMap<>(snapMeta.getSnapshotConfigurations()); SnapshotLifecyclePolicyMetadata oldLifecycle = snapLifecycles.get(id); - SnapshotLifecyclePolicyMetadata newLifecycle = new SnapshotLifecyclePolicyMetadata(request.getLifecycle(), - filteredHeaders, oldLifecycle == null ? 0L : oldLifecycle.getVersion() + 1, Instant.now().toEpochMilli()); + SnapshotLifecyclePolicyMetadata newLifecycle = SnapshotLifecyclePolicyMetadata.builder(oldLifecycle) + .setPolicy(request.getLifecycle()) + .setHeaders(filteredHeaders) + .setVersion(oldLifecycle == null ? 1L : oldLifecycle.getVersion() + 1) + .setModifiedDate(Instant.now().toEpochMilli()) + .build(); snapLifecycles.put(id, newLifecycle); lifecycleMetadata = new SnapshotLifecycleMetadata(snapLifecycles); if (oldLifecycle == null) { diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleServiceTests.java index 8fc6ecdc29742..730d31fd7cda0 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleServiceTests.java @@ -41,7 +41,12 @@ public void testGetJobId() { String id = randomAlphaOfLengthBetween(1, 10) + (randomBoolean() ? "" : randomLong()); SnapshotLifecyclePolicy policy = createPolicy(id); long version = randomNonNegativeLong(); - SnapshotLifecyclePolicyMetadata meta = new SnapshotLifecyclePolicyMetadata(policy, Collections.emptyMap(), version, 1); + SnapshotLifecyclePolicyMetadata meta = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(policy) + .setHeaders(Collections.emptyMap()) + .setVersion(version) + .setModifiedDate(1) + .build(); assertThat(SnapshotLifecycleService.getJobId(meta), equalTo(id + "-" + version)); } @@ -63,9 +68,11 @@ public void testPolicyCRUD() throws Exception { ClusterState previousState = createState(snapMeta); Map policies = new HashMap<>(); - SnapshotLifecyclePolicyMetadata policy = - new SnapshotLifecyclePolicyMetadata(createPolicy("foo", "*/1 * * * * ?"), // trigger every second - Collections.emptyMap(), 1, 1); + SnapshotLifecyclePolicyMetadata policy = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(createPolicy("foo", "*/1 * * * * ?")) + .setHeaders(Collections.emptyMap()) + .setModifiedDate(1) + .build(); policies.put(policy.getPolicy().getId(), policy); snapMeta = new SnapshotLifecycleMetadata(policies); ClusterState state = createState(snapMeta); @@ -89,8 +96,12 @@ public void testPolicyCRUD() throws Exception { clock.freeze(); int currentCount = triggerCount.get(); previousState = state; - SnapshotLifecyclePolicyMetadata newPolicy = - new SnapshotLifecyclePolicyMetadata(createPolicy("foo", "*/1 * * * * ?"), Collections.emptyMap(), 2, 2); + SnapshotLifecyclePolicyMetadata newPolicy = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(createPolicy("foo", "*/1 * * * * ?")) + .setHeaders(Collections.emptyMap()) + .setVersion(2) + .setModifiedDate(2) + .build(); policies.put(policy.getPolicy().getId(), newPolicy); state = createState(new SnapshotLifecycleMetadata(policies)); event = new ClusterChangedEvent("2", state, previousState); @@ -119,9 +130,12 @@ public void testPolicyCRUD() throws Exception { assertThat(sls.getScheduler().scheduledJobIds(), equalTo(Collections.emptySet())); // When the service is no longer master, all jobs should be automatically cancelled - policy = - new SnapshotLifecyclePolicyMetadata(createPolicy("foo", "*/1 * * * * ?"), // trigger every second - Collections.emptyMap(), 3, 1); + policy = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(createPolicy("foo", "*/1 * * * * ?")) + .setHeaders(Collections.emptyMap()) + .setVersion(3) + .setModifiedDate(1) + .build(); policies.put(policy.getPolicy().getId(), policy); snapMeta = new SnapshotLifecycleMetadata(policies); previousState = state; @@ -160,9 +174,12 @@ public void testPolicyNamesEndingInNumbers() throws Exception { ClusterState previousState = createState(snapMeta); Map policies = new HashMap<>(); - SnapshotLifecyclePolicyMetadata policy = - new SnapshotLifecyclePolicyMetadata(createPolicy("foo-2", "30 * * * * ?"), - Collections.emptyMap(), 1, 1); + SnapshotLifecyclePolicyMetadata policy = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(createPolicy("foo-2", "30 * * * * ?")) + .setHeaders(Collections.emptyMap()) + .setVersion(1) + .setModifiedDate(1) + .build(); policies.put(policy.getPolicy().getId(), policy); snapMeta = new SnapshotLifecycleMetadata(policies); ClusterState state = createState(snapMeta); @@ -172,9 +189,12 @@ public void testPolicyNamesEndingInNumbers() throws Exception { assertThat(sls.getScheduler().scheduledJobIds(), equalTo(Collections.singleton("foo-2-1"))); previousState = state; - SnapshotLifecyclePolicyMetadata secondPolicy = - new SnapshotLifecyclePolicyMetadata(createPolicy("foo-1", "45 * * * * ?"), - Collections.emptyMap(), 2, 1); + SnapshotLifecyclePolicyMetadata secondPolicy = SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(createPolicy("foo-1", "45 * * * * ?")) + .setHeaders(Collections.emptyMap()) + .setVersion(2) + .setModifiedDate(1) + .build(); policies.put(secondPolicy.getPolicy().getId(), secondPolicy); snapMeta = new SnapshotLifecycleMetadata(policies); state = createState(snapMeta); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTaskTests.java index 9676f7eab4f5b..835de7cf095f0 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/snapshotlifecycle/SnapshotLifecycleTaskTests.java @@ -195,6 +195,11 @@ private SnapshotLifecyclePolicyMetadata makePolicyMeta(final String id) { SnapshotLifecyclePolicy policy = SnapshotLifecycleServiceTests.createPolicy(id); Map headers = new HashMap<>(); headers.put("X-Opaque-ID", randomAlphaOfLength(4)); - return new SnapshotLifecyclePolicyMetadata(policy, headers, 1, 1); + return SnapshotLifecyclePolicyMetadata.builder() + .setPolicy(policy) + .setHeaders(headers) + .setVersion(1) + .setModifiedDate(1) + .build(); } }