getNamedWriteables() {
new NamedWriteableRegistry.Entry(LifecycleAction.class, SetPriorityAction.NAME, SetPriorityAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, UnfollowAction.NAME, UnfollowAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, WaitForSnapshotAction.NAME, WaitForSnapshotAction::new),
+ new NamedWriteableRegistry.Entry(LifecycleAction.class, SearchableSnapshotAction.NAME, SearchableSnapshotAction::new),
// Transforms
new NamedWriteableRegistry.Entry(XPackFeatureSet.Usage.class, XPackField.TRANSFORM, TransformFeatureSetUsage::new),
new NamedWriteableRegistry.Entry(PersistentTaskParams.class, TransformField.TASK_NAME, TransformTaskParams::new),
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncActionBranchingStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncActionBranchingStep.java
new file mode 100644
index 0000000000000..501f202c4c03c
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncActionBranchingStep.java
@@ -0,0 +1,105 @@
+/*
+ * 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.ilm;
+
+import org.apache.lucene.util.SetOnce;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.ClusterStateObserver;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+
+import java.util.Objects;
+
+/**
+ * This step wraps an {@link AsyncActionStep} in order to be able to manipulate what the next step will be, depending on the result of the
+ * wrapped {@link AsyncActionStep}.
+ *
+ * If the action response is complete, the {@link AsyncActionBranchingStep}'s nextStepKey will be the nextStepKey of the wrapped action. If
+ * the response is incomplete the nextStepKey will be the provided {@link AsyncActionBranchingStep#nextKeyOnIncompleteResponse}.
+ * Failures encountered whilst executing the wrapped action will be propagated directly.
+ */
+public class AsyncActionBranchingStep extends AsyncActionStep {
+ private final AsyncActionStep stepToExecute;
+
+ private StepKey nextKeyOnIncompleteResponse;
+ private SetOnce onResponseResult;
+
+ public AsyncActionBranchingStep(AsyncActionStep stepToExecute, StepKey nextKeyOnIncompleteResponse, Client client) {
+ // super.nextStepKey is set to null since it is not used by this step
+ super(stepToExecute.getKey(), null, client);
+ this.stepToExecute = stepToExecute;
+ this.nextKeyOnIncompleteResponse = nextKeyOnIncompleteResponse;
+ this.onResponseResult = new SetOnce<>();
+ }
+
+ @Override
+ public boolean isRetryable() {
+ return true;
+ }
+
+ @Override
+ public void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState, ClusterStateObserver observer,
+ Listener listener) {
+ stepToExecute.performAction(indexMetadata, currentClusterState, observer, new Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ onResponseResult.set(complete);
+ listener.onResponse(complete);
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ listener.onFailure(e);
+ }
+ });
+ }
+
+ @Override
+ public final StepKey getNextStepKey() {
+ if (onResponseResult.get() == null) {
+ throw new IllegalStateException("cannot call getNextStepKey before performAction");
+ }
+ return onResponseResult.get() ? stepToExecute.getNextStepKey() : nextKeyOnIncompleteResponse;
+ }
+
+ /**
+ * Represents the {@link AsyncActionStep} that's wrapped by this branching step.
+ */
+ AsyncActionStep getStepToExecute() {
+ return stepToExecute;
+ }
+
+ /**
+ * The step key to be reported as the {@link AsyncActionBranchingStep#getNextStepKey()} if the response of the wrapped
+ * {@link AsyncActionBranchingStep#getStepToExecute()} is incomplete.
+ */
+ StepKey getNextKeyOnIncompleteResponse() {
+ return nextKeyOnIncompleteResponse;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ AsyncActionBranchingStep that = (AsyncActionBranchingStep) o;
+ return super.equals(o)
+ && Objects.equals(stepToExecute, that.stepToExecute)
+ && Objects.equals(nextKeyOnIncompleteResponse, that.nextKeyOnIncompleteResponse);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), stepToExecute, nextKeyOnIncompleteResponse);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncRetryDuringSnapshotActionStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncRetryDuringSnapshotActionStep.java
index d3ac8f852cb42..d480c24cb7c4e 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncRetryDuringSnapshotActionStep.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AsyncRetryDuringSnapshotActionStep.java
@@ -33,8 +33,8 @@ public AsyncRetryDuringSnapshotActionStep(StepKey key, StepKey nextStepKey, Clie
}
@Override
- public void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState,
- ClusterStateObserver observer, Listener listener) {
+ public final void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState,
+ ClusterStateObserver observer, Listener listener) {
// Wrap the original listener to handle exceptions caused by ongoing snapshots
SnapshotExceptionListener snapshotExceptionListener = new SnapshotExceptionListener(indexMetadata.getIndex(), listener, observer);
performDuringNoSnapshot(indexMetadata, currentClusterState, snapshotExceptionListener);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CleanupSnapshotStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CleanupSnapshotStep.java
new file mode 100644
index 0000000000000..3392a5e5d78b7
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CleanupSnapshotStep.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.repositories.RepositoryMissingException;
+import org.elasticsearch.snapshots.SnapshotMissingException;
+
+import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata;
+
+/**
+ * Deletes the snapshot designated by the repository and snapshot name present in the lifecycle execution state.
+ */
+public class CleanupSnapshotStep extends AsyncRetryDuringSnapshotActionStep {
+ public static final String NAME = "cleanup-snapshot";
+
+ public CleanupSnapshotStep(StepKey key, StepKey nextStepKey, Client client) {
+ super(key, nextStepKey, client);
+ }
+
+ @Override
+ public boolean isRetryable() {
+ return true;
+ }
+
+ @Override
+ void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState currentClusterState, Listener listener) {
+ final String indexName = indexMetadata.getIndex().getName();
+
+ LifecycleExecutionState lifecycleState = fromIndexMetadata(indexMetadata);
+ final String repositoryName = lifecycleState.getSnapshotRepository();
+ // if the snapshot information is missing from the ILM execution state there is nothing to delete so we move on
+ if (Strings.hasText(repositoryName) == false) {
+ listener.onResponse(true);
+ return;
+ }
+ final String snapshotName = lifecycleState.getSnapshotName();
+ if (Strings.hasText(snapshotName) == false) {
+ listener.onResponse(true);
+ return;
+ }
+ DeleteSnapshotRequest deleteSnapshotRequest = new DeleteSnapshotRequest(repositoryName, snapshotName);
+ getClient().admin().cluster().deleteSnapshot(deleteSnapshotRequest, new ActionListener<>() {
+
+ @Override
+ public void onResponse(AcknowledgedResponse acknowledgedResponse) {
+ if (acknowledgedResponse.isAcknowledged() == false) {
+ String policyName = indexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME);
+ throw new ElasticsearchException("cleanup snapshot step request for repository [" + repositoryName + "] and snapshot " +
+ "[" + snapshotName + "] policy [" + policyName + "] and index [" + indexName + "] failed to be acknowledged");
+ }
+ listener.onResponse(true);
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ if (e instanceof SnapshotMissingException) {
+ // during the happy flow we generate a snapshot name and that snapshot doesn't exist in the repository
+ listener.onResponse(true);
+ } else {
+ if (e instanceof RepositoryMissingException) {
+ String policyName = indexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME);
+ listener.onFailure(new IllegalStateException("repository [" + repositoryName + "] is missing. [" + policyName +
+ "] policy for index [" + indexName + "] cannot continue until the repository is created", e));
+ } else {
+ listener.onFailure(e);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java
index 3e15900f91914..349bec41b9c4a 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStep.java
@@ -19,23 +19,32 @@
/**
* Copies the execution state data from one index to another, typically after a
- * new index has been created. Useful for actions such as shrink.
+ * new index has been created. As part of the execution state copy it will set the target index
+ * "current step" to the provided step name (part of the same phase and action as the current step's, unless
+ * the "complete" step is configured in which case the action will be changed to "complete" as well)
+ *
+ * Useful for actions such as shrink.
*/
public class CopyExecutionStateStep extends ClusterStateActionStep {
public static final String NAME = "copy-execution-state";
private static final Logger logger = LogManager.getLogger(CopyExecutionStateStep.class);
- private String shrunkIndexPrefix;
+ private final String targetIndexPrefix;
+ private final String targetNextStepName;
-
- public CopyExecutionStateStep(StepKey key, StepKey nextStepKey, String shrunkIndexPrefix) {
+ public CopyExecutionStateStep(StepKey key, StepKey nextStepKey, String targetIndexPrefix, String targetNextStepName) {
super(key, nextStepKey);
- this.shrunkIndexPrefix = shrunkIndexPrefix;
+ this.targetIndexPrefix = targetIndexPrefix;
+ this.targetNextStepName = targetNextStepName;
+ }
+
+ String getTargetIndexPrefix() {
+ return targetIndexPrefix;
}
- String getShrunkIndexPrefix() {
- return shrunkIndexPrefix;
+ String getTargetNextStepName() {
+ return targetNextStepName;
}
@Override
@@ -48,8 +57,8 @@ public ClusterState performAction(Index index, ClusterState clusterState) {
}
// get source index
String indexName = indexMetadata.getIndex().getName();
- // get target shrink index
- String targetIndexName = shrunkIndexPrefix + indexName;
+ // get target index
+ String targetIndexName = targetIndexPrefix + indexName;
IndexMetadata targetIndexMetadata = clusterState.metadata().index(targetIndexName);
if (targetIndexMetadata == null) {
@@ -67,8 +76,14 @@ public ClusterState performAction(Index index, ClusterState clusterState) {
LifecycleExecutionState.Builder relevantTargetCustomData = LifecycleExecutionState.builder();
relevantTargetCustomData.setIndexCreationDate(lifecycleDate);
relevantTargetCustomData.setPhase(phase);
- relevantTargetCustomData.setAction(action);
- relevantTargetCustomData.setStep(ShrunkenIndexCheckStep.NAME);
+ relevantTargetCustomData.setStep(targetNextStepName);
+ if (targetNextStepName.equals(PhaseCompleteStep.NAME)) {
+ relevantTargetCustomData.setAction(PhaseCompleteStep.NAME);
+ } else {
+ relevantTargetCustomData.setAction(action);
+ }
+ relevantTargetCustomData.setSnapshotRepository(lifecycleState.getSnapshotRepository());
+ relevantTargetCustomData.setSnapshotName(lifecycleState.getSnapshotName());
Metadata.Builder newMetadata = Metadata.builder(clusterState.getMetadata())
.put(IndexMetadata.builder(targetIndexMetadata)
@@ -79,15 +94,22 @@ public ClusterState performAction(Index index, ClusterState clusterState) {
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
CopyExecutionStateStep that = (CopyExecutionStateStep) o;
- return Objects.equals(shrunkIndexPrefix, that.shrunkIndexPrefix);
+ return Objects.equals(targetIndexPrefix, that.targetIndexPrefix) &&
+ Objects.equals(targetNextStepName, that.targetNextStepName);
}
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), shrunkIndexPrefix);
+ return Objects.hash(super.hashCode(), targetIndexPrefix, targetNextStepName);
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopySettingsStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopySettingsStep.java
new file mode 100644
index 0000000000000..c6f7cbdb8d599
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CopySettingsStep.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ilm;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.Index;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Copy the provided settings from the source to the target index.
+ *
+ * The target index is derived from the source index using the provided prefix.
+ * This is useful for actions like shrink or searchable snapshot that create a new index and migrate the ILM execution from the source
+ * to the target index.
+ */
+public class CopySettingsStep extends ClusterStateActionStep {
+ public static final String NAME = "copy-settings";
+
+ private static final Logger logger = LogManager.getLogger(CopySettingsStep.class);
+
+ private final String[] settingsKeys;
+ private final String indexPrefix;
+
+ public CopySettingsStep(StepKey key, StepKey nextStepKey, String indexPrefix, String... settingsKeys) {
+ super(key, nextStepKey);
+ Objects.requireNonNull(indexPrefix);
+ Objects.requireNonNull(settingsKeys);
+ this.indexPrefix = indexPrefix;
+ this.settingsKeys = settingsKeys;
+ }
+
+ @Override
+ public boolean isRetryable() {
+ return true;
+ }
+
+ public String[] getSettingsKeys() {
+ return settingsKeys;
+ }
+
+ public String getIndexPrefix() {
+ return indexPrefix;
+ }
+
+ @Override
+ public ClusterState performAction(Index index, ClusterState clusterState) {
+ String sourceIndexName = index.getName();
+ IndexMetadata sourceIndexMetadata = clusterState.metadata().index(sourceIndexName);
+ String targetIndexName = indexPrefix + sourceIndexName;
+ IndexMetadata targetIndexMetadata = clusterState.metadata().index(targetIndexName);
+
+ if (sourceIndexMetadata == null) {
+ // Index must have been since deleted, ignore it
+ logger.debug("[{}] lifecycle action for index [{}] executed but index no longer exists", getKey().getAction(), sourceIndexName);
+ return clusterState;
+ }
+
+ if (settingsKeys == null || settingsKeys.length == 0) {
+ return clusterState;
+ }
+
+ if (targetIndexMetadata == null) {
+ String errorMessage = String.format(Locale.ROOT, "index [%s] is being referenced by ILM action [%s] on step [%s] but " +
+ "it doesn't exist", targetIndexName, getKey().getAction(), getKey().getName());
+ logger.debug(errorMessage);
+ throw new IllegalStateException(errorMessage);
+ }
+
+ Settings.Builder settings = Settings.builder().put(targetIndexMetadata.getSettings());
+ for (String key : settingsKeys) {
+ String value = sourceIndexMetadata.getSettings().get(key);
+ settings.put(key, value);
+ }
+
+ Metadata.Builder newMetaData = Metadata.builder(clusterState.getMetadata())
+ .put(IndexMetadata.builder(targetIndexMetadata)
+ .settingsVersion(targetIndexMetadata.getSettingsVersion() + 1)
+ .settings(settings));
+ return ClusterState.builder(clusterState).metadata(newMetaData).build();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ CopySettingsStep that = (CopySettingsStep) o;
+ return Objects.equals(settingsKeys, that.settingsKeys) &&
+ Objects.equals(indexPrefix, that.indexPrefix);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), settingsKeys, indexPrefix);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CreateSnapshotStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CreateSnapshotStep.java
new file mode 100644
index 0000000000000..9caf0647cb5f4
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/CreateSnapshotStep.java
@@ -0,0 +1,83 @@
+/*
+ * 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.ilm;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.snapshots.SnapshotInfo;
+
+import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata;
+
+/**
+ * Creates a snapshot of the managed index into the configured repository and snapshot name. The repository and snapshot names are expected
+ * to be present in the lifecycle execution state (usually generated and stored by a different ILM step)
+ */
+public class CreateSnapshotStep extends AsyncRetryDuringSnapshotActionStep {
+ public static final String NAME = "create-snapshot";
+
+ private static final Logger logger = LogManager.getLogger(CreateSnapshotStep.class);
+
+ public CreateSnapshotStep(StepKey key, StepKey nextStepKey, Client client) {
+ super(key, nextStepKey, client);
+ }
+
+ @Override
+ public boolean isRetryable() {
+ return true;
+ }
+
+ @Override
+ void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState currentClusterState, Listener listener) {
+ final String indexName = indexMetadata.getIndex().getName();
+
+ final LifecycleExecutionState lifecycleState = fromIndexMetadata(indexMetadata);
+
+ final String policyName = indexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME);
+ final String snapshotRepository = lifecycleState.getSnapshotRepository();
+ if (Strings.hasText(snapshotRepository) == false) {
+ listener.onFailure(new IllegalStateException("snapshot repository is not present for policy [" + policyName + "] and index [" +
+ indexName + "]"));
+ return;
+ }
+
+ final String snapshotName = lifecycleState.getSnapshotName();
+ if (Strings.hasText(snapshotName) == false) {
+ listener.onFailure(
+ new IllegalStateException("snapshot name was not generated for policy [" + policyName + "] and index [" + indexName + "]"));
+ return;
+ }
+ CreateSnapshotRequest request = new CreateSnapshotRequest(snapshotRepository, snapshotName);
+ request.indices(indexName);
+ // this is safe as the snapshot creation will still be async, it's just that the listener will be notified when the snapshot is
+ // complete
+ request.waitForCompletion(true);
+ request.includeGlobalState(false);
+ request.masterNodeTimeout(getMasterTimeout(currentClusterState));
+ getClient().admin().cluster().createSnapshot(request,
+ ActionListener.wrap(response -> {
+ logger.debug("create snapshot response for policy [{}] and index [{}] is: {}", policyName, indexName,
+ Strings.toString(response));
+ final SnapshotInfo snapInfo = response.getSnapshotInfo();
+
+ // Check that there are no failed shards, since the request may not entirely
+ // fail, but may still have failures (such as in the case of an aborted snapshot)
+ if (snapInfo.failedShards() == 0) {
+ listener.onResponse(true);
+ } else {
+ int failures = snapInfo.failedShards();
+ int total = snapInfo.totalShards();
+ logger.warn("failed to create snapshot successfully, {} failures out of {} total shards failed", failures, total);
+ listener.onResponse(false);
+ }
+ }, listener::onFailure));
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java
index 0f3e6d70fd8cd..9f0a06e013f4d 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java
@@ -5,17 +5,20 @@
*/
package org.elasticsearch.xpack.core.ilm;
+import org.elasticsearch.Version;
import org.elasticsearch.client.Client;
+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.ObjectParser;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
/**
* A {@link LifecycleAction} which deletes the index.
@@ -23,20 +26,42 @@
public class DeleteAction implements LifecycleAction {
public static final String NAME = "delete";
- private static final ObjectParser PARSER = new ObjectParser<>(NAME, DeleteAction::new);
+ public static final ParseField DELETE_SEARCHABLE_SNAPSHOT_FIELD = new ParseField("delete_searchable_snapshot");
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME,
+ a -> new DeleteAction(a[0] == null ? true : (boolean) a[0]));
+
+ static {
+ PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), DELETE_SEARCHABLE_SNAPSHOT_FIELD);
+ }
public static DeleteAction parse(XContentParser parser) {
return PARSER.apply(parser, null);
}
+ private final boolean deleteSearchableSnapshot;
+
public DeleteAction() {
+ this(true);
+ }
+
+ public DeleteAction(boolean deleteSearchableSnapshot) {
+ this.deleteSearchableSnapshot = deleteSearchableSnapshot;
}
public DeleteAction(StreamInput in) throws IOException {
+ if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
+ this.deleteSearchableSnapshot = in.readBoolean();
+ } else {
+ this.deleteSearchableSnapshot = true;
+ }
}
@Override
public void writeTo(StreamOutput out) throws IOException {
+ if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
+ out.writeBoolean(deleteSearchableSnapshot);
+ }
}
@Override
@@ -47,6 +72,7 @@ public String getWriteableName() {
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
+ builder.field(DELETE_SEARCHABLE_SNAPSHOT_FIELD.getPreferredName(), deleteSearchableSnapshot);
builder.endObject();
return builder;
}
@@ -60,15 +86,23 @@ public boolean isSafeAction() {
public List toSteps(Client client, String phase, Step.StepKey nextStepKey) {
Step.StepKey waitForNoFollowerStepKey = new Step.StepKey(phase, NAME, WaitForNoFollowersStep.NAME);
Step.StepKey deleteStepKey = new Step.StepKey(phase, NAME, DeleteStep.NAME);
-
- WaitForNoFollowersStep waitForNoFollowersStep = new WaitForNoFollowersStep(waitForNoFollowerStepKey, deleteStepKey, client);
- DeleteStep deleteStep = new DeleteStep(deleteStepKey, nextStepKey, client);
- return Arrays.asList(waitForNoFollowersStep, deleteStep);
+ Step.StepKey cleanSnapshotKey = new Step.StepKey(phase, NAME, CleanupSnapshotStep.NAME);
+
+ if (deleteSearchableSnapshot) {
+ WaitForNoFollowersStep waitForNoFollowersStep = new WaitForNoFollowersStep(waitForNoFollowerStepKey, cleanSnapshotKey, client);
+ CleanupSnapshotStep cleanupSnapshotStep = new CleanupSnapshotStep(cleanSnapshotKey, deleteStepKey, client);
+ DeleteStep deleteStep = new DeleteStep(deleteStepKey, nextStepKey, client);
+ return Arrays.asList(waitForNoFollowersStep, cleanupSnapshotStep, deleteStep);
+ } else {
+ WaitForNoFollowersStep waitForNoFollowersStep = new WaitForNoFollowersStep(waitForNoFollowerStepKey, deleteStepKey, client);
+ DeleteStep deleteStep = new DeleteStep(deleteStepKey, nextStepKey, client);
+ return Arrays.asList(waitForNoFollowersStep, deleteStep);
+ }
}
@Override
public int hashCode() {
- return 1;
+ return Objects.hash(deleteSearchableSnapshot);
}
@Override
@@ -79,7 +113,8 @@ public boolean equals(Object obj) {
if (obj.getClass() != getClass()) {
return false;
}
- return true;
+ DeleteAction that = (DeleteAction) obj;
+ return deleteSearchableSnapshot == that.deleteSearchableSnapshot;
}
@Override
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java
new file mode 100644
index 0000000000000..49d05cc4a68b8
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStep.java
@@ -0,0 +1,176 @@
+/*
+ * 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.ilm;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.IndicesOptions;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.UUIDs;
+import org.elasticsearch.index.Index;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY;
+import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata;
+
+/**
+ * Generates a snapshot name for the given index and records it in the index metadata along with the provided snapshot repository.
+ *
+ * The generated snapshot name will be in the format {day-indexName-policyName-randomUUID}
+ * eg.: 2020.03.30-myindex-mypolicy-cmuce-qfvmn_dstqw-ivmjc1etsa
+ */
+public class GenerateSnapshotNameStep extends ClusterStateActionStep {
+
+ public static final String NAME = "generate-snapshot-name";
+
+ private static final Logger logger = LogManager.getLogger(CreateSnapshotStep.class);
+
+ private static final IndexNameExpressionResolver.DateMathExpressionResolver DATE_MATH_RESOLVER =
+ new IndexNameExpressionResolver.DateMathExpressionResolver();
+
+ private final String snapshotRepository;
+
+ public GenerateSnapshotNameStep(StepKey key, StepKey nextStepKey, String snapshotRepository) {
+ super(key, nextStepKey);
+ this.snapshotRepository = snapshotRepository;
+ }
+
+ public String getSnapshotRepository() {
+ return snapshotRepository;
+ }
+
+ @Override
+ public ClusterState performAction(Index index, ClusterState clusterState) {
+ IndexMetadata indexMetaData = clusterState.metadata().index(index);
+ if (indexMetaData == null) {
+ // Index must have been since deleted, ignore it
+ logger.debug("[{}] lifecycle action for index [{}] executed but index no longer exists", getKey().getAction(), index.getName());
+ return clusterState;
+ }
+
+ ClusterState.Builder newClusterStateBuilder = ClusterState.builder(clusterState);
+
+ LifecycleExecutionState lifecycleState = fromIndexMetadata(indexMetaData);
+ assert lifecycleState.getSnapshotName() == null : "index " + index.getName() + " should not have a snapshot generated by " +
+ "the ilm policy but has " + lifecycleState.getSnapshotName();
+ LifecycleExecutionState.Builder newCustomData = LifecycleExecutionState.builder(lifecycleState);
+ String policy = indexMetaData.getSettings().get(LifecycleSettings.LIFECYCLE_NAME);
+ String snapshotNamePrefix = ("<{now/d}-" + index.getName() + "-" + policy + ">").toLowerCase(Locale.ROOT);
+ String snapshotName = generateSnapshotName(snapshotNamePrefix);
+ ActionRequestValidationException validationException = validateGeneratedSnapshotName(snapshotNamePrefix, snapshotName);
+ if (validationException != null) {
+ logger.warn("unable to generate a snapshot name as part of policy [{}] for index [{}] due to [{}]",
+ policy, index.getName(), validationException.getMessage());
+ throw validationException;
+ }
+ newCustomData.setSnapshotName(snapshotName);
+ newCustomData.setSnapshotRepository(snapshotRepository);
+
+ IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(indexMetaData);
+ indexMetadataBuilder.putCustom(ILM_CUSTOM_METADATA_KEY, newCustomData.build().asMap());
+ newClusterStateBuilder.metadata(Metadata.builder(clusterState.getMetadata()).put(indexMetadataBuilder));
+ return newClusterStateBuilder.build();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), snapshotRepository);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ GenerateSnapshotNameStep other = (GenerateSnapshotNameStep) obj;
+ return super.equals(obj) &&
+ Objects.equals(snapshotRepository, other.snapshotRepository);
+ }
+
+ /**
+ * Since snapshots need to be uniquely named, this method will resolve any date math used in
+ * the provided name, as well as appending a unique identifier so expressions that may overlap
+ * still result in unique snapshot names.
+ */
+ public static String generateSnapshotName(String name) {
+ return generateSnapshotName(name, new ResolverContext());
+ }
+
+ public static String generateSnapshotName(String name, IndexNameExpressionResolver.Context context) {
+ List candidates = DATE_MATH_RESOLVER.resolve(context, Collections.singletonList(name));
+ if (candidates.size() != 1) {
+ throw new IllegalStateException("resolving snapshot name " + name + " generated more than one candidate: " + candidates);
+ }
+ // TODO: we are breaking the rules of UUIDs by lowercasing this here, find an alternative (snapshot names must be lowercase)
+ return candidates.get(0) + "-" + UUIDs.randomBase64UUID().toLowerCase(Locale.ROOT);
+ }
+
+ /**
+ * This is a context for the DateMathExpressionResolver, which does not require
+ * {@code IndicesOptions} or {@code ClusterState} since it only uses the start
+ * time to resolve expressions
+ */
+ public static final class ResolverContext extends IndexNameExpressionResolver.Context {
+ public ResolverContext() {
+ this(System.currentTimeMillis());
+ }
+
+ public ResolverContext(long startTime) {
+ super(null, null, startTime, false, false);
+ }
+
+ @Override
+ public ClusterState getState() {
+ throw new UnsupportedOperationException("should never be called");
+ }
+
+ @Override
+ public IndicesOptions getOptions() {
+ throw new UnsupportedOperationException("should never be called");
+ }
+ }
+
+ @Nullable
+ public static ActionRequestValidationException validateGeneratedSnapshotName(String snapshotPrefix, String snapshotName) {
+ ActionRequestValidationException err = new ActionRequestValidationException();
+ if (Strings.hasText(snapshotPrefix) == false) {
+ err.addValidationError("invalid snapshot name [" + snapshotPrefix + "]: cannot be empty");
+ }
+ if (snapshotName.contains("#")) {
+ err.addValidationError("invalid snapshot name [" + snapshotPrefix + "]: must not contain '#'");
+ }
+ if (snapshotName.charAt(0) == '_') {
+ err.addValidationError("invalid snapshot name [" + snapshotPrefix + "]: must not start with '_'");
+ }
+ if (snapshotName.toLowerCase(Locale.ROOT).equals(snapshotName) == false) {
+ err.addValidationError("invalid snapshot name [" + snapshotPrefix + "]: must be lowercase");
+ }
+ if (Strings.validFileName(snapshotName) == false) {
+ err.addValidationError("invalid snapshot name [" + snapshotPrefix + "]: must not contain contain the following characters " +
+ Strings.INVALID_FILENAME_CHARS);
+ }
+
+ if (err.validationErrors().size() > 0) {
+ return err;
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java
index 711e57d3b6d8e..b872893a2fc9b 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponse.java
@@ -46,6 +46,8 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl
private static final ParseField STEP_INFO_FIELD = new ParseField("step_info");
private static final ParseField PHASE_EXECUTION_INFO = new ParseField("phase_execution");
private static final ParseField AGE_FIELD = new ParseField("age");
+ private static final ParseField REPOSITORY_NAME = new ParseField("repository_name");
+ private static final ParseField SNAPSHOT_NAME = new ParseField("snapshot_name");
public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
"index_lifecycle_explain_response",
@@ -63,6 +65,8 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl
(Long) (a[8]),
(Long) (a[9]),
(Long) (a[10]),
+ (String) a[16],
+ (String) a[17],
(BytesReference) a[11],
(PhaseExecutionInfo) a[12]
// a[13] == "age"
@@ -89,6 +93,8 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), AGE_FIELD);
PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), IS_AUTO_RETRYABLE_ERROR_FIELD);
PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), FAILED_STEP_RETRY_COUNT_FIELD);
+ PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), REPOSITORY_NAME);
+ PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), SNAPSHOT_NAME);
}
private final String index;
@@ -106,23 +112,28 @@ public class IndexLifecycleExplainResponse implements ToXContentObject, Writeabl
private final PhaseExecutionInfo phaseExecutionInfo;
private final Boolean isAutoRetryableError;
private final Integer failedStepRetryCount;
+ private final String repositoryName;
+ private final String snapshotName;
public static IndexLifecycleExplainResponse newManagedIndexResponse(String index, String policyName, Long lifecycleDate,
String phase, String action, String step, String failedStep, Boolean isAutoRetryableError, Integer failedStepRetryCount,
- Long phaseTime, Long actionTime, Long stepTime, BytesReference stepInfo, PhaseExecutionInfo phaseExecutionInfo) {
+ Long phaseTime, Long actionTime, Long stepTime, String repositoryName, String snapshotName, BytesReference stepInfo,
+ PhaseExecutionInfo phaseExecutionInfo) {
return new IndexLifecycleExplainResponse(index, true, policyName, lifecycleDate, phase, action, step, failedStep,
- isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, stepInfo, phaseExecutionInfo);
+ isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, stepInfo,
+ phaseExecutionInfo);
}
public static IndexLifecycleExplainResponse newUnmanagedIndexResponse(String index) {
return new IndexLifecycleExplainResponse(index, false, null, null, null, null, null, null, null, null, null, null, null, null,
- null);
+ null, null, null);
}
private IndexLifecycleExplainResponse(String index, boolean managedByILM, String policyName, Long lifecycleDate,
String phase, String action, String step, String failedStep, Boolean isAutoRetryableError,
Integer failedStepRetryCount, Long phaseTime, Long actionTime, Long stepTime,
- BytesReference stepInfo, PhaseExecutionInfo phaseExecutionInfo) {
+ String repositoryName, String snapshotName, BytesReference stepInfo,
+ PhaseExecutionInfo phaseExecutionInfo) {
if (managedByILM) {
if (policyName == null) {
throw new IllegalArgumentException("[" + POLICY_NAME_FIELD.getPreferredName() + "] cannot be null for managed index");
@@ -157,6 +168,8 @@ private IndexLifecycleExplainResponse(String index, boolean managedByILM, String
this.failedStepRetryCount = failedStepRetryCount;
this.stepInfo = stepInfo;
this.phaseExecutionInfo = phaseExecutionInfo;
+ this.repositoryName = repositoryName;
+ this.snapshotName = snapshotName;
}
public IndexLifecycleExplainResponse(StreamInput in) throws IOException {
@@ -181,6 +194,13 @@ public IndexLifecycleExplainResponse(StreamInput in) throws IOException {
isAutoRetryableError = null;
failedStepRetryCount = null;
}
+ if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
+ repositoryName = in.readOptionalString();
+ snapshotName = in.readOptionalString();
+ } else {
+ repositoryName = null;
+ snapshotName = null;
+ }
} else {
policyName = null;
lifecycleDate = null;
@@ -195,6 +215,8 @@ public IndexLifecycleExplainResponse(StreamInput in) throws IOException {
stepTime = null;
stepInfo = null;
phaseExecutionInfo = null;
+ repositoryName = null;
+ snapshotName = null;
}
}
@@ -218,6 +240,10 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeOptionalBoolean(isAutoRetryableError);
out.writeOptionalVInt(failedStepRetryCount);
}
+ if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
+ out.writeOptionalString(repositoryName);
+ out.writeOptionalString(snapshotName);
+ }
}
}
@@ -289,6 +315,14 @@ public TimeValue getAge() {
}
}
+ public String getRepositoryName() {
+ return repositoryName;
+ }
+
+ public String getSnapshotName() {
+ return snapshotName;
+ }
+
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
@@ -327,6 +361,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
if (failedStepRetryCount != null) {
builder.field(FAILED_STEP_RETRY_COUNT_FIELD.getPreferredName(), failedStepRetryCount);
}
+ if (repositoryName != null) {
+ builder.field(REPOSITORY_NAME.getPreferredName(), repositoryName);
+ }
+ if (snapshotName != null) {
+ builder.field(SNAPSHOT_NAME.getPreferredName(), snapshotName);
+ }
if (stepInfo != null && stepInfo.length() > 0) {
builder.rawField(STEP_INFO_FIELD.getPreferredName(), stepInfo.streamInput(), XContentType.JSON);
}
@@ -341,7 +381,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
@Override
public int hashCode() {
return Objects.hash(index, managedByILM, policyName, lifecycleDate, phase, action, step, failedStep, isAutoRetryableError,
- failedStepRetryCount, phaseTime, actionTime, stepTime, stepInfo, phaseExecutionInfo);
+ failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, stepInfo, phaseExecutionInfo);
}
@Override
@@ -366,6 +406,8 @@ public boolean equals(Object obj) {
Objects.equals(phaseTime, other.phaseTime) &&
Objects.equals(actionTime, other.actionTime) &&
Objects.equals(stepTime, other.stepTime) &&
+ Objects.equals(repositoryName, other.repositoryName) &&
+ Objects.equals(snapshotName, other.snapshotName) &&
Objects.equals(stepInfo, other.stepInfo) &&
Objects.equals(phaseExecutionInfo, other.phaseExecutionInfo);
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java
index 0607470f82be5..2746b07531038 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionState.java
@@ -36,6 +36,8 @@ public class LifecycleExecutionState {
private static final String FAILED_STEP_RETRY_COUNT = "failed_step_retry_count";
private static final String STEP_INFO = "step_info";
private static final String PHASE_DEFINITION = "phase_definition";
+ private static final String SNAPSHOT_NAME ="snapshot_name";
+ private static final String SNAPSHOT_REPOSITORY ="snapshot_repository";
private final String phase;
private final String action;
@@ -49,10 +51,12 @@ public class LifecycleExecutionState {
private final Long phaseTime;
private final Long actionTime;
private final Long stepTime;
+ private final String snapshotName;
+ private final String snapshotRepository;
private LifecycleExecutionState(String phase, String action, String step, String failedStep, Boolean isAutoRetryableError,
Integer failedStepRetryCount, String stepInfo, String phaseDefinition, Long lifecycleDate,
- Long phaseTime, Long actionTime, Long stepTime) {
+ Long phaseTime, Long actionTime, Long stepTime, String snapshotRepository, String snapshotName) {
this.phase = phase;
this.action = action;
this.step = step;
@@ -65,6 +69,8 @@ private LifecycleExecutionState(String phase, String action, String step, String
this.phaseTime = phaseTime;
this.actionTime = actionTime;
this.stepTime = stepTime;
+ this.snapshotRepository = snapshotRepository;
+ this.snapshotName = snapshotName;
}
/**
@@ -122,6 +128,8 @@ public static Builder builder(LifecycleExecutionState state) {
.setIndexCreationDate(state.lifecycleDate)
.setPhaseTime(state.phaseTime)
.setActionTime(state.actionTime)
+ .setSnapshotRepository(state.snapshotRepository)
+ .setSnapshotName(state.snapshotName)
.setStepTime(state.stepTime);
}
@@ -151,6 +159,12 @@ static LifecycleExecutionState fromCustomMetadata(Map customData
if (customData.containsKey(PHASE_DEFINITION)) {
builder.setPhaseDefinition(customData.get(PHASE_DEFINITION));
}
+ if (customData.containsKey(SNAPSHOT_REPOSITORY)) {
+ builder.setSnapshotRepository(customData.get(SNAPSHOT_REPOSITORY));
+ }
+ if (customData.containsKey(SNAPSHOT_NAME)) {
+ builder.setSnapshotName(customData.get(SNAPSHOT_NAME));
+ }
if (customData.containsKey(INDEX_CREATION_DATE)) {
try {
builder.setIndexCreationDate(Long.parseLong(customData.get(INDEX_CREATION_DATE)));
@@ -229,6 +243,12 @@ public Map asMap() {
if (phaseDefinition != null) {
result.put(PHASE_DEFINITION, String.valueOf(phaseDefinition));
}
+ if (snapshotRepository != null) {
+ result.put(SNAPSHOT_REPOSITORY, snapshotRepository);
+ }
+ if (snapshotName != null) {
+ result.put(SNAPSHOT_NAME, snapshotName);
+ }
return Collections.unmodifiableMap(result);
}
@@ -280,6 +300,14 @@ public Long getStepTime() {
return stepTime;
}
+ public String getSnapshotName() {
+ return snapshotName;
+ }
+
+ public String getSnapshotRepository() {
+ return snapshotRepository;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -296,13 +324,16 @@ public boolean equals(Object o) {
Objects.equals(isAutoRetryableError(), that.isAutoRetryableError()) &&
Objects.equals(getFailedStepRetryCount(), that.getFailedStepRetryCount()) &&
Objects.equals(getStepInfo(), that.getStepInfo()) &&
+ Objects.equals(getSnapshotRepository(), that.getSnapshotRepository()) &&
+ Objects.equals(getSnapshotName(), that.getSnapshotName()) &&
Objects.equals(getPhaseDefinition(), that.getPhaseDefinition());
}
@Override
public int hashCode() {
return Objects.hash(getPhase(), getAction(), getStep(), getFailedStep(), isAutoRetryableError(), getFailedStepRetryCount(),
- getStepInfo(), getPhaseDefinition(), getLifecycleDate(), getPhaseTime(), getActionTime(), getStepTime());
+ getStepInfo(), getPhaseDefinition(), getLifecycleDate(), getPhaseTime(), getActionTime(), getStepTime(),
+ getSnapshotRepository(), getSnapshotName());
}
@Override
@@ -323,6 +354,8 @@ public static class Builder {
private Long stepTime;
private Boolean isAutoRetryableError;
private Integer failedStepRetryCount;
+ private String snapshotName;
+ private String snapshotRepository;
public Builder setPhase(String phase) {
this.phase = phase;
@@ -384,9 +417,19 @@ public Builder setFailedStepRetryCount(Integer failedStepRetryCount) {
return this;
}
+ public Builder setSnapshotRepository(String snapshotRepository) {
+ this.snapshotRepository = snapshotRepository;
+ return this;
+ }
+
+ public Builder setSnapshotName(String snapshotName) {
+ this.snapshotName = snapshotName;
+ return this;
+ }
+
public LifecycleExecutionState build() {
return new LifecycleExecutionState(phase, action, step, failedStep, isAutoRetryableError, failedStepRetryCount, stepInfo,
- phaseDefinition, indexCreationDate, phaseTime, actionTime, stepTime);
+ phaseDefinition, indexCreationDate, phaseTime, actionTime, stepTime, snapshotRepository, snapshotName);
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStep.java
new file mode 100644
index 0000000000000..5f1160a07e5eb
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStep.java
@@ -0,0 +1,119 @@
+/*
+ * 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.ilm;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexSettings;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotAction;
+import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest;
+
+import java.util.Objects;
+
+import static org.elasticsearch.xpack.core.ilm.LifecycleExecutionState.fromIndexMetadata;
+
+/**
+ * Restores the snapshot created for the designated index via the ILM policy to an index named using the provided prefix appended to the
+ * designated index name.
+ */
+public class MountSnapshotStep extends AsyncRetryDuringSnapshotActionStep {
+ public static final String NAME = "mount-snapshot";
+
+ private static final Logger logger = LogManager.getLogger(MountSnapshotStep.class);
+
+ private final String restoredIndexPrefix;
+
+ public MountSnapshotStep(StepKey key, StepKey nextStepKey, Client client, String restoredIndexPrefix) {
+ super(key, nextStepKey, client);
+ this.restoredIndexPrefix = restoredIndexPrefix;
+ }
+
+ @Override
+ public boolean isRetryable() {
+ return true;
+ }
+
+ public String getRestoredIndexPrefix() {
+ return restoredIndexPrefix;
+ }
+
+ @Override
+ void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState currentClusterState, Listener listener) {
+ final String indexName = indexMetadata.getIndex().getName();
+
+ LifecycleExecutionState lifecycleState = fromIndexMetadata(indexMetadata);
+
+ String policyName = indexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME);
+ final String snapshotRepository = lifecycleState.getSnapshotRepository();
+ if (Strings.hasText(snapshotRepository) == false) {
+ listener.onFailure(new IllegalStateException("snapshot repository is not present for policy [" + policyName + "] and index [" +
+ indexName + "]"));
+ return;
+ }
+
+ final String snapshotName = lifecycleState.getSnapshotName();
+ if (Strings.hasText(snapshotName) == false) {
+ listener.onFailure(
+ new IllegalStateException("snapshot name was not generated for policy [" + policyName + "] and index [" + indexName + "]"));
+ return;
+ }
+
+ String mountedIndexName = restoredIndexPrefix + indexName;
+ if(currentClusterState.metadata().index(mountedIndexName) != null) {
+ logger.debug("mounted index [{}] for policy [{}] and index [{}] already exists. will not attempt to mount the index again",
+ mountedIndexName, policyName, indexName);
+ listener.onResponse(true);
+ return;
+ }
+
+ final MountSearchableSnapshotRequest mountSearchableSnapshotRequest = new MountSearchableSnapshotRequest(mountedIndexName,
+ snapshotRepository, snapshotName, indexName, Settings.builder()
+ .put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), Boolean.FALSE.toString())
+ .build(),
+ // we captured the index metadata when we took the snapshot. the index likely had the ILM execution state in the metadata.
+ // if we were to restore the lifecycle.name setting, the restored index would be captured by the ILM runner and,
+ // depending on what ILM execution state was captured at snapshot time, make it's way forward from _that_ step forward in
+ // the ILM policy.
+ // we'll re-set this setting on the restored index at a later step once we restored a deterministic execution state
+ new String[]{LifecycleSettings.LIFECYCLE_NAME},
+ // we'll not wait for the snapshot to complete in this step as the async steps are executed from threads that shouldn't
+ // perform expensive operations (ie. clusterStateProcessed)
+ false);
+ getClient().execute(MountSearchableSnapshotAction.INSTANCE, mountSearchableSnapshotRequest,
+ ActionListener.wrap(response -> {
+ if (response.status() != RestStatus.OK && response.status() != RestStatus.ACCEPTED) {
+ logger.debug("mount snapshot response failed to complete");
+ throw new ElasticsearchException("mount snapshot response failed to complete, got response " + response.status());
+ }
+ listener.onResponse(true);
+ }, listener::onFailure));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), restoredIndexPrefix);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ MountSnapshotStep other = (MountSnapshotStep) obj;
+ return super.equals(obj) && Objects.equals(restoredIndexPrefix, other.restoredIndexPrefix);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java
new file mode 100644
index 0000000000000..5a9b315ca7866
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotAction.java
@@ -0,0 +1,137 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+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;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.xpack.core.ilm.Step.StepKey;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A {@link LifecycleAction} that will convert the index into a searchable snapshot, by taking a snapshot of the index, creating a
+ * searchable snapshot and the corresponding "searchable snapshot index", deleting the original index and swapping its aliases to the
+ * newly created searchable snapshot backed index.
+ */
+public class SearchableSnapshotAction implements LifecycleAction {
+ public static final String NAME = "searchable_snapshot";
+
+ public static final ParseField SNAPSHOT_REPOSITORY = new ParseField("snapshot_repository");
+
+ public static final String RESTORED_INDEX_PREFIX = "restored-";
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME,
+ a -> new SearchableSnapshotAction((String) a[0]));
+
+ static {
+ PARSER.declareString(ConstructingObjectParser.constructorArg(), SNAPSHOT_REPOSITORY);
+ }
+
+ public static SearchableSnapshotAction parse(XContentParser parser) {
+ return PARSER.apply(parser, null);
+ }
+
+ private final String snapshotRepository;
+
+ public SearchableSnapshotAction(String snapshotRepository) {
+ if (Strings.hasText(snapshotRepository) == false) {
+ throw new IllegalArgumentException("the snapshot repository must be specified");
+ }
+ this.snapshotRepository = snapshotRepository;
+ }
+
+ public SearchableSnapshotAction(StreamInput in) throws IOException {
+ this(in.readString());
+ }
+
+ @Override
+ public List toSteps(Client client, String phase, StepKey nextStepKey) {
+ StepKey waitForNoFollowerStepKey = new StepKey(phase, NAME, WaitForNoFollowersStep.NAME);
+ StepKey generateSnapshotNameKey = new StepKey(phase, NAME, GenerateSnapshotNameStep.NAME);
+ StepKey cleanSnapshotKey = new StepKey(phase, NAME, CleanupSnapshotStep.NAME);
+ StepKey createSnapshotKey = new StepKey(phase, NAME, CreateSnapshotStep.NAME);
+ StepKey mountSnapshotKey = new StepKey(phase, NAME, MountSnapshotStep.NAME);
+ StepKey waitForGreenRestoredIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME);
+ StepKey copyMetadataKey = new StepKey(phase, NAME, CopyExecutionStateStep.NAME);
+ StepKey copyLifecyclePolicySettingKey = new StepKey(phase, NAME, CopySettingsStep.NAME);
+ StepKey swapAliasesKey = new StepKey(phase, NAME, SwapAliasesAndDeleteSourceIndexStep.NAME);
+
+ WaitForNoFollowersStep waitForNoFollowersStep = new WaitForNoFollowersStep(waitForNoFollowerStepKey, generateSnapshotNameKey,
+ client);
+ GenerateSnapshotNameStep generateSnapshotNameStep = new GenerateSnapshotNameStep(generateSnapshotNameKey, cleanSnapshotKey,
+ snapshotRepository);
+ CleanupSnapshotStep cleanupSnapshotStep = new CleanupSnapshotStep(cleanSnapshotKey, createSnapshotKey, client);
+ AsyncActionBranchingStep createSnapshotBranchingStep = new AsyncActionBranchingStep(
+ new CreateSnapshotStep(createSnapshotKey, mountSnapshotKey, client), cleanSnapshotKey, client);
+ MountSnapshotStep mountSnapshotStep = new MountSnapshotStep(mountSnapshotKey, waitForGreenRestoredIndexKey,
+ client, RESTORED_INDEX_PREFIX);
+ WaitForIndexColorStep waitForGreenIndexHealthStep = new WaitForIndexColorStep(waitForGreenRestoredIndexKey,
+ copyMetadataKey, ClusterHealthStatus.GREEN, RESTORED_INDEX_PREFIX);
+ // a policy with only the cold phase will have a null "nextStepKey", hence the "null" nextStepKey passed in below when that's the
+ // case
+ CopyExecutionStateStep copyMetadataStep = new CopyExecutionStateStep(copyMetadataKey, copyLifecyclePolicySettingKey,
+ RESTORED_INDEX_PREFIX, nextStepKey != null ? nextStepKey.getName() : "null");
+ CopySettingsStep copySettingsStep = new CopySettingsStep(copyLifecyclePolicySettingKey, swapAliasesKey, RESTORED_INDEX_PREFIX,
+ LifecycleSettings.LIFECYCLE_NAME);
+ // sending this step to null as the restored index (which will after this step essentially be the source index) was sent to the next
+ // key after we restored the lifecycle execution state
+ SwapAliasesAndDeleteSourceIndexStep swapAliasesAndDeleteSourceIndexStep = new SwapAliasesAndDeleteSourceIndexStep(swapAliasesKey,
+ null, client, RESTORED_INDEX_PREFIX);
+
+ return Arrays.asList(waitForNoFollowersStep, generateSnapshotNameStep, cleanupSnapshotStep, createSnapshotBranchingStep,
+ mountSnapshotStep, waitForGreenIndexHealthStep, copyMetadataStep, copySettingsStep, swapAliasesAndDeleteSourceIndexStep);
+ }
+
+ @Override
+ public boolean isSafeAction() {
+ return true;
+ }
+
+ @Override
+ public String getWriteableName() {
+ return NAME;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(snapshotRepository);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ builder.field(SNAPSHOT_REPOSITORY.getPreferredName(), snapshotRepository);
+ builder.endObject();
+ return builder;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SearchableSnapshotAction that = (SearchableSnapshotAction) o;
+ return Objects.equals(snapshotRepository, that.snapshotRepository);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(snapshotRepository);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java
index e94d3c8ae824e..62a09f027c724 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java
@@ -104,7 +104,8 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey)
CheckShrinkReadyStep checkShrinkReadyStep = new CheckShrinkReadyStep(allocationRoutedKey, shrinkKey);
ShrinkStep shrink = new ShrinkStep(shrinkKey, enoughShardsKey, client, numberOfShards, SHRUNKEN_INDEX_PREFIX);
ShrunkShardsAllocatedStep allocated = new ShrunkShardsAllocatedStep(enoughShardsKey, copyMetadataKey, SHRUNKEN_INDEX_PREFIX);
- CopyExecutionStateStep copyMetadata = new CopyExecutionStateStep(copyMetadataKey, aliasKey, SHRUNKEN_INDEX_PREFIX);
+ CopyExecutionStateStep copyMetadata = new CopyExecutionStateStep(copyMetadataKey, aliasKey, SHRUNKEN_INDEX_PREFIX,
+ ShrunkenIndexCheckStep.NAME);
ShrinkSetAliasStep aliasSwapAndDelete = new ShrinkSetAliasStep(aliasKey, isShrunkIndexKey, client, SHRUNKEN_INDEX_PREFIX);
ShrunkenIndexCheckStep waitOnShrinkTakeover = new ShrunkenIndexCheckStep(isShrunkIndexKey, nextStepKey, SHRUNKEN_INDEX_PREFIX);
return Arrays.asList(conditionalSkipShrinkStep, waitForNoFollowersStep, readOnlyStep, setSingleNodeStep, checkShrinkReadyStep,
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java
index 1444cbc147d30..ba227667610ac 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStep.java
@@ -5,15 +5,14 @@
*/
package org.elasticsearch.xpack.core.ilm;
-import org.elasticsearch.action.ActionListener;
-import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.ClusterState;
-import org.elasticsearch.cluster.metadata.AliasMetadata;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import java.util.Objects;
+import static org.elasticsearch.xpack.core.ilm.SwapAliasesAndDeleteSourceIndexStep.deleteSourceIndexAndTransferAliases;
+
/**
* Following shrinking an index and deleting the original index, this step creates an alias with the same name as the original index which
* points to the new shrunken index to allow clients to continue to use the original index name without being aware that it has shrunk.
@@ -37,23 +36,7 @@ public void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState cu
String index = indexMetadata.getIndex().getName();
// get target shrink index
String targetIndexName = shrunkIndexPrefix + index;
- IndicesAliasesRequest aliasesRequest = new IndicesAliasesRequest()
- .masterNodeTimeout(getMasterTimeout(currentState))
- .addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index(index))
- .addAliasAction(IndicesAliasesRequest.AliasActions.add().index(targetIndexName).alias(index));
- // copy over other aliases from original index
- indexMetadata.getAliases().values().spliterator().forEachRemaining(aliasMetadataObjectCursor -> {
- AliasMetadata aliasMetadataToAdd = aliasMetadataObjectCursor.value;
- // inherit all alias properties except `is_write_index`
- aliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add()
- .index(targetIndexName).alias(aliasMetadataToAdd.alias())
- .indexRouting(aliasMetadataToAdd.indexRouting())
- .searchRouting(aliasMetadataToAdd.searchRouting())
- .filter(aliasMetadataToAdd.filter() == null ? null : aliasMetadataToAdd.filter().string())
- .writeIndex(null));
- });
- getClient().admin().indices().aliases(aliasesRequest, ActionListener.wrap(response ->
- listener.onResponse(true), listener::onFailure));
+ deleteSourceIndexAndTransferAliases(getClient(), indexMetadata, getMasterTimeout(currentState), targetIndexName, listener);
}
@Override
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStep.java
new file mode 100644
index 0000000000000..55a00fd2fb224
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStep.java
@@ -0,0 +1,121 @@
+/*
+ * 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.ilm;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.ClusterStateObserver;
+import org.elasticsearch.cluster.metadata.AliasMetadata;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.unit.TimeValue;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * This step swaps all the aliases from the source index to the restored index and deletes the source index. This is useful in scenarios
+ * following a restore from snapshot operation where the restored index will take the place of the source index in the ILM lifecycle.
+ */
+public class SwapAliasesAndDeleteSourceIndexStep extends AsyncActionStep {
+ public static final String NAME = "swap-aliases";
+ private static final Logger logger = LogManager.getLogger(SwapAliasesAndDeleteSourceIndexStep.class);
+
+ private final String targetIndexPrefix;
+
+ public SwapAliasesAndDeleteSourceIndexStep(StepKey key, StepKey nextStepKey, Client client, String targetIndexPrefix) {
+ super(key, nextStepKey, client);
+ this.targetIndexPrefix = targetIndexPrefix;
+ }
+
+ @Override
+ public boolean isRetryable() {
+ return true;
+ }
+
+ public String getTargetIndexPrefix() {
+ return targetIndexPrefix;
+ }
+
+ @Override
+ public void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState, ClusterStateObserver observer,
+ Listener listener) {
+ String originalIndex = indexMetadata.getIndex().getName();
+ final String targetIndexName = targetIndexPrefix + originalIndex;
+ IndexMetadata targetIndexMetadata = currentClusterState.metadata().index(targetIndexName);
+
+ if (targetIndexMetadata == null) {
+ String policyName = indexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME);
+ String errorMessage = String.format(Locale.ROOT, "target index [%s] doesn't exist. stopping execution of lifecycle [%s] for" +
+ " index [%s]", targetIndexName, policyName, originalIndex);
+ logger.debug(errorMessage);
+ listener.onFailure(new IllegalStateException(errorMessage));
+ return;
+ }
+
+ deleteSourceIndexAndTransferAliases(getClient(), indexMetadata, getMasterTimeout(currentClusterState), targetIndexName, listener);
+ }
+
+ /**
+ * Executes an {@link IndicesAliasesRequest} to copy over all the aliases from the source to the target index, and remove the source
+ * index.
+ *
+ * The is_write_index will *not* be set on the target index as this operation is currently executed on read-only indices.
+ */
+ static void deleteSourceIndexAndTransferAliases(Client client, IndexMetadata sourceIndex, TimeValue masterTimeoutValue,
+ String targetIndex, Listener listener) {
+ String sourceIndexName = sourceIndex.getIndex().getName();
+ IndicesAliasesRequest aliasesRequest = new IndicesAliasesRequest()
+ .masterNodeTimeout(masterTimeoutValue)
+ .addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index(sourceIndexName))
+ .addAliasAction(IndicesAliasesRequest.AliasActions.add().index(targetIndex).alias(sourceIndexName));
+ // copy over other aliases from source index
+ sourceIndex.getAliases().values().spliterator().forEachRemaining(aliasMetaDataObjectCursor -> {
+ AliasMetadata aliasMetaDataToAdd = aliasMetaDataObjectCursor.value;
+ // inherit all alias properties except `is_write_index`
+ aliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add()
+ .index(targetIndex).alias(aliasMetaDataToAdd.alias())
+ .indexRouting(aliasMetaDataToAdd.indexRouting())
+ .searchRouting(aliasMetaDataToAdd.searchRouting())
+ .filter(aliasMetaDataToAdd.filter() == null ? null : aliasMetaDataToAdd.filter().string())
+ .writeIndex(null));
+ });
+
+ client.admin().indices().aliases(aliasesRequest,
+ ActionListener.wrap(response -> {
+ if (response.isAcknowledged() == false) {
+ logger.warn("aliases swap from [{}] to [{}] response was not acknowledged", sourceIndexName, targetIndex);
+ }
+ listener.onResponse(true);
+ }, listener::onFailure));
+ }
+
+ @Override
+ public boolean indexSurvives() {
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), targetIndexPrefix);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ SwapAliasesAndDeleteSourceIndexStep other = (SwapAliasesAndDeleteSourceIndexStep) obj;
+ return super.equals(obj) &&
+ Objects.equals(targetIndexPrefix, other.targetIndexPrefix);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java
index b520a41e44911..5a4991796d6e8 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java
@@ -41,7 +41,7 @@ public class TimeseriesLifecycleType implements LifecycleType {
static final List ORDERED_VALID_WARM_ACTIONS = Arrays.asList(SetPriorityAction.NAME, UnfollowAction.NAME, ReadOnlyAction.NAME,
AllocateAction.NAME, ShrinkAction.NAME, ForceMergeAction.NAME);
static final List ORDERED_VALID_COLD_ACTIONS = Arrays.asList(SetPriorityAction.NAME, UnfollowAction.NAME, AllocateAction.NAME,
- FreezeAction.NAME);
+ FreezeAction.NAME, SearchableSnapshotAction.NAME);
static final List ORDERED_VALID_DELETE_ACTIONS = Arrays.asList(WaitForSnapshotAction.NAME, DeleteAction.NAME);
static final Set VALID_HOT_ACTIONS = Sets.newHashSet(ORDERED_VALID_HOT_ACTIONS);
static final Set VALID_WARM_ACTIONS = Sets.newHashSet(ORDERED_VALID_WARM_ACTIONS);
@@ -74,8 +74,9 @@ public List getOrderedPhases(Map phases) {
Phase phase = phases.get(phaseName);
if (phase != null) {
Map actions = phase.getActions();
- if (actions.containsKey(UnfollowAction.NAME) == false
- && (actions.containsKey(RolloverAction.NAME) || actions.containsKey(ShrinkAction.NAME))) {
+ if (actions.containsKey(UnfollowAction.NAME) == false &&
+ (actions.containsKey(RolloverAction.NAME) || actions.containsKey(ShrinkAction.NAME) ||
+ actions.containsKey(SearchableSnapshotAction.NAME))) {
Map actionMap = new HashMap<>(phase.getActions());
actionMap.put(UnfollowAction.NAME, new UnfollowAction());
phase = new Phase(phase.getName(), phase.getMinimumAge(), actionMap);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java
index fd02a69999c73..2984581cfe9b4 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStep.java
@@ -7,42 +7,58 @@
package org.elasticsearch.xpack.core.ilm;
import com.carrotsearch.hppc.cursors.ObjectCursor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.routing.IndexRoutingTable;
import org.elasticsearch.cluster.routing.IndexShardRoutingTable;
-import org.elasticsearch.cluster.routing.RoutingTable;
import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.Index;
import java.io.IOException;
+import java.util.Locale;
import java.util.Objects;
/**
- * Wait Step for index based on color
+ * Wait Step for index based on color. Optionally derives the index name using the provided prefix (if any).
*/
-
class WaitForIndexColorStep extends ClusterStateWaitStep {
static final String NAME = "wait-for-index-color";
+ private static final Logger logger = LogManager.getLogger(WaitForIndexColorStep.class);
+
private final ClusterHealthStatus color;
+ @Nullable
+ private final String indexNamePrefix;
WaitForIndexColorStep(StepKey key, StepKey nextStepKey, ClusterHealthStatus color) {
+ this(key, nextStepKey, color, null);
+ }
+
+ WaitForIndexColorStep(StepKey key, StepKey nextStepKey, ClusterHealthStatus color, @Nullable String indexNamePrefix) {
super(key, nextStepKey);
this.color = color;
+ this.indexNamePrefix = indexNamePrefix;
}
public ClusterHealthStatus getColor() {
return this.color;
}
+ public String getIndexNamePrefix() {
+ return indexNamePrefix;
+ }
+
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), this.color);
+ return Objects.hash(super.hashCode(), this.color, this.indexNamePrefix);
}
@Override
@@ -54,13 +70,23 @@ public boolean equals(Object obj) {
return false;
}
WaitForIndexColorStep other = (WaitForIndexColorStep) obj;
- return super.equals(obj) && Objects.equals(this.color, other.color);
+ return super.equals(obj) && Objects.equals(this.color, other.color) && Objects.equals(this.indexNamePrefix, other.indexNamePrefix);
}
@Override
public Result isConditionMet(Index index, ClusterState clusterState) {
- RoutingTable routingTable = clusterState.routingTable();
- IndexRoutingTable indexRoutingTable = routingTable.index(index);
+ String indexName = indexNamePrefix != null ? indexNamePrefix + index.getName() : index.getName();
+ IndexMetadata indexMetadata = clusterState.metadata().index(index);
+
+ if (indexMetadata == null) {
+ String errorMessage = String.format(Locale.ROOT, "[%s] lifecycle action for index [%s] executed but index no longer exists",
+ getKey().getAction(), indexName);
+ // Index must have been since deleted
+ logger.debug(errorMessage);
+ return new Result(false, new Info(errorMessage));
+ }
+
+ IndexRoutingTable indexRoutingTable = clusterState.routingTable().index(indexMetadata.getIndex());
Result result;
switch (this.color) {
case GREEN:
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/MountSearchableSnapshotAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/MountSearchableSnapshotAction.java
new file mode 100644
index 0000000000000..fe31ff5d69d0f
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/MountSearchableSnapshotAction.java
@@ -0,0 +1,20 @@
+/*
+ * 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.searchablesnapshots;
+
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
+
+public class MountSearchableSnapshotAction extends ActionType {
+
+ public static final MountSearchableSnapshotAction INSTANCE = new MountSearchableSnapshotAction();
+ public static final String NAME = "cluster:admin/snapshot/mount";
+
+ private MountSearchableSnapshotAction() {
+ super(NAME, RestoreSnapshotResponse::new);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/MountSearchableSnapshotRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/MountSearchableSnapshotRequest.java
new file mode 100644
index 0000000000000..7ec7d53049f32
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/MountSearchableSnapshotRequest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.searchablesnapshots;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.master.MasterNodeRequest;
+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.settings.Settings;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.rest.RestRequest;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.elasticsearch.common.settings.Settings.readSettingsFromStream;
+import static org.elasticsearch.common.settings.Settings.writeSettingsToStream;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public class MountSearchableSnapshotRequest extends MasterNodeRequest {
+
+ public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "mount_searchable_snapshot", true,
+ (a, request) -> new MountSearchableSnapshotRequest(
+ Objects.requireNonNullElse((String)a[1], (String)a[0]),
+ request.param("repository"),
+ request.param("snapshot"),
+ (String)a[0],
+ Objects.requireNonNullElse((Settings)a[2], Settings.EMPTY),
+ Objects.requireNonNullElse((String[])a[3], Strings.EMPTY_ARRAY),
+ request.paramAsBoolean("wait_for_completion", false)));
+
+ private static final ParseField INDEX_FIELD = new ParseField("index");
+ private static final ParseField RENAMED_INDEX_FIELD = new ParseField("renamed_index");
+ private static final ParseField INDEX_SETTINGS_FIELD = new ParseField("index_settings");
+ private static final ParseField IGNORE_INDEX_SETTINGS_FIELD = new ParseField("ignore_index_settings");
+
+ static {
+ PARSER.declareField(constructorArg(), XContentParser::text, INDEX_FIELD, ObjectParser.ValueType.STRING);
+ PARSER.declareField(optionalConstructorArg(), XContentParser::text, RENAMED_INDEX_FIELD, ObjectParser.ValueType.STRING);
+ PARSER.declareField(optionalConstructorArg(), Settings::fromXContent, INDEX_SETTINGS_FIELD, ObjectParser.ValueType.OBJECT);
+ PARSER.declareField(optionalConstructorArg(),
+ p -> p.list().stream().map(s -> (String) s).collect(Collectors.toList()).toArray(Strings.EMPTY_ARRAY),
+ IGNORE_INDEX_SETTINGS_FIELD, ObjectParser.ValueType.STRING_ARRAY);
+ }
+
+ private final String mountedIndexName;
+ private final String repositoryName;
+ private final String snapshotName;
+ private final String snapshotIndexName;
+ private final Settings indexSettings;
+ private final String[] ignoredIndexSettings;
+ private final boolean waitForCompletion;
+
+ /**
+ * Constructs a new mount searchable snapshot request, restoring an index with the settings needed to make it a searchable snapshot.
+ */
+ public MountSearchableSnapshotRequest(String mountedIndexName, String repositoryName, String snapshotName, String snapshotIndexName,
+ Settings indexSettings, String[] ignoredIndexSettings, boolean waitForCompletion) {
+ this.mountedIndexName = Objects.requireNonNull(mountedIndexName);
+ this.repositoryName = Objects.requireNonNull(repositoryName);
+ this.snapshotName = Objects.requireNonNull(snapshotName);
+ this.snapshotIndexName = Objects.requireNonNull(snapshotIndexName);
+ this.indexSettings = Objects.requireNonNull(indexSettings);
+ this.ignoredIndexSettings = Objects.requireNonNull(ignoredIndexSettings);
+ this.waitForCompletion = waitForCompletion;
+ }
+
+ public MountSearchableSnapshotRequest(StreamInput in) throws IOException {
+ super(in);
+ this.mountedIndexName = in.readString();
+ this.repositoryName = in.readString();
+ this.snapshotName = in.readString();
+ this.snapshotIndexName = in.readString();
+ this.indexSettings = readSettingsFromStream(in);
+ this.ignoredIndexSettings = in.readStringArray();
+ this.waitForCompletion = in.readBoolean();
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeString(mountedIndexName);
+ out.writeString(repositoryName);
+ out.writeString(snapshotName);
+ out.writeString(snapshotIndexName);
+ writeSettingsToStream(indexSettings, out);
+ out.writeStringArray(ignoredIndexSettings);
+ out.writeBoolean(waitForCompletion);
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ return null;
+ }
+
+ /**
+ * @return the name of the index that will be created
+ */
+ public String mountedIndexName() {
+ return mountedIndexName;
+ }
+
+ /**
+ * @return the name of the repository
+ */
+ public String repositoryName() {
+ return this.repositoryName;
+ }
+
+ /**
+ * @return the name of the snapshot.
+ */
+ public String snapshotName() {
+ return this.snapshotName;
+ }
+
+ /**
+ * @return the name of the index contained in the snapshot
+ */
+ public String snapshotIndexName() {
+ return snapshotIndexName;
+ }
+
+ /**
+ * @return true if the operation will wait for completion
+ */
+ public boolean waitForCompletion() {
+ return waitForCompletion;
+ }
+
+ /**
+ * @return settings that should be added to the index when it is mounted
+ */
+ public Settings indexSettings() {
+ return this.indexSettings;
+ }
+
+ /**
+ * @return the names of settings that should be removed from the index when it is mounted
+ */
+ public String[] ignoreIndexSettings() {
+ return ignoredIndexSettings;
+ }
+
+ @Override
+ public String getDescription() {
+ return "mount snapshot [" + repositoryName + ":" + snapshotName + ":" + snapshotIndexName + "] as [" + mountedIndexName + "]";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ MountSearchableSnapshotRequest that = (MountSearchableSnapshotRequest) o;
+ return waitForCompletion == that.waitForCompletion &&
+ Objects.equals(mountedIndexName, that.mountedIndexName) &&
+ Objects.equals(repositoryName, that.repositoryName) &&
+ Objects.equals(snapshotName, that.snapshotName) &&
+ Objects.equals(snapshotIndexName, that.snapshotIndexName) &&
+ Objects.equals(indexSettings, that.indexSettings) &&
+ Arrays.equals(ignoredIndexSettings, that.ignoredIndexSettings) &&
+ Objects.equals(masterNodeTimeout, that.masterNodeTimeout);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(mountedIndexName, repositoryName, snapshotName, snapshotIndexName, indexSettings, waitForCompletion,
+ masterNodeTimeout);
+ result = 31 * result + Arrays.hashCode(ignoredIndexSettings);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return getDescription();
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java
new file mode 100644
index 0000000000000..36ea9ba4c7303
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java
@@ -0,0 +1,452 @@
+/*
+ * 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.searchablesnapshots;
+
+import org.elasticsearch.cluster.routing.ShardRouting;
+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.unit.TimeValue;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.repositories.IndexId;
+import org.elasticsearch.snapshots.SnapshotId;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+import static java.util.Collections.unmodifiableList;
+import static java.util.stream.Collectors.toList;
+
+public class SearchableSnapshotShardStats implements Writeable, ToXContentObject {
+
+ private final List inputStats;
+ private final ShardRouting shardRouting;
+ private final SnapshotId snapshotId;
+ private final IndexId indexId;
+
+ public SearchableSnapshotShardStats(ShardRouting shardRouting, SnapshotId snapshotId, IndexId indexId,
+ List stats) {
+ this.shardRouting = Objects.requireNonNull(shardRouting);
+ this.snapshotId = Objects.requireNonNull(snapshotId);
+ this.indexId = Objects.requireNonNull(indexId);
+ this.inputStats = unmodifiableList(Objects.requireNonNull(stats));
+ }
+
+ public SearchableSnapshotShardStats(StreamInput in) throws IOException {
+ this.shardRouting = new ShardRouting(in);
+ this.snapshotId = new SnapshotId(in);
+ this.indexId = new IndexId(in);
+ this.inputStats = in.readList(CacheIndexInputStats::new);
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ shardRouting.writeTo(out);
+ snapshotId.writeTo(out);
+ indexId.writeTo(out);
+ out.writeList(inputStats);
+ }
+
+ public ShardRouting getShardRouting() {
+ return shardRouting;
+ }
+
+ public SnapshotId getSnapshotId() {
+ return snapshotId;
+ }
+
+ public IndexId getIndexId() {
+ return indexId;
+ }
+
+ public List getStats() {
+ return inputStats;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ {
+ builder.field("snapshot_uuid", getSnapshotId().getUUID());
+ builder.field("index_uuid", getIndexId().getId());
+ builder.startObject("shard");
+ {
+ builder.field("state", shardRouting.state());
+ builder.field("primary", shardRouting.primary());
+ builder.field("node", shardRouting.currentNodeId());
+ if (shardRouting.relocatingNodeId() != null) {
+ builder.field("relocating_node", shardRouting.relocatingNodeId());
+ }
+ }
+ builder.endObject();
+ builder.startArray("files");
+ {
+ List stats = inputStats.stream()
+ .sorted(Comparator.comparing(CacheIndexInputStats::getFileName)).collect(toList());
+ for (CacheIndexInputStats stat : stats) {
+ stat.toXContent(builder, params);
+ }
+ }
+ builder.endArray();
+ }
+ return builder.endObject();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ SearchableSnapshotShardStats that = (SearchableSnapshotShardStats) other;
+ return Objects.equals(shardRouting, that.shardRouting)
+ && Objects.equals(snapshotId, that.snapshotId)
+ && Objects.equals(indexId, that.indexId)
+ && Objects.equals(inputStats, that.inputStats);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(shardRouting, snapshotId, indexId, inputStats);
+ }
+
+ public static class CacheIndexInputStats implements Writeable, ToXContentObject {
+
+ private final String fileName;
+ private final long fileLength;
+
+ private final long openCount;
+ private final long closeCount;
+
+ private final Counter forwardSmallSeeks;
+ private final Counter backwardSmallSeeks;
+ private final Counter forwardLargeSeeks;
+ private final Counter backwardLargeSeeks;
+ private final Counter contiguousReads;
+ private final Counter nonContiguousReads;
+ private final Counter cachedBytesRead;
+ private final TimedCounter cachedBytesWritten;
+ private final TimedCounter directBytesRead;
+ private final TimedCounter optimizedBytesRead;
+
+ public CacheIndexInputStats(String fileName, long fileLength, long openCount, long closeCount,
+ Counter forwardSmallSeeks, Counter backwardSmallSeeks,
+ Counter forwardLargeSeeks, Counter backwardLargeSeeks,
+ Counter contiguousReads, Counter nonContiguousReads,
+ Counter cachedBytesRead, TimedCounter cachedBytesWritten,
+ TimedCounter directBytesRead, TimedCounter optimizedBytesRead) {
+ this.fileName = fileName;
+ this.fileLength = fileLength;
+ this.openCount = openCount;
+ this.closeCount = closeCount;
+ this.forwardSmallSeeks = forwardSmallSeeks;
+ this.backwardSmallSeeks = backwardSmallSeeks;
+ this.forwardLargeSeeks = forwardLargeSeeks;
+ this.backwardLargeSeeks = backwardLargeSeeks;
+ this.contiguousReads = contiguousReads;
+ this.nonContiguousReads = nonContiguousReads;
+ this.cachedBytesRead = cachedBytesRead;
+ this.cachedBytesWritten = cachedBytesWritten;
+ this.directBytesRead = directBytesRead;
+ this.optimizedBytesRead = optimizedBytesRead;
+ }
+
+ CacheIndexInputStats(final StreamInput in) throws IOException {
+ this.fileName = in.readString();
+ this.fileLength = in.readVLong();
+ this.openCount = in.readVLong();
+ this.closeCount = in.readVLong();
+ this.forwardSmallSeeks = new Counter(in);
+ this.backwardSmallSeeks = new Counter(in);
+ this.forwardLargeSeeks = new Counter(in);
+ this.backwardLargeSeeks = new Counter(in);
+ this.contiguousReads = new Counter(in);
+ this.nonContiguousReads = new Counter(in);
+ this.cachedBytesRead = new Counter(in);
+ this.cachedBytesWritten = new TimedCounter(in);
+ this.directBytesRead = new TimedCounter(in);
+ this.optimizedBytesRead = new TimedCounter(in);
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(fileName);
+ out.writeVLong(fileLength);
+ out.writeVLong(openCount);
+ out.writeVLong(closeCount);
+
+ forwardSmallSeeks.writeTo(out);
+ backwardSmallSeeks.writeTo(out);
+ forwardLargeSeeks.writeTo(out);
+ backwardLargeSeeks.writeTo(out);
+ contiguousReads.writeTo(out);
+ nonContiguousReads.writeTo(out);
+ cachedBytesRead.writeTo(out);
+ cachedBytesWritten.writeTo(out);
+ directBytesRead.writeTo(out);
+ optimizedBytesRead.writeTo(out);
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public long getFileLength() {
+ return fileLength;
+ }
+
+ public long getOpenCount() {
+ return openCount;
+ }
+
+ public long getCloseCount() {
+ return closeCount;
+ }
+
+ public Counter getForwardSmallSeeks() {
+ return forwardSmallSeeks;
+ }
+
+ public Counter getBackwardSmallSeeks() {
+ return backwardSmallSeeks;
+ }
+
+ public Counter getForwardLargeSeeks() {
+ return forwardLargeSeeks;
+ }
+
+ public Counter getBackwardLargeSeeks() {
+ return backwardLargeSeeks;
+ }
+
+ public Counter getContiguousReads() {
+ return contiguousReads;
+ }
+
+ public Counter getNonContiguousReads() {
+ return nonContiguousReads;
+ }
+
+ public Counter getCachedBytesRead() {
+ return cachedBytesRead;
+ }
+
+ public TimedCounter getCachedBytesWritten() {
+ return cachedBytesWritten;
+ }
+
+ public TimedCounter getDirectBytesRead() {
+ return directBytesRead;
+ }
+
+ public TimedCounter getOptimizedBytesRead() {
+ return optimizedBytesRead;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ {
+ builder.field("name", getFileName());
+ builder.field("length", getFileLength());
+ builder.field("open_count", getOpenCount());
+ builder.field("close_count", getCloseCount());
+ builder.field("contiguous_bytes_read", getContiguousReads());
+ builder.field("non_contiguous_bytes_read", getNonContiguousReads());
+ builder.field("cached_bytes_read", getCachedBytesRead());
+ builder.field("cached_bytes_written", getCachedBytesWritten());
+ builder.field("direct_bytes_read", getDirectBytesRead());
+ builder.field("optimized_bytes_read", getOptimizedBytesRead());
+ {
+ builder.startObject("forward_seeks");
+ builder.field("small", getForwardSmallSeeks());
+ builder.field("large", getForwardLargeSeeks());
+ builder.endObject();
+ }
+ {
+ builder.startObject("backward_seeks");
+ builder.field("small", getBackwardSmallSeeks());
+ builder.field("large", getBackwardLargeSeeks());
+ builder.endObject();
+ }
+ }
+ return builder.endObject();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ CacheIndexInputStats stats = (CacheIndexInputStats) other;
+ return fileLength == stats.fileLength
+ && openCount == stats.openCount
+ && closeCount == stats.closeCount
+ && Objects.equals(fileName, stats.fileName)
+ && Objects.equals(forwardSmallSeeks, stats.forwardSmallSeeks)
+ && Objects.equals(backwardSmallSeeks, stats.backwardSmallSeeks)
+ && Objects.equals(forwardLargeSeeks, stats.forwardLargeSeeks)
+ && Objects.equals(backwardLargeSeeks, stats.backwardLargeSeeks)
+ && Objects.equals(contiguousReads, stats.contiguousReads)
+ && Objects.equals(nonContiguousReads, stats.nonContiguousReads)
+ && Objects.equals(cachedBytesRead, stats.cachedBytesRead)
+ && Objects.equals(cachedBytesWritten, stats.cachedBytesWritten)
+ && Objects.equals(directBytesRead, stats.directBytesRead)
+ && Objects.equals(optimizedBytesRead, stats.optimizedBytesRead);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fileName, fileLength, openCount, closeCount,
+ forwardSmallSeeks, backwardSmallSeeks,
+ forwardLargeSeeks, backwardLargeSeeks,
+ contiguousReads, nonContiguousReads,
+ cachedBytesRead, cachedBytesWritten,
+ directBytesRead, optimizedBytesRead);
+ }
+ }
+
+ public static class Counter implements Writeable, ToXContentObject {
+
+ private final long count;
+ private final long total;
+ private final long min;
+ private final long max;
+
+ public Counter(final long count, final long total, final long min, final long max) {
+ this.count = count;
+ this.total = total;
+ this.min = min;
+ this.max = max;
+ }
+
+ Counter(final StreamInput in) throws IOException {
+ this.count = in.readZLong();
+ this.total = in.readZLong();
+ this.min = in.readZLong();
+ this.max = in.readZLong();
+ }
+
+ @Override
+ public void writeTo(final StreamOutput out) throws IOException {
+ out.writeZLong(count);
+ out.writeZLong(total);
+ out.writeZLong(min);
+ out.writeZLong(max);
+ }
+
+ @Override
+ public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject();
+ {
+ builder.field("count", count);
+ builder.field("sum", total);
+ builder.field("min", min);
+ builder.field("max", max);
+ innerToXContent(builder, params);
+ }
+ builder.endObject();
+ return builder;
+ }
+
+ void innerToXContent(XContentBuilder builder, Params params) throws IOException {
+ }
+
+ public long getCount() {
+ return count;
+ }
+
+ public long getTotal() {
+ return total;
+ }
+
+ public long getMin() {
+ return min;
+ }
+
+ public long getMax() {
+ return max;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ Counter that = (Counter) other;
+ return count == that.count
+ && total == that.total
+ && min == that.min
+ && max == that.max;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(count, total, min, max);
+ }
+ }
+
+ public static class TimedCounter extends Counter {
+
+ private final long totalNanoseconds;
+
+ public TimedCounter(long count, long total, long min, long max, long totalNanoseconds) {
+ super(count, total, min, max);
+ this.totalNanoseconds = totalNanoseconds;
+ }
+
+ TimedCounter(StreamInput in) throws IOException {
+ super(in);
+ totalNanoseconds = in.readZLong();
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeZLong(totalNanoseconds);
+ }
+
+ @Override
+ void innerToXContent(XContentBuilder builder, Params params) throws IOException {
+ if (builder.humanReadable()) {
+ builder.field("time", TimeValue.timeValueNanos(totalNanoseconds).toString());
+ }
+ builder.field("time_in_nanos", totalNanoseconds);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ if (super.equals(other) == false) {
+ return false;
+ }
+ TimedCounter that = (TimedCounter) other;
+ return totalNanoseconds == that.totalNanoseconds;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), totalNanoseconds);
+ }
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java
index 72c74de5ceee5..bc32be04234d7 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicy.java
@@ -10,16 +10,11 @@
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
-import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.cluster.AbstractDiffable;
-import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.Diffable;
-import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
-import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.Context;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
@@ -31,14 +26,13 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import static org.elasticsearch.cluster.metadata.MetadataCreateIndexService.MAX_INDEX_NAME_BYTES;
+import static org.elasticsearch.xpack.core.ilm.GenerateSnapshotNameStep.generateSnapshotName;
+import static org.elasticsearch.xpack.core.ilm.GenerateSnapshotNameStep.validateGeneratedSnapshotName;
/**
* A {@code SnapshotLifecyclePolicy} is a policy for the cluster including a schedule of when a
@@ -62,8 +56,6 @@ public class SnapshotLifecyclePolicy extends AbstractDiffable addPolicyNameToMetadata(final Map me
return newMetadata;
}
- /**
- * Since snapshots need to be uniquely named, this method will resolve any date math used in
- * the provided name, as well as appending a unique identifier so expressions that may overlap
- * still result in unique snapshot names.
- */
- public String generateSnapshotName(Context context) {
- List candidates = DATE_MATH_RESOLVER.resolve(context, Collections.singletonList(this.name));
- if (candidates.size() != 1) {
- throw new IllegalStateException("resolving snapshot name " + this.name + " generated more than one candidate: " + candidates);
- }
- // TODO: we are breaking the rules of UUIDs by lowercasing this here, find an alternative (snapshot names must be lowercase)
- return candidates.get(0) + "-" + UUIDs.randomBase64UUID().toLowerCase(Locale.ROOT);
- }
-
/**
* Generate a new create snapshot request from this policy. The name of the snapshot is
* generated at this time based on any date math expressions in the "name" field.
*/
public CreateSnapshotRequest toRequest() {
- CreateSnapshotRequest req = new CreateSnapshotRequest(repository, generateSnapshotName(new ResolverContext()));
+ CreateSnapshotRequest req = new CreateSnapshotRequest(repository, generateSnapshotName(this.name));
Map mergedConfiguration = configuration == null ? new HashMap<>() : new HashMap<>(configuration);
@SuppressWarnings("unchecked")
Map metadata = (Map) mergedConfiguration.get("metadata");
@@ -324,28 +290,4 @@ public String toString() {
return Strings.toString(this);
}
- /**
- * This is a context for the DateMathExpressionResolver, which does not require
- * {@code IndicesOptions} or {@code ClusterState} since it only uses the start
- * time to resolve expressions
- */
- public static final class ResolverContext extends Context {
- public ResolverContext() {
- this(System.currentTimeMillis());
- }
-
- public ResolverContext(long startTime) {
- super(null, null, startTime, false, false);
- }
-
- @Override
- public ClusterState getState() {
- throw new UnsupportedOperationException("should never be called");
- }
-
- @Override
- public IndicesOptions getOptions() {
- throw new UnsupportedOperationException("should never be called");
- }
- }
}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AsyncActionBranchingStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AsyncActionBranchingStepTests.java
new file mode 100644
index 0000000000000..4efac73fc6c25
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AsyncActionBranchingStepTests.java
@@ -0,0 +1,145 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.ClusterStateObserver;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.common.settings.Settings;
+
+import static org.hamcrest.Matchers.is;
+
+public class AsyncActionBranchingStepTests extends AbstractStepMasterTimeoutTestCase {
+
+ @Override
+ protected AsyncActionBranchingStep createRandomInstance() {
+ return new AsyncActionBranchingStep(new UpdateSettingsStep(randomStepKey(), randomStepKey(), client, Settings.EMPTY),
+ randomStepKey(), client);
+ }
+
+ @Override
+ protected AsyncActionBranchingStep mutateInstance(AsyncActionBranchingStep instance) {
+ AsyncActionStep wrappedStep = instance.getStepToExecute();
+ Step.StepKey nextKeyOnIncompleteResponse = instance.getNextKeyOnIncompleteResponse();
+
+ switch (between(0, 1)) {
+ case 0:
+ wrappedStep = new UpdateSettingsStep(randomStepKey(), randomStepKey(), client, Settings.EMPTY);
+ break;
+ case 1:
+ nextKeyOnIncompleteResponse = randomStepKey();
+ break;
+ default:
+ throw new AssertionError("Illegal randomisation branch");
+ }
+ return new AsyncActionBranchingStep(wrappedStep, nextKeyOnIncompleteResponse, client);
+ }
+
+ @Override
+ protected AsyncActionBranchingStep copyInstance(AsyncActionBranchingStep instance) {
+ return new AsyncActionBranchingStep(instance.getStepToExecute(), instance.getNextKeyOnIncompleteResponse(), instance.getClient());
+ }
+
+ protected IndexMetadata getIndexMetadata() {
+ return IndexMetadata.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build();
+ }
+
+ public void testBranchStepKeyIsTheWrappedStepKey() {
+ AsyncActionStep stepToExecute = new AsyncActionStep(randomStepKey(), randomStepKey(), client) {
+ @Override
+ public void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState, ClusterStateObserver observer,
+ Listener listener) {
+ }
+ };
+
+ AsyncActionBranchingStep asyncActionBranchingStep = new AsyncActionBranchingStep(stepToExecute, randomStepKey(), client);
+ assertThat(asyncActionBranchingStep.getKey(), is(stepToExecute.getKey()));
+ }
+
+ public void testBranchStepNextKeyOnCompleteResponse() {
+ AsyncActionStep stepToExecute = new AsyncActionStep(randomStepKey(), randomStepKey(), client) {
+ @Override
+ public void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState, ClusterStateObserver observer,
+ Listener listener) {
+ listener.onResponse(true);
+ }
+ };
+
+ AsyncActionBranchingStep asyncActionBranchingStep = new AsyncActionBranchingStep(stepToExecute, randomStepKey(), client);
+
+ asyncActionBranchingStep.performAction(getIndexMetadata(), emptyClusterState(), null, new AsyncActionStep.Listener() {
+
+ @Override
+ public void onResponse(boolean complete) {
+ assertThat(complete, is(true));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ fail("not expecting a failure as the wrapped step was successful");
+ }
+ });
+ assertThat(asyncActionBranchingStep.getNextStepKey(), is(stepToExecute.getNextStepKey()));
+ }
+
+ public void testBranchStepNextKeyOnInCompleteResponse() {
+ AsyncActionStep stepToExecute = new AsyncActionStep(randomStepKey(), randomStepKey(), client) {
+ @Override
+ public void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState, ClusterStateObserver observer,
+ Listener listener) {
+ listener.onResponse(false);
+ }
+ };
+
+ Step.StepKey nextKeyOnIncompleteResponse = randomStepKey();
+ AsyncActionBranchingStep asyncActionBranchingStep = new AsyncActionBranchingStep(stepToExecute, nextKeyOnIncompleteResponse,
+ client);
+
+ asyncActionBranchingStep.performAction(getIndexMetadata(), emptyClusterState(), null, new AsyncActionStep.Listener() {
+
+ @Override
+ public void onResponse(boolean complete) {
+ assertThat(complete, is(false));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ fail("not expecting a failure as the wrapped step was successful");
+ }
+ });
+ assertThat(asyncActionBranchingStep.getNextStepKey(), is(nextKeyOnIncompleteResponse));
+ }
+
+ public void testBranchStepPropagatesFailure() {
+ NullPointerException failException = new NullPointerException("fail");
+ AsyncActionStep stepToExecute = new AsyncActionStep(randomStepKey(), randomStepKey(), client) {
+ @Override
+ public void performAction(IndexMetadata indexMetadata, ClusterState currentClusterState, ClusterStateObserver observer,
+ Listener listener) {
+ listener.onFailure(failException);
+ }
+ };
+
+ AsyncActionBranchingStep asyncActionBranchingStep = new AsyncActionBranchingStep(stepToExecute, randomStepKey(), client);
+
+ asyncActionBranchingStep.performAction(getIndexMetadata(), emptyClusterState(), null, new AsyncActionStep.Listener() {
+
+ @Override
+ public void onResponse(boolean complete) {
+ fail("expecting a failure as the wrapped step failed");
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ assertThat(e, is(failException));
+ }
+ });
+ expectThrows(IllegalStateException.class, () -> asyncActionBranchingStep.getNextStepKey());
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CleanupSnapshotStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CleanupSnapshotStepTests.java
new file mode 100644
index 0000000000000..756d3cd78caca
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CleanupSnapshotStepTests.java
@@ -0,0 +1,156 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotAction;
+import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.test.client.NoOpClient;
+import org.elasticsearch.xpack.core.ilm.Step.StepKey;
+
+import java.util.Map;
+
+import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+public class CleanupSnapshotStepTests extends AbstractStepTestCase {
+
+ @Override
+ public CleanupSnapshotStep createRandomInstance() {
+ StepKey stepKey = randomStepKey();
+ StepKey nextStepKey = randomStepKey();
+ return new CleanupSnapshotStep(stepKey, nextStepKey, client);
+ }
+
+ @Override
+ protected CleanupSnapshotStep copyInstance(CleanupSnapshotStep instance) {
+ return new CleanupSnapshotStep(instance.getKey(), instance.getNextStepKey(), instance.getClient());
+ }
+
+ @Override
+ public CleanupSnapshotStep mutateInstance(CleanupSnapshotStep instance) {
+ StepKey key = instance.getKey();
+ StepKey nextKey = instance.getNextStepKey();
+ switch (between(0, 1)) {
+ case 0:
+ key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 1:
+ nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ default:
+ throw new AssertionError("Illegal randomisation branch");
+ }
+ return new CleanupSnapshotStep(key, nextKey, instance.getClient());
+ }
+
+ public void testPerformActionDoesntFailIfSnapshotInfoIsMissing() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+
+ {
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ CleanupSnapshotStep cleanupSnapshotStep = createRandomInstance();
+ cleanupSnapshotStep.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ assertThat(complete, is(true));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ fail("expecting the step to report success if repository information is missing from the ILM execution state as there" +
+ " is no snapshot to delete");
+ }
+ });
+ }
+
+ {
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ Map ilmCustom = Map.of("snapshot_repository", "repository_name");
+ indexMetadataBuilder.putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom);
+
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ CleanupSnapshotStep cleanupSnapshotStep = createRandomInstance();
+ cleanupSnapshotStep.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ assertThat(complete, is(true));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ fail("expecting the step to report success if the snapshot name is missing from the ILM execution state as there is " +
+ "no snapshot to delete");
+ }
+ });
+ }
+ }
+
+ public void testPerformAction() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+ String snapshotName = indexName + "-" + policyName;
+ Map ilmCustom = Map.of("snapshot_name", snapshotName);
+
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom)
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ try (NoOpClient client = getDeleteSnapshotRequestAssertingClient(snapshotName)) {
+ CleanupSnapshotStep step = new CleanupSnapshotStep(randomStepKey(), randomStepKey(), client);
+ step.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ }
+ });
+ }
+ }
+
+ private NoOpClient getDeleteSnapshotRequestAssertingClient(String expectedSnapshotName) {
+ return new NoOpClient(getTestName()) {
+ @Override
+ protected void doExecute(ActionType action,
+ Request request,
+ ActionListener listener) {
+ assertThat(action.name(), is(DeleteSnapshotAction.NAME));
+ assertTrue(request instanceof DeleteSnapshotRequest);
+ assertThat(((DeleteSnapshotRequest) request).snapshot(), equalTo(expectedSnapshotName));
+ }
+ };
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java
index 615ad0156b62d..4d97d02e9e955 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopyExecutionStateStepTests.java
@@ -25,14 +25,16 @@ protected CopyExecutionStateStep createRandomInstance() {
StepKey stepKey = randomStepKey();
StepKey nextStepKey = randomStepKey();
String shrunkIndexPrefix = randomAlphaOfLength(10);
- return new CopyExecutionStateStep(stepKey, nextStepKey, shrunkIndexPrefix);
+ String nextStepName = randomStepKey().getName();
+ return new CopyExecutionStateStep(stepKey, nextStepKey, shrunkIndexPrefix, nextStepName);
}
@Override
protected CopyExecutionStateStep mutateInstance(CopyExecutionStateStep instance) {
StepKey key = instance.getKey();
StepKey nextKey = instance.getNextStepKey();
- String shrunkIndexPrefix = instance.getShrunkIndexPrefix();
+ String shrunkIndexPrefix = instance.getTargetIndexPrefix();
+ String nextStepName = instance.getTargetNextStepName();
switch (between(0, 2)) {
case 0:
@@ -44,16 +46,20 @@ protected CopyExecutionStateStep mutateInstance(CopyExecutionStateStep instance)
case 2:
shrunkIndexPrefix += randomAlphaOfLength(5);
break;
+ case 3:
+ nextStepName = randomAlphaOfLengthBetween(1, 10);
+ break;
default:
throw new AssertionError("Illegal randomisation branch");
}
- return new CopyExecutionStateStep(key, nextKey, shrunkIndexPrefix);
+ return new CopyExecutionStateStep(key, nextKey, shrunkIndexPrefix, nextStepName);
}
@Override
protected CopyExecutionStateStep copyInstance(CopyExecutionStateStep instance) {
- return new CopyExecutionStateStep(instance.getKey(), instance.getNextStepKey(), instance.getShrunkIndexPrefix());
+ return new CopyExecutionStateStep(instance.getKey(), instance.getNextStepKey(), instance.getTargetIndexPrefix(),
+ instance.getTargetNextStepName());
}
public void testPerformAction() {
@@ -66,7 +72,7 @@ public void testPerformAction() {
.numberOfReplicas(randomIntBetween(1,5))
.putCustom(ILM_CUSTOM_METADATA_KEY, customMetadata)
.build();
- IndexMetadata shrunkIndexMetadata = IndexMetadata.builder(step.getShrunkIndexPrefix() + indexName)
+ IndexMetadata shrunkIndexMetadata = IndexMetadata.builder(step.getTargetIndexPrefix() + indexName)
.settings(settings(Version.CURRENT)).numberOfShards(randomIntBetween(1,5))
.numberOfReplicas(randomIntBetween(1,5))
.build();
@@ -80,12 +86,14 @@ public void testPerformAction() {
LifecycleExecutionState oldIndexData = LifecycleExecutionState.fromIndexMetadata(originalIndexMetadata);
LifecycleExecutionState newIndexData = LifecycleExecutionState
- .fromIndexMetadata(newClusterState.metadata().index(step.getShrunkIndexPrefix() + indexName));
-
- assertEquals(oldIndexData.getLifecycleDate(), newIndexData.getLifecycleDate());
- assertEquals(oldIndexData.getPhase(), newIndexData.getPhase());
- assertEquals(oldIndexData.getAction(), newIndexData.getAction());
- assertEquals(ShrunkenIndexCheckStep.NAME, newIndexData.getStep());
+ .fromIndexMetadata(newClusterState.metadata().index(step.getTargetIndexPrefix() + indexName));
+
+ assertEquals(newIndexData.getLifecycleDate(), oldIndexData.getLifecycleDate());
+ assertEquals(newIndexData.getPhase(), oldIndexData.getPhase());
+ assertEquals(newIndexData.getAction(), oldIndexData.getAction());
+ assertEquals(newIndexData.getStep(), step.getTargetNextStepName());
+ assertEquals(newIndexData.getSnapshotRepository(), oldIndexData.getSnapshotRepository());
+ assertEquals(newIndexData.getSnapshotName(), oldIndexData.getSnapshotName());
}
public void testPerformActionWithNoTarget() {
CopyExecutionStateStep step = createRandomInstance();
@@ -106,6 +114,6 @@ public void testPerformActionWithNoTarget() {
() -> step.performAction(originalIndexMetadata.getIndex(), originalClusterState));
assertThat(e.getMessage(), equalTo("unable to copy execution state from [" +
- indexName + "] to [" + step.getShrunkIndexPrefix() + indexName + "] as target index does not exist"));
+ indexName + "] to [" + step.getTargetIndexPrefix() + indexName + "] as target index does not exist"));
}
}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopySettingsStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopySettingsStepTests.java
new file mode 100644
index 0000000000000..71b59653f5f85
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CopySettingsStepTests.java
@@ -0,0 +1,79 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+
+import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState;
+import static org.hamcrest.Matchers.is;
+
+public class CopySettingsStepTests extends AbstractStepTestCase {
+
+ @Override
+ protected CopySettingsStep createRandomInstance() {
+ return new CopySettingsStep(randomStepKey(), randomStepKey(), randomAlphaOfLengthBetween(1, 10),
+ IndexMetadata.SETTING_NUMBER_OF_SHARDS);
+ }
+
+ @Override
+ protected CopySettingsStep mutateInstance(CopySettingsStep instance) {
+ Step.StepKey key = instance.getKey();
+ Step.StepKey nextKey = instance.getNextStepKey();
+ String indexPrefix = instance.getIndexPrefix();
+ String[] settingsKeys = instance.getSettingsKeys();
+
+ switch (between(0, 3)) {
+ case 0:
+ key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 1:
+ nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 2:
+ indexPrefix = randomValueOtherThan(indexPrefix, () -> randomAlphaOfLengthBetween(1, 10));
+ break;
+ case 3:
+ settingsKeys = new String[]{randomAlphaOfLengthBetween(1, 10)};
+ break;
+ default:
+ throw new AssertionError("Illegal randomisation branch");
+ }
+ return new CopySettingsStep(key, nextKey, indexPrefix, settingsKeys);
+ }
+
+ @Override
+ protected CopySettingsStep copyInstance(CopySettingsStep instance) {
+ return new CopySettingsStep(instance.getKey(), instance.getNextStepKey(), instance.getIndexPrefix(), instance.getSettingsKeys());
+ }
+
+ public void testPerformAction() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+ IndexMetadata.Builder sourceIndexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+
+ String indexPrefix = "test-prefix-";
+ String targetIndex = indexPrefix + indexName;
+
+ IndexMetadata.Builder targetIndexMetadataBuilder = IndexMetadata.builder(targetIndex).settings(settings(Version.CURRENT))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+
+ ClusterState clusterState = ClusterState.builder(emptyClusterState()).metadata(
+ Metadata.builder().put(sourceIndexMetadataBuilder).put(targetIndexMetadataBuilder).build()
+ ).build();
+
+ CopySettingsStep copySettingsStep = new CopySettingsStep(randomStepKey(), randomStepKey(), indexPrefix,
+ LifecycleSettings.LIFECYCLE_NAME);
+
+ ClusterState newClusterState = copySettingsStep.performAction(sourceIndexMetadataBuilder.build().getIndex(), clusterState);
+ IndexMetadata newTargetIndexMetadata = newClusterState.metadata().index(targetIndex);
+ assertThat(newTargetIndexMetadata.getSettings().get(LifecycleSettings.LIFECYCLE_NAME), is(policyName));
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CreateSnapshotStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CreateSnapshotStepTests.java
new file mode 100644
index 0000000000000..8b70bdb7caf51
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CreateSnapshotStepTests.java
@@ -0,0 +1,171 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotAction;
+import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.test.client.NoOpClient;
+import org.elasticsearch.xpack.core.ilm.Step.StepKey;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+public class CreateSnapshotStepTests extends AbstractStepTestCase {
+
+ @Override
+ public CreateSnapshotStep createRandomInstance() {
+ StepKey stepKey = randomStepKey();
+ StepKey nextStepKey = randomStepKey();
+ return new CreateSnapshotStep(stepKey, nextStepKey, client);
+ }
+
+ @Override
+ protected CreateSnapshotStep copyInstance(CreateSnapshotStep instance) {
+ return new CreateSnapshotStep(instance.getKey(), instance.getNextStepKey(), instance.getClient());
+ }
+
+ @Override
+ public CreateSnapshotStep mutateInstance(CreateSnapshotStep instance) {
+ StepKey key = instance.getKey();
+ StepKey nextKey = instance.getNextStepKey();
+ switch (between(0, 1)) {
+ case 0:
+ key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 1:
+ nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ default:
+ throw new AssertionError("Illegal randomisation branch");
+ }
+ return new CreateSnapshotStep(key, nextKey, instance.getClient());
+ }
+
+ public void testPerformActionFailure() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+
+ {
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ Map ilmCustom = new HashMap<>();
+ String repository = "repository";
+ ilmCustom.put("snapshot_repository", repository);
+ indexMetadataBuilder.putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom);
+
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ CreateSnapshotStep createSnapshotStep = createRandomInstance();
+ createSnapshotStep.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ fail("expecting a failure as the index doesn't have any snapshot name in its ILM execution state");
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ assertThat(e, instanceOf(IllegalStateException.class));
+ assertThat(e.getMessage(),
+ is("snapshot name was not generated for policy [" + policyName + "] and index [" + indexName + "]"));
+ }
+ });
+ }
+
+ {
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ CreateSnapshotStep createSnapshotStep = createRandomInstance();
+ createSnapshotStep.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ fail("expecting a failure as the index doesn't have any snapshot name in its ILM execution state");
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ assertThat(e, instanceOf(IllegalStateException.class));
+ assertThat(e.getMessage(),
+ is("snapshot repository is not present for policy [" + policyName + "] and index [" + indexName + "]"));
+ }
+ });
+ }
+ }
+
+ public void testPerformAction() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+ Map ilmCustom = new HashMap<>();
+ String snapshotName = indexName + "-" + policyName;
+ ilmCustom.put("snapshot_name", snapshotName);
+ String repository = "repository";
+ ilmCustom.put("snapshot_repository", repository);
+
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom)
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ try (NoOpClient client = getCreateSnapshotRequestAssertingClient(repository, snapshotName, indexName)) {
+ CreateSnapshotStep step = new CreateSnapshotStep(randomStepKey(), randomStepKey(), client);
+ step.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ }
+ });
+ }
+ }
+
+ private NoOpClient getCreateSnapshotRequestAssertingClient(String expectedRepoName, String expectedSnapshotName, String indexName) {
+ return new NoOpClient(getTestName()) {
+ @Override
+ protected void doExecute(ActionType action,
+ Request request,
+ ActionListener listener) {
+ assertThat(action.name(), is(CreateSnapshotAction.NAME));
+ assertTrue(request instanceof CreateSnapshotRequest);
+ CreateSnapshotRequest createSnapshotRequest = (CreateSnapshotRequest) request;
+ assertThat(createSnapshotRequest.indices().length, is(1));
+ assertThat(createSnapshotRequest.indices()[0], is(indexName));
+ assertThat(createSnapshotRequest.repository(), is(expectedRepoName));
+ assertThat(createSnapshotRequest.snapshot(), is(expectedSnapshotName));
+ assertThat(CreateSnapshotStep.NAME + " waits for the create snapshot request to complete",
+ createSnapshotRequest.waitForCompletion(), is(true));
+ assertThat("ILM generated snapshots should not include global state", createSnapshotRequest.includeGlobalState(),
+ is(false));
+ }
+ };
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteActionTests.java
index 09db90ce01447..bfb5cf8f9a5d9 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteActionTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteActionTests.java
@@ -30,20 +30,39 @@ protected Reader instanceReader() {
}
public void testToSteps() {
- DeleteAction action = createTestInstance();
String phase = randomAlphaOfLengthBetween(1, 10);
StepKey nextStepKey = new StepKey(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10),
- randomAlphaOfLengthBetween(1, 10));
- List steps = action.toSteps(null, phase, nextStepKey);
- assertNotNull(steps);
- assertEquals(2, steps.size());
- StepKey expectedFirstStepKey = new StepKey(phase, DeleteAction.NAME, WaitForNoFollowersStep.NAME);
- StepKey expectedSecondStepKey = new StepKey(phase, DeleteAction.NAME, DeleteStep.NAME);
- WaitForNoFollowersStep firstStep = (WaitForNoFollowersStep) steps.get(0);
- DeleteStep secondStep = (DeleteStep) steps.get(1);
- assertEquals(expectedFirstStepKey, firstStep.getKey());
- assertEquals(expectedSecondStepKey, firstStep.getNextStepKey());
- assertEquals(expectedSecondStepKey, secondStep.getKey());
- assertEquals(nextStepKey, secondStep.getNextStepKey());
+ randomAlphaOfLengthBetween(1, 10));
+ {
+ DeleteAction action = new DeleteAction(true);
+ List steps = action.toSteps(null, phase, nextStepKey);
+ assertNotNull(steps);
+ assertEquals(3, steps.size());
+ StepKey expectedFirstStepKey = new StepKey(phase, DeleteAction.NAME, WaitForNoFollowersStep.NAME);
+ StepKey expectedSecondStepKey = new StepKey(phase, DeleteAction.NAME, CleanupSnapshotStep.NAME);
+ StepKey expectedThirdKey = new StepKey(phase, DeleteAction.NAME, DeleteStep.NAME);
+ WaitForNoFollowersStep firstStep = (WaitForNoFollowersStep) steps.get(0);
+ CleanupSnapshotStep secondStep = (CleanupSnapshotStep) steps.get(1);
+ DeleteStep thirdStep = (DeleteStep) steps.get(2);
+ assertEquals(expectedFirstStepKey, firstStep.getKey());
+ assertEquals(expectedSecondStepKey, firstStep.getNextStepKey());
+ assertEquals(expectedSecondStepKey, secondStep.getKey());
+ assertEquals(expectedThirdKey, thirdStep.getKey());
+ assertEquals(nextStepKey, thirdStep.getNextStepKey());
+ }
+
+ {
+ DeleteAction actionKeepsSnapshot = new DeleteAction(false);
+ List steps = actionKeepsSnapshot.toSteps(null, phase, nextStepKey);
+ StepKey expectedFirstStepKey = new StepKey(phase, DeleteAction.NAME, WaitForNoFollowersStep.NAME);
+ StepKey expectedSecondStepKey = new StepKey(phase, DeleteAction.NAME, DeleteStep.NAME);
+ assertEquals(2, steps.size());
+ assertNotNull(steps);
+ WaitForNoFollowersStep firstStep = (WaitForNoFollowersStep) steps.get(0);
+ DeleteStep secondStep = (DeleteStep) steps.get(1);
+ assertEquals(expectedFirstStepKey, firstStep.getKey());
+ assertEquals(expectedSecondStepKey, firstStep.getNextStepKey());
+ assertEquals(nextStepKey, secondStep.getNextStepKey());
+ }
}
}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java
new file mode 100644
index 0000000000000..dda26e2796cc8
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java
@@ -0,0 +1,127 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.common.Strings;
+
+import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState;
+import static org.elasticsearch.xpack.core.ilm.GenerateSnapshotNameStep.generateSnapshotName;
+import static org.elasticsearch.xpack.core.ilm.GenerateSnapshotNameStep.validateGeneratedSnapshotName;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+
+public class GenerateSnapshotNameStepTests extends AbstractStepTestCase {
+
+ @Override
+ protected GenerateSnapshotNameStep createRandomInstance() {
+ return new GenerateSnapshotNameStep(randomStepKey(), randomStepKey(), randomAlphaOfLengthBetween(5, 10));
+ }
+
+ @Override
+ protected GenerateSnapshotNameStep mutateInstance(GenerateSnapshotNameStep instance) {
+ Step.StepKey key = instance.getKey();
+ Step.StepKey nextKey = instance.getNextStepKey();
+ String snapshotRepository = instance.getSnapshotRepository();
+
+ switch (between(0, 2)) {
+ case 0:
+ key = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 1:
+ nextKey = new Step.StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 2:
+ snapshotRepository = randomValueOtherThan(snapshotRepository, () -> randomAlphaOfLengthBetween(5, 10));
+ break;
+ default:
+ throw new AssertionError("Illegal randomisation branch");
+ }
+ return new GenerateSnapshotNameStep(key, nextKey, snapshotRepository);
+ }
+
+ @Override
+ protected GenerateSnapshotNameStep copyInstance(GenerateSnapshotNameStep instance) {
+ return new GenerateSnapshotNameStep(instance.getKey(), instance.getNextStepKey(), instance.getSnapshotRepository());
+ }
+
+ public void testPerformAction() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetadataBuilder).build()).build();
+
+ GenerateSnapshotNameStep generateSnapshotNameStep = createRandomInstance();
+ ClusterState newClusterState = generateSnapshotNameStep.performAction(indexMetadataBuilder.build().getIndex(), clusterState);
+
+ LifecycleExecutionState executionState = LifecycleExecutionState.fromIndexMetadata(newClusterState.metadata().index(indexName));
+ assertThat("the " + GenerateSnapshotNameStep.NAME + " step must generate a snapshot name", executionState.getSnapshotName(),
+ notNullValue());
+ assertThat(executionState.getSnapshotRepository(), is(generateSnapshotNameStep.getSnapshotRepository()));
+ assertThat(executionState.getSnapshotName(), containsStringIgnoringCase(indexName));
+ assertThat(executionState.getSnapshotName(), containsStringIgnoringCase(policyName));
+ }
+
+ public void testNameGeneration() {
+ long time = 1552684146542L; // Fri Mar 15 2019 21:09:06 UTC
+ assertThat(generateSnapshotName("name"), startsWith("name-"));
+ assertThat(generateSnapshotName("name").length(), greaterThan("name-".length()));
+
+ GenerateSnapshotNameStep.ResolverContext resolverContext = new GenerateSnapshotNameStep.ResolverContext(time);
+ assertThat(generateSnapshotName("", resolverContext), startsWith("name-2019.03.15-"));
+ assertThat(generateSnapshotName("", resolverContext).length(), greaterThan("name-2019.03.15-".length()));
+
+ assertThat(generateSnapshotName("", resolverContext), startsWith("name-2019.03.01-"));
+
+ assertThat(generateSnapshotName("", resolverContext), startsWith("name-2019-03-15.21:09:00-"));
+ }
+
+ public void testNameValidation() {
+ assertThat(validateGeneratedSnapshotName("name-", generateSnapshotName("name-")), nullValue());
+ assertThat(validateGeneratedSnapshotName("", generateSnapshotName("")), nullValue());
+
+ {
+ ActionRequestValidationException validationException = validateGeneratedSnapshotName("", generateSnapshotName(""));
+ assertThat(validationException, notNullValue());
+ assertThat(validationException.validationErrors(), containsInAnyOrder("invalid snapshot name []: cannot be empty"));
+ }
+ {
+ ActionRequestValidationException validationException = validateGeneratedSnapshotName("#start", generateSnapshotName("#start"));
+ assertThat(validationException, notNullValue());
+ assertThat(validationException.validationErrors(), containsInAnyOrder("invalid snapshot name [#start]: must not contain '#'"));
+ }
+ {
+ ActionRequestValidationException validationException = validateGeneratedSnapshotName("_start", generateSnapshotName("_start"));
+ assertThat(validationException, notNullValue());
+ assertThat(validationException.validationErrors(), containsInAnyOrder("invalid snapshot name [_start]: must not start with " +
+ "'_'"));
+ }
+ {
+ ActionRequestValidationException validationException = validateGeneratedSnapshotName("aBcD", generateSnapshotName("aBcD"));
+ assertThat(validationException, notNullValue());
+ assertThat(validationException.validationErrors(), containsInAnyOrder("invalid snapshot name [aBcD]: must be lowercase"));
+ }
+ {
+ ActionRequestValidationException validationException = validateGeneratedSnapshotName("na>me", generateSnapshotName("na>me"));
+ assertThat(validationException, notNullValue());
+ assertThat(validationException.validationErrors(), containsInAnyOrder("invalid snapshot name [na>me]: must not contain " +
+ "contain the following characters " + Strings.INVALID_FILENAME_CHARS));
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java
index 862c408e6d645..c2692731f47b0 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java
@@ -57,6 +57,8 @@ private static IndexLifecycleExplainResponse randomManagedIndexExplainResponse()
stepNull ? null : randomNonNegativeLong(),
stepNull ? null : randomNonNegativeLong(),
stepNull ? null : randomNonNegativeLong(),
+ stepNull ? null : randomAlphaOfLength(10),
+ stepNull ? null : randomAlphaOfLength(10),
randomBoolean() ? null : new BytesArray(new RandomStepInfo(() -> randomAlphaOfLength(10)).toString()),
randomBoolean() ? null : PhaseExecutionInfoTests.randomPhaseExecutionInfo(""));
}
@@ -76,6 +78,8 @@ public void testInvalidStepDetails() {
randomBoolean() ? null : randomNonNegativeLong(),
randomBoolean() ? null : randomNonNegativeLong(),
randomBoolean() ? null : randomNonNegativeLong(),
+ randomBoolean() ? null : randomAlphaOfLength(10),
+ randomBoolean() ? null : randomAlphaOfLength(10),
randomBoolean() ? null : new BytesArray(new RandomStepInfo(() -> randomAlphaOfLength(10)).toString()),
randomBoolean() ? null : PhaseExecutionInfoTests.randomPhaseExecutionInfo("")));
assertThat(exception.getMessage(), startsWith("managed index response must have complete step details"));
@@ -116,11 +120,13 @@ protected IndexLifecycleExplainResponse mutateInstance(IndexLifecycleExplainResp
Long phaseTime = instance.getPhaseTime();
Long actionTime = instance.getActionTime();
Long stepTime = instance.getStepTime();
+ String repositoryName = instance.getRepositoryName();
+ String snapshotName = instance.getSnapshotName();
boolean managed = instance.managedByILM();
BytesReference stepInfo = instance.getStepInfo();
PhaseExecutionInfo phaseExecutionInfo = instance.getPhaseExecutionInfo();
if (managed) {
- switch (between(0, 11)) {
+ switch (between(0, 13)) {
case 0:
index = index + randomAlphaOfLengthBetween(1, 5);
break;
@@ -172,11 +178,18 @@ protected IndexLifecycleExplainResponse mutateInstance(IndexLifecycleExplainResp
isAutoRetryableError = true;
failedStepRetryCount = randomValueOtherThan(failedStepRetryCount, () -> randomInt(10));
break;
+ case 12:
+ repositoryName = randomValueOtherThan(repositoryName, () -> randomAlphaOfLengthBetween(5, 10));
+ break;
+ case 13:
+ snapshotName = randomValueOtherThan(snapshotName, () -> randomAlphaOfLengthBetween(5, 10));
+ break;
default:
throw new AssertionError("Illegal randomisation branch");
}
return IndexLifecycleExplainResponse.newManagedIndexResponse(index, policy, policyTime, phase, action, step, failedStep,
- isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, stepInfo, phaseExecutionInfo);
+ isAutoRetryableError, failedStepRetryCount, phaseTime, actionTime, stepTime, repositoryName, snapshotName, stepInfo,
+ phaseExecutionInfo);
} else {
switch (between(0, 1)) {
case 0:
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java
index 7a7782fb389ca..a7729f453b759 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecycleExecutionStateTests.java
@@ -186,12 +186,14 @@ private static LifecycleExecutionState mutate(LifecycleExecutionState toMutate)
}
static Map createCustomMetadata() {
- String phase = randomAlphaOfLengthBetween(5,20);
- String action = randomAlphaOfLengthBetween(5,20);
- String step = randomAlphaOfLengthBetween(5,20);
- String failedStep = randomAlphaOfLengthBetween(5,20);
- String stepInfo = randomAlphaOfLengthBetween(15,50);
- String phaseDefinition = randomAlphaOfLengthBetween(15,50);
+ String phase = randomAlphaOfLengthBetween(5, 20);
+ String action = randomAlphaOfLengthBetween(5, 20);
+ String step = randomAlphaOfLengthBetween(5, 20);
+ String failedStep = randomAlphaOfLengthBetween(5, 20);
+ String stepInfo = randomAlphaOfLengthBetween(15, 50);
+ String phaseDefinition = randomAlphaOfLengthBetween(15, 50);
+ String repositoryName = randomAlphaOfLengthBetween(10, 20);
+ String snapshotName = randomAlphaOfLengthBetween(10, 20);
long indexCreationDate = randomLong();
long phaseTime = randomLong();
long actionTime = randomLong();
@@ -208,6 +210,8 @@ static Map createCustomMetadata() {
customMetadata.put("phase_time", String.valueOf(phaseTime));
customMetadata.put("action_time", String.valueOf(actionTime));
customMetadata.put("step_time", String.valueOf(stepTime));
+ customMetadata.put("snapshot_repository", repositoryName);
+ customMetadata.put("snapshot_name", snapshotName);
return customMetadata;
}
}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java
index 9b50616a128dd..684b32a19a277 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java
@@ -40,6 +40,7 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() {
(in) -> TimeseriesLifecycleType.INSTANCE),
new NamedWriteableRegistry.Entry(LifecycleAction.class, AllocateAction.NAME, AllocateAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, WaitForSnapshotAction.NAME, WaitForSnapshotAction::new),
+ new NamedWriteableRegistry.Entry(LifecycleAction.class, SearchableSnapshotAction.NAME, SearchableSnapshotAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, DeleteAction.NAME, DeleteAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, ForceMergeAction.NAME, ForceMergeAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, ReadOnlyAction.NAME, ReadOnlyAction::new),
@@ -60,6 +61,8 @@ protected NamedXContentRegistry xContentRegistry() {
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(AllocateAction.NAME), AllocateAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class,
new ParseField(WaitForSnapshotAction.NAME), WaitForSnapshotAction::parse),
+ new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SearchableSnapshotAction.NAME),
+ SearchableSnapshotAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(DeleteAction.NAME), DeleteAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ForceMergeAction.NAME), ForceMergeAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ReadOnlyAction.NAME), ReadOnlyAction::parse),
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java
index ba98123072982..308c3df20d0bd 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java
@@ -56,7 +56,8 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() {
new NamedWriteableRegistry.Entry(LifecycleAction.class, ShrinkAction.NAME, ShrinkAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, FreezeAction.NAME, FreezeAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, SetPriorityAction.NAME, SetPriorityAction::new),
- new NamedWriteableRegistry.Entry(LifecycleAction.class, UnfollowAction.NAME, UnfollowAction::new)
+ new NamedWriteableRegistry.Entry(LifecycleAction.class, UnfollowAction.NAME, UnfollowAction::new),
+ new NamedWriteableRegistry.Entry(LifecycleAction.class, SearchableSnapshotAction.NAME, SearchableSnapshotAction::new)
));
}
@@ -76,7 +77,9 @@ protected NamedXContentRegistry xContentRegistry() {
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(FreezeAction.NAME), FreezeAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse),
- new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(UnfollowAction.NAME), UnfollowAction::parse)
+ new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(UnfollowAction.NAME), UnfollowAction::parse),
+ new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SearchableSnapshotAction.NAME),
+ SearchableSnapshotAction::parse)
));
return new NamedXContentRegistry(entries);
}
@@ -129,6 +132,8 @@ public static LifecyclePolicy randomTimeseriesLifecyclePolicyWithAllPhases(@Null
return SetPriorityActionTests.randomInstance();
case UnfollowAction.NAME:
return new UnfollowAction();
+ case SearchableSnapshotAction.NAME:
+ return new SearchableSnapshotAction(randomAlphaOfLengthBetween(1, 10));
default:
throw new IllegalArgumentException("invalid action [" + action + "]");
}};
@@ -183,6 +188,8 @@ public static LifecyclePolicy randomTimeseriesLifecyclePolicy(@Nullable String l
return SetPriorityActionTests.randomInstance();
case UnfollowAction.NAME:
return new UnfollowAction();
+ case SearchableSnapshotAction.NAME:
+ return new SearchableSnapshotAction(randomAlphaOfLengthBetween(1, 10));
default:
throw new IllegalArgumentException("invalid action [" + action + "]");
}};
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStepTests.java
new file mode 100644
index 0000000000000..80651c84db170
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MountSnapshotStepTests.java
@@ -0,0 +1,253 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.snapshots.RestoreInfo;
+import org.elasticsearch.test.client.NoOpClient;
+import org.elasticsearch.xpack.core.ilm.Step.StepKey;
+import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotAction;
+import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+public class MountSnapshotStepTests extends AbstractStepTestCase {
+
+ private static final String RESTORED_INDEX_PREFIX = "restored-";
+
+ @Override
+ public MountSnapshotStep createRandomInstance() {
+ StepKey stepKey = randomStepKey();
+ StepKey nextStepKey = randomStepKey();
+ String restoredIndexPrefix = randomAlphaOfLength(10);
+ return new MountSnapshotStep(stepKey, nextStepKey, client, restoredIndexPrefix);
+ }
+
+ @Override
+ protected MountSnapshotStep copyInstance(MountSnapshotStep instance) {
+ return new MountSnapshotStep(instance.getKey(), instance.getNextStepKey(), instance.getClient(), instance.getRestoredIndexPrefix());
+ }
+
+ @Override
+ public MountSnapshotStep mutateInstance(MountSnapshotStep instance) {
+ StepKey key = instance.getKey();
+ StepKey nextKey = instance.getNextStepKey();
+ String restoredIndexPrefix = instance.getRestoredIndexPrefix();
+ switch (between(0, 2)) {
+ case 0:
+ key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 1:
+ nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 2:
+ restoredIndexPrefix = randomValueOtherThan(restoredIndexPrefix, () -> randomAlphaOfLengthBetween(1, 10));
+ break;
+ default:
+ throw new AssertionError("Illegal randomisation branch");
+ }
+ return new MountSnapshotStep(key, nextKey, instance.getClient(), restoredIndexPrefix);
+ }
+
+ public void testPerformActionFailure() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+
+ {
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ MountSnapshotStep mountSnapshotStep = createRandomInstance();
+ mountSnapshotStep.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ fail("expecting a failure as the index doesn't have any repository name in its ILM execution state");
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ assertThat(e, instanceOf(IllegalStateException.class));
+ assertThat(e.getMessage(),
+ is("snapshot repository is not present for policy [" + policyName + "] and index [" + indexName + "]"));
+ }
+ });
+ }
+
+ {
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ Map ilmCustom = new HashMap<>();
+ String repository = "repository";
+ ilmCustom.put("snapshot_repository", repository);
+ indexMetadataBuilder.putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom);
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ MountSnapshotStep mountSnapshotStep = createRandomInstance();
+ mountSnapshotStep.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ fail("expecting a failure as the index doesn't have any snapshot name in its ILM execution state");
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ assertThat(e, instanceOf(IllegalStateException.class));
+ assertThat(e.getMessage(),
+ is("snapshot name was not generated for policy [" + policyName + "] and index [" + indexName + "]"));
+ }
+ });
+ }
+ }
+
+ public void testPerformAction() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+ Map ilmCustom = new HashMap<>();
+ String snapshotName = indexName + "-" + policyName;
+ ilmCustom.put("snapshot_name", snapshotName);
+ String repository = "repository";
+ ilmCustom.put("snapshot_repository", repository);
+
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom)
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ try (NoOpClient client = getRestoreSnapshotRequestAssertingClient(repository, snapshotName, indexName, RESTORED_INDEX_PREFIX)) {
+ MountSnapshotStep step = new MountSnapshotStep(randomStepKey(), randomStepKey(), client, RESTORED_INDEX_PREFIX);
+ step.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ assertThat(complete, is(true));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ fail("expecting successful response but got: [" + e.getMessage() + "]");
+ }
+ });
+ }
+ }
+
+ public void testResponseStatusHandling() {
+ String indexName = randomAlphaOfLength(10);
+ String policyName = "test-ilm-policy";
+ Map ilmCustom = new HashMap<>();
+ String snapshotName = indexName + "-" + policyName;
+ ilmCustom.put("snapshot_name", snapshotName);
+ String repository = "repository";
+ ilmCustom.put("snapshot_repository", repository);
+
+ IndexMetadata.Builder indexMetadataBuilder =
+ IndexMetadata.builder(indexName).settings(settings(Version.CURRENT).put(LifecycleSettings.LIFECYCLE_NAME, policyName))
+ .putCustom(LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY, ilmCustom)
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ IndexMetadata indexMetaData = indexMetadataBuilder.build();
+
+ ClusterState clusterState =
+ ClusterState.builder(emptyClusterState()).metadata(Metadata.builder().put(indexMetaData, true).build()).build();
+
+ {
+ RestoreSnapshotResponse responseWithOKStatus = new RestoreSnapshotResponse(new RestoreInfo("test", List.of(), 1, 1));
+ try (NoOpClient clientPropagatingOKResponse = getClientTriggeringResponse(responseWithOKStatus)) {
+ MountSnapshotStep step = new MountSnapshotStep(randomStepKey(), randomStepKey(), clientPropagatingOKResponse,
+ RESTORED_INDEX_PREFIX);
+ step.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ assertThat(complete, is(true));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ fail("expecting successful response but got: [" + e.getMessage() + "]");
+ }
+ });
+ }
+ }
+
+ {
+ RestoreSnapshotResponse responseWithACCEPTEDStatus = new RestoreSnapshotResponse((RestoreInfo) null);
+ try (NoOpClient clientPropagatingACCEPTEDResponse = getClientTriggeringResponse(responseWithACCEPTEDStatus)) {
+ MountSnapshotStep step = new MountSnapshotStep(randomStepKey(), randomStepKey(), clientPropagatingACCEPTEDResponse,
+ RESTORED_INDEX_PREFIX);
+ step.performAction(indexMetaData, clusterState, null, new AsyncActionStep.Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ assertThat(complete, is(true));
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ fail("expecting successful response but got: [" + e.getMessage() + "]");
+ }
+ });
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private NoOpClient getClientTriggeringResponse(RestoreSnapshotResponse response) {
+ return new NoOpClient(getTestName()) {
+ @Override
+ protected void doExecute(ActionType action,
+ Request request,
+ ActionListener listener) {
+ listener.onResponse((Response) response);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ private NoOpClient getRestoreSnapshotRequestAssertingClient(String expectedRepoName, String expectedSnapshotName, String indexName,
+ String restoredIndexPrefix) {
+ return new NoOpClient(getTestName()) {
+ @Override
+ protected void doExecute(ActionType action,
+ Request request,
+ ActionListener listener) {
+ assertThat(action.name(), is(MountSearchableSnapshotAction.NAME));
+ assertTrue(request instanceof MountSearchableSnapshotRequest);
+ MountSearchableSnapshotRequest mountSearchableSnapshotRequest = (MountSearchableSnapshotRequest) request;
+ assertThat(mountSearchableSnapshotRequest.repositoryName(), is(expectedRepoName));
+ assertThat(mountSearchableSnapshotRequest.snapshotName(), is(expectedSnapshotName));
+ assertThat("another ILM step will wait for the restore to complete. the " + MountSnapshotStep.NAME + " step should not",
+ mountSearchableSnapshotRequest.waitForCompletion(), is(false));
+ assertThat(mountSearchableSnapshotRequest.ignoreIndexSettings(), is(notNullValue()));
+ assertThat(mountSearchableSnapshotRequest.ignoreIndexSettings()[0], is(LifecycleSettings.LIFECYCLE_NAME));
+ assertThat(mountSearchableSnapshotRequest.mountedIndexName(), is(restoredIndexPrefix + indexName));
+ }
+ };
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotActionTests.java
new file mode 100644
index 0000000000000..1c48d3cca4bd2
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SearchableSnapshotActionTests.java
@@ -0,0 +1,76 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.xpack.core.ilm.Step.StepKey;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.xpack.core.ilm.SearchableSnapshotAction.NAME;
+import static org.hamcrest.Matchers.is;
+
+public class SearchableSnapshotActionTests extends AbstractActionTestCase {
+
+ @Override
+ public void testToSteps() {
+ String phase = randomAlphaOfLengthBetween(1, 10);
+ StepKey expectedFirstStep = new StepKey(phase, NAME, WaitForNoFollowersStep.NAME);
+ StepKey expectedSecondStep = new StepKey(phase, NAME, GenerateSnapshotNameStep.NAME);
+ StepKey expectedThirdStep = new StepKey(phase, NAME, CleanupSnapshotStep.NAME);
+ StepKey expectedFourthStep = new StepKey(phase, NAME, CreateSnapshotStep.NAME);
+ StepKey expectedFifthStep = new StepKey(phase, NAME, MountSnapshotStep.NAME);
+ StepKey expectedSixthStep = new StepKey(phase, NAME, WaitForIndexColorStep.NAME);
+ StepKey expectedSeventhStep = new StepKey(phase, NAME, CopyExecutionStateStep.NAME);
+ StepKey expectedEighthStep = new StepKey(phase, NAME, CopySettingsStep.NAME);
+ StepKey expectedNinthStep = new StepKey(phase, NAME, SwapAliasesAndDeleteSourceIndexStep.NAME);
+
+ SearchableSnapshotAction action = createTestInstance();
+ StepKey nextStepKey = new StepKey(phase, randomAlphaOfLengthBetween(1, 5), randomAlphaOfLengthBetween(1, 5));
+
+ List steps = action.toSteps(null, phase, nextStepKey);
+ assertThat(steps.size(), is(9));
+
+ assertThat(steps.get(0).getKey(), is(expectedFirstStep));
+ assertThat(steps.get(1).getKey(), is(expectedSecondStep));
+ assertThat(steps.get(2).getKey(), is(expectedThirdStep));
+ assertThat(steps.get(3).getKey(), is(expectedFourthStep));
+ assertThat(steps.get(4).getKey(), is(expectedFifthStep));
+ assertThat(steps.get(5).getKey(), is(expectedSixthStep));
+ assertThat(steps.get(6).getKey(), is(expectedSeventhStep));
+ assertThat(steps.get(7).getKey(), is(expectedEighthStep));
+ assertThat(steps.get(8).getKey(), is(expectedNinthStep));
+
+ AsyncActionBranchingStep branchStep = (AsyncActionBranchingStep) steps.get(3);
+ assertThat(branchStep.getNextKeyOnIncompleteResponse(), is(expectedThirdStep));
+ }
+
+ @Override
+ protected SearchableSnapshotAction doParseInstance(XContentParser parser) throws IOException {
+ return SearchableSnapshotAction.parse(parser);
+ }
+
+ @Override
+ protected SearchableSnapshotAction createTestInstance() {
+ return randomInstance();
+ }
+
+ @Override
+ protected Writeable.Reader instanceReader() {
+ return SearchableSnapshotAction::new;
+ }
+
+ @Override
+ protected SearchableSnapshotAction mutateInstance(SearchableSnapshotAction instance) throws IOException {
+ return randomInstance();
+ }
+
+ static SearchableSnapshotAction randomInstance() {
+ return new SearchableSnapshotAction(randomAlphaOfLengthBetween(5, 10));
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java
index b2a0dcfcc355a..0e575f8482c35 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java
@@ -174,7 +174,7 @@ public void testToSteps() {
assertTrue(steps.get(7) instanceof CopyExecutionStateStep);
assertThat(steps.get(7).getKey(), equalTo(expectedEighthKey));
assertThat(steps.get(7).getNextStepKey(), equalTo(expectedNinthKey));
- assertThat(((CopyExecutionStateStep) steps.get(7)).getShrunkIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX));
+ assertThat(((CopyExecutionStateStep) steps.get(7)).getTargetIndexPrefix(), equalTo(ShrinkAction.SHRUNKEN_INDEX_PREFIX));
assertTrue(steps.get(8) instanceof ShrinkSetAliasStep);
assertThat(steps.get(8).getKey(), equalTo(expectedNinthKey));
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStepTests.java
new file mode 100644
index 0000000000000..0fc6489c22f8a
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStepTests.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ilm;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.AliasMetadata;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.test.client.NoOpClient;
+import org.elasticsearch.xpack.core.ilm.AsyncActionStep.Listener;
+import org.elasticsearch.xpack.core.ilm.Step.StepKey;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.elasticsearch.xpack.core.ilm.AbstractStepMasterTimeoutTestCase.emptyClusterState;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+
+public class SwapAliasesAndDeleteSourceIndexStepTests extends AbstractStepTestCase {
+
+ @Override
+ public SwapAliasesAndDeleteSourceIndexStep createRandomInstance() {
+ StepKey stepKey = randomStepKey();
+ StepKey nextStepKey = randomStepKey();
+ String restoredIndexPrefix = randomAlphaOfLength(10);
+ return new SwapAliasesAndDeleteSourceIndexStep(stepKey, nextStepKey, client, restoredIndexPrefix);
+ }
+
+ @Override
+ protected SwapAliasesAndDeleteSourceIndexStep copyInstance(SwapAliasesAndDeleteSourceIndexStep instance) {
+ return new SwapAliasesAndDeleteSourceIndexStep(instance.getKey(), instance.getNextStepKey(), instance.getClient(),
+ instance.getTargetIndexPrefix());
+ }
+
+ @Override
+ public SwapAliasesAndDeleteSourceIndexStep mutateInstance(SwapAliasesAndDeleteSourceIndexStep instance) {
+ StepKey key = instance.getKey();
+ StepKey nextKey = instance.getNextStepKey();
+ String restoredIndexPrefix = instance.getTargetIndexPrefix();
+ switch (between(0, 2)) {
+ case 0:
+ key = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 1:
+ nextKey = new StepKey(key.getPhase(), key.getAction(), key.getName() + randomAlphaOfLength(5));
+ break;
+ case 2:
+ restoredIndexPrefix += randomAlphaOfLength(5);
+ break;
+ default:
+ throw new AssertionError("Illegal randomisation branch");
+ }
+ return new SwapAliasesAndDeleteSourceIndexStep(key, nextKey, instance.getClient(), restoredIndexPrefix);
+ }
+
+ public void testPerformAction() {
+ String sourceIndexName = randomAlphaOfLength(10);
+ IndexMetadata.Builder sourceIndexMetadataBuilder = IndexMetadata.builder(sourceIndexName).settings(settings(Version.CURRENT))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+ AliasMetadata.Builder aliasBuilder = AliasMetadata.builder(randomAlphaOfLengthBetween(3, 10));
+ if (randomBoolean()) {
+ aliasBuilder.routing(randomAlphaOfLengthBetween(1, 10));
+ }
+ if (randomBoolean()) {
+ aliasBuilder.searchRouting(randomAlphaOfLengthBetween(1, 10));
+ }
+ if (randomBoolean()) {
+ aliasBuilder.indexRouting(randomAlphaOfLengthBetween(1, 10));
+ }
+ aliasBuilder.writeIndex(randomBoolean());
+ AliasMetadata aliasMetaData = aliasBuilder.build();
+ IndexMetadata sourceIndexMetaData = sourceIndexMetadataBuilder.putAlias(aliasMetaData).build();
+
+ String targetIndexPrefix = "index_prefix";
+ String targetIndexName = targetIndexPrefix + sourceIndexName;
+
+ List expectedAliasActions = Arrays.asList(
+ AliasActions.removeIndex().index(sourceIndexName),
+ AliasActions.add().index(targetIndexName).alias(sourceIndexName),
+ AliasActions.add().index(targetIndexName).alias(aliasMetaData.alias())
+ .searchRouting(aliasMetaData.searchRouting()).indexRouting(aliasMetaData.indexRouting())
+ .writeIndex(null));
+
+ try (NoOpClient client = getIndicesAliasAssertingClient(expectedAliasActions)) {
+ SwapAliasesAndDeleteSourceIndexStep step = new SwapAliasesAndDeleteSourceIndexStep(randomStepKey(), randomStepKey(),
+ client, targetIndexPrefix);
+
+ IndexMetadata.Builder targetIndexMetaDataBuilder = IndexMetadata.builder(targetIndexName).settings(settings(Version.CURRENT))
+ .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5));
+
+ ClusterState clusterState = ClusterState.builder(emptyClusterState())
+ .metadata(
+ Metadata.builder()
+ .put(sourceIndexMetaData, true)
+ .put(targetIndexMetaDataBuilder)
+ .build()
+ ).build();
+
+ step.performAction(sourceIndexMetaData, clusterState, null, new Listener() {
+ @Override
+ public void onResponse(boolean complete) {
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ }
+ });
+ }
+ }
+
+ private NoOpClient getIndicesAliasAssertingClient(List expectedAliasActions) {
+ return new NoOpClient(getTestName()) {
+ @Override
+ protected void doExecute(ActionType action,
+ Request request,
+ ActionListener listener) {
+ assertThat(action.name(), is(IndicesAliasesAction.NAME));
+ assertTrue(request instanceof IndicesAliasesRequest);
+ assertThat(((IndicesAliasesRequest) request).getAliasActions(), equalTo(expectedAliasActions));
+ }
+ };
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java
index a9de66469a43d..cfd11bafeae02 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java
@@ -45,6 +45,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase {
private static final FreezeAction TEST_FREEZE_ACTION = new FreezeAction();
private static final SetPriorityAction TEST_PRIORITY_ACTION = new SetPriorityAction(0);
private static final UnfollowAction TEST_UNFOLLOW_ACTION = new UnfollowAction();
+ private static final SearchableSnapshotAction TEST_SEARCHABLE_SNAPSHOT_ACTION = new SearchableSnapshotAction("repo");
public void testValidatePhases() {
boolean invalid = randomBoolean();
@@ -595,6 +596,8 @@ private LifecycleAction getTestAction(String actionName) {
return TEST_PRIORITY_ACTION;
case UnfollowAction.NAME:
return TEST_UNFOLLOW_ACTION;
+ case SearchableSnapshotAction.NAME:
+ return TEST_SEARCHABLE_SNAPSHOT_ACTION;
default:
throw new IllegalArgumentException("unsupported timeseries phase action [" + actionName + "]");
}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java
index 7461918a5df24..dcbbd56e3a116 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java
@@ -37,7 +37,8 @@ protected WaitForIndexColorStep createRandomInstance() {
StepKey stepKey = randomStepKey();
StepKey nextStepKey = randomStepKey();
ClusterHealthStatus color = randomColor();
- return new WaitForIndexColorStep(stepKey, nextStepKey, color);
+ String indexPrefix = randomAlphaOfLengthBetween(1, 10);
+ return new WaitForIndexColorStep(stepKey, nextStepKey, color, indexPrefix);
}
@Override
@@ -45,6 +46,8 @@ protected WaitForIndexColorStep mutateInstance(WaitForIndexColorStep instance) {
StepKey key = instance.getKey();
StepKey nextKey = instance.getNextStepKey();
ClusterHealthStatus color = instance.getColor(), newColor = randomColor();
+ String indexPrefix = instance.getIndexNamePrefix();
+
while (color.equals(newColor)) {
newColor = randomColor();
}
@@ -59,14 +62,17 @@ protected WaitForIndexColorStep mutateInstance(WaitForIndexColorStep instance) {
case 2:
color = newColor;
break;
+ case 3:
+ indexPrefix = randomAlphaOfLengthBetween(1, 10);
+ break;
}
- return new WaitForIndexColorStep(key, nextKey, color);
+ return new WaitForIndexColorStep(key, nextKey, color, indexPrefix);
}
@Override
protected WaitForIndexColorStep copyInstance(WaitForIndexColorStep instance) {
- return new WaitForIndexColorStep(instance.getKey(), instance.getNextStepKey(), instance.getColor());
+ return new WaitForIndexColorStep(instance.getKey(), instance.getNextStepKey(), instance.getColor(), instance.getIndexNamePrefix());
}
public void testConditionMetForGreen() {
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java
index f0fd2f7dd5d76..6b1e20c5551df 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java
@@ -22,6 +22,7 @@
import org.elasticsearch.xpack.core.ilm.LifecycleType;
import org.elasticsearch.xpack.core.ilm.ReadOnlyAction;
import org.elasticsearch.xpack.core.ilm.RolloverAction;
+import org.elasticsearch.xpack.core.ilm.SearchableSnapshotAction;
import org.elasticsearch.xpack.core.ilm.SetPriorityAction;
import org.elasticsearch.xpack.core.ilm.ShrinkAction;
import org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType;
@@ -73,7 +74,8 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() {
new NamedWriteableRegistry.Entry(LifecycleAction.class, ShrinkAction.NAME, ShrinkAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, FreezeAction.NAME, FreezeAction::new),
new NamedWriteableRegistry.Entry(LifecycleAction.class, SetPriorityAction.NAME, SetPriorityAction::new),
- new NamedWriteableRegistry.Entry(LifecycleAction.class, UnfollowAction.NAME, UnfollowAction::new)
+ new NamedWriteableRegistry.Entry(LifecycleAction.class, UnfollowAction.NAME, UnfollowAction::new),
+ new NamedWriteableRegistry.Entry(LifecycleAction.class, SearchableSnapshotAction.NAME, SearchableSnapshotAction::new)
));
}
@@ -93,6 +95,8 @@ protected NamedXContentRegistry xContentRegistry() {
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(FreezeAction.NAME), FreezeAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse),
+ new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SearchableSnapshotAction.NAME),
+ SearchableSnapshotAction::parse),
new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(UnfollowAction.NAME), UnfollowAction::parse)
));
return new NamedXContentRegistry(entries);
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java
new file mode 100644
index 0000000000000..dade0a0ca4204
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java
@@ -0,0 +1,60 @@
+/*
+ * 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.searchablesnapshots;
+
+import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.cluster.routing.ShardRoutingState;
+import org.elasticsearch.cluster.routing.TestShardRouting;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.repositories.IndexId;
+import org.elasticsearch.snapshots.SnapshotId;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.CacheIndexInputStats;
+import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.Counter;
+import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.TimedCounter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchableSnapshotShardStatsTests extends AbstractWireSerializingTestCase {
+
+ @Override
+ protected Writeable.Reader instanceReader() {
+ return SearchableSnapshotShardStats::new;
+ }
+
+ @Override
+ protected SearchableSnapshotShardStats createTestInstance() {
+ SnapshotId snapshotId = new SnapshotId(randomAlphaOfLength(5), randomAlphaOfLength(5));
+ IndexId indexId = new IndexId(randomAlphaOfLength(5), randomAlphaOfLength(5));
+ ShardRouting shardRouting = TestShardRouting.newShardRouting(randomAlphaOfLength(5), randomInt(10), randomAlphaOfLength(5),
+ randomBoolean(), ShardRoutingState.STARTED);
+
+ final List inputStats = new ArrayList<>();
+ for (int j = 0; j < randomInt(20); j++) {
+ inputStats.add(randomCacheIndexInputStats());
+ }
+ return new SearchableSnapshotShardStats(shardRouting, snapshotId, indexId, inputStats);
+ }
+
+ private CacheIndexInputStats randomCacheIndexInputStats() {
+ return new CacheIndexInputStats(randomAlphaOfLength(10), randomNonNegativeLong(),
+ randomNonNegativeLong(), randomNonNegativeLong(),
+ randomCounter(), randomCounter(),
+ randomCounter(), randomCounter(),
+ randomCounter(), randomCounter(),
+ randomCounter(), randomTimedCounter(),
+ randomTimedCounter(), randomTimedCounter());
+ }
+
+ private Counter randomCounter() {
+ return new Counter(randomLong(), randomLong(), randomLong(), randomLong());
+ }
+
+ private TimedCounter randomTimedCounter() {
+ return new TimedCounter(randomLong(), randomLong(), randomLong(), randomLong(), randomLong());
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/history/SnapshotHistoryStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/history/SnapshotHistoryStoreTests.java
index 8cbe263928869..3b6a6d621532c 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/history/SnapshotHistoryStoreTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/slm/history/SnapshotHistoryStoreTests.java
@@ -40,6 +40,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.awaitLatch;
+import static org.elasticsearch.xpack.core.ilm.GenerateSnapshotNameStep.generateSnapshotName;
import static org.elasticsearch.xpack.core.ilm.LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING;
import static org.elasticsearch.xpack.core.slm.history.SnapshotHistoryStore.SLM_HISTORY_ALIAS;
import static org.elasticsearch.xpack.core.slm.history.SnapshotHistoryStore.SLM_HISTORY_INDEX_PREFIX;
@@ -75,8 +76,7 @@ public void testNoActionIfDisabled() {
String policyId = randomAlphaOfLength(5);
SnapshotLifecyclePolicy policy = randomSnapshotLifecyclePolicy(policyId);
final long timestamp = randomNonNegativeLong();
- SnapshotLifecyclePolicy.ResolverContext context = new SnapshotLifecyclePolicy.ResolverContext(timestamp);
- String snapshotId = policy.generateSnapshotName(context);
+ String snapshotId = generateSnapshotName(policy.getName());
SnapshotHistoryItem record = SnapshotHistoryItem.creationSuccessRecord(timestamp, policy, snapshotId);
client.setVerifier((a, r, l) -> {
@@ -91,8 +91,7 @@ public void testPut() throws Exception {
String policyId = randomAlphaOfLength(5);
SnapshotLifecyclePolicy policy = randomSnapshotLifecyclePolicy(policyId);
final long timestamp = randomNonNegativeLong();
- SnapshotLifecyclePolicy.ResolverContext context = new SnapshotLifecyclePolicy.ResolverContext(timestamp);
- String snapshotId = policy.generateSnapshotName(context);
+ String snapshotId = generateSnapshotName(policy.getName());
{
SnapshotHistoryItem record = SnapshotHistoryItem.creationSuccessRecord(timestamp, policy, snapshotId);
diff --git a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java
index dab88939a1494..ce591105d4f9e 100644
--- a/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java
+++ b/x-pack/plugin/ilm/qa/multi-node/src/test/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java
@@ -8,6 +8,7 @@
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
+import org.apache.http.util.EntityUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.client.Request;
@@ -25,6 +26,7 @@
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.IndexSettings;
+import org.elasticsearch.snapshots.SnapshotState;
import org.elasticsearch.test.rest.ESRestTestCase;
import org.elasticsearch.xpack.core.ilm.AllocateAction;
import org.elasticsearch.xpack.core.ilm.DeleteAction;
@@ -40,6 +42,7 @@
import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep;
import org.elasticsearch.xpack.core.ilm.ReadOnlyAction;
import org.elasticsearch.xpack.core.ilm.RolloverAction;
+import org.elasticsearch.xpack.core.ilm.SearchableSnapshotAction;
import org.elasticsearch.xpack.core.ilm.SetPriorityAction;
import org.elasticsearch.xpack.core.ilm.SetSingleNodeAllocateStep;
import org.elasticsearch.xpack.core.ilm.ShrinkAction;
@@ -55,6 +58,7 @@
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -1552,6 +1556,159 @@ public void testHaltAtEndOfPhase() throws Exception {
assertBusy(() -> assertFalse("expected " + index + " to be deleted by ILM", indexExists(index)));
}
+ public void testSearchableSnapshotAction() throws Exception {
+ String snapshotRepo = createSnapshotRepo();
+ createNewSingletonPolicy("cold", new SearchableSnapshotAction(snapshotRepo));
+
+ createIndexWithSettings(index,
+ Settings.builder()
+ .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
+ .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)
+ .put(LifecycleSettings.LIFECYCLE_NAME, policy),
+ randomBoolean());
+
+ String restoredIndexName = SearchableSnapshotAction.RESTORED_INDEX_PREFIX + this.index;
+ assertTrue(waitUntil(() -> {
+ try {
+ return indexExists(restoredIndexName);
+ } catch (IOException e) {
+ return false;
+ }
+ }, 30, TimeUnit.SECONDS));
+
+ assertBusy(() -> assertThat(explainIndex(restoredIndexName).get("step"), is(PhaseCompleteStep.NAME)), 30, TimeUnit.SECONDS);
+ }
+
+ public void testDeleteActionDeletesSearchableSnapshot() throws Exception {
+ String snapshotRepo = createSnapshotRepo();
+
+ // create policy with cold and delete phases
+ Map coldActions =
+ Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo));
+ Map phases = new HashMap<>();
+ phases.put("cold", new Phase("cold", TimeValue.ZERO, coldActions));
+ phases.put("delete", new Phase("delete", TimeValue.timeValueMillis(10000), singletonMap(DeleteAction.NAME,
+ new DeleteAction(true))));
+ LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, phases);
+ // PUT policy
+ XContentBuilder builder = jsonBuilder();
+ lifecyclePolicy.toXContent(builder, null);
+ final StringEntity entity = new StringEntity(
+ "{ \"policy\":" + Strings.toString(builder) + "}", ContentType.APPLICATION_JSON);
+ Request createPolicyRequest = new Request("PUT", "_ilm/policy/" + policy);
+ createPolicyRequest.setEntity(entity);
+ assertOK(client().performRequest(createPolicyRequest));
+
+ createIndexWithSettings(index,
+ Settings.builder()
+ .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
+ .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)
+ .put(LifecycleSettings.LIFECYCLE_NAME, policy),
+ randomBoolean());
+
+ String[] snapshotName = new String[1];
+ String restoredIndexName = SearchableSnapshotAction.RESTORED_INDEX_PREFIX + this.index;
+ assertTrue(waitUntil(() -> {
+ try {
+ Map explainIndex = explainIndex(index);
+ if(explainIndex == null) {
+ // in case we missed the original index and it was deleted
+ explainIndex = explainIndex(restoredIndexName);
+ }
+ snapshotName[0] = (String) explainIndex.get("snapshot_name");
+ return snapshotName[0] != null;
+ } catch (IOException e) {
+ return false;
+ }
+ }, 30, TimeUnit.SECONDS));
+ assertBusy(() -> assertFalse(indexExists(restoredIndexName)));
+
+ assertTrue("the snapshot we generate in the cold phase should be deleted by the delete phase", waitUntil(() -> {
+ try {
+ Request getSnapshotsRequest = new Request("GET", "_snapshot/" + snapshotRepo + "/" + snapshotName[0]);
+ Response getSnapshotsResponse = client().performRequest(getSnapshotsRequest);
+ return EntityUtils.toString(getSnapshotsResponse.getEntity()).contains("snapshot_missing_exception");
+ } catch (IOException e) {
+ return false;
+ }
+ }, 30, TimeUnit.SECONDS));
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testDeleteActionDoesntDeleteSearchableSnapshot() throws Exception {
+ String snapshotRepo = createSnapshotRepo();
+
+ // create policy with cold and delete phases
+ Map coldActions =
+ Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo));
+ Map phases = new HashMap<>();
+ phases.put("cold", new Phase("cold", TimeValue.ZERO, coldActions));
+ phases.put("delete", new Phase("delete", TimeValue.timeValueMillis(10000), singletonMap(DeleteAction.NAME,
+ new DeleteAction(false))));
+ LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, phases);
+ // PUT policy
+ XContentBuilder builder = jsonBuilder();
+ lifecyclePolicy.toXContent(builder, null);
+ final StringEntity entity = new StringEntity(
+ "{ \"policy\":" + Strings.toString(builder) + "}", ContentType.APPLICATION_JSON);
+ Request createPolicyRequest = new Request("PUT", "_ilm/policy/" + policy);
+ createPolicyRequest.setEntity(entity);
+ assertOK(client().performRequest(createPolicyRequest));
+
+ createIndexWithSettings(index,
+ Settings.builder()
+ .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1)
+ .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0)
+ .put(LifecycleSettings.LIFECYCLE_NAME, policy),
+ randomBoolean());
+
+ String[] snapshotName = new String[1];
+ String restoredIndexName = SearchableSnapshotAction.RESTORED_INDEX_PREFIX + this.index;
+ assertTrue(waitUntil(() -> {
+ try {
+ Map explainIndex = explainIndex(index);
+ if(explainIndex == null) {
+ // in case we missed the original index and it was deleted
+ explainIndex = explainIndex(restoredIndexName);
+ }
+ snapshotName[0] = (String) explainIndex.get("snapshot_name");
+ return snapshotName[0] != null;
+ } catch (IOException e) {
+ return false;
+ }
+ }, 30, TimeUnit.SECONDS));
+ assertBusy(() -> assertFalse(indexExists(restoredIndexName)));
+
+ assertTrue("the snapshot we generate in the cold phase should not be deleted by the delete phase", waitUntil(() -> {
+ try {
+ Request getSnapshotsRequest = new Request("GET", "_snapshot/" + snapshotRepo + "/" + snapshotName[0]);
+ Response getSnapshotsResponse = client().performRequest(getSnapshotsRequest);
+ Map snapshotsResponseMap;
+ try (InputStream is = getSnapshotsResponse.getEntity().getContent()) {
+ snapshotsResponseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true);
+ }
+ ArrayList