diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteAction.kt index 89eaca62c..ede0a5faf 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteAction.kt @@ -16,6 +16,9 @@ import org.opensearch.indexmanagement.spi.indexstatemanagement.model.StepContext class ConvertIndexToRemoteAction( val repository: String, val snapshot: String, + val includeAliases: Boolean = false, + val ignoreIndexSettings: String = "", + val numberOfReplicas: Int = 0, index: Int, ) : Action(name, index) { @@ -23,6 +26,9 @@ class ConvertIndexToRemoteAction( const val name = "convert_index_to_remote" const val REPOSITORY_FIELD = "repository" const val SNAPSHOT_FIELD = "snapshot" + const val INCLUDE_ALIASES_FIELD = "include_aliases" + const val IGNORE_INDEX_SETTINGS_FIELD = "ignore_index_settings" + const val NUMBER_OF_REPLICAS_FIELD = "number_of_replicas" } private val attemptRestoreStep = AttemptRestoreStep(this) @@ -37,12 +43,18 @@ class ConvertIndexToRemoteAction( builder.startObject(type) builder.field(REPOSITORY_FIELD, repository) builder.field(SNAPSHOT_FIELD, snapshot) + builder.field(INCLUDE_ALIASES_FIELD, includeAliases) + builder.field(IGNORE_INDEX_SETTINGS_FIELD, ignoreIndexSettings) + builder.field(NUMBER_OF_REPLICAS_FIELD, numberOfReplicas) builder.endObject() } override fun populateAction(out: StreamOutput) { out.writeString(repository) out.writeString(snapshot) + out.writeBoolean(includeAliases) + out.writeString(ignoreIndexSettings) + out.writeInt(numberOfReplicas) out.writeInt(actionIndex) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionParser.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionParser.kt index dfe97111d..9d81f79e3 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionParser.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionParser.kt @@ -9,6 +9,9 @@ import org.opensearch.core.common.io.stream.StreamInput import org.opensearch.core.xcontent.XContentParser import org.opensearch.core.xcontent.XContentParser.Token import org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken +import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction.Companion.IGNORE_INDEX_SETTINGS_FIELD +import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction.Companion.INCLUDE_ALIASES_FIELD +import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction.Companion.NUMBER_OF_REPLICAS_FIELD import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction.Companion.REPOSITORY_FIELD import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction.Companion.SNAPSHOT_FIELD import org.opensearch.indexmanagement.spi.indexstatemanagement.Action @@ -18,13 +21,19 @@ class ConvertIndexToRemoteActionParser : ActionParser() { override fun fromStreamInput(sin: StreamInput): Action { val repository = sin.readString() val snapshot = sin.readString() + val includeAliases = sin.readBoolean() + val ignoreIndexSettings = sin.readString() + val numberOfReplicas = sin.readInt() val index = sin.readInt() - return ConvertIndexToRemoteAction(repository, snapshot, index) + return ConvertIndexToRemoteAction(repository, snapshot, includeAliases, ignoreIndexSettings, numberOfReplicas, index) } override fun fromXContent(xcp: XContentParser, index: Int): Action { var repository: String? = null var snapshot: String? = null + var includeAliases: Boolean = false + var ignoreIndexSettings: String = "" + var numberOfReplicas: Int = 0 ensureExpectedToken(Token.START_OBJECT, xcp.currentToken(), xcp) while (xcp.nextToken() != Token.END_OBJECT) { @@ -34,6 +43,9 @@ class ConvertIndexToRemoteActionParser : ActionParser() { when (fieldName) { REPOSITORY_FIELD -> repository = xcp.text() SNAPSHOT_FIELD -> snapshot = xcp.text() + INCLUDE_ALIASES_FIELD -> includeAliases = xcp.booleanValue() + IGNORE_INDEX_SETTINGS_FIELD -> ignoreIndexSettings = xcp.text() + NUMBER_OF_REPLICAS_FIELD -> numberOfReplicas = xcp.intValue() else -> throw IllegalArgumentException("Invalid field: [$fieldName] found in ConvertIndexToRemoteAction.") } } @@ -41,6 +53,9 @@ class ConvertIndexToRemoteActionParser : ActionParser() { return ConvertIndexToRemoteAction( repository = requireNotNull(repository) { "ConvertIndexToRemoteAction repository must be specified" }, snapshot = requireNotNull(snapshot) { "ConvertIndexToRemoteAction snapshot must be specified" }, + includeAliases = includeAliases, + ignoreIndexSettings = ignoreIndexSettings, + numberOfReplicas = numberOfReplicas, index = index, ) } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/restore/AttemptRestoreStep.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/restore/AttemptRestoreStep.kt index 1038d7238..cb3d8551a 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/restore/AttemptRestoreStep.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/restore/AttemptRestoreStep.kt @@ -11,6 +11,12 @@ import org.opensearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest import org.opensearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse +import org.opensearch.action.admin.indices.delete.DeleteIndexRequest +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsResponse +import org.opensearch.action.support.clustermanager.AcknowledgedResponse +import org.opensearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_REPLICAS +import org.opensearch.common.settings.Settings import org.opensearch.core.rest.RestStatus import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction import org.opensearch.indexmanagement.opensearchapi.convertToMap @@ -18,6 +24,7 @@ import org.opensearch.indexmanagement.opensearchapi.suspendUntil import org.opensearch.indexmanagement.spi.indexstatemanagement.Step import org.opensearch.indexmanagement.spi.indexstatemanagement.model.ActionProperties import org.opensearch.indexmanagement.spi.indexstatemanagement.model.ManagedIndexMetaData +import org.opensearch.indexmanagement.spi.indexstatemanagement.model.StepContext import org.opensearch.indexmanagement.spi.indexstatemanagement.model.StepMetaData import org.opensearch.script.Script import org.opensearch.script.ScriptService @@ -34,7 +41,7 @@ class AttemptRestoreStep(private val action: ConvertIndexToRemoteAction) : Step( private var info: Map? = null private var snapshotName: String? = null - @Suppress("TooGenericExceptionCaught", "ComplexMethod", "ReturnCount", "LongMethod") + @Suppress("TooGenericExceptionCaught", "ComplexMethod", "ReturnCount", "LongMethod", "NestedBlockDepth") override suspend fun execute(): Step { val context = this.context ?: return this val managedIndexMetadata = context.metadata @@ -44,7 +51,7 @@ class AttemptRestoreStep(private val action: ConvertIndexToRemoteAction) : Step( val snapshot = action.snapshot try { - val mutableInfo = mutableMapOf() + val mutableInfo = mutableMapOf() val snapshotScript = Script(ScriptType.INLINE, Script.DEFAULT_TEMPLATE_LANG, snapshot, mapOf()) val defaultSnapshotPattern = snapshot.ifBlank { indexName } val snapshotPattern = compileTemplate(snapshotScript, managedIndexMetadata, defaultSnapshotPattern, scriptService) @@ -86,29 +93,17 @@ class AttemptRestoreStep(private val action: ConvertIndexToRemoteAction) : Step( // Use the snapshot name from the selected SnapshotInfo snapshotName = latestSnapshotInfo.snapshotId().name - // Proceed with the restore operation - val restoreSnapshotRequest = RestoreSnapshotRequest(repository, snapshotName) - .indices(indexName) - .storageType(RestoreSnapshotRequest.StorageType.REMOTE_SNAPSHOT) - .renamePattern("^(.*)\$") - .renameReplacement("$1_remote") - .waitForCompletion(false) - val response: RestoreSnapshotResponse = context.client.admin().cluster().suspendUntil { - restoreSnapshot(restoreSnapshotRequest, it) - } + val remoteIndexName = "${indexName}_remote" + + // Check if remote index exists + val remoteIndexExists = checkRemoteIndexExists(context, remoteIndexName) - when (response.status()) { - RestStatus.ACCEPTED, RestStatus.OK -> { - stepStatus = StepStatus.COMPLETED - mutableInfo["message"] = getSuccessMessage(indexName) - } - else -> { - val message = getFailedMessage(indexName, "Unexpected response status: ${response.status()}") - logger.warn("$message - $response") - stepStatus = StepStatus.FAILED - mutableInfo["message"] = message - mutableInfo["cause"] = response.toString() - } + if (remoteIndexExists) { + // Restore completed, mark as completed + stepStatus = StepStatus.COMPLETED + mutableInfo["message"] = getSuccessMessage(indexName) + } else { + performRestore(context, indexName, repository, snapshotName, mutableInfo) } info = mutableInfo.toMap() } catch (e: RemoteTransportException) { @@ -127,6 +122,87 @@ class AttemptRestoreStep(private val action: ConvertIndexToRemoteAction) : Step( return this } + private suspend fun checkRemoteIndexExists(context: StepContext, remoteIndexName: String): Boolean = + try { + val existsResponse: IndicesExistsResponse = context.client.admin().indices().suspendUntil { + exists(IndicesExistsRequest(remoteIndexName), it) + } + existsResponse.isExists + } catch (e: Exception) { + // Index doesn't exist yet + false + } + + private suspend fun performRestore( + context: StepContext, + indexName: String, + repository: String, + snapshotName: String?, + mutableInfo: MutableMap, + ) { + val remoteIndexName = "${indexName}_remote" + // Proceed with the restore operation + val restoreSnapshotRequest = RestoreSnapshotRequest(repository, snapshotName) + .indices(indexName) + .storageType(RestoreSnapshotRequest.StorageType.REMOTE_SNAPSHOT) + .renamePattern("^(.*)\$") + .renameReplacement("$1_remote") + .waitForCompletion(false) + .includeAliases(action.includeAliases) + .ignoreIndexSettings(action.ignoreIndexSettings) + + // Set number_of_replicas (defaults to 0 if not specified) + val indexSettings = Settings.builder() + .put(SETTING_NUMBER_OF_REPLICAS, action.numberOfReplicas) + .build() + restoreSnapshotRequest.indexSettings(indexSettings) + + val response: RestoreSnapshotResponse = context.client.admin().cluster().suspendUntil { + restoreSnapshot(restoreSnapshotRequest, it) + } + + when (response.status()) { + RestStatus.ACCEPTED, RestStatus.OK -> { + deleteOriginalIndex(context, indexName, mutableInfo) + // Mark as waiting for completion + stepStatus = StepStatus.CONDITION_NOT_MET + mutableInfo["message"] = "Waiting for remote index [$remoteIndexName] to be created" + logger.info("Restore accepted for snapshot [$snapshotName], waiting for remote index [$remoteIndexName] to be created") + } + else -> { + val message = getFailedMessage(indexName, "Unexpected response status: ${response.status()}") + logger.warn("$message - $response") + stepStatus = StepStatus.FAILED + mutableInfo["message"] = message + mutableInfo["cause"] = response.toString() + } + } + } + + private suspend fun deleteOriginalIndex( + context: StepContext, + indexName: String, + mutableInfo: MutableMap, + ) { + // Restore accepted, delete original index + try { + val deleteResponse: AcknowledgedResponse = context.client.admin().indices().suspendUntil { + delete(DeleteIndexRequest(indexName), it) + } + if (deleteResponse.isAcknowledged) { + logger.info("Successfully deleted original index [$indexName] after restore was accepted") + mutableInfo["deleted_original_index"] = true + } else { + logger.warn("Delete request for original index [$indexName] was not acknowledged") + mutableInfo["deleted_original_index"] = false + } + } catch (e: Exception) { + logger.warn("Failed to delete original index [$indexName] after restore was accepted: ${e.message}", e) + mutableInfo["deleted_original_index"] = false + mutableInfo["delete_error"] = e.message ?: "Unknown error" + } + } + private fun compileTemplate( template: Script, managedIndexMetaData: ManagedIndexMetaData, diff --git a/src/main/resources/mappings/opendistro-ism-config.json b/src/main/resources/mappings/opendistro-ism-config.json index 6250ba15d..25cabdbd5 100644 --- a/src/main/resources/mappings/opendistro-ism-config.json +++ b/src/main/resources/mappings/opendistro-ism-config.json @@ -1,6 +1,6 @@ { "_meta" : { - "schema_version": 24 + "schema_version": 25 }, "dynamic": "strict", "properties": { @@ -226,6 +226,15 @@ }, "snapshot": { "type": "keyword" + }, + "include_aliases": { + "type": "boolean" + }, + "ignore_index_settings": { + "type": "keyword" + }, + "number_of_replicas": { + "type": "integer" } } }, diff --git a/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt index 4a18d5c9a..ee9d4d2a5 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt @@ -41,7 +41,7 @@ import javax.management.remote.JMXConnectorFactory import javax.management.remote.JMXServiceURL abstract class IndexManagementRestTestCase : ODFERestTestCase() { - val configSchemaVersion = 24 + val configSchemaVersion = 25 val historySchemaVersion = 7 // Having issues with tests leaking into other tests and mappings being incorrect and they are not caught by any pending task wait check as diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt index f52481878..c75d8c772 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt @@ -239,7 +239,7 @@ fun randomTemplateScript( fun randomSnapshotActionConfig(repository: String = "repo", snapshot: String = "sp"): SnapshotAction = SnapshotAction(repository, snapshot, index = 0) -fun randomRestoreActionConfig(repository: String = "repo", snapshot: String = "sp"): ConvertIndexToRemoteAction = ConvertIndexToRemoteAction(repository, snapshot, index = 0) +fun randomRestoreActionConfig(repository: String = "repo", snapshot: String = "sp"): ConvertIndexToRemoteAction = ConvertIndexToRemoteAction(repository, snapshot, includeAliases = false, ignoreIndexSettings = "", numberOfReplicas = 0, index = 0) /** * Helper functions for creating a random Conditions object @@ -471,6 +471,11 @@ fun AliasAction.toJsonString(): String { return this.toXContent(builder, ToXContent.EMPTY_PARAMS).string() } +fun ConvertIndexToRemoteAction.toJsonString(): String { + val builder = XContentFactory.jsonBuilder() + return this.toXContent(builder, ToXContent.EMPTY_PARAMS).string() +} + fun ISMTemplate.toJsonString(): String { val builder = XContentFactory.jsonBuilder() return this.toXContent(builder, ToXContent.EMPTY_PARAMS).string() diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionIT.kt index b3dc64de3..ff006becf 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ConvertIndexToRemoteActionIT.kt @@ -5,6 +5,8 @@ package org.opensearch.indexmanagement.indexstatemanagement.action +import org.opensearch.client.ResponseException +import org.opensearch.core.rest.RestStatus import org.opensearch.indexmanagement.indexstatemanagement.IndexStateManagementRestTestCase import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.model.State @@ -41,7 +43,8 @@ class ConvertIndexToRemoteActionIT : IndexStateManagementRestTestCase() { val convertAction = ConvertIndexToRemoteAction( repository = repository, snapshot = indexName, - 0, + numberOfReplicas = 0, + index = 0, ) val snapshotState = State( @@ -79,6 +82,7 @@ class ConvertIndexToRemoteActionIT : IndexStateManagementRestTestCase() { explainMetaData.info?.get("message"), ) } + // Change the start time so attempt snapshot step will execute updateManagedIndexConfigStartTime(managedIndexConfig) waitFor { val explainMetaData = getExplainManagedIndexMetaData(indexName) @@ -87,6 +91,8 @@ class ConvertIndexToRemoteActionIT : IndexStateManagementRestTestCase() { explainMetaData.info?.get("message"), ) } + + // Change the start time so wait for snapshot step will execute updateManagedIndexConfigStartTime(managedIndexConfig) waitFor { val explainMetaData = getExplainManagedIndexMetaData(indexName) @@ -96,21 +102,46 @@ class ConvertIndexToRemoteActionIT : IndexStateManagementRestTestCase() { ) } + // Change the start time so transition will execute updateManagedIndexConfigStartTime(managedIndexConfig) waitFor { val explainMetaData = getExplainManagedIndexMetaData(indexName) - assertEquals( - "Transitioning to convertToRemoteState [index=convertindextoremoteactionit_index_snapshot_and_convert]", - explainMetaData.info?.get("message"), + // Check if we've transitioned to convertToRemoteState or restore step has started + val message = explainMetaData.info?.get("message") as? String + val stateName = explainMetaData.stateMetaData?.name + assertTrue( + "Expected transition to convertToRemoteState or restore step, but got message: $message, state: $stateName", + message == AttemptRestoreStep.getSuccessMessage(indexName) || + stateName == "convertToRemoteState" || + message == "Transitioning to convertToRemoteState [index=convertindextoremoteactionit_index_snapshot_and_convert]", ) } - updateManagedIndexConfigStartTime(managedIndexConfig) - waitFor { - val explainMetaData = getExplainManagedIndexMetaData(indexName) - assertEquals( - AttemptRestoreStep.getSuccessMessage(indexName), - explainMetaData.info?.get("message"), - ) + + // If we're in convertToRemoteState but restore hasn't started yet, trigger another execution + // Note: The original index may be deleted after restore, so we need to handle that case + try { + val explainAfterTransition = getExplainManagedIndexMetaData(indexName) + if (explainAfterTransition.info?.get("message") != AttemptRestoreStep.getSuccessMessage(indexName)) { + updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { + try { + val explainMetaData = getExplainManagedIndexMetaData(indexName) + assertEquals( + AttemptRestoreStep.getSuccessMessage(indexName), + explainMetaData.info?.get("message"), + ) + } catch (e: ResponseException) { + handleIndexDeletedException(e) + // Index was deleted, which is expected - restore succeeded + // Just verify remote index exists below + return@waitFor + } + } + } + } catch (e: ResponseException) { + handleIndexDeletedException(e) + // Index was deleted, which is expected - restore succeeded + // Continue to verify remote index exists } val remoteIndexName = "${indexName}_remote" @@ -119,4 +150,18 @@ class ConvertIndexToRemoteActionIT : IndexStateManagementRestTestCase() { val isRemote = isIndexRemote(remoteIndexName) assertTrue("Index $remoteIndexName is not a remote index", isRemote) } + + private fun handleIndexDeletedException(e: ResponseException) { + // If we get a 400 "no documents to get", the index was deleted (expected after restore) + if (e.response.restStatus() == RestStatus.BAD_REQUEST) { + val errorBody = e.response.asMap() + val error = errorBody["error"] as? Map<*, *> + val reason = error?.get("reason") as? String + if (reason?.contains("no documents to get") != true) { + throw e // Re-throw if it's a different error + } + } else { + throw e // Re-throw if it's not a 400 + } + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ActionTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ActionTests.kt index ef08a1651..4992beaed 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ActionTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ActionTests.kt @@ -16,6 +16,7 @@ import org.opensearch.core.common.io.stream.InputStreamStreamInput import org.opensearch.core.common.io.stream.OutputStreamStreamOutput import org.opensearch.core.common.unit.ByteSizeValue import org.opensearch.indexmanagement.indexstatemanagement.ISMActionsParser +import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction import org.opensearch.indexmanagement.indexstatemanagement.action.DeleteAction import org.opensearch.indexmanagement.indexstatemanagement.action.NotificationAction import org.opensearch.indexmanagement.indexstatemanagement.randomAllocationActionConfig @@ -31,6 +32,7 @@ import org.opensearch.indexmanagement.indexstatemanagement.randomOpenActionConfi import org.opensearch.indexmanagement.indexstatemanagement.randomReadOnlyActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomReadWriteActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomReplicaCountActionConfig +import org.opensearch.indexmanagement.indexstatemanagement.randomRestoreActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomRolloverActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomRollupActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomShrinkAction @@ -161,6 +163,58 @@ class ActionTests : OpenSearchTestCase() { roundTripAction(randomShrinkAction()) } + fun `test convert index to remote action round trip with defaults`() { + roundTripAction(randomRestoreActionConfig()) + } + + fun `test convert index to remote action round trip with all fields`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "test-repo", + snapshot = "test-snapshot", + includeAliases = true, + ignoreIndexSettings = "index.refresh_interval,index.number_of_replicas", + numberOfReplicas = 2, + index = 0, + ) + roundTripAction(convertAction) + } + + fun `test convert index to remote action round trip with includeAliases only`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "test-repo", + snapshot = "test-snapshot", + includeAliases = true, + ignoreIndexSettings = "", + numberOfReplicas = 0, + index = 0, + ) + roundTripAction(convertAction) + } + + fun `test convert index to remote action round trip with ignoreIndexSettings only`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "test-repo", + snapshot = "test-snapshot", + includeAliases = false, + ignoreIndexSettings = "index.refresh_interval", + numberOfReplicas = 0, + index = 0, + ) + roundTripAction(convertAction) + } + + fun `test convert index to remote action round trip with numberOfReplicas only`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "test-repo", + snapshot = "test-snapshot", + includeAliases = false, + ignoreIndexSettings = "", + numberOfReplicas = 3, + index = 0, + ) + roundTripAction(convertAction) + } + fun `test action timeout and retry round trip`() { val builder = XContentFactory.jsonBuilder() diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/XContentTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/XContentTests.kt index be4e95845..d5dd97a75 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/XContentTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/XContentTests.kt @@ -10,6 +10,7 @@ import org.opensearch.common.xcontent.XContentType import org.opensearch.core.xcontent.XContentParser import org.opensearch.indexmanagement.common.model.notification.Channel import org.opensearch.indexmanagement.indexstatemanagement.ISMActionsParser +import org.opensearch.indexmanagement.indexstatemanagement.action.ConvertIndexToRemoteAction import org.opensearch.indexmanagement.indexstatemanagement.action.RollupAction import org.opensearch.indexmanagement.indexstatemanagement.model.destination.DestinationType import org.opensearch.indexmanagement.indexstatemanagement.nonNullRandomConditions @@ -30,6 +31,7 @@ import org.opensearch.indexmanagement.indexstatemanagement.randomPolicy import org.opensearch.indexmanagement.indexstatemanagement.randomReadOnlyActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomReadWriteActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomReplicaCountActionConfig +import org.opensearch.indexmanagement.indexstatemanagement.randomRestoreActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomRolloverActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomRollupActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomShrinkAction @@ -170,6 +172,96 @@ class XContentTests : OpenSearchTestCase() { ) } + fun `test convert index to remote action config parsing with defaults`() { + val convertAction = randomRestoreActionConfig("repository", "snapshot") + + val convertActionString = convertAction.toJsonString() + val parsedConvertAction = ISMActionsParser.instance.parse(parser(convertActionString), 0) + assertEquals( + "Round tripping ConvertIndexToRemoteAction doesn't work with defaults", + convertAction.convertToMap(), parsedConvertAction.convertToMap(), + ) + } + + fun `test convert index to remote action config parsing with include_aliases and ignore_index_settings`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "repository", + snapshot = "snapshot", + includeAliases = true, + ignoreIndexSettings = "index.refresh_interval,index.number_of_replicas", + numberOfReplicas = 0, + index = 0, + ) + + val convertActionString = convertAction.toJsonString() + val parsedConvertAction = ISMActionsParser.instance.parse(parser(convertActionString), 0) as ConvertIndexToRemoteAction + assertEquals( + "Round tripping ConvertIndexToRemoteAction doesn't work with include_aliases and ignore_index_settings", + convertAction.convertToMap(), parsedConvertAction.convertToMap(), + ) + assertEquals("includeAliases should be true", true, parsedConvertAction.includeAliases) + assertEquals("ignoreIndexSettings should match", "index.refresh_interval,index.number_of_replicas", parsedConvertAction.ignoreIndexSettings) + } + + fun `test convert index to remote action config parsing with only include_aliases`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "repository", + snapshot = "snapshot", + includeAliases = true, + ignoreIndexSettings = "", + numberOfReplicas = 0, + index = 0, + ) + + val convertActionString = convertAction.toJsonString() + val parsedConvertAction = ISMActionsParser.instance.parse(parser(convertActionString), 0) as ConvertIndexToRemoteAction + assertEquals( + "Round tripping ConvertIndexToRemoteAction doesn't work with only include_aliases", + convertAction.convertToMap(), parsedConvertAction.convertToMap(), + ) + assertEquals("includeAliases should be true", true, parsedConvertAction.includeAliases) + assertEquals("ignoreIndexSettings should be empty", "", parsedConvertAction.ignoreIndexSettings) + } + + fun `test convert index to remote action config parsing with only ignore_index_settings`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "repository", + snapshot = "snapshot", + includeAliases = false, + ignoreIndexSettings = "index.refresh_interval", + numberOfReplicas = 0, + index = 0, + ) + + val convertActionString = convertAction.toJsonString() + val parsedConvertAction = ISMActionsParser.instance.parse(parser(convertActionString), 0) as ConvertIndexToRemoteAction + assertEquals( + "Round tripping ConvertIndexToRemoteAction doesn't work with only ignore_index_settings", + convertAction.convertToMap(), parsedConvertAction.convertToMap(), + ) + assertEquals("includeAliases should be false", false, parsedConvertAction.includeAliases) + assertEquals("ignoreIndexSettings should match", "index.refresh_interval", parsedConvertAction.ignoreIndexSettings) + } + + fun `test convert index to remote action config parsing with number_of_replicas`() { + val convertAction = ConvertIndexToRemoteAction( + repository = "repository", + snapshot = "snapshot", + includeAliases = false, + ignoreIndexSettings = "", + numberOfReplicas = 2, + index = 0, + ) + + val convertActionString = convertAction.toJsonString() + val parsedConvertAction = ISMActionsParser.instance.parse(parser(convertActionString), 0) as ConvertIndexToRemoteAction + assertEquals( + "Round tripping ConvertIndexToRemoteAction doesn't work with number_of_replicas", + convertAction.convertToMap(), parsedConvertAction.convertToMap(), + ) + assertEquals("numberOfReplicas should be 2", 2, parsedConvertAction.numberOfReplicas) + } + fun `test allocation action config parsing`() { val allocationAction = randomAllocationActionConfig( diff --git a/src/test/resources/mappings/cached-opendistro-ism-config.json b/src/test/resources/mappings/cached-opendistro-ism-config.json index 6250ba15d..25cabdbd5 100644 --- a/src/test/resources/mappings/cached-opendistro-ism-config.json +++ b/src/test/resources/mappings/cached-opendistro-ism-config.json @@ -1,6 +1,6 @@ { "_meta" : { - "schema_version": 24 + "schema_version": 25 }, "dynamic": "strict", "properties": { @@ -226,6 +226,15 @@ }, "snapshot": { "type": "keyword" + }, + "include_aliases": { + "type": "boolean" + }, + "ignore_index_settings": { + "type": "keyword" + }, + "number_of_replicas": { + "type": "integer" } } },