diff --git a/spi/src/main/kotlin/org.opensearch.indexmanagement.spi/indexstatemanagement/Action.kt b/spi/src/main/kotlin/org.opensearch.indexmanagement.spi/indexstatemanagement/Action.kt index d3d68d58d..58780b6ca 100644 --- a/spi/src/main/kotlin/org.opensearch.indexmanagement.spi/indexstatemanagement/Action.kt +++ b/spi/src/main/kotlin/org.opensearch.indexmanagement.spi/indexstatemanagement/Action.kt @@ -21,13 +21,17 @@ abstract class Action( var configTimeout: ActionTimeout? = null var configRetry: ActionRetry? = ActionRetry(DEFAULT_RETRIES) + var customAction: Boolean = false final override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { builder.startObject() configTimeout?.toXContent(builder, params) configRetry?.toXContent(builder, params) - // TODO: We should add "custom" object wrapper based on the params + // Include a "custom" object wrapper for custom actions to allow extensions to put arbitrary action configs in the config + // index. The EXCLUDE_CUSTOM_FIELD_PARAM is used to not include this wrapper in api responses + if (customAction && !params.paramAsBoolean(EXCLUDE_CUSTOM_FIELD_PARAM, false)) builder.startObject(CUSTOM_ACTION_FIELD) populateAction(builder, params) + if (customAction && !params.paramAsBoolean(EXCLUDE_CUSTOM_FIELD_PARAM, false)) builder.endObject() return builder.endObject() } @@ -70,5 +74,7 @@ abstract class Action( companion object { const val DEFAULT_RETRIES = 3L + const val CUSTOM_ACTION_FIELD = "custom" + const val EXCLUDE_CUSTOM_FIELD_PARAM = "exclude_custom" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMActionsParser.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMActionsParser.kt index 35c947655..d5d3d4245 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMActionsParser.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMActionsParser.kt @@ -32,7 +32,6 @@ class ISMActionsParser private constructor() { val instance = ISMActionsParser() } - // TODO: Add other action parsers as they are implemented val parsers = mutableListOf( AllocationActionParser(), CloseActionParser(), @@ -50,6 +49,9 @@ class ISMActionsParser private constructor() { ) fun addParser(parser: ActionParser) { + if (parsers.map { it.getActionType() }.contains(parser.getActionType())) { + throw IllegalArgumentException(getDuplicateActionTypesMessage(parser.getActionType())) + } parsers.add(parser) } @@ -77,13 +79,17 @@ class ISMActionsParser private constructor() { when (type) { ActionTimeout.TIMEOUT_FIELD -> timeout = ActionTimeout.parse(xcp) ActionRetry.RETRY_FIELD -> retry = ActionRetry.parse(xcp) + Action.CUSTOM_ACTION_FIELD -> { + // The "custom" wrapper allows extensions to create arbitrary actions without updating the config mappings + // We consume the full custom wrapper and parse the action in this step + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp) + val customActionType = xcp.currentName() + XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp) + action = parseAction(xcp, totalActions, customActionType) + XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, xcp.nextToken(), xcp) + } else -> { - val parser = parsers.firstOrNull { it.getActionType() == type } - if (parser != null) { - action = parser.fromXContent(xcp, totalActions) - } else { - throw IllegalArgumentException("Invalid field: [$type] found in Actions.") - } + action = parseAction(xcp, totalActions, type) } } } @@ -96,7 +102,21 @@ class ISMActionsParser private constructor() { return action } + private fun parseAction(xcp: XContentParser, totalActions: Int, type: String): Action { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) + val action: Action? + val parser = parsers.firstOrNull { it.getActionType() == type } + if (parser != null) { + action = parser.fromXContent(xcp, totalActions) + action.customAction = parser.customAction + } else { + throw IllegalArgumentException("Invalid field: [$type] found in Actions.") + } + return action + } + companion object { val instance: ISMActionsParser by lazy { HOLDER.instance } + fun getDuplicateActionTypesMessage(actionType: String) = "Multiple action parsers attempted to register the same action type [$actionType]" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/Policy.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/Policy.kt index 0ef18d1b0..432fa8e0d 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/Policy.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/Policy.kt @@ -68,7 +68,9 @@ data class Policy( .field(SCHEMA_VERSION_FIELD, schemaVersion) .field(ERROR_NOTIFICATION_FIELD, errorNotification) .field(DEFAULT_STATE_FIELD, defaultState) - .field(STATES_FIELD, states.toTypedArray()) + .startArray(STATES_FIELD) + .also { states.forEach { state -> state.toXContent(it, params) } } + .endArray() .optionalISMTemplateField(ISM_TEMPLATE, ismTemplate) if (params.paramAsBoolean(WITH_USER, true)) builder.optionalUserField(USER_FIELD, user) if (params.paramAsBoolean(WITH_TYPE, true)) builder.endObject() diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/State.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/State.kt index 7277d1fb0..43d28b454 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/State.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/State.kt @@ -45,7 +45,9 @@ data class State( builder .startObject() .field(NAME_FIELD, name) - .field(ACTIONS_FIELD, actions.toTypedArray()) + .startArray(ACTIONS_FIELD) + .also { actions.forEach { action -> action.toXContent(it, params) } } + .endArray() .field(TRANSITIONS_FIELD, transitions.toTypedArray()) .endObject() return builder diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponse.kt index 300f48356..cf5e038b7 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponse.kt @@ -12,7 +12,9 @@ import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.indexmanagement.indexstatemanagement.model.Policy -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_USER +import org.opensearch.indexmanagement.spi.indexstatemanagement.Action import org.opensearch.indexmanagement.util._ID import org.opensearch.indexmanagement.util._PRIMARY_TERM import org.opensearch.indexmanagement.util._SEQ_NO @@ -43,6 +45,7 @@ class GetPoliciesResponse : ActionResponse, ToXContentObject { } override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + val policyParams = ToXContent.MapParams(mapOf(WITH_TYPE to "false", WITH_USER to "false", Action.EXCLUDE_CUSTOM_FIELD_PARAM to "true")) return builder.startObject() .startArray("policies") .apply { @@ -51,7 +54,7 @@ class GetPoliciesResponse : ActionResponse, ToXContentObject { .field(_ID, policy.id) .field(_SEQ_NO, policy.seqNo) .field(_PRIMARY_TERM, policy.primaryTerm) - .field(Policy.POLICY_TYPE, policy, XCONTENT_WITHOUT_TYPE_AND_USER) + .field(Policy.POLICY_TYPE, policy, policyParams) .endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponse.kt index 43eef2d5b..d632c5453 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponse.kt @@ -12,7 +12,9 @@ import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.indexmanagement.indexstatemanagement.model.Policy -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_USER +import org.opensearch.indexmanagement.spi.indexstatemanagement.Action import org.opensearch.indexmanagement.util._ID import org.opensearch.indexmanagement.util._PRIMARY_TERM import org.opensearch.indexmanagement.util._SEQ_NO @@ -65,7 +67,8 @@ class GetPolicyResponse : ActionResponse, ToXContentObject { .field(_SEQ_NO, seqNo) .field(_PRIMARY_TERM, primaryTerm) if (policy != null) { - builder.field(Policy.POLICY_TYPE, policy, XCONTENT_WITHOUT_TYPE_AND_USER) + val policyParams = ToXContent.MapParams(mapOf(WITH_TYPE to "false", WITH_USER to "false", Action.EXCLUDE_CUSTOM_FIELD_PARAM to "true")) + builder.field(Policy.POLICY_TYPE, policy, policyParams) } return builder.endObject() diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponse.kt index 20546f2dd..226359165 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponse.kt @@ -12,7 +12,8 @@ import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.indexmanagement.indexstatemanagement.model.Policy -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_USER +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_USER +import org.opensearch.indexmanagement.spi.indexstatemanagement.Action.Companion.EXCLUDE_CUSTOM_FIELD_PARAM import org.opensearch.indexmanagement.util._ID import org.opensearch.indexmanagement.util._PRIMARY_TERM import org.opensearch.indexmanagement.util._SEQ_NO @@ -66,12 +67,13 @@ class IndexPolicyResponse : ActionResponse, ToXContentObject { } override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + val policyParams = ToXContent.MapParams(mapOf(WITH_USER to "false", EXCLUDE_CUSTOM_FIELD_PARAM to "true")) return builder.startObject() .field(_ID, id) .field(_VERSION, version) .field(_PRIMARY_TERM, primaryTerm) .field(_SEQ_NO, seqNo) - .field(Policy.POLICY_TYPE, policy, XCONTENT_WITHOUT_USER) + .field(Policy.POLICY_TYPE, policy, policyParams) .endObject() } } diff --git a/src/main/resources/mappings/opendistro-ism-config.json b/src/main/resources/mappings/opendistro-ism-config.json index 2144b3ce3..5c93e39c5 100644 --- a/src/main/resources/mappings/opendistro-ism-config.json +++ b/src/main/resources/mappings/opendistro-ism-config.json @@ -429,6 +429,10 @@ } } } + }, + "custom": { + "enabled": false, + "type": "object" } } }, diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/extension/ISMActionsParserTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/extension/ISMActionsParserTests.kt new file mode 100644 index 000000000..4eae87889 --- /dev/null +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/extension/ISMActionsParserTests.kt @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.indexmanagement.indexstatemanagement.extension + +import org.junit.After +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentType +import org.opensearch.indexmanagement.indexstatemanagement.ISMActionsParser +import org.opensearch.indexmanagement.opensearchapi.convertToMap +import org.opensearch.indexmanagement.opensearchapi.string +import org.opensearch.test.OpenSearchTestCase +import kotlin.test.assertFailsWith + +class ISMActionsParserTests : OpenSearchTestCase() { + + /* + * If any tests added the custom action parser, it should be removed from the static instance to not impact other tests + */ + @After + @Suppress("UnusedPrivateMember") + private fun removeCustomActionParser() { + ISMActionsParser.instance.parsers.removeIf { it.getActionType() == SampleCustomActionParser.SampleCustomAction.name } + } + + fun `test duplicate action names fail`() { + val customActionParser = SampleCustomActionParser() + customActionParser.customAction = true + // Duplicate custom parser names should fail + ISMActionsParser.instance.addParser(customActionParser) + assertFailsWith("Expected IllegalArgumentException for duplicate action names") { + ISMActionsParser.instance.addParser(customActionParser) + } + // Adding any duplicate parser should fail + assertFailsWith("Expected IllegalArgumentException for duplicate action names") { + val randomExistingParser = ISMActionsParser.instance.parsers.random() + ISMActionsParser.instance.addParser(randomExistingParser) + } + } + + fun `test custom action parsing`() { + val customActionParser = SampleCustomActionParser() + customActionParser.customAction = true + ISMActionsParser.instance.addParser(customActionParser) + val customAction = SampleCustomActionParser.SampleCustomAction(randomInt(), 0) + val builder = XContentFactory.jsonBuilder() + + val customActionString = customAction.toXContent(builder, ToXContent.EMPTY_PARAMS).string() + val parser = XContentType.JSON.xContent().createParser(xContentRegistry(), LoggingDeprecationHandler.INSTANCE, customActionString) + parser.nextToken() + val parsedCustomAction = ISMActionsParser.instance.parse(parser, 1) + assertTrue("Action was not set to be custom after parsing", parsedCustomAction.customAction) + customAction.customAction = true + assertEquals("Round tripping custom action doesn't work", customAction.convertToMap(), parsedCustomAction.convertToMap()) + } + + fun `test parsing custom action without custom flag`() { + val customActionParser = SampleCustomActionParser() + customActionParser.customAction = true + ISMActionsParser.instance.addParser(customActionParser) + val customAction = SampleCustomActionParser.SampleCustomAction(randomInt(), 0) + customAction.customAction = true + + val customActionString = "{\"retry\":{\"count\":3,\"backoff\":\"exponential\",\"delay\":\"1m\"},\"some_custom_action\":{\"some_int_field\":${customAction.someInt}}}" + val parser = XContentType.JSON.xContent().createParser(xContentRegistry(), LoggingDeprecationHandler.INSTANCE, customActionString) + parser.nextToken() + val parsedCustomAction = ISMActionsParser.instance.parse(parser, 1) + assertTrue("Action was not set to be custom after parsing", parsedCustomAction.customAction) + assertEquals("Round tripping custom action doesn't work", customAction.convertToMap(), parsedCustomAction.convertToMap()) + assertTrue("Custom action did not have custom keyword after parsing", parsedCustomAction.convertToMap().containsKey("custom")) + } +} diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/extension/SampleCustomActionParser.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/extension/SampleCustomActionParser.kt new file mode 100644 index 000000000..8da8e0a6f --- /dev/null +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/extension/SampleCustomActionParser.kt @@ -0,0 +1,94 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.indexmanagement.indexstatemanagement.extension + +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.indexmanagement.spi.indexstatemanagement.Action +import org.opensearch.indexmanagement.spi.indexstatemanagement.ActionParser +import org.opensearch.indexmanagement.spi.indexstatemanagement.Step +import org.opensearch.indexmanagement.spi.indexstatemanagement.model.ManagedIndexMetaData +import org.opensearch.indexmanagement.spi.indexstatemanagement.model.StepContext +import org.opensearch.indexmanagement.spi.indexstatemanagement.model.StepMetaData + +class SampleCustomActionParser : ActionParser() { + override fun fromStreamInput(sin: StreamInput): Action { + val someInt = sin.readInt() + val index = sin.readInt() + return SampleCustomAction(someInt, index) + } + + override fun fromXContent(xcp: XContentParser, index: Int): Action { + var someInt: Int? = null + + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) + while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = xcp.currentName() + xcp.nextToken() + + when (fieldName) { + SampleCustomAction.SOME_INT_FIELD -> someInt = xcp.intValue() + else -> throw IllegalArgumentException("Invalid field: [$fieldName] found in SampleCustomAction.") + } + } + return SampleCustomAction(someInt = requireNotNull(someInt) { "SomeInt field must be specified" }, index) + } + + override fun getActionType(): String { + return SampleCustomAction.name + } + class SampleCustomAction(val someInt: Int, index: Int) : Action(name, index) { + + private val sampleCustomStep = SampleCustomStep() + private val steps = listOf(sampleCustomStep) + + override fun getStepToExecute(context: StepContext): Step = sampleCustomStep + + override fun getSteps(): List = steps + + override fun populateAction(builder: XContentBuilder, params: ToXContent.Params) { + builder.startObject(type) + builder.field(SOME_INT_FIELD, someInt) + builder.endObject() + } + + override fun populateAction(out: StreamOutput) { + out.writeInt(someInt) + out.writeInt(actionIndex) + } + + companion object { + const val name = "some_custom_action" + const val SOME_INT_FIELD = "some_int_field" + } + } + class SampleCustomStep : Step(name) { + private var stepStatus = StepStatus.STARTING + + override suspend fun execute(): Step { + stepStatus = StepStatus.COMPLETED + return this + } + + override fun getUpdatedManagedIndexMetadata(currentMetadata: ManagedIndexMetaData): ManagedIndexMetaData { + return currentMetadata.copy( + stepMetaData = StepMetaData(name, getStepStartTime(currentMetadata).toEpochMilli(), stepStatus), + transitionTo = null, + info = null + ) + } + + override fun isIdempotent(): Boolean = true + + companion object { + const val name = "some_custom_step" + } + } +} 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 b1d141325..e255ec23c 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/XContentTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/XContentTests.kt @@ -34,16 +34,12 @@ import org.opensearch.indexmanagement.indexstatemanagement.randomTransition import org.opensearch.indexmanagement.indexstatemanagement.toJsonString import org.opensearch.indexmanagement.opensearchapi.convertToMap import org.opensearch.indexmanagement.opensearchapi.parseWithType -import org.opensearch.indexmanagement.spi.indexstatemanagement.Action import org.opensearch.indexmanagement.spi.indexstatemanagement.model.ManagedIndexMetaData import org.opensearch.test.OpenSearchTestCase -// TODO: fixme - enable tests -// TODO remove this suppression when refactoring is done -@Suppress("UnusedPrivateMember") class XContentTests : OpenSearchTestCase() { - private fun `test policy parsing`() { + fun `test policy parsing`() { val policy = randomPolicy() val policyString = policy.toJsonString() @@ -51,7 +47,7 @@ class XContentTests : OpenSearchTestCase() { assertEquals("Round tripping Policy doesn't work", policy, parsedPolicy) } - private fun `test state parsing`() { + fun `test state parsing`() { val state = randomState() val stateString = state.toJsonString() @@ -75,14 +71,6 @@ class XContentTests : OpenSearchTestCase() { assertEquals("Round tripping Conditions doesn't work", conditions, parsedConditions) } - private fun `test action config parsing`() { - val deleteActionConfig = randomDeleteActionConfig() - - val deleteActionConfigString = deleteActionConfig.toJsonString() - val parsedActionConfig = ISMActionsParser.instance.parse((parser(deleteActionConfigString)), 0) - assertEquals("Round tripping ActionConfig doesn't work", deleteActionConfig as Action, parsedActionConfig) - } - fun `test delete action parsing`() { val deleteAction = randomDeleteActionConfig() @@ -256,7 +244,7 @@ class XContentTests : OpenSearchTestCase() { assertEquals("Round tripping ManagedIndexMetaData doesn't work", metadata, parsedMetaData) } - private fun `test change policy parsing`() { + fun `test change policy parsing`() { val changePolicy = randomChangePolicy() val changePolicyString = changePolicy.toJsonString() diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponseTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponseTests.kt index 94c96a8b9..7239b1d1d 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponseTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesResponseTests.kt @@ -7,8 +7,21 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.get import org.opensearch.common.io.stream.BytesStreamOutput import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentHelper +import org.opensearch.common.xcontent.json.JsonXContent +import org.opensearch.indexmanagement.indexstatemanagement.ISMActionsParser +import org.opensearch.indexmanagement.indexstatemanagement.extension.SampleCustomActionParser +import org.opensearch.indexmanagement.indexstatemanagement.model.Policy +import org.opensearch.indexmanagement.indexstatemanagement.model.State +import org.opensearch.indexmanagement.indexstatemanagement.randomErrorNotification import org.opensearch.indexmanagement.indexstatemanagement.randomPolicy +import org.opensearch.indexmanagement.opensearchapi.convertToMap +import org.opensearch.indexmanagement.opensearchapi.string import org.opensearch.test.OpenSearchTestCase +import java.time.Instant +import java.time.temporal.ChronoUnit class GetPoliciesResponseTests : OpenSearchTestCase() { @@ -24,4 +37,34 @@ class GetPoliciesResponseTests : OpenSearchTestCase() { assertEquals(1, newRes.policies.size) assertEquals(policy, newRes.policies[0]) } + + @Suppress("UNCHECKED_CAST") + fun `test get policies response custom action`() { + val customActionParser = SampleCustomActionParser() + customActionParser.customAction = true + ISMActionsParser.instance.addParser(customActionParser) + val policyID = "policyID" + val action = SampleCustomActionParser.SampleCustomAction(someInt = randomInt(), index = 0) + val states = listOf(State(name = "CustomState", actions = listOf(action), transitions = listOf())) + val policy = Policy( + id = policyID, + description = "description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + val res = GetPoliciesResponse(listOf(policy), 1) + + val responseString = res.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS).string() + val responseMap = XContentHelper.convertToMap(JsonXContent.jsonXContent, responseString, false) + assertEquals("Round tripping custom action doesn't work", res.convertToMap(), responseMap) + assertNotEquals("Get policies response should change the policy output", responseMap, policy.convertToMap()) + val parsedPolicy = (responseMap["policies"] as ArrayList>).first()["policy"] as Map + val parsedStates = parsedPolicy["states"] as ArrayList> + val parsedActions = parsedStates.first()["actions"] as ArrayList> + assertFalse("Get policies response should not contain the custom keyword", parsedActions.first().containsKey("custom")) + ISMActionsParser.instance.parsers.removeIf { it.getActionType() == SampleCustomActionParser.SampleCustomAction.name } + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponseTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponseTests.kt index f0fcdbaf5..6a9686548 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponseTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyResponseTests.kt @@ -7,20 +7,25 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.get import org.opensearch.common.io.stream.BytesStreamOutput import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentHelper +import org.opensearch.common.xcontent.json.JsonXContent +import org.opensearch.indexmanagement.indexstatemanagement.ISMActionsParser import org.opensearch.indexmanagement.indexstatemanagement.action.IndexPriorityAction +import org.opensearch.indexmanagement.indexstatemanagement.extension.SampleCustomActionParser import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.model.State import org.opensearch.indexmanagement.indexstatemanagement.randomErrorNotification +import org.opensearch.indexmanagement.opensearchapi.convertToMap +import org.opensearch.indexmanagement.opensearchapi.string import org.opensearch.test.OpenSearchTestCase import java.time.Instant import java.time.temporal.ChronoUnit -// TODO remove this suppression when refactoring is done -@Suppress("UnusedPrivateMember") class GetPolicyResponseTests : OpenSearchTestCase() { - // TODO: fixme - enable the test - private fun `test get policy response`() { + fun `test get policy response`() { val id = "id" val version: Long = 1 val primaryTerm: Long = 123 @@ -46,6 +51,40 @@ class GetPolicyResponseTests : OpenSearchTestCase() { assertEquals(version, newRes.version) assertEquals(primaryTerm, newRes.primaryTerm) assertEquals(seqNo, newRes.seqNo) - assertEquals(policy, newRes.policy) + assertEquals(policy.convertToMap(), newRes.policy?.convertToMap()) + } + + @Suppress("UNCHECKED_CAST") + fun `test get policy response custom action`() { + val customActionParser = SampleCustomActionParser() + customActionParser.customAction = true + ISMActionsParser.instance.addParser(customActionParser) + val id = "id" + val version: Long = 1 + val primaryTerm: Long = 123 + val seqNo: Long = 456 + val policyID = "policyID" + val action = SampleCustomActionParser.SampleCustomAction(someInt = randomInt(), index = 0) + val states = listOf(State(name = "CustomState", actions = listOf(action), transitions = listOf())) + val policy = Policy( + id = policyID, + description = "description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + val res = GetPolicyResponse(id, version, seqNo, primaryTerm, policy) + + val responseString = res.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS).string() + val responseMap = XContentHelper.convertToMap(JsonXContent.jsonXContent, responseString, false) + assertEquals("Round tripping custom action doesn't work", res.convertToMap(), responseMap) + assertNotEquals("Get policy response should change the policy output", responseMap, policy.convertToMap()) + val parsedPolicy = responseMap["policy"] as Map + val parsedStates = parsedPolicy["states"] as ArrayList> + val parsedActions = parsedStates.first()["actions"] as ArrayList> + assertFalse("Get policy response should not contain the custom keyword", parsedActions.first().containsKey("custom")) + ISMActionsParser.instance.parsers.removeIf { it.getActionType() == SampleCustomActionParser.SampleCustomAction.name } } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyRequestTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyRequestTests.kt index 27f9ba8be..65e389df4 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyRequestTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyRequestTests.kt @@ -8,25 +8,25 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.ind import org.opensearch.action.support.WriteRequest import org.opensearch.common.io.stream.BytesStreamOutput import org.opensearch.common.io.stream.StreamInput +import org.opensearch.indexmanagement.indexstatemanagement.ISMActionsParser import org.opensearch.indexmanagement.indexstatemanagement.action.AllocationAction import org.opensearch.indexmanagement.indexstatemanagement.action.DeleteAction import org.opensearch.indexmanagement.indexstatemanagement.action.IndexPriorityAction +import org.opensearch.indexmanagement.indexstatemanagement.extension.SampleCustomActionParser import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.model.State import org.opensearch.indexmanagement.indexstatemanagement.randomErrorNotification +import org.opensearch.indexmanagement.opensearchapi.convertToMap import org.opensearch.test.OpenSearchTestCase import java.time.Instant import java.time.temporal.ChronoUnit -// TODO remove this suppression when refactoring is done -@Suppress("UnusedPrivateMember") class IndexPolicyRequestTests : OpenSearchTestCase() { - // TODO: fixme - enable the test - private fun `test index policy request index priority action`() { + fun `test index policy request index priority action`() { val policyID = "policyID" - val actionConfig = IndexPriorityAction(50, 0) - val states = listOf(State(name = "SetPriorityState", actions = listOf(actionConfig), transitions = listOf())) + val action = IndexPriorityAction(50, 0) + val states = listOf(State(name = "SetPriorityState", actions = listOf(action), transitions = listOf())) val policy = Policy( id = policyID, description = "description", @@ -46,17 +46,15 @@ class IndexPolicyRequestTests : OpenSearchTestCase() { val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) val newReq = IndexPolicyRequest(sin) assertEquals(policyID, newReq.policyID) - assertEquals(policy, newReq.policy) + assertEquals(policy.convertToMap(), newReq.policy.convertToMap()) assertEquals(seqNo, newReq.seqNo) assertEquals(primaryTerm, newReq.primaryTerm) - assertEquals(policy, newReq.policy) } - // TODO: fixme - enable the test - private fun `test index policy request allocation action`() { + fun `test index policy request allocation action`() { val policyID = "policyID" - val actionConfig = AllocationAction(require = mapOf("box_type" to "hot"), exclude = emptyMap(), include = emptyMap(), index = 0) - val states = listOf(State("Allocate", listOf(actionConfig), listOf())) + val action = AllocationAction(require = mapOf("box_type" to "hot"), exclude = emptyMap(), include = emptyMap(), index = 0) + val states = listOf(State("Allocate", listOf(action), listOf())) val policy = Policy( id = policyID, @@ -77,17 +75,15 @@ class IndexPolicyRequestTests : OpenSearchTestCase() { val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) val newReq = IndexPolicyRequest(sin) assertEquals(policyID, newReq.policyID) - assertEquals(policy, newReq.policy) + assertEquals(policy.convertToMap(), newReq.policy.convertToMap()) assertEquals(seqNo, newReq.seqNo) assertEquals(primaryTerm, newReq.primaryTerm) - assertEquals(policy, newReq.policy) } - // TODO: fixme - enable the test - private fun `test index policy request delete action`() { + fun `test index policy request delete action`() { val policyID = "policyID" - val actionConfig = DeleteAction(index = 0) - val states = listOf(State("Delete", listOf(actionConfig), listOf())) + val action = DeleteAction(index = 0) + val states = listOf(State("Delete", listOf(action), listOf())) val policy = Policy( id = policyID, @@ -108,9 +104,42 @@ class IndexPolicyRequestTests : OpenSearchTestCase() { val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) val newReq = IndexPolicyRequest(sin) assertEquals(policyID, newReq.policyID) - assertEquals(policy, newReq.policy) + assertEquals(policy.convertToMap(), newReq.policy.convertToMap()) assertEquals(seqNo, newReq.seqNo) assertEquals(primaryTerm, newReq.primaryTerm) - assertEquals(policy, newReq.policy) + } + + fun `test index policy request custom action`() { + val customActionParser = SampleCustomActionParser() + customActionParser.customAction = true + ISMActionsParser.instance.addParser(customActionParser) + val policyID = "policyID" + val action = SampleCustomActionParser.SampleCustomAction(someInt = randomInt(), index = 0) + val states = listOf(State("MyState", listOf(action), listOf())) + + val policy = Policy( + id = policyID, + description = "description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + val seqNo: Long = 123 + val primaryTerm: Long = 456 + val refreshPolicy = WriteRequest.RefreshPolicy.NONE + val req = IndexPolicyRequest(policyID, policy, seqNo, primaryTerm, refreshPolicy) + + val out = BytesStreamOutput() + req.writeTo(out) + val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) + val newReq = IndexPolicyRequest(sin) + assertEquals(policyID, newReq.policyID) + assertEquals(policy.convertToMap(), newReq.policy.convertToMap()) + assertEquals(seqNo, newReq.seqNo) + assertEquals(primaryTerm, newReq.primaryTerm) + + ISMActionsParser.instance.parsers.removeIf { it.getActionType() == SampleCustomActionParser.SampleCustomAction.name } } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponseTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponseTests.kt index acc7c728d..8aba32476 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponseTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyResponseTests.kt @@ -7,21 +7,26 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.ind import org.opensearch.common.io.stream.BytesStreamOutput import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentHelper +import org.opensearch.common.xcontent.json.JsonXContent +import org.opensearch.indexmanagement.indexstatemanagement.ISMActionsParser import org.opensearch.indexmanagement.indexstatemanagement.action.IndexPriorityAction +import org.opensearch.indexmanagement.indexstatemanagement.extension.SampleCustomActionParser import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.model.State import org.opensearch.indexmanagement.indexstatemanagement.randomErrorNotification +import org.opensearch.indexmanagement.opensearchapi.convertToMap +import org.opensearch.indexmanagement.opensearchapi.string import org.opensearch.rest.RestStatus import org.opensearch.test.OpenSearchTestCase import java.time.Instant import java.time.temporal.ChronoUnit -// TODO remove this suppression when refactoring is done -@Suppress("UnusedPrivateMember") class IndexPolicyResponseTests : OpenSearchTestCase() { - // TODO: fixme - enable the test - private fun `test index policy response index priority action`() { + fun `test index policy response index priority action`() { val id = "id" val version: Long = 1 val primaryTerm: Long = 123 @@ -50,7 +55,42 @@ class IndexPolicyResponseTests : OpenSearchTestCase() { assertEquals(version, newRes.version) assertEquals(primaryTerm, newRes.primaryTerm) assertEquals(seqNo, newRes.seqNo) - assertEquals(policy, newRes.policy) + assertEquals(policy.convertToMap(), newRes.policy.convertToMap()) assertEquals(status, newRes.status) } + + @Suppress("UNCHECKED_CAST") + fun `test index policy response custom action`() { + val customActionParser = SampleCustomActionParser() + customActionParser.customAction = true + ISMActionsParser.instance.addParser(customActionParser) + val id = "id" + val version: Long = 1 + val primaryTerm: Long = 123 + val seqNo: Long = 456 + val policyID = "policyID" + val action = SampleCustomActionParser.SampleCustomAction(someInt = randomInt(), index = 0) + val states = listOf(State(name = "CustomState", actions = listOf(action), transitions = listOf())) + val policy = Policy( + id = policyID, + description = "description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + val status = RestStatus.CREATED + + val res = IndexPolicyResponse(id, version, primaryTerm, seqNo, policy, status) + val responseString = res.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS).string() + val responseMap = XContentHelper.convertToMap(JsonXContent.jsonXContent, responseString, false) + assertEquals("Round tripping custom action doesn't work", res.convertToMap(), responseMap) + assertNotEquals("Index policy response should change the policy output", responseMap, policy.convertToMap()) + val parsedPolicy = (responseMap["policy"] as Map)["policy"] as Map + val parsedStates = parsedPolicy["states"] as ArrayList> + val parsedActions = parsedStates.first()["actions"] as ArrayList> + assertFalse("Index policy response should not contain the custom keyword", parsedActions.first().containsKey("custom")) + ISMActionsParser.instance.parsers.removeIf { it.getActionType() == SampleCustomActionParser.SampleCustomAction.name } + } } diff --git a/src/test/resources/mappings/cached-opendistro-ism-config.json b/src/test/resources/mappings/cached-opendistro-ism-config.json index 2144b3ce3..5c93e39c5 100644 --- a/src/test/resources/mappings/cached-opendistro-ism-config.json +++ b/src/test/resources/mappings/cached-opendistro-ism-config.json @@ -429,6 +429,10 @@ } } } + }, + "custom": { + "enabled": false, + "type": "object" } } },