diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f497235fa..fbc76ee75 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ *Description of changes:* *CheckList:* -[ ] Commits are signed per the DCO using --signoff +- [ ] Commits are signed per the DCO using --signoff By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/index-management/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). diff --git a/.github/draft-release-notes-config.yml b/.github/draft-release-notes-config.yml index 8f4636f0c..bd620b93d 100644 --- a/.github/draft-release-notes-config.yml +++ b/.github/draft-release-notes-config.yml @@ -14,7 +14,7 @@ replacers: - search: '##' replace: '###' -# Organizing the tagged PRs into unified ODFE categories +# Organizing the tagged PRs into unified OpenSearch categories categories: - title: 'Breaking changes' labels: @@ -45,4 +45,4 @@ categories: - 'backwards-compatibility' - title: 'Refactoring' labels: - - 'Refactor' + - 'refactor' diff --git a/.github/workflows/multi-node-test-workflow.yml b/.github/workflows/multi-node-test-workflow.yml index d1fc68499..98ad25fbf 100644 --- a/.github/workflows/multi-node-test-workflow.yml +++ b/.github/workflows/multi-node-test-workflow.yml @@ -20,51 +20,11 @@ jobs: uses: actions/setup-java@v1 with: java-version: 14 - # dependencies: OpenSearch - - name: Checkout OpenSearch - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/OpenSearch' - path: OpenSearch - ref: '1.0' - - name: Build OpenSearch - working-directory: ./OpenSearch - run: ./gradlew publishToMavenLocal -Dbuild.snapshot=false - # dependencies: common-utils - - name: Checkout common-utils - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/common-utils' - path: common-utils - ref: '1.0' - - name: Build common-utils - working-directory: ./common-utils - run: ./gradlew publishToMavenLocal -Dopensearch.version=1.0.0 - # dependencies: job-scheduler - - name: Checkout job-scheduler - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/job-scheduler' - path: job-scheduler - ref: '1.0' - - name: Build job-scheduler - working-directory: ./job-scheduler - run: ./gradlew publishToMavenLocal -Dopensearch.version=1.0.0 -Dbuild.snapshot=false - # dependencies: alerting-notification - - name: Checkout alerting - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/alerting' - path: alerting - ref: '1.0' - - name: Build alerting - working-directory: ./alerting - run: ./gradlew :alerting-notification:publishToMavenLocal -Dopensearch.version=1.0.0 -Dbuild.snapshot=false # index-management - name: Checkout Branch uses: actions/checkout@v2 - name: Run integration tests with multi node config - run: ./gradlew integTest -PnumNodes=3 + run: ./gradlew integTest -PnumNodes=3 -Dopensearch.version=1.2.0-SNAPSHOT - name: Upload failed logs uses: actions/upload-artifact@v2 if: failure() diff --git a/.github/workflows/test-and-build-workflow.yml b/.github/workflows/test-and-build-workflow.yml index 2f2d55c70..011d3ee9f 100644 --- a/.github/workflows/test-and-build-workflow.yml +++ b/.github/workflows/test-and-build-workflow.yml @@ -20,51 +20,11 @@ jobs: uses: actions/setup-java@v1 with: java-version: 14 - # dependencies: OpenSearch - - name: Checkout OpenSearch - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/OpenSearch' - path: OpenSearch - ref: '1.0' - - name: Build OpenSearch - working-directory: ./OpenSearch - run: ./gradlew publishToMavenLocal -Dbuild.snapshot=false - # dependencies: common-utils - - name: Checkout common-utils - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/common-utils' - path: common-utils - ref: '1.0' - - name: Build common-utils - working-directory: ./common-utils - run: ./gradlew publishToMavenLocal -Dopensearch.version=1.0.0 - # dependencies: job-scheduler - - name: Checkout job-scheduler - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/job-scheduler' - path: job-scheduler - ref: '1.0' - - name: Build job-scheduler - working-directory: ./job-scheduler - run: ./gradlew publishToMavenLocal -Dopensearch.version=1.0.0 -Dbuild.snapshot=false - # dependencies: alerting-notification - - name: Checkout alerting - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/alerting' - path: alerting - ref: '1.0' - - name: Build alerting - working-directory: ./alerting - run: ./gradlew :alerting-notification:publishToMavenLocal -Dopensearch.version=1.0.0 -Dbuild.snapshot=false - # index-management + # build index management - name: Checkout Branch uses: actions/checkout@v2 - name: Build with Gradle - run: ./gradlew build + run: ./gradlew build -Dopensearch.version=1.2.0-SNAPSHOT - name: Upload failed logs uses: actions/upload-artifact@v2 if: failure() diff --git a/.gitignore b/.gitignore index 4f755f873..037f7a3e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ .gradle/ build/ out/ -.idea/ +.idea/* +!.idea/copyright *.ipr *.iws .DS_Store *.log -http \ No newline at end of file +http +.project +.settings \ No newline at end of file diff --git a/.idea/copyright/OpenSearch.xml b/.idea/copyright/OpenSearch.xml new file mode 100644 index 000000000..dadc9ed84 --- /dev/null +++ b/.idea/copyright/OpenSearch.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 000000000..5f45523cc --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/NOTICE b/NOTICE index be83767d4..731cb6006 100644 --- a/NOTICE +++ b/NOTICE @@ -1,12 +1,2 @@ -OpenSearch -Copyright 2021 OpenSearch Contributors - -This product includes software developed by -Elasticsearch (http://www.elastic.co). -Copyright 2009-2018 Elasticsearch - -This product includes software developed by The Apache Software -Foundation (http://www.apache.org/). - -This product includes software developed by -Joda.org (http://www.joda.org/). +OpenSearch (https://opensearch.org/) +Copyright OpenSearch Contributors diff --git a/README.md b/README.md index 848c0de4d..a98c73bfa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Test and Build Workflow](https://github.com/opensearch-project/index-management/workflows/Test%20and%20Build%20Workflow/badge.svg)](https://github.com/opensearch-project/index-management/actions) [![codecov](https://codecov.io/gh/opensearch-project/index-management/branch/main/graph/badge.svg)](https://codecov.io/gh/opensearch-project/index-management) -[![Documentation](https://img.shields.io/badge/api-reference-blue.svg)](https://docs-beta.opensearch.org/im-plugin/index/) +[![Documentation](https://img.shields.io/badge/api-reference-blue.svg)](https://opensearch.org/docs/im-plugin/index/) [![Chat](https://img.shields.io/badge/chat-on%20forums-blue)](https://discuss.opendistrocommunity.dev/c/index-management/) ![PRs welcome!](https://img.shields.io/badge/PRs-welcome!-success) @@ -58,7 +58,7 @@ See [developer guide](DEVELOPER_GUIDE.md) and [how to contribute to this project If you find a bug, or have a feature request, please don't hesitate to open an issue in this repository. -For more information, see [project website](https://opensearch.org/) and [documentation](https://docs-beta.opensearch.org/). If you need help and are unsure where to open an issue, try [forums](https://discuss.opendistrocommunity.dev/). +For more information, see [project website](https://opensearch.org/) and [documentation](https://opensearch.org/docs/). If you need help and are unsure where to open an issue, try [forums](https://discuss.opendistrocommunity.dev/). ## Code of Conduct @@ -74,4 +74,4 @@ This project is licensed under the [Apache v2.0 License](./LICENSE) ## Copyright -Copyright 2020-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright OpenSearch Contributors. See [NOTICE](NOTICE) for details. diff --git a/build-tools/coverage.gradle b/build-tools/coverage.gradle index d24749dc8..33a12c8c7 100644 --- a/build-tools/coverage.gradle +++ b/build-tools/coverage.gradle @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + /* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * diff --git a/build-tools/pkgbuild.gradle b/build-tools/pkgbuild.gradle index 7aaa7fe43..ee9c92b4e 100644 --- a/build-tools/pkgbuild.gradle +++ b/build-tools/pkgbuild.gradle @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + /* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * diff --git a/build.gradle b/build.gradle index 474c9c2da..06e1046cd 100644 --- a/build.gradle +++ b/build.gradle @@ -31,14 +31,20 @@ import java.util.function.Predicate buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.0.0") - kotlin_version = System.getProperty("kotlin.version", "1.3.72") + opensearch_version = System.getProperty("opensearch.version", "1.2.0-SNAPSHOT") + // 1.1.0 -> 1.1.0.0, and 1.1.0-SNAPSHOT -> 1.1.0.0-SNAPSHOT + opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') + notification_version = System.getProperty("notification.version", opensearch_build) + common_utils_version = System.getProperty("common_utils.version", opensearch_build) + job_scheduler_version = System.getProperty("job_scheduler_version.version", opensearch_build) + kotlin_version = System.getProperty("kotlin.version", "1.4.0") } repositories { mavenLocal() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } } dependencies { @@ -139,30 +145,35 @@ configurations.testCompile { ext { projectSubstitutions = [:] - opensearchVersion = "${version}" isSnapshot = "true" == System.getProperty("build.snapshot", "true") licenseFile = rootProject.file('LICENSE') noticeFile = rootProject.file('NOTICE') } -group = "org.opensearch" -version = "${opensearchVersion}.0" +allprojects { + group = "org.opensearch" + version = "${opensearch_version}" - "-SNAPSHOT" + ".0" + if (isSnapshot) { + version += "-SNAPSHOT" + } +} dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" - compileOnly "org.opensearch:opensearch-job-scheduler-spi:1.0.0.0" + compileOnly "org.opensearch:opensearch-job-scheduler-spi:${job_scheduler_version}" compile "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" compile "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" - compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" + compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9' compile "org.jetbrains:annotations:13.0" - compile "org.opensearch:notification:1.0.0.0" - compile "org.opensearch:common-utils:1.0.0.0" + compile "org.opensearch:notification:${notification_version}" + compile "org.opensearch:common-utils:${common_utils_version}" compile "com.github.seancfoley:ipaddress:5.3.3" testCompile "org.opensearch.test:framework:${opensearch_version}" testCompile "org.jetbrains.kotlin:kotlin-test:${kotlin_version}" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" - testCompile "org.mockito:mockito-core:2.23.0" + testCompile "org.mockito:mockito-core:3.12.4" add("ktlint", "com.pinterest:ktlint:0.41.0") { attributes { @@ -173,10 +184,7 @@ dependencies { repositories { mavenLocal() -} - -if (isSnapshot) { - version += "-SNAPSHOT" + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } } plugins.withId('java') { @@ -186,7 +194,6 @@ plugins.withId('java') { plugins.withId('org.jetbrains.kotlin.jvm') { compileKotlin.kotlinOptions.jvmTarget = compileTestKotlin.kotlinOptions.jvmTarget = "1.8" compileKotlin.dependsOn ktlint - } javadoc.enabled = false // turn off javadoc as it barfs on Kotlin code @@ -261,6 +268,7 @@ testClusters.integTest { File getAsFile() { fileTree("src/test/resources/job-scheduler").getSingleFile() } } })) + if (securityEnabled) { plugin(provider({ new RegularFile() { diff --git a/docs/rfc.md b/docs/rfc.md index d0fafc584..f860bc4e7 100644 --- a/docs/rfc.md +++ b/docs/rfc.md @@ -113,4 +113,4 @@ After building Index State Management API, we will build an administrative panel ## Providing Feedback -If you have comments or feedback on our plans for Index Management, please comment on [the RFC Github issue](../../issues/1) in this project to discuss. +If you have comments or feedback on our plans for Index Management, please comment on [the RFC Github issue](https://github.com/opendistro-for-elasticsearch/index-management/issues/1) in this project to discuss. diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 43ca52b80..000000000 --- a/gradle.properties +++ /dev/null @@ -1,26 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# -# The OpenSearch Contributors require contributions made to -# this file be licensed under the Apache-2.0 license or a -# compatible open source license. -# -# Modifications Copyright OpenSearch Contributors. See -# GitHub history for details. -# - -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -version = 1.0.0 diff --git a/release-notes/opensearch-index-management.release-notes-1.1.0.0.md b/release-notes/opensearch-index-management.release-notes-1.1.0.0.md new file mode 100644 index 000000000..27b5552c9 --- /dev/null +++ b/release-notes/opensearch-index-management.release-notes-1.1.0.0.md @@ -0,0 +1,23 @@ +## Version 1.1.0.0 2021-09-03 + +Compatible with OpenSearch 1.1.0 + +### Infrastructure + +* Upgrade dependencies to 1.1 and build snapshot by default. ([#121](https://github.com/opensearch-project/index-management/pull/121)) + +### Features + +* Storing user information as part of the job when security plugin is installed ([#113](https://github.com/opensearch-project/index-management/pull/113)) +* Storing user object in all APIs and enabling filter of response based on user ([#115](https://github.com/opensearch-project/index-management/pull/115)) +* Security improvements ([#126](https://github.com/opensearch-project/index-management/pull/126)) +* Updating security filtering logic ([#137](https://github.com/opensearch-project/index-management/pull/137)) + +### Enhancements + +* Enhance ISM template ([#105](https://github.com/opensearch-project/index-management/pull/105)) + +### Bug Fixes + +* Removing Usages of Action Get Call and using listeners ([#100](https://github.com/opensearch-project/index-management/pull/100)) +* Explain response still use old opendistro policy id ([#109](https://github.com/opensearch-project/index-management/pull/109)) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/IndexManagementPlugin.kt b/src/main/kotlin/org/opensearch/indexmanagement/IndexManagementPlugin.kt index d704f73d4..f5130a257 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/IndexManagementPlugin.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/IndexManagementPlugin.kt @@ -82,6 +82,8 @@ import org.opensearch.indexmanagement.indexstatemanagement.transport.action.getp import org.opensearch.indexmanagement.indexstatemanagement.transport.action.getpolicy.TransportGetPolicyAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.indexpolicy.IndexPolicyAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.indexpolicy.TransportIndexPolicyAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.TransportManagedIndexAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.removepolicy.RemovePolicyAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.removepolicy.TransportRemovePolicyAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.retryfailedmanagedindex.RetryFailedManagedIndexAction @@ -124,6 +126,7 @@ import org.opensearch.indexmanagement.rollup.resthandler.RestStartRollupAction import org.opensearch.indexmanagement.rollup.resthandler.RestStopRollupAction import org.opensearch.indexmanagement.rollup.settings.LegacyOpenDistroRollupSettings import org.opensearch.indexmanagement.rollup.settings.RollupSettings +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.TransformRunner import org.opensearch.indexmanagement.transform.action.delete.DeleteTransformsAction import org.opensearch.indexmanagement.transform.action.delete.TransportDeleteTransformsAction @@ -301,7 +304,15 @@ class IndexManagementPlugin : JobSchedulerExtension, NetworkPlugin, ActionPlugin this.clusterService = clusterService rollupInterceptor = RollupInterceptor(clusterService, settings, indexNameExpressionResolver) val jvmService = JvmService(environment.settings()) - val transformRunner = TransformRunner.initialize(client, clusterService, xContentRegistry, settings, indexNameExpressionResolver, jvmService) + val transformRunner = TransformRunner.initialize( + client, + clusterService, + xContentRegistry, + settings, + indexNameExpressionResolver, + jvmService, + threadPool + ) fieldCapsFilter = FieldCapsFilter(clusterService, settings, indexNameExpressionResolver) this.indexNameExpressionResolver = indexNameExpressionResolver @@ -339,6 +350,7 @@ class IndexManagementPlugin : JobSchedulerExtension, NetworkPlugin, ActionPlugin .registerIMIndex(indexManagementIndices) .registerHistoryIndex(indexStateManagementHistory) .registerSkipFlag(skipFlag) + .registerThreadPool(threadPool) val metadataService = MetadataService(client, clusterService, skipFlag, indexManagementIndices) @@ -367,6 +379,8 @@ class IndexManagementPlugin : JobSchedulerExtension, NetworkPlugin, ActionPlugin ManagedIndexSettings.ROLLOVER_SKIP, ManagedIndexSettings.INDEX_STATE_MANAGEMENT_ENABLED, ManagedIndexSettings.METADATA_SERVICE_ENABLED, + ManagedIndexSettings.AUTO_MANAGE, + ManagedIndexSettings.JITTER, ManagedIndexSettings.JOB_INTERVAL, ManagedIndexSettings.SWEEP_PERIOD, ManagedIndexSettings.COORDINATOR_BACKOFF_COUNT, @@ -381,12 +395,14 @@ class IndexManagementPlugin : JobSchedulerExtension, NetworkPlugin, ActionPlugin RollupSettings.ROLLUP_ENABLED, RollupSettings.ROLLUP_SEARCH_ENABLED, RollupSettings.ROLLUP_DASHBOARDS, + RollupSettings.ROLLUP_SEARCH_ALL_JOBS, TransformSettings.TRANSFORM_JOB_INDEX_BACKOFF_COUNT, TransformSettings.TRANSFORM_JOB_INDEX_BACKOFF_MILLIS, TransformSettings.TRANSFORM_JOB_SEARCH_BACKOFF_COUNT, TransformSettings.TRANSFORM_JOB_SEARCH_BACKOFF_MILLIS, TransformSettings.TRANSFORM_CIRCUIT_BREAKER_ENABLED, TransformSettings.TRANSFORM_CIRCUIT_BREAKER_JVM_THRESHOLD, + IndexManagementSettings.FILTER_BY_BACKEND_ROLES, LegacyOpenDistroManagedIndexSettings.HISTORY_ENABLED, LegacyOpenDistroManagedIndexSettings.HISTORY_INDEX_MAX_AGE, LegacyOpenDistroManagedIndexSettings.HISTORY_MAX_DOCS, @@ -443,7 +459,8 @@ class IndexManagementPlugin : JobSchedulerExtension, NetworkPlugin, ActionPlugin ActionPlugin.ActionHandler(DeleteTransformsAction.INSTANCE, TransportDeleteTransformsAction::class.java), ActionPlugin.ActionHandler(ExplainTransformAction.INSTANCE, TransportExplainTransformAction::class.java), ActionPlugin.ActionHandler(StartTransformAction.INSTANCE, TransportStartTransformAction::class.java), - ActionPlugin.ActionHandler(StopTransformAction.INSTANCE, TransportStopTransformAction::class.java) + ActionPlugin.ActionHandler(StopTransformAction.INSTANCE, TransportStopTransformAction::class.java), + ActionPlugin.ActionHandler(ManagedIndexAction.INSTANCE, TransportManagedIndexAction::class.java) ) } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMTemplateService.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMTemplateService.kt index 8d896ce45..fdd71543f 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMTemplateService.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ISMTemplateService.kt @@ -26,65 +26,14 @@ package org.opensearch.indexmanagement.indexstatemanagement -import org.apache.logging.log4j.LogManager import org.apache.lucene.util.automaton.Operations import org.opensearch.OpenSearchException -import org.opensearch.cluster.ClusterState -import org.opensearch.cluster.metadata.IndexMetadata import org.opensearch.common.Strings import org.opensearch.common.ValidationException import org.opensearch.common.regex.Regex import org.opensearch.indexmanagement.indexstatemanagement.model.ISMTemplate import org.opensearch.indexmanagement.util.IndexManagementException -private val log = LogManager.getLogger("ISMTemplateService") - -/** - * find the matching policy based on ISM template field for the given index - * - * filter out hidden index - * filter out older index than template lastUpdateTime - * - * @param ismTemplates current ISM templates saved in metadata - * @param indexMetadata cluster state index metadata - * @return policyID - */ -@Suppress("ReturnCount") -fun Map.findMatchingPolicy(clusterState: ClusterState, indexName: String): String? { - if (this.isEmpty()) return null - - val indexMetadata = clusterState.metadata.index(indexName) - val indexAbstraction = clusterState.metadata.indicesLookup[indexName] - val isDataStreamIndex = indexAbstraction?.parentDataStream != null - - // Don't include hidden index unless it belongs to a data stream. - val isHidden = IndexMetadata.INDEX_HIDDEN_SETTING.get(indexMetadata.settings) - if (!isDataStreamIndex && isHidden) return null - - // If the index belongs to a data stream, then find the matching policy using the data stream name. - val lookupName = when { - isDataStreamIndex -> indexAbstraction?.parentDataStream?.name - else -> indexName - } - - // only process indices created after template - // traverse all ism templates for matching ones - val patternMatchPredicate = { pattern: String -> Regex.simpleMatch(pattern, lookupName) } - var matchedPolicy: String? = null - var highestPriority: Int = -1 - this.filter { (_, template) -> - template.lastUpdatedTime.toEpochMilli() < indexMetadata.creationDate - }.forEach { (policyID, template) -> - val matched = template.indexPatterns.stream().anyMatch(patternMatchPredicate) - if (matched && highestPriority < template.priority) { - highestPriority = template.priority - matchedPolicy = policyID - } - } - - return matchedPolicy -} - /** * validate the template Name and indexPattern provided in the template * @@ -120,30 +69,61 @@ fun validateFormat(indexPatterns: List): OpenSearchException? { return null } +fun List.findSelfConflictingTemplates(): Pair, List>? { + val priorityToTemplates = mutableMapOf>() + this.forEach { + val templateList = priorityToTemplates[it.priority] + if (templateList != null) { + priorityToTemplates[it.priority] = templateList.plus(it) + } else { + priorityToTemplates[it.priority] = mutableListOf(it) + } + } + priorityToTemplates.forEach { (_, templateList) -> + // same priority + val indexPatternsList = templateList.map { it.indexPatterns } + if (indexPatternsList.size > 1) { + indexPatternsList.forEachIndexed { ind, indexPatterns -> + val comparePatterns = indexPatternsList.subList(ind + 1, indexPatternsList.size).flatten() + if (overlapping(indexPatterns, comparePatterns)) { + return indexPatterns to comparePatterns + } + } + } + } + + return null +} + +@Suppress("SpreadOperator") +fun overlapping(p1: List, p2: List): Boolean { + if (p1.isEmpty() || p2.isEmpty()) return false + val a1 = Regex.simpleMatchToAutomaton(*p1.toTypedArray()) + val a2 = Regex.simpleMatchToAutomaton(*p2.toTypedArray()) + return !Operations.isEmpty(Operations.intersection(a1, a2)) +} + /** * find policy templates whose index patterns overlap with given template * * @return map of overlapping template name to its index patterns */ -@Suppress("SpreadOperator") -fun Map.findConflictingPolicyTemplates( +fun Map>.findConflictingPolicyTemplates( candidate: String, indexPatterns: List, priority: Int ): Map> { - val automaton1 = Regex.simpleMatchToAutomaton(*indexPatterns.toTypedArray()) val overlappingTemplates = mutableMapOf>() - // focus on template with same priority - this.filter { it.value.priority == priority } - .forEach { (policyID, template) -> - val automaton2 = Regex.simpleMatchToAutomaton(*template.indexPatterns.toTypedArray()) - if (!Operations.isEmpty(Operations.intersection(automaton1, automaton2))) { - log.info("Existing ism_template for $policyID overlaps candidate $candidate") - overlappingTemplates[policyID] = template.indexPatterns + this.forEach { (policyID, templateList) -> + templateList.filter { it.priority == priority } + .map { it.indexPatterns } + .forEach { + if (overlapping(indexPatterns, it)) { + overlappingTemplates[policyID] = it + } } - } + } overlappingTemplates.remove(candidate) - return overlappingTemplates } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementHistory.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementHistory.kt index 69ff4ffa3..fd51d9a09 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementHistory.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementHistory.kt @@ -30,6 +30,7 @@ import org.apache.logging.log4j.LogManager import org.opensearch.action.ActionListener import org.opensearch.action.DocWriteRequest import org.opensearch.action.admin.cluster.state.ClusterStateRequest +import org.opensearch.action.admin.cluster.state.ClusterStateResponse import org.opensearch.action.admin.indices.delete.DeleteIndexRequest import org.opensearch.action.admin.indices.rollover.RolloverRequest import org.opensearch.action.admin.indices.rollover.RolloverResponse @@ -37,6 +38,7 @@ import org.opensearch.action.bulk.BulkRequest import org.opensearch.action.bulk.BulkResponse import org.opensearch.action.index.IndexRequest import org.opensearch.action.support.IndicesOptions +import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.client.Client import org.opensearch.cluster.LocalNodeMasterListener import org.opensearch.cluster.service.ClusterService @@ -44,7 +46,6 @@ import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.XContentFactory import org.opensearch.common.xcontent.XContentType -import org.opensearch.index.IndexNotFoundException import org.opensearch.indexmanagement.IndexManagementIndices import org.opensearch.indexmanagement.IndexManagementPlugin import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData @@ -61,6 +62,7 @@ import org.opensearch.threadpool.ThreadPool import java.time.Instant @OpenForTesting +@Suppress("TooManyFunctions") class IndexStateManagementHistory( settings: Settings, private val client: Client, @@ -105,7 +107,7 @@ class IndexStateManagementHistory( override fun onMaster() { try { // try to rollover immediately as we might be restarting the cluster - rolloverHistoryIndex() + if (historyEnabled) rolloverHistoryIndex() // schedule the next rollover for approx MAX_AGE later scheduledRollover = threadPool.scheduleWithFixedDelay( { rolloverAndDeleteHistoryIndex() }, @@ -176,7 +178,6 @@ class IndexStateManagementHistory( @Suppress("SpreadOperator", "NestedBlockDepth", "ComplexMethod") private fun deleteOldHistoryIndex() { - val indexToDelete = mutableListOf() val clusterStateRequest = ClusterStateRequest() .clear() @@ -185,8 +186,28 @@ class IndexStateManagementHistory( .local(true) .indicesOptions(IndicesOptions.strictExpand()) - val clusterStateResponse = client.admin().cluster().state(clusterStateRequest).actionGet() + client.admin().cluster().state( + clusterStateRequest, + object : ActionListener { + override fun onResponse(clusterStateResponse: ClusterStateResponse) { + if (!clusterStateResponse.state.metadata.indices.isEmpty) { + val indicesToDelete = getIndicesToDelete(clusterStateResponse) + logger.info("Deleting old history indices viz $indicesToDelete") + deleteAllOldHistoryIndices(indicesToDelete) + } else { + logger.info("No Old History Indices to delete") + } + } + + override fun onFailure(exception: Exception) { + logger.error("Error fetching cluster state ${exception.message}") + } + } + ) + } + private fun getIndicesToDelete(clusterStateResponse: ClusterStateResponse): List { + var indicesToDelete = mutableListOf() for (entry in clusterStateResponse.state.metadata.indices()) { val indexMetaData = entry.value val creationTime = indexMetaData.creationDate @@ -198,27 +219,51 @@ class IndexStateManagementHistory( continue } - indexToDelete.add(indexMetaData.index.name) + indicesToDelete.add(indexMetaData.index.name) } } + return indicesToDelete + } + + @Suppress("SpreadOperator") + private fun deleteAllOldHistoryIndices(indicesToDelete: List) { + if (indicesToDelete.isNotEmpty()) { + val deleteRequest = DeleteIndexRequest(*indicesToDelete.toTypedArray()) + client.admin().indices().delete( + deleteRequest, + object : ActionListener { + override fun onResponse(deleteIndicesResponse: AcknowledgedResponse) { + if (!deleteIndicesResponse.isAcknowledged) { + logger.error("could not delete one or more ISM history index. $indicesToDelete. Retrying one by one.") + deleteOldHistoryIndex(indicesToDelete) + } + } + override fun onFailure(exception: Exception) { + logger.error("Error deleting old history indices ${exception.message}") + deleteOldHistoryIndex(indicesToDelete) + } + } + ) + } + } - if (indexToDelete.isNotEmpty()) { - val deleteRequest = DeleteIndexRequest(*indexToDelete.toTypedArray()) - val deleteResponse = client.admin().indices().delete(deleteRequest).actionGet() - if (!deleteResponse.isAcknowledged) { - logger.error("could not delete one or more ISM history index. $indexToDelete. Retrying one by one.") - for (index in indexToDelete) { - try { - val singleDeleteRequest = DeleteIndexRequest(*indexToDelete.toTypedArray()) - val singleDeleteResponse = client.admin().indices().delete(singleDeleteRequest).actionGet() + @Suppress("SpreadOperator") + private fun deleteOldHistoryIndex(indicesToDelete: List) { + for (index in indicesToDelete) { + val singleDeleteRequest = DeleteIndexRequest(*indicesToDelete.toTypedArray()) + client.admin().indices().delete( + singleDeleteRequest, + object : ActionListener { + override fun onResponse(singleDeleteResponse: AcknowledgedResponse) { if (!singleDeleteResponse.isAcknowledged) { logger.error("could not delete one or more ISM history index. $index.") } - } catch (e: IndexNotFoundException) { - logger.debug("$index was already deleted. ${e.message}") + } + override fun onFailure(exception: Exception) { + logger.debug("Exception ${exception.message} while deleting the index $index") } } - } + ) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexCoordinator.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexCoordinator.kt index 4441d475d..4018bb703 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexCoordinator.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexCoordinator.kt @@ -34,45 +34,53 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.action.DocWriteRequest import org.opensearch.action.bulk.BackoffPolicy import org.opensearch.action.bulk.BulkRequest import org.opensearch.action.bulk.BulkResponse import org.opensearch.action.get.MultiGetRequest import org.opensearch.action.get.MultiGetResponse +import org.opensearch.action.index.IndexRequest import org.opensearch.action.search.SearchPhaseExecutionException import org.opensearch.action.search.SearchRequest import org.opensearch.action.search.SearchResponse +import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.update.UpdateRequest import org.opensearch.client.Client import org.opensearch.cluster.ClusterChangedEvent import org.opensearch.cluster.ClusterState import org.opensearch.cluster.ClusterStateListener import org.opensearch.cluster.block.ClusterBlockException +import org.opensearch.cluster.metadata.IndexMetadata import org.opensearch.cluster.service.ClusterService import org.opensearch.common.component.LifecycleListener +import org.opensearch.common.regex.Regex import org.opensearch.common.settings.Settings import org.opensearch.common.unit.TimeValue +import org.opensearch.commons.authuser.User import org.opensearch.index.Index import org.opensearch.index.IndexNotFoundException import org.opensearch.index.query.QueryBuilders import org.opensearch.indexmanagement.IndexManagementIndices import org.opensearch.indexmanagement.IndexManagementPlugin import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX -import org.opensearch.indexmanagement.indexstatemanagement.model.ISMTemplate import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexConfig import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData +import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.model.coordinator.ClusterStateManagedIndexConfig import org.opensearch.indexmanagement.indexstatemanagement.model.coordinator.SweptManagedIndexConfig -import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.filterNotNullValues -import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.getPolicyToTemplateMap import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.mgetManagedIndexMetadata +import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.AUTO_MANAGE import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.COORDINATOR_BACKOFF_COUNT import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.COORDINATOR_BACKOFF_MILLIS import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.INDEX_STATE_MANAGEMENT_ENABLED +import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.JITTER import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.JOB_INTERVAL import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.METADATA_SERVICE_ENABLED import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.SWEEP_PERIOD +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexRequest import org.opensearch.indexmanagement.indexstatemanagement.util.ISM_TEMPLATE_FIELD import org.opensearch.indexmanagement.indexstatemanagement.util.deleteManagedIndexMetadataRequest import org.opensearch.indexmanagement.indexstatemanagement.util.deleteManagedIndexRequest @@ -83,10 +91,13 @@ import org.opensearch.indexmanagement.indexstatemanagement.util.isFailed import org.opensearch.indexmanagement.indexstatemanagement.util.isPolicyCompleted import org.opensearch.indexmanagement.indexstatemanagement.util.managedIndexConfigIndexRequest import org.opensearch.indexmanagement.indexstatemanagement.util.updateEnableManagedIndexRequest +import org.opensearch.indexmanagement.opensearchapi.IndexManagementSecurityContext import org.opensearch.indexmanagement.opensearchapi.contentParser +import org.opensearch.indexmanagement.opensearchapi.parseFromSearchResponse import org.opensearch.indexmanagement.opensearchapi.parseWithType import org.opensearch.indexmanagement.opensearchapi.retry import org.opensearch.indexmanagement.opensearchapi.suspendUntil +import org.opensearch.indexmanagement.opensearchapi.withClosableContext import org.opensearch.indexmanagement.util.NO_ID import org.opensearch.indexmanagement.util.OpenForTesting import org.opensearch.rest.RestStatus @@ -112,7 +123,7 @@ import org.opensearch.threadpool.ThreadPool @Suppress("TooManyFunctions") @OpenForTesting class ManagedIndexCoordinator( - settings: Settings, + private val settings: Settings, private val client: Client, private val clusterService: ClusterService, private val threadPool: ThreadPool, @@ -135,6 +146,7 @@ class ManagedIndexCoordinator( @Volatile private var retryPolicy = BackoffPolicy.constantBackoff(COORDINATOR_BACKOFF_MILLIS.get(settings), COORDINATOR_BACKOFF_COUNT.get(settings)) @Volatile private var jobInterval = JOB_INTERVAL.get(settings) + @Volatile private var jobJitter = JITTER.get(settings) @Volatile private var isMaster = false @@ -148,6 +160,9 @@ class ManagedIndexCoordinator( clusterService.clusterSettings.addSettingsUpdateConsumer(JOB_INTERVAL) { jobInterval = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(JITTER) { + jobJitter = it + } clusterService.clusterSettings.addSettingsUpdateConsumer(INDEX_STATE_MANAGEMENT_ENABLED) { indexStateManagementEnabled = it if (!indexStateManagementEnabled) disable() else enable() @@ -292,62 +307,146 @@ class ManagedIndexCoordinator( /** * build requests to create jobs for indices matching ISM templates */ + @Suppress("NestedBlockDepth") suspend fun getMatchingIndicesUpdateReq( clusterState: ClusterState, indexNames: List ): List> { - val updateManagedIndexReqs = mutableListOf>() - if (indexNames.isEmpty()) return updateManagedIndexReqs + val updateManagedIndexReqs = mutableListOf>() + if (indexNames.isEmpty()) return updateManagedIndexReqs.toList() + + val policiesWithTemplates = getPoliciesWithISMTemplates() + + // Iterate over each unmanaged hot/warm index and if it matches an ISM template add a managed index config index request + indexNames.forEach { indexName -> + val lookupName = findIndexLookupName(indexName, clusterState) + if (lookupName != null) { + val indexMetadata = clusterState.metadata.index(indexName) + val creationDate = indexMetadata.creationDate + val indexUuid = indexMetadata.indexUUID + findMatchingPolicy(lookupName, creationDate, policiesWithTemplates) + ?.let { policy -> + logger.info("Index [$indexName] matched ISM policy template and will be managed by ${policy.id}") + updateManagedIndexReqs.add( + managedIndexConfigIndexRequest( + indexName, + indexUuid, + policy.id, + jobInterval, + policy, + jobJitter + ) + ) + } + } + } - val indexMetadatas = clusterState.metadata.indices - val templates = getISMTemplates() + return updateManagedIndexReqs.toList() + } - val indexToMatchedPolicy = indexNames.map { indexName -> - indexName to templates.findMatchingPolicy(clusterState, indexName) - }.toMap() + private fun findIndexLookupName(indexName: String, clusterState: ClusterState): String? { + if (clusterState.metadata.hasIndex(indexName)) { + val indexMetadata = clusterState.metadata.index(indexName) + val autoManage = indexMetadata.settings.getAsBoolean(AUTO_MANAGE.key, true) + if (autoManage) { + val isHiddenIndex = + IndexMetadata.INDEX_HIDDEN_SETTING.get(indexMetadata.settings) || indexName.startsWith(".") + val indexAbstraction = clusterState.metadata.indicesLookup[indexName] + val isDataStreamIndex = indexAbstraction?.parentDataStream != null + if (!isDataStreamIndex && isHiddenIndex) { + return null + } - indexToMatchedPolicy.filterNotNullValues() - .forEach { (index, policyID) -> - val indexUuid = indexMetadatas[index].indexUUID - val ismTemplate = templates[policyID] - if (indexUuid != null && ismTemplate != null) { - logger.info("Index [$index] will be managed by policy [$policyID]") - updateManagedIndexReqs.add( - managedIndexConfigIndexRequest(index, indexUuid, policyID, jobInterval) - ) - } else { - logger.warn( - "Index [$index] has index uuid [$indexUuid] and/or " + - "a matching template [$ismTemplate] that is null." - ) + return when { + isDataStreamIndex -> indexAbstraction?.parentDataStream?.name + else -> indexName + } + } + } + + return null + } + + /** + * Find a policy that has highest priority ism template with matching index pattern to the index and is created before index creation date. If + * the policy has user, ensure that the user can manage the index if not find the one that can. + * */ + private suspend fun findMatchingPolicy(indexName: String, creationDate: Long, policies: List): Policy? { + val patternMatchPredicate = { pattern: String -> Regex.simpleMatch(pattern, indexName) } + val priorityPolicyMap = mutableMapOf() + policies.forEach { policy -> + var highestPriorityForPolicy = -1 + policy.ismTemplate?.filter { template -> + template.lastUpdatedTime.toEpochMilli() < creationDate + }?.forEach { template -> + if (template.indexPatterns.stream().anyMatch(patternMatchPredicate)) { + if (highestPriorityForPolicy < template.priority) { + highestPriorityForPolicy = template.priority + } + } + } + if (highestPriorityForPolicy > -1) { + priorityPolicyMap[highestPriorityForPolicy] = policy + } + } + + val previouslyCheckedUsers = mutableSetOf() + // sorting the applicable policies based on the priority highest to lowest + val sortedPriorityPolicyMap = priorityPolicyMap.toSortedMap(reverseOrder()) + sortedPriorityPolicyMap.forEach { (_, policy) -> + if (!previouslyCheckedUsers.contains(policy.user) && canPolicyManagedIndex(policy, indexName)) { + return policy + } + + policy.user?.let { previouslyCheckedUsers.add(it) } + } + + logger.debug("Couldn't find any matching policy with appropriate permissions that can manage index $indexName") + return null + } + + suspend fun canPolicyManagedIndex(policy: Policy, indexName: String): Boolean { + if (policy.user != null) { + try { + val request = ManagedIndexRequest().indices(indexName) + withClosableContext(IndexManagementSecurityContext("ApplyPolicyOnIndexCreation", settings, threadPool.threadContext, policy.user)) { + val response: AcknowledgedResponse = client.suspendUntil { execute(ManagedIndexAction.INSTANCE, request, it) } } + } catch (e: OpenSearchSecurityException) { + logger.debug("Skipping applying policy ${policy.id} on $indexName as the policy user is missing perimissions", e) + return false + } catch (e: Exception) { + // Ignore other exceptions } + } - return updateManagedIndexReqs + return true } - suspend fun getISMTemplates(): Map { + suspend fun getPoliciesWithISMTemplates(): List { + val errorMessage = "Failed to get ISM policies with templates" val searchRequest = SearchRequest() .source( SearchSourceBuilder().query( QueryBuilders.existsQuery(ISM_TEMPLATE_FIELD) - ) + ).size(MAX_HITS) ) .indices(INDEX_MANAGEMENT_INDEX) return try { val response: SearchResponse = client.suspendUntil { search(searchRequest, it) } - getPolicyToTemplateMap(response).filterNotNullValues() + parseFromSearchResponse(response = response, parse = Policy.Companion::parse) } catch (ex: IndexNotFoundException) { - emptyMap() + emptyList() } catch (ex: ClusterBlockException) { - emptyMap() + logger.error(errorMessage) + emptyList() } catch (e: SearchPhaseExecutionException) { - logger.error("Failed to get ISM templates: $e") - emptyMap() + logger.error("$errorMessage: $e") + emptyList() } catch (e: Exception) { - logger.error("Failed to get ISM templates", e) - emptyMap() + logger.error(errorMessage, e) + emptyList() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexRunner.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexRunner.kt index 865be64d8..562b20d28 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexRunner.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/ManagedIndexRunner.kt @@ -36,16 +36,13 @@ import kotlinx.coroutines.withContext import org.apache.logging.log4j.LogManager import org.opensearch.action.admin.cluster.state.ClusterStateRequest import org.opensearch.action.admin.cluster.state.ClusterStateResponse -import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest import org.opensearch.action.bulk.BackoffPolicy import org.opensearch.action.get.GetRequest import org.opensearch.action.get.GetResponse import org.opensearch.action.index.IndexResponse import org.opensearch.action.support.IndicesOptions -import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.update.UpdateResponse import org.opensearch.client.Client -import org.opensearch.cluster.block.ClusterBlockException import org.opensearch.cluster.health.ClusterHealthStatus import org.opensearch.cluster.health.ClusterStateHealth import org.opensearch.cluster.metadata.IndexMetadata @@ -60,7 +57,6 @@ import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentParser.Token import org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken import org.opensearch.common.xcontent.XContentType -import org.opensearch.index.Index import org.opensearch.index.engine.VersionConflictEngineException import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.IndexManagementIndices @@ -82,8 +78,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndex import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.INDEX_STATE_MANAGEMENT_ENABLED import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.JOB_INTERVAL import org.opensearch.indexmanagement.indexstatemanagement.step.Step -import org.opensearch.indexmanagement.indexstatemanagement.transport.action.updateindexmetadata.UpdateManagedIndexMetaDataAction -import org.opensearch.indexmanagement.indexstatemanagement.transport.action.updateindexmetadata.UpdateManagedIndexMetaDataRequest import org.opensearch.indexmanagement.indexstatemanagement.util.getActionToExecute import org.opensearch.indexmanagement.indexstatemanagement.util.getCompletedManagedIndexMetaData import org.opensearch.indexmanagement.indexstatemanagement.util.getStartingManagedIndexMetaData @@ -102,11 +96,13 @@ import org.opensearch.indexmanagement.indexstatemanagement.util.managedIndexMeta import org.opensearch.indexmanagement.indexstatemanagement.util.shouldBackoff import org.opensearch.indexmanagement.indexstatemanagement.util.shouldChangePolicy import org.opensearch.indexmanagement.indexstatemanagement.util.updateDisableManagedIndexRequest +import org.opensearch.indexmanagement.opensearchapi.IndexManagementSecurityContext import org.opensearch.indexmanagement.opensearchapi.convertToMap import org.opensearch.indexmanagement.opensearchapi.parseWithType import org.opensearch.indexmanagement.opensearchapi.retry import org.opensearch.indexmanagement.opensearchapi.string import org.opensearch.indexmanagement.opensearchapi.suspendUntil +import org.opensearch.indexmanagement.opensearchapi.withClosableContext import org.opensearch.jobscheduler.spi.JobExecutionContext import org.opensearch.jobscheduler.spi.LockModel import org.opensearch.jobscheduler.spi.ScheduledJobParameter @@ -116,6 +112,7 @@ import org.opensearch.rest.RestStatus import org.opensearch.script.Script import org.opensearch.script.ScriptService import org.opensearch.script.TemplateScript +import org.opensearch.threadpool.ThreadPool import java.time.Instant import java.time.temporal.ChronoUnit @@ -134,6 +131,7 @@ object ManagedIndexRunner : private lateinit var imIndices: IndexManagementIndices private lateinit var ismHistory: IndexStateManagementHistory private lateinit var skipExecFlag: SkipExecution + private lateinit var threadPool: ThreadPool private var indexStateManagementEnabled: Boolean = DEFAULT_ISM_ENABLED @Suppress("MagicNumber") private val savePolicyRetryPolicy = BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(250), 3) @@ -206,6 +204,11 @@ object ManagedIndexRunner : return this } + fun registerThreadPool(threadPool: ThreadPool): ManagedIndexRunner { + this.threadPool = threadPool + return this + } + override fun runJob(job: ScheduledJobParameter, context: JobExecutionContext) { if (job !is ManagedIndexConfig) { throw IllegalArgumentException("Invalid job type, found ${job.javaClass.simpleName} with id: ${context.jobId}") @@ -284,7 +287,9 @@ object ManagedIndexRunner : } val state = policy.getStateToExecute(managedIndexMetaData) - val action: Action? = state?.getActionToExecute(clusterService, scriptService, client, settings, managedIndexMetaData) + val action: Action? = state?.getActionToExecute( + clusterService, scriptService, client, settings, managedIndexMetaData.copy(user = policy.user, threadContext = threadPool.threadContext) + ) val step: Step? = action?.getStepToExecute() val currentActionMetaData = action?.getUpdatedActionMetaData(managedIndexMetaData, state) @@ -353,7 +358,13 @@ object ManagedIndexRunner : @Suppress("ComplexCondition") if (updateResult.metadataSaved && state != null && action != null && step != null && currentActionMetaData != null) { // Step null check is done in getStartingManagedIndexMetaData - step.preExecute(logger).execute().postExecute(logger) + withClosableContext( + IndexManagementSecurityContext( + managedIndexConfig.id, settings, threadPool.threadContext, managedIndexConfig.policy.user + ) + ) { + step.preExecute(logger).execute().postExecute(logger) + } var executedManagedIndexMetaData = startingManagedIndexMetaData.getCompletedManagedIndexMetaData(action, step) if (executedManagedIndexMetaData.isFailed) { @@ -573,29 +584,6 @@ object ManagedIndexRunner : } } - // delete metadata in cluster state - private suspend fun deleteManagedIndexMetaData(managedIndexMetaData: ManagedIndexMetaData): Boolean { - var result = false - try { - val request = UpdateManagedIndexMetaDataRequest( - indicesToRemoveManagedIndexMetaDataFrom = listOf(Index(managedIndexMetaData.index, managedIndexMetaData.indexUuid)) - ) - updateMetaDataRetryPolicy.retry(logger) { - val response: AcknowledgedResponse = client.suspendUntil { execute(UpdateManagedIndexMetaDataAction.INSTANCE, request, it) } - if (response.isAcknowledged) { - result = true - } else { - logger.error("Failed to delete ManagedIndexMetaData for [index=${managedIndexMetaData.index}]") - } - } - } catch (e: ClusterBlockException) { - logger.error("There was ClusterBlockException trying to delete the metadata for ${managedIndexMetaData.index}. Message: ${e.message}", e) - } catch (e: Exception) { - logger.error("Failed to delete ManagedIndexMetaData for [index=${managedIndexMetaData.index}]", e) - } - return result - } - /** * update metadata in config index, and save metadata in history after update * this can be called 2 times in one job run, so need to save seqNo & primeTerm @@ -721,18 +709,8 @@ object ManagedIndexRunner : if (!updated.metadataSaved || policy == null) return - // this will save the new policy on the job and reset the change policy back to null - val saved = savePolicyToManagedIndexConfig(managedIndexConfig, policy) - - if (saved) { - /* - * If we successfully saved the the new policy then the last thing we need to do is update the - * opendistro.indexstatemanagement.policy_id setting to the new policy id we don't care that much if this fails, because we'll - * have a check in the beginning of the runner to read in the setting and compare it with the policy_id on the job and update - * the setting if they ever differ, as we do not allow someone to change an existing policy using _settings API - * */ - updateIndexPolicyIDSetting(managedIndexConfig.index, changePolicy.policyID) - } + // Change the policy and user stored on the job from changePolicy, this will also set the changePolicy to null on the job + savePolicyToManagedIndexConfig(managedIndexConfig, policy.copy(user = changePolicy.user)) } @Suppress("TooGenericExceptionCaught") @@ -750,29 +728,6 @@ object ManagedIndexRunner : } } - /** - * Once we successfully swap over a ChangePolicy then we need to update the [ManagedIndexSettings.POLICY_ID] setting. - * - * We will constantly check the [ManagedIndexSettings.POLICY_ID] against the [ManagedIndexConfig] policyID and if - * there is ever a mismatch we will overwrite the [ManagedIndexSettings.POLICY_ID] with the [ManagedIndexConfig] policyID. - * - * We do this because if this fails we want to ensure we try again on the next execution of the job. At the same time, this - * will disallow the user from directly using the _settings API to change the policy_id. We do not want to allow this, - * they must use the ChangePolicy API as the [ManagedIndexSettings.POLICY_ID] is referring to the currently running policy. - */ - private suspend fun updateIndexPolicyIDSetting(index: String, policyID: String) { - try { - val settings = Settings.builder().put(ManagedIndexSettings.POLICY_ID.key, policyID).build() - val updateSettingsRequest = UpdateSettingsRequest(index).settings(settings) - val response: AcknowledgedResponse = client.admin().indices().suspendUntil { updateSettings(updateSettingsRequest, it) } - if (!response.isAcknowledged) { - logger.warn("Updating policy_id ($policyID) for $index was not acknowledged") - } - } catch (e: Exception) { - logger.error("There was an error while trying to update the policy_id ($policyID) setting for $index", e) - } - } - private suspend fun publishErrorNotification(policy: Policy, managedIndexMetaData: ManagedIndexMetaData) { policy.errorNotification?.run { errorNotificationRetryPolicy.retry(logger) { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotAction.kt index f06e1e870..00fea6a2d 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotAction.kt @@ -34,14 +34,16 @@ import org.opensearch.indexmanagement.indexstatemanagement.model.action.Snapshot import org.opensearch.indexmanagement.indexstatemanagement.step.Step import org.opensearch.indexmanagement.indexstatemanagement.step.snapshot.AttemptSnapshotStep import org.opensearch.indexmanagement.indexstatemanagement.step.snapshot.WaitForSnapshotStep +import org.opensearch.script.ScriptService class SnapshotAction( clusterService: ClusterService, + scriptService: ScriptService, client: Client, managedIndexMetaData: ManagedIndexMetaData, config: SnapshotActionConfig ) : Action(ActionType.SNAPSHOT, config, managedIndexMetaData) { - private val attemptSnapshotStep = AttemptSnapshotStep(clusterService, client, config, managedIndexMetaData) + private val attemptSnapshotStep = AttemptSnapshotStep(clusterService, scriptService, client, config, managedIndexMetaData) private val waitForSnapshotStep = WaitForSnapshotStep(clusterService, client, config, managedIndexMetaData) override fun getSteps(): List = listOf(attemptSnapshotStep, waitForSnapshotStep) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ChangePolicy.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ChangePolicy.kt index 33f897341..cce5ca2a5 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ChangePolicy.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ChangePolicy.kt @@ -35,7 +35,10 @@ import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParser.Token import org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.StateMetaData +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_USER +import org.opensearch.indexmanagement.opensearchapi.optionalUserField import java.io.IOException /** @@ -52,7 +55,8 @@ data class ChangePolicy( val policyID: String, val state: String?, val include: List, - val isSafe: Boolean + val isSafe: Boolean, + val user: User? = null ) : Writeable, ToXContentObject { @Throws(IOException::class) @@ -60,7 +64,10 @@ data class ChangePolicy( policyID = sin.readString(), state = sin.readOptionalString(), include = sin.readList(::StateFilter), - isSafe = sin.readBoolean() + isSafe = sin.readBoolean(), + user = if (sin.readBoolean()) { + User(sin) + } else null ) override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { @@ -69,8 +76,8 @@ data class ChangePolicy( .field(ManagedIndexConfig.POLICY_ID_FIELD, policyID) .field(StateMetaData.STATE, state) .field(IS_SAFE_FIELD, isSafe) - .endObject() - return builder + if (params.paramAsBoolean(WITH_USER, true)) builder.optionalUserField(USER_FIELD, user) + return builder.endObject() } @Throws(IOException::class) @@ -79,6 +86,8 @@ data class ChangePolicy( out.writeOptionalString(state) out.writeList(include) out.writeBoolean(isSafe) + out.writeBoolean(user != null) + user?.writeTo(out) } companion object { @@ -86,6 +95,7 @@ data class ChangePolicy( const val STATE_FIELD = "state" const val INCLUDE_FIELD = "include" const val IS_SAFE_FIELD = "is_safe" + const val USER_FIELD = "user" @JvmStatic @Throws(IOException::class) @@ -93,6 +103,7 @@ data class ChangePolicy( var policyID: String? = null var state: String? = null var isSafe: Boolean = false + var user: User? = null val include = mutableListOf() ensureExpectedToken(Token.START_OBJECT, xcp.currentToken(), xcp) @@ -110,6 +121,9 @@ data class ChangePolicy( } } IS_SAFE_FIELD -> isSafe = xcp.booleanValue() + USER_FIELD -> { + user = if (xcp.currentToken() == Token.VALUE_NULL) null else User.parse(xcp) + } else -> throw IllegalArgumentException("Invalid field: [$fieldName] found in ChangePolicy.") } } @@ -118,7 +132,8 @@ data class ChangePolicy( requireNotNull(policyID) { "ChangePolicy policy id is null" }, state, include.toList(), - isSafe + isSafe, + user ) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexConfig.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexConfig.kt index 0645c0b2f..69cb6e6af 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexConfig.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexConfig.kt @@ -56,7 +56,8 @@ data class ManagedIndexConfig( val policySeqNo: Long?, val policyPrimaryTerm: Long?, val policy: Policy?, - val changePolicy: ChangePolicy? + val changePolicy: ChangePolicy?, + val jobJitter: Double? ) : ScheduledJobParameter { init { @@ -79,6 +80,10 @@ data class ManagedIndexConfig( override fun getLockDurationSeconds(): Long = 3600L // 1 hour + override fun getJitter(): Double? { + return jobJitter + } + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { builder .startObject() @@ -95,9 +100,9 @@ data class ManagedIndexConfig( .field(POLICY_PRIMARY_TERM_FIELD, policyPrimaryTerm) .field(POLICY_FIELD, policy, XCONTENT_WITHOUT_TYPE) .field(CHANGE_POLICY_FIELD, changePolicy) - .endObject() - .endObject() - return builder + .field(JITTER, jobJitter) + builder.endObject() + return builder.endObject() } companion object { @@ -115,6 +120,7 @@ data class ManagedIndexConfig( const val POLICY_SEQ_NO_FIELD = "policy_seq_no" const val POLICY_PRIMARY_TERM_FIELD = "policy_primary_term" const val CHANGE_POLICY_FIELD = "change_policy" + const val JITTER = "jitter" @Suppress("ComplexMethod", "LongMethod") @JvmStatic @@ -138,6 +144,7 @@ data class ManagedIndexConfig( var enabled = true var policyPrimaryTerm: Long? = SequenceNumbers.UNASSIGNED_PRIMARY_TERM var policySeqNo: Long? = SequenceNumbers.UNASSIGNED_SEQ_NO + var jitter: Double? = null ensureExpectedToken(Token.START_OBJECT, xcp.currentToken(), xcp) while (xcp.nextToken() != Token.END_OBJECT) { @@ -165,6 +172,9 @@ data class ManagedIndexConfig( CHANGE_POLICY_FIELD -> { changePolicy = if (xcp.currentToken() == Token.VALUE_NULL) null else ChangePolicy.parse(xcp) } + JITTER -> { + jitter = if (xcp.currentToken() == Token.VALUE_NULL) null else xcp.doubleValue() + } else -> throw IllegalArgumentException("Invalid field: [$fieldName] found in ManagedIndexConfig.") } } @@ -193,7 +203,8 @@ data class ManagedIndexConfig( seqNo = policySeqNo ?: SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm = policyPrimaryTerm ?: SequenceNumbers.UNASSIGNED_PRIMARY_TERM ), - changePolicy = changePolicy + changePolicy = changePolicy, + jobJitter = jitter ) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexMetaData.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexMetaData.kt index 0b89901f9..79c04291a 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexMetaData.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/ManagedIndexMetaData.kt @@ -30,6 +30,7 @@ import org.opensearch.common.Strings import org.opensearch.common.io.stream.StreamInput import org.opensearch.common.io.stream.StreamOutput import org.opensearch.common.io.stream.Writeable +import org.opensearch.common.util.concurrent.ThreadContext import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentFragment import org.opensearch.common.xcontent.XContentBuilder @@ -39,6 +40,7 @@ import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParser.Token import org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken import org.opensearch.common.xcontent.json.JsonXContent +import org.opensearch.commons.authuser.User import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.indexstatemanagement.model.action.ActionConfig import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.ActionMetaData @@ -64,7 +66,13 @@ data class ManagedIndexMetaData( val info: Map?, val id: String = NO_ID, val seqNo: Long = SequenceNumbers.UNASSIGNED_SEQ_NO, - val primaryTerm: Long = SequenceNumbers.UNASSIGNED_PRIMARY_TERM + val primaryTerm: Long = SequenceNumbers.UNASSIGNED_PRIMARY_TERM, + // TODO: Remove this once the step interface is updated to pass in user information. + // The user information is not being stored/written anywhere, this is only intended to be used during the step execution. + val user: User? = null, + // TODO: Remove this once the step interface is updated to pass in thread context information. + // This information is not being stored/written anywhere, this is only intended to be used during the step execution. + val threadContext: ThreadContext? = null ) : Writeable, ToXContentFragment { @Suppress("ComplexMethod") 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 f60cda4d9..bed51d43c 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/Policy.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/Policy.kt @@ -35,11 +35,14 @@ import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParser.Token import org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken +import org.opensearch.commons.authuser.User import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_USER import org.opensearch.indexmanagement.opensearchapi.instant import org.opensearch.indexmanagement.opensearchapi.optionalISMTemplateField import org.opensearch.indexmanagement.opensearchapi.optionalTimeField +import org.opensearch.indexmanagement.opensearchapi.optionalUserField import org.opensearch.indexmanagement.util.IndexUtils import java.io.IOException import java.time.Instant @@ -54,7 +57,8 @@ data class Policy( val errorNotification: ErrorNotification?, val defaultState: String, val states: List, - val ismTemplate: ISMTemplate? = null + val ismTemplate: List? = null, + val user: User? = null ) : ToXContentObject, Writeable { init { @@ -86,6 +90,7 @@ data class Policy( .field(DEFAULT_STATE_FIELD, defaultState) .field(STATES_FIELD, states.toTypedArray()) .optionalISMTemplateField(ISM_TEMPLATE, ismTemplate) + if (params.paramAsBoolean(WITH_USER, true)) builder.optionalUserField(USER_FIELD, user) if (params.paramAsBoolean(WITH_TYPE, true)) builder.endObject() return builder.endObject() } @@ -101,7 +106,12 @@ data class Policy( errorNotification = sin.readOptionalWriteable(::ErrorNotification), defaultState = sin.readString(), states = sin.readList(::State), - ismTemplate = sin.readOptionalWriteable(::ISMTemplate) + ismTemplate = if (sin.readBoolean()) { + sin.readList(::ISMTemplate) + } else null, + user = if (sin.readBoolean()) { + User(sin) + } else null ) @Throws(IOException::class) @@ -115,7 +125,14 @@ data class Policy( out.writeOptionalWriteable(errorNotification) out.writeString(defaultState) out.writeList(states) - out.writeOptionalWriteable(ismTemplate) + if (ismTemplate != null) { + out.writeBoolean(true) + out.writeList(ismTemplate) + } else { + out.writeBoolean(false) + } + out.writeBoolean(user != null) + user?.writeTo(out) } companion object { @@ -129,8 +146,9 @@ data class Policy( const val DEFAULT_STATE_FIELD = "default_state" const val STATES_FIELD = "states" const val ISM_TEMPLATE = "ism_template" + const val USER_FIELD = "user" - @Suppress("ComplexMethod") + @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth") @JvmStatic @JvmOverloads @Throws(IOException::class) @@ -146,7 +164,8 @@ data class Policy( var lastUpdatedTime: Instant? = null var schemaVersion: Long = IndexUtils.DEFAULT_SCHEMA_VERSION val states: MutableList = mutableListOf() - var ismTemplate: ISMTemplate? = null + var ismTemplates: List? = null + var user: User? = null ensureExpectedToken(Token.START_OBJECT, xcp.currentToken(), xcp) while (xcp.nextToken() != Token.END_OBJECT) { @@ -166,7 +185,23 @@ data class Policy( states.add(State.parse(xcp)) } } - ISM_TEMPLATE -> ismTemplate = if (xcp.currentToken() == Token.VALUE_NULL) null else ISMTemplate.parse(xcp) + ISM_TEMPLATE -> { + if (xcp.currentToken() != Token.VALUE_NULL) { + ismTemplates = mutableListOf() + when (xcp.currentToken()) { + Token.START_ARRAY -> { + while (xcp.nextToken() != Token.END_ARRAY) { + ismTemplates.add(ISMTemplate.parse(xcp)) + } + } + Token.START_OBJECT -> { + ismTemplates.add(ISMTemplate.parse(xcp)) + } + else -> ensureExpectedToken(Token.START_ARRAY, xcp.currentToken(), xcp) + } + } + } + USER_FIELD -> user = if (xcp.currentToken() == Token.VALUE_NULL) null else User.parse(xcp) else -> throw IllegalArgumentException("Invalid field: [$fieldName] found in Policy.") } } @@ -181,7 +216,8 @@ data class Policy( errorNotification = errorNotification, defaultState = requireNotNull(defaultState) { "$DEFAULT_STATE_FIELD is null" }, states = states.toList(), - ismTemplate = ismTemplate + ismTemplate = ismTemplates, + user = user ) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/action/SnapshotActionConfig.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/action/SnapshotActionConfig.kt index a09f7c475..7243497cb 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/action/SnapshotActionConfig.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/model/action/SnapshotActionConfig.kt @@ -66,7 +66,7 @@ data class SnapshotActionConfig( client: Client, settings: Settings, managedIndexMetaData: ManagedIndexMetaData - ): Action = SnapshotAction(clusterService, client, managedIndexMetaData, this) + ): Action = SnapshotAction(clusterService, scriptService, client, managedIndexMetaData, this) @Throws(IOException::class) constructor(sin: StreamInput) : this( diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/opensearchapi/OpenSearchExtensions.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/opensearchapi/OpenSearchExtensions.kt index 361e0de6d..b3cf68ea8 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/opensearchapi/OpenSearchExtensions.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/opensearchapi/OpenSearchExtensions.kt @@ -37,7 +37,6 @@ import org.opensearch.action.get.GetRequest import org.opensearch.action.get.GetResponse import org.opensearch.action.get.MultiGetRequest import org.opensearch.action.get.MultiGetResponse -import org.opensearch.action.search.SearchResponse import org.opensearch.client.Client import org.opensearch.cluster.ClusterState import org.opensearch.cluster.metadata.IndexMetadata @@ -46,20 +45,16 @@ import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentFragment import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.common.xcontent.XContentFactory import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType import org.opensearch.index.Index import org.opensearch.index.IndexNotFoundException import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX -import org.opensearch.indexmanagement.indexstatemanagement.model.ISMTemplate import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData -import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.settings.LegacyOpenDistroManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.util.managedIndexMetadataID import org.opensearch.indexmanagement.opensearchapi.contentParser -import org.opensearch.indexmanagement.opensearchapi.parseWithType import org.opensearch.indexmanagement.opensearchapi.suspendUntil private val log = LogManager.getLogger("Index Management Helper") @@ -104,27 +99,6 @@ fun getUuidsForClosedIndices(state: ClusterState): MutableList { return closeList } -/** - * Do a exists search query to retrieve all policy with ism_template field - * parse search response with this function - * - * @return map of policyID to ISMTemplate in this policy - * @throws [IllegalArgumentException] - */ -@Throws(Exception::class) -fun getPolicyToTemplateMap(response: SearchResponse, xContentRegistry: NamedXContentRegistry = NamedXContentRegistry.EMPTY): - Map { - return response.hits.hits.map { - val id = it.id - val seqNo = it.seqNo - val primaryTerm = it.primaryTerm - val xcp = XContentFactory.xContent(XContentType.JSON) - .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, it.sourceAsString) - xcp.parseWithType(id, seqNo, primaryTerm, Policy.Companion::parse) - .copy(id = id, seqNo = seqNo, primaryTerm = primaryTerm) - }.map { it.id to it.ismTemplate }.toMap() -} - @Suppress("UNCHECKED_CAST") fun Map.filterNotNullValues(): Map = filterValues { it != null } as Map diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/settings/ManagedIndexSettings.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/settings/ManagedIndexSettings.kt index d83cb5304..dd26a25f8 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/settings/ManagedIndexSettings.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/settings/ManagedIndexSettings.kt @@ -36,6 +36,7 @@ class ManagedIndexSettings { const val DEFAULT_ISM_ENABLED = true const val DEFAULT_METADATA_SERVICE_ENABLED = true const val DEFAULT_JOB_INTERVAL = 5 + const val DEFAULT_JITTER = 0.6 private val ALLOW_LIST_ALL = ActionConfig.ActionType.values().toList().map { it.type } val ALLOW_LIST_NONE = emptyList() val SNAPSHOT_DENY_LIST_NONE = emptyList() @@ -76,6 +77,13 @@ class ManagedIndexSettings { Setting.Property.Dynamic ) + val AUTO_MANAGE: Setting = Setting.boolSetting( + "index.plugins.index_state_management.auto_manage", + true, + Setting.Property.IndexScope, + Setting.Property.Dynamic + ) + val JOB_INTERVAL: Setting = Setting.intSetting( "plugins.index_state_management.job_interval", LegacyOpenDistroManagedIndexSettings.JOB_INTERVAL, @@ -172,5 +180,14 @@ class ManagedIndexSettings { Setting.Property.NodeScope, Setting.Property.Dynamic ) + + val JITTER: Setting = Setting.doubleSetting( + "plugins.index_state_management.jitter", + DEFAULT_JITTER, + 0.0, + 1.0, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/notification/AttemptNotificationStep.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/notification/AttemptNotificationStep.kt index 26cea8962..b53cdaa48 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/notification/AttemptNotificationStep.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/notification/AttemptNotificationStep.kt @@ -66,6 +66,7 @@ class AttemptNotificationStep( } // publish internally throws an error for any invalid responses so its safe to assume if we reach this point it was successful + // publish and send throws an error for any invalid responses so its safe to assume if we reach this point it was successful stepStatus = StepStatus.COMPLETED info = mapOf("message" to getSuccessMessage(indexName)) } catch (e: Exception) { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollover/AttemptRolloverStep.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollover/AttemptRolloverStep.kt index cd9bbb1ca..3f2137891 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollover/AttemptRolloverStep.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollover/AttemptRolloverStep.kt @@ -63,7 +63,7 @@ class AttemptRolloverStep( override fun isIdempotent() = false - @Suppress("TooGenericExceptionCaught") + @Suppress("ComplexMethod", "LongMethod", "TooGenericExceptionCaught") override suspend fun execute(): AttemptRolloverStep { val skipRollover = clusterService.state().metadata.index(indexName).getRolloverSkip() if (skipRollover) { @@ -72,18 +72,16 @@ class AttemptRolloverStep( return this } - // If we have already rolled over this index then fail as we only allow an index to be rolled over once - if (managedIndexMetaData.rolledOver == true) { - logger.warn("$indexName was already rolled over, cannot execute rollover step") - stepStatus = StepStatus.FAILED - info = mapOf("message" to getFailedDuplicateRolloverMessage(indexName)) - return this - } - val (rolloverTarget, isDataStream) = getRolloverTargetOrUpdateInfo() // If the rolloverTarget is null, we would've already updated the failed info from getRolloverTargetOrUpdateInfo and can return early rolloverTarget ?: return this + if (clusterService.state().metadata.index(indexName).rolloverInfos.containsKey(rolloverTarget)) { + stepStatus = StepStatus.COMPLETED + info = mapOf("message" to getAlreadyRolledOverMessage(indexName, rolloverTarget)) + return this + } + if (!isDataStream && !preCheckIndexAlias(rolloverTarget)) { stepStatus = StepStatus.FAILED info = mapOf("message" to getFailedPreCheckMessage(indexName)) @@ -278,13 +276,13 @@ class AttemptRolloverStep( ) } + @Suppress("TooManyFunctions") companion object { fun getFailedMessage(index: String) = "Failed to rollover index [index=$index]" fun getFailedAliasUpdateMessage(index: String, newIndex: String) = "New index created, but failed to update alias [index=$index, newIndex=$newIndex]" fun getFailedDataStreamRolloverMessage(dataStream: String) = "Failed to rollover data stream [data_stream=$dataStream]" fun getFailedNoValidAliasMessage(index: String) = "Missing rollover_alias index setting [index=$index]" - fun getFailedDuplicateRolloverMessage(index: String) = "Index has already been rolled over [index=$index]" fun getFailedEvaluateMessage(index: String) = "Failed to evaluate conditions for rollover [index=$index]" fun getPendingMessage(index: String) = "Pending rollover of index [index=$index]" fun getSuccessMessage(index: String) = "Successfully rolled over index [index=$index]" @@ -292,5 +290,7 @@ class AttemptRolloverStep( "Successfully rolled over data stream [data_stream=$dataStream index=$index]" fun getFailedPreCheckMessage(index: String) = "Missing alias or not the write index when rollover [index=$index]" fun getSkipRolloverMessage(index: String) = "Skipped rollover action for [index=$index]" + fun getAlreadyRolledOverMessage(index: String, alias: String) = + "This index has already been rolled over using this alias, treating as a success [index=$index, alias=$alias]" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollup/AttemptCreateRollupJobStep.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollup/AttemptCreateRollupJobStep.kt index bc4635c12..edc0a0418 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollup/AttemptCreateRollupJobStep.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/rollup/AttemptCreateRollupJobStep.kt @@ -68,7 +68,7 @@ class AttemptCreateRollupJobStep( hasPreviousRollupAttemptFailed = managedIndexMetaData.actionMetaData?.actionProperties?.hasRollupFailed // Creating a rollup job - val rollup = ismRollup.toRollup(indexName) + val rollup = ismRollup.toRollup(indexName, managedIndexMetaData.user) rollupId = rollup.id logger.info("Attempting to create a rollup job $rollupId for index $indexName") diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/snapshot/AttemptSnapshotStep.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/snapshot/AttemptSnapshotStep.kt index 34b5bc9f5..dcc3ead71 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/snapshot/AttemptSnapshotStep.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/snapshot/AttemptSnapshotStep.kt @@ -39,8 +39,13 @@ import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmet import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.StepMetaData import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.SNAPSHOT_DENY_LIST import org.opensearch.indexmanagement.indexstatemanagement.step.Step +import org.opensearch.indexmanagement.opensearchapi.convertToMap import org.opensearch.indexmanagement.opensearchapi.suspendUntil import org.opensearch.rest.RestStatus +import org.opensearch.script.Script +import org.opensearch.script.ScriptService +import org.opensearch.script.ScriptType +import org.opensearch.script.TemplateScript import org.opensearch.snapshots.ConcurrentSnapshotExecutionException import org.opensearch.transport.RemoteTransportException import java.time.LocalDateTime @@ -50,6 +55,7 @@ import java.util.Locale class AttemptSnapshotStep( val clusterService: ClusterService, + val scriptService: ScriptService, val client: Client, val config: SnapshotActionConfig, managedIndexMetaData: ManagedIndexMetaData @@ -74,15 +80,15 @@ class AttemptSnapshotStep( info = mutableInfo.toMap() return this } + val snapshotNameSuffix = "-".plus( + LocalDateTime.now(ZoneId.of("UTC")) + .format(DateTimeFormatter.ofPattern("uuuu.MM.dd-HH:mm:ss.SSS", Locale.ROOT)) + ) - snapshotName = config - .snapshot - .plus("-") - .plus( - LocalDateTime - .now(ZoneId.of("UTC")) - .format(DateTimeFormatter.ofPattern("uuuu.MM.dd-HH:mm:ss.SSS", Locale.ROOT)) - ) + val snapshotScript = Script(ScriptType.INLINE, Script.DEFAULT_TEMPLATE_LANG, config.snapshot, mapOf()) + // If user intentionally set the snapshot name empty then we are going to honor it + val defaultSnapshotName = if (config.snapshot.isBlank()) config.snapshot else indexName + snapshotName = compileTemplate(snapshotScript, managedIndexMetaData, defaultSnapshotName).plus(snapshotNameSuffix) val createSnapshotRequest = CreateSnapshotRequest() .userMetadata(mapOf("snapshot_created" to "Open Distro for Elasticsearch Index Management")) @@ -148,6 +154,16 @@ class AttemptSnapshotStep( info = mutableInfo.toMap() } + private fun compileTemplate(template: Script, managedIndexMetaData: ManagedIndexMetaData, defaultValue: String): String { + val contextMap = managedIndexMetaData.convertToMap().filterKeys { key -> + key in validTopContextFields + } + val compiledValue = scriptService.compile(template, TemplateScript.CONTEXT) + .newInstance(template.params + mapOf("ctx" to contextMap)) + .execute() + return if (compiledValue.isBlank()) defaultValue else compiledValue + } + override fun getUpdatedManagedIndexMetaData(currentMetaData: ManagedIndexMetaData): ManagedIndexMetaData { val currentActionMetaData = currentMetaData.actionMetaData return currentMetaData.copy( @@ -159,6 +175,7 @@ class AttemptSnapshotStep( } companion object { + val validTopContextFields = setOf("index", "indexUuid") const val name = "attempt_snapshot" fun getBlockedMessage(denyList: List, repoName: String, index: String) = "Snapshot repository [$repoName] is blocked in $denyList [index=$index]" diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyAction.kt index dcb24cb8b..9cdf8f3d8 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyAction.kt @@ -32,6 +32,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMS class AddPolicyAction private constructor() : ActionType(NAME, ::ISMStatusResponse) { companion object { val INSTANCE = AddPolicyAction() - val NAME = "cluster:admin/opendistro/ism/managedindex/add" + const val NAME = "cluster:admin/opendistro/ism/managedindex/add" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyRequest.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyRequest.kt index 58da2ed21..8d2a493bb 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyRequest.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/AddPolicyRequest.kt @@ -33,18 +33,10 @@ import org.opensearch.common.io.stream.StreamInput import org.opensearch.common.io.stream.StreamOutput import java.io.IOException -class AddPolicyRequest : ActionRequest { - - val indices: List +class AddPolicyRequest( + val indices: List, val policyID: String - - constructor( - indices: List, - policyID: String - ) : super() { - this.indices = indices - this.policyID = policyID - } +) : ActionRequest() { @Throws(IOException::class) constructor(sin: StreamInput) : this( diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/TransportAddPolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/TransportAddPolicyAction.kt index bf5d898a3..64ec1a869 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/TransportAddPolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/addpolicy/TransportAddPolicyAction.kt @@ -28,6 +28,7 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.add import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.OpenSearchStatusException import org.opensearch.OpenSearchTimeoutException import org.opensearch.action.ActionListener @@ -35,6 +36,8 @@ import org.opensearch.action.admin.cluster.state.ClusterStateRequest import org.opensearch.action.admin.cluster.state.ClusterStateResponse import org.opensearch.action.bulk.BulkRequest import org.opensearch.action.bulk.BulkResponse +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse import org.opensearch.action.get.MultiGetRequest import org.opensearch.action.get.MultiGetResponse import org.opensearch.action.support.ActionFilters @@ -44,43 +47,65 @@ import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.client.node.NodeClient import org.opensearch.cluster.ClusterState import org.opensearch.cluster.block.ClusterBlockException +import org.opensearch.cluster.metadata.IndexNameExpressionResolver import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject import org.opensearch.common.settings.Settings import org.opensearch.common.unit.TimeValue -import org.opensearch.indexmanagement.IndexManagementIndices +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX +import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.getUuidsForClosedIndices import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMStatusResponse +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexRequest import org.opensearch.indexmanagement.indexstatemanagement.util.FailedIndex import org.opensearch.indexmanagement.indexstatemanagement.util.managedIndexConfigIndexRequest +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.IndexUtils +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.validateUserConfiguration import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService import java.lang.Exception +import java.lang.IllegalArgumentException import java.time.Duration import java.time.Instant private val log = LogManager.getLogger(TransportAddPolicyAction::class.java) +@Suppress("SpreadOperator", "ReturnCount") class TransportAddPolicyAction @Inject constructor( val client: NodeClient, transportService: TransportService, actionFilters: ActionFilters, val settings: Settings, val clusterService: ClusterService, - val ismIndices: IndexManagementIndices + val xContentRegistry: NamedXContentRegistry, + val indexNameExpressionResolver: IndexNameExpressionResolver ) : HandledTransportAction( AddPolicyAction.NAME, transportService, actionFilters, ::AddPolicyRequest ) { @Volatile private var jobInterval = ManagedIndexSettings.JOB_INTERVAL.get(settings) + @Volatile private var jobJitter = ManagedIndexSettings.JITTER.get(settings) + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(ManagedIndexSettings.JOB_INTERVAL) { jobInterval = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(ManagedIndexSettings.JITTER) { + jobJitter = it + } + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } } override fun doExecute(task: Task, request: AddPolicyRequest, listener: ActionListener) { @@ -90,25 +115,119 @@ class TransportAddPolicyAction @Inject constructor( inner class AddPolicyHandler( private val client: NodeClient, private val actionListener: ActionListener, - private val request: AddPolicyRequest + private val request: AddPolicyRequest, + private val user: User? = buildUser(client.threadPool().threadContext) ) { private lateinit var startTime: Instant + private lateinit var policy: Policy + private val resolvedIndices = mutableListOf() private val indicesToAdd = mutableMapOf() // uuid: name private val failedIndices: MutableList = mutableListOf() fun start() { - ismIndices.checkAndUpdateIMConfigIndex(object : ActionListener { - override fun onResponse(response: AcknowledgedResponse) { - onCreateMappingsResponse(response) + if (!validateUserConfiguration(user, filterByEnabled, actionListener)) { + return + } + val requestedIndices = mutableListOf() + request.indices.forEach { index -> + requestedIndices.addAll( + indexNameExpressionResolver.concreteIndexNames( + clusterService.state(), + IndicesOptions.lenientExpand(), + true, + index + ) + ) + } + if (requestedIndices.isEmpty()) { + // Nothing to do will ignore since found no matching indices + actionListener.onResponse(ISMStatusResponse(0, failedIndices)) + return + } + if (user == null) { + resolvedIndices.addAll(requestedIndices) + getPolicy() + } else { + validateAndGetPolicy(0, requestedIndices) + } + } + + /** + * We filter the requested indices to the indices user has permission to manage and apply policies only on top of those + */ + private fun validateAndGetPolicy(current: Int, indices: List) { + val request = ManagedIndexRequest().indices(indices[current]) + client.execute( + ManagedIndexAction.INSTANCE, + request, + object : ActionListener { + override fun onResponse(response: AcknowledgedResponse) { + resolvedIndices.add(indices[current]) + proceed(current, indices) + } + + override fun onFailure(e: Exception) { + when (e is OpenSearchSecurityException) { + true -> { + proceed(current, indices) + } + false -> { + // failing the request for any other exception + actionListener.onFailure(e) + } + } + } + } + ) + } + + private fun proceed(current: Int, indices: List) { + if (current < indices.count() - 1) { + validateAndGetPolicy(current + 1, indices) + } else { + // sanity check that there are indices - if none then return + if (resolvedIndices.isEmpty()) { + actionListener.onResponse(ISMStatusResponse(0, failedIndices)) + return } + getPolicy() + } + } - override fun onFailure(t: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + private fun getPolicy() { + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.policyID) + + client.threadPool().threadContext.stashContext().use { + if (!validateUserConfiguration(user, filterByEnabled, actionListener)) { + return } - }) + client.get(getRequest, ActionListener.wrap(::onGetPolicyResponse, ::onFailure)) + } } - private fun onCreateMappingsResponse(response: AcknowledgedResponse) { + private fun onGetPolicyResponse(response: GetResponse) { + if (!response.isExists || response.isSourceEmpty) { + actionListener.onFailure(OpenSearchStatusException("Could not find policy=${request.policyID}", RestStatus.NOT_FOUND)) + return + } + try { + this.policy = parseFromGetResponse(response, xContentRegistry, Policy.Companion::parse) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Could not find policy=${request.policyID}", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, policy.user, filterByEnabled, "policy", request.policyID, actionListener)) { + return + } + + IndexUtils.checkAndUpdateConfigIndexMapping( + clusterService.state(), + client.admin().indices(), + ActionListener.wrap(::onUpdateMapping, ::onFailure) + ) + } + + private fun onUpdateMapping(response: AcknowledgedResponse) { if (response.isAcknowledged) { log.info("Successfully created or updated $INDEX_MANAGEMENT_INDEX with newest mappings.") getClusterState() @@ -130,7 +249,7 @@ class TransportAddPolicyAction @Inject constructor( val clusterStateRequest = ClusterStateRequest() .clear() - .indices(*request.indices.toTypedArray()) + .indices(*resolvedIndices.toTypedArray()) .metadata(true) .local(false) .waitForTimeout(TimeValue.timeValueMillis(ADD_POLICY_TIMEOUT_IN_MILLIS)) @@ -213,7 +332,9 @@ class TransportAddPolicyAction @Inject constructor( val bulkReq = BulkRequest().timeout(TimeValue.timeValueMillis(bulkReqTimeout)) indicesToAdd.forEach { (uuid, name) -> - bulkReq.add(managedIndexConfigIndexRequest(name, uuid, request.policyID, jobInterval)) + bulkReq.add( + managedIndexConfigIndexRequest(name, uuid, request.policyID, jobInterval, policy = policy.copy(user = this.user), jobJitter) + ) } client.bulk( @@ -251,6 +372,10 @@ class TransportAddPolicyAction @Inject constructor( actionListener.onResponse(ISMStatusResponse(0, failedIndices)) } } + + private fun onFailure(t: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + } } companion object { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyAction.kt index 37cdfbff2..94fdfd732 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyAction.kt @@ -32,6 +32,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMS class ChangePolicyAction private constructor() : ActionType(NAME, ::ISMStatusResponse) { companion object { val INSTANCE = ChangePolicyAction() - val NAME = "cluster:admin/opendistro/ism/managedindex/change" + const val NAME = "cluster:admin/opendistro/ism/managedindex/change" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyRequest.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyRequest.kt index 2df9a4f8e..28e3851e2 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyRequest.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/ChangePolicyRequest.kt @@ -34,18 +34,10 @@ import org.opensearch.common.io.stream.StreamOutput import org.opensearch.indexmanagement.indexstatemanagement.model.ChangePolicy import java.io.IOException -class ChangePolicyRequest : ActionRequest { - - val indices: List +class ChangePolicyRequest( + val indices: List, val changePolicy: ChangePolicy - - constructor( - indices: List, - changePolicy: ChangePolicy - ) : super() { - this.indices = indices - this.changePolicy = changePolicy - } +) : ActionRequest() { @Throws(IOException::class) constructor(sin: StreamInput) : this( diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/TransportChangePolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/TransportChangePolicyAction.kt index 06e98196c..6981ea46f 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/TransportChangePolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/changepolicy/TransportChangePolicyAction.kt @@ -28,6 +28,7 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.cha import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionListener import org.opensearch.action.admin.cluster.state.ClusterStateRequest @@ -46,10 +47,9 @@ import org.opensearch.client.node.NodeClient import org.opensearch.cluster.ClusterState import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject -import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry -import org.opensearch.common.xcontent.XContentHelper -import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.authuser.User import org.opensearch.index.Index import org.opensearch.indexmanagement.IndexManagementPlugin import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexConfig @@ -61,38 +61,58 @@ import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.getMana import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.mgetResponseToList import org.opensearch.indexmanagement.indexstatemanagement.resthandler.RestChangePolicyAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMStatusResponse +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexRequest import org.opensearch.indexmanagement.indexstatemanagement.util.FailedIndex import org.opensearch.indexmanagement.indexstatemanagement.util.isSafeToChange import org.opensearch.indexmanagement.indexstatemanagement.util.updateManagedIndexRequest import org.opensearch.indexmanagement.opensearchapi.contentParser +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse import org.opensearch.indexmanagement.opensearchapi.parseWithType +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.IndexManagementException import org.opensearch.indexmanagement.util.IndexUtils import org.opensearch.indexmanagement.util.NO_ID +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.validateUserConfiguration import org.opensearch.rest.RestStatus import org.opensearch.search.fetch.subphase.FetchSourceContext import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +import java.lang.IllegalArgumentException private val log = LogManager.getLogger(TransportChangePolicyAction::class.java) +@Suppress("SpreadOperator", "TooManyFunctions") class TransportChangePolicyAction @Inject constructor( val client: NodeClient, transportService: TransportService, actionFilters: ActionFilters, val clusterService: ClusterService, + val settings: Settings, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( ChangePolicyAction.NAME, transportService, actionFilters, ::ChangePolicyRequest ) { + + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: ChangePolicyRequest, listener: ActionListener) { ChangePolicyHandler(client, listener, request).start() } - @Suppress("TooManyFunctions") inner class ChangePolicyHandler( private val client: NodeClient, private val actionListener: ActionListener, - private val request: ChangePolicyRequest + private val request: ChangePolicyRequest, + private val user: User? = buildUser(client.threadPool().threadContext) ) { private val failedIndices = mutableListOf() @@ -100,14 +120,53 @@ class TransportChangePolicyAction @Inject constructor( private val indexUuidToCurrentState = mutableMapOf() private val changePolicy = request.changePolicy private lateinit var policy: Policy - private lateinit var getPolicyResponse: GetResponse private lateinit var clusterState: ClusterState private var updated: Int = 0 fun start() { + if (user == null) { + getPolicy() + } else { + validateAndGetPolicy() + } + } + + private fun validateAndGetPolicy() { + val request = ManagedIndexRequest().indices(*request.indices.toTypedArray()) + client.execute( + ManagedIndexAction.INSTANCE, + request, + object : ActionListener { + override fun onResponse(response: AcknowledgedResponse) { + getPolicy() + } + + override fun onFailure(e: java.lang.Exception) { + actionListener.onFailure( + IndexManagementException.wrap( + when (e is OpenSearchSecurityException) { + true -> OpenSearchStatusException( + "User doesn't have required index permissions on one or more requested indices: ${e.localizedMessage}", + RestStatus.FORBIDDEN + ) + false -> e + } + ) + ) + } + } + ) + } + + private fun getPolicy() { val getRequest = GetRequest(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX, changePolicy.policyID) - client.get(getRequest, ActionListener.wrap(::onGetPolicyResponse, ::onFailure)) + client.threadPool().threadContext.stashContext().use { + if (!validateUserConfiguration(user, filterByEnabled, actionListener)) { + return + } + client.get(getRequest, ActionListener.wrap(::onGetPolicyResponse, ::onFailure)) + } } private fun onGetPolicyResponse(response: GetResponse) { @@ -115,7 +174,15 @@ class TransportChangePolicyAction @Inject constructor( actionListener.onFailure(OpenSearchStatusException("Could not find policy=${request.changePolicy.policyID}", RestStatus.NOT_FOUND)) return } - this.getPolicyResponse = response + try { + policy = parseFromGetResponse(response, xContentRegistry, Policy.Companion::parse) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Could not find policy=${request.changePolicy.policyID}", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, policy.user, filterByEnabled, "policy", request.changePolicy.policyID, actionListener)) { + return + } IndexUtils.checkAndUpdateConfigIndexMapping( clusterService.state(), @@ -135,13 +202,6 @@ class TransportChangePolicyAction @Inject constructor( return } - policy = XContentHelper.createParser( - xContentRegistry, - LoggingDeprecationHandler.INSTANCE, - getPolicyResponse.sourceAsBytesRef, - XContentType.JSON - ).use { it.parseWithType(getPolicyResponse.id, getPolicyResponse.seqNo, getPolicyResponse.primaryTerm, Policy.Companion::parse) } - getClusterState() } @@ -271,7 +331,7 @@ class TransportChangePolicyAction @Inject constructor( // compare the sweptConfig policy to the get policy here and update changePolicy val currentStateName = indexUuidToCurrentState[sweptConfig.uuid] val updatedChangePolicy = changePolicy - .copy(isSafe = sweptConfig.policy?.isSafeToChange(currentStateName, policy, changePolicy) == true) + .copy(isSafe = sweptConfig.policy?.isSafeToChange(currentStateName, policy, changePolicy) == true, user = this.user) bulkUpdateManagedIndexRequest.add(updateManagedIndexRequest(sweptConfig.copy(changePolicy = updatedChangePolicy))) mapOfItemIdToIndex[id] = Index(sweptConfig.index, sweptConfig.uuid) } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyAction.kt index 87f6b7548..04a93d257 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyAction.kt @@ -32,6 +32,6 @@ import org.opensearch.action.delete.DeleteResponse class DeletePolicyAction private constructor() : ActionType(NAME, ::DeleteResponse) { companion object { val INSTANCE = DeletePolicyAction() - val NAME = "cluster:admin/opendistro/ism/policy/delete" + const val NAME = "cluster:admin/opendistro/ism/policy/delete" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyRequest.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyRequest.kt index 500aa9366..ff8d98a28 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyRequest.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/DeletePolicyRequest.kt @@ -34,18 +34,7 @@ import org.opensearch.common.io.stream.StreamInput import org.opensearch.common.io.stream.StreamOutput import java.io.IOException -class DeletePolicyRequest : ActionRequest { - - val policyID: String - val refreshPolicy: WriteRequest.RefreshPolicy - - constructor( - policyID: String, - refreshPolicy: WriteRequest.RefreshPolicy - ) : super() { - this.policyID = policyID - this.refreshPolicy = refreshPolicy - } +class DeletePolicyRequest(val policyID: String, val refreshPolicy: WriteRequest.RefreshPolicy) : ActionRequest() { @Throws(IOException::class) constructor(sin: StreamInput) : this( diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/TransportDeletePolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/TransportDeletePolicyAction.kt index 4b24089bd..400c4bf30 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/TransportDeletePolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/deletepolicy/TransportDeletePolicyAction.kt @@ -26,28 +26,109 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.deletepolicy +import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionListener import org.opensearch.action.delete.DeleteRequest import org.opensearch.action.delete.DeleteResponse +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction +import org.opensearch.client.Client import org.opensearch.client.node.NodeClient +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementPlugin +import org.opensearch.indexmanagement.indexstatemanagement.model.Policy +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource +import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +import java.lang.IllegalArgumentException +@Suppress("ReturnCount") class TransportDeletePolicyAction @Inject constructor( val client: NodeClient, transportService: TransportService, - actionFilters: ActionFilters + actionFilters: ActionFilters, + val clusterService: ClusterService, + val settings: Settings, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( DeletePolicyAction.NAME, transportService, actionFilters, ::DeletePolicyRequest ) { + + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: DeletePolicyRequest, listener: ActionListener) { - val deleteRequest = DeleteRequest(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX, request.policyID) - .setRefreshPolicy(request.refreshPolicy) + DeletePolicyHandler(client, listener, request).start() + } + + inner class DeletePolicyHandler( + private val client: Client, + private val actionListener: ActionListener, + private val request: DeletePolicyRequest, + private val user: User? = buildUser(client.threadPool().threadContext) + ) { + + fun start() { + client.threadPool().threadContext.stashContext().use { + getPolicy() + } + } + + private fun getPolicy() { + val getRequest = GetRequest(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX, request.policyID) + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Policy ${request.policyID} is not found", RestStatus.NOT_FOUND)) + return + } + + val policy: Policy? + try { + policy = parseFromGetResponse(response, xContentRegistry, Policy.Companion::parse) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Policy ${request.policyID} is not found", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, policy.user, filterByEnabled, "policy", request.policyID, actionListener)) { + return + } else { + delete() + } + } + + override fun onFailure(t: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + } + } + ) + } + + private fun delete() { + val deleteRequest = DeleteRequest(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX, request.policyID) + .setRefreshPolicy(request.refreshPolicy) - client.delete(deleteRequest, listener) + client.threadPool().threadContext.stashContext().use { + client.delete(deleteRequest, actionListener) + } + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAction.kt index bdf5c7388..807eb32c5 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class ExplainAction private constructor() : ActionType(NAME, ::ExplainResponse) { companion object { val INSTANCE = ExplainAction() - val NAME = "cluster:admin/opendistro/ism/managedindex/explain" + const val NAME = "cluster:admin/opendistro/ism/managedindex/explain" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAllResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAllResponse.kt index 66484e4b7..5f5e21cff 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAllResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainAllResponse.kt @@ -32,6 +32,7 @@ import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData +import org.opensearch.indexmanagement.indexstatemanagement.settings.LegacyOpenDistroManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import java.io.IOException @@ -71,6 +72,7 @@ class ExplainAllResponse : ExplainResponse, ToXContentObject { builder.startObject() indexNames.forEachIndexed { ind, name -> builder.startObject(name) + builder.field(LegacyOpenDistroManagedIndexSettings.POLICY_ID.key, indexPolicyIDs[ind]) builder.field(ManagedIndexSettings.POLICY_ID.key, indexPolicyIDs[ind]) indexMetadatas[ind]?.toXContent(builder, ToXContent.EMPTY_PARAMS) builder.field("enabled", enabledState[name]) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainResponse.kt index cdb7c2f02..fd57065cd 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/ExplainResponse.kt @@ -33,6 +33,7 @@ import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData +import org.opensearch.indexmanagement.indexstatemanagement.settings.LegacyOpenDistroManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import java.io.IOException @@ -71,6 +72,7 @@ open class ExplainResponse : ActionResponse, ToXContentObject { builder.startObject() indexNames.forEachIndexed { ind, name -> builder.startObject(name) + builder.field(LegacyOpenDistroManagedIndexSettings.POLICY_ID.key, indexPolicyIDs[ind]) builder.field(ManagedIndexSettings.POLICY_ID.key, indexPolicyIDs[ind]) indexMetadatas[ind]?.toXContent(builder, ToXContent.EMPTY_PARAMS) builder.endObject() diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/TransportExplainAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/TransportExplainAction.kt index 425332e46..115026100 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/TransportExplainAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/explain/TransportExplainAction.kt @@ -28,6 +28,7 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.exp import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.action.ActionListener import org.opensearch.action.admin.cluster.state.ClusterStateRequest import org.opensearch.action.admin.cluster.state.ClusterStateResponse @@ -39,13 +40,17 @@ import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.action.support.IndicesOptions +import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.client.node.NodeClient import org.opensearch.cluster.metadata.IndexMetadata +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.util.concurrent.ThreadContext import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.authuser.User import org.opensearch.index.IndexNotFoundException import org.opensearch.index.query.Operator import org.opensearch.index.query.QueryBuilders @@ -53,8 +58,11 @@ import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANA import org.opensearch.indexmanagement.indexstatemanagement.ManagedIndexCoordinator.Companion.MAX_HITS import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.getManagedIndexMetadata +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexRequest import org.opensearch.indexmanagement.indexstatemanagement.util.isMetadataMoved import org.opensearch.indexmanagement.indexstatemanagement.util.managedIndexMetadataID +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.search.fetch.subphase.FetchSourceContext.FETCH_SOURCE import org.opensearch.search.sort.SortBuilders @@ -64,14 +72,17 @@ import org.opensearch.transport.TransportService private val log = LogManager.getLogger(TransportExplainAction::class.java) +@Suppress("SpreadOperator") class TransportExplainAction @Inject constructor( val client: NodeClient, transportService: TransportService, actionFilters: ActionFilters, + val clusterService: ClusterService, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( ExplainAction.NAME, transportService, actionFilters, ::ExplainRequest ) { + override fun doExecute(task: Task, request: ExplainRequest, listener: ActionListener) { ExplainHandler(client, listener, request).start() } @@ -85,7 +96,8 @@ class TransportExplainAction @Inject constructor( inner class ExplainHandler( private val client: NodeClient, private val actionListener: ActionListener, - private val request: ExplainRequest + private val request: ExplainRequest, + private val user: User? = buildUser(client.threadPool().threadContext) ) { private val indices: List = request.indices private val explainAll: Boolean = indices.isEmpty() @@ -97,7 +109,8 @@ class TransportExplainAction @Inject constructor( private val indexNames: MutableList = mutableListOf() private val enabledState: MutableMap = mutableMapOf() - private val rolesMap: MutableMap?> = mutableMapOf() + private val indexPolicyIDs = mutableListOf() + private val indexMetadatas = mutableListOf() private var totalManagedIndices = 0 @Suppress("SpreadOperator", "NestedBlockDepth") @@ -147,69 +160,69 @@ class TransportExplainAction @Inject constructor( .indices(INDEX_MANAGEMENT_INDEX) .source(searchSourceBuilder) - client.search( - searchRequest, - object : ActionListener { - override fun onResponse(response: SearchResponse) { - val totalHits = response.hits.totalHits - if (totalHits != null) { - totalManagedIndices = totalHits.value.toInt() - } + client.threadPool().threadContext.stashContext().use { threadContext -> + client.search( + searchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + val totalHits = response.hits.totalHits + if (totalHits != null) { + totalManagedIndices = totalHits.value.toInt() + } - response.hits.hits.map { - val hitMap = it.sourceAsMap["managed_index"] as Map - val managedIndex = hitMap["index"] as String - managedIndices.add(managedIndex) - enabledState[managedIndex] = hitMap["enabled"] as Boolean - val user = hitMap["user"] as Map? - rolesMap[managedIndex] = user?.get("roles") as List? - managedIndicesMetaDataMap[managedIndex] = mapOf( - "index" to hitMap["index"] as String?, - "index_uuid" to hitMap["index_uuid"] as String?, - "policy_id" to hitMap["policy_id"] as String?, - "enabled" to hitMap["enabled"]?.toString() - ) - } + response.hits.hits.map { + val hitMap = it.sourceAsMap["managed_index"] as Map + val managedIndex = hitMap["index"] as String + managedIndices.add(managedIndex) + enabledState[managedIndex] = hitMap["enabled"] as Boolean + managedIndicesMetaDataMap[managedIndex] = mapOf( + "index" to hitMap["index"] as String?, + "index_uuid" to hitMap["index_uuid"] as String?, + "policy_id" to hitMap["policy_id"] as String?, + "enabled" to hitMap["enabled"]?.toString() + ) + } - // explain all only return managed indices - if (explainAll) { - if (managedIndices.size == 0) { - // edge case: if specify query param pagination size to be 0 - // we still show total managed indices - emptyResponse(totalManagedIndices) - return - } else { - indexNames.addAll(managedIndices) - getMetadata(managedIndices) - return + // explain all only return managed indices + if (explainAll) { + if (managedIndices.size == 0) { + // edge case: if specify query param pagination size to be 0 + // we still show total managed indices + sendResponse() + return + } else { + indexNames.addAll(managedIndices) + getMetadata(managedIndices, threadContext) + return + } } - } - // explain/{index} return results for all indices - indexNames.addAll(indices) - getMetadata(indices) - } + // explain/{index} return results for all indices + indexNames.addAll(indices) + getMetadata(indices, threadContext) + } - override fun onFailure(t: Exception) { - if (t is IndexNotFoundException) { - // config index hasn't been initialized - // show all requested indices not managed - if (indices.isNotEmpty()) { - indexNames.addAll(indices) - getMetadata(indices) + override fun onFailure(t: Exception) { + if (t is IndexNotFoundException) { + // config index hasn't been initialized + // show all requested indices not managed + if (indices.isNotEmpty()) { + indexNames.addAll(indices) + getMetadata(indices, threadContext) + return + } + sendResponse() return } - emptyResponse() - return + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) } - actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) } - } - ) + ) + } } @Suppress("SpreadOperator") - fun getMetadata(indices: List) { + fun getMetadata(indices: List, threadContext: ThreadContext.StoredContext) { val clusterStateRequest = ClusterStateRequest() val strictExpandIndicesOptions = IndicesOptions.strictExpand() @@ -224,7 +237,7 @@ class TransportExplainAction @Inject constructor( clusterStateRequest, object : ActionListener { override fun onResponse(response: ClusterStateResponse) { - onClusterStateResponse(response) + onClusterStateResponse(response, threadContext) } override fun onFailure(t: Exception) { @@ -234,7 +247,7 @@ class TransportExplainAction @Inject constructor( ) } - fun onClusterStateResponse(clusterStateResponse: ClusterStateResponse) { + fun onClusterStateResponse(clusterStateResponse: ClusterStateResponse, threadContext: ThreadContext.StoredContext) { val clusterStateIndexMetadatas = clusterStateResponse.state.metadata.indices.map { it.key to it.value }.toMap() if (wildcard) { @@ -252,7 +265,7 @@ class TransportExplainAction @Inject constructor( object : ActionListener { override fun onResponse(response: MultiGetResponse) { val metadataMap = response.responses.map { it.id to getMetadata(it.response)?.toMap() }.toMap() - buildResponse(indices, metadataMap, clusterStateIndexMetadatas) + buildResponse(indices, metadataMap, clusterStateIndexMetadatas, threadContext) } override fun onFailure(t: Exception) { @@ -266,10 +279,9 @@ class TransportExplainAction @Inject constructor( fun buildResponse( indices: Map, metadataMap: Map?>, - clusterStateIndexMetadatas: Map + clusterStateIndexMetadatas: Map, + threadContext: ThreadContext.StoredContext ) { - val indexPolicyIDs = mutableListOf() - val indexMetadatas = mutableListOf() // cluster state response will not resisting the sort order // so use the order from previous search result saved in indexNames @@ -297,11 +309,79 @@ class TransportExplainAction @Inject constructor( } managedIndicesMetaDataMap.clear() + if (user == null || indexNames.isEmpty()) { + sendResponse() + } else { + filterAndSendResponse(threadContext) + } + } + + private fun filterAndSendResponse(threadContext: ThreadContext.StoredContext) { + threadContext.restore() + val filteredIndices = mutableListOf() + val filteredMetadata = mutableListOf() + val filteredPolicies = mutableListOf() + val enabledStatus = mutableMapOf() + filter(0, filteredIndices, filteredMetadata, filteredPolicies, enabledStatus) + } + + private fun filter( + current: Int, + filteredIndices: MutableList, + filteredMetadata: MutableList, + filteredPolicies: MutableList, + enabledStatus: MutableMap + ) { + val request = ManagedIndexRequest().indices(indexNames[current]) + client.execute( + ManagedIndexAction.INSTANCE, + request, + object : ActionListener { + override fun onResponse(response: AcknowledgedResponse) { + filteredIndices.add(indexNames[current]) + filteredMetadata.add(indexMetadatas[current]) + filteredPolicies.add(indexPolicyIDs[current]) + enabledStatus[indexNames[current]] = enabledState.getOrDefault(indexNames[current], false) + if (current < indexNames.count() - 1) { + // do nothing - skip the index and go to next one + filter(current + 1, filteredIndices, filteredMetadata, filteredPolicies, enabledStatus) + } else { + sendResponse(filteredIndices, filteredMetadata, filteredPolicies, enabledStatus) + } + } + + override fun onFailure(e: Exception) { + when (e is OpenSearchSecurityException) { + true -> { + totalManagedIndices -= 1 + if (current < indexNames.count() - 1) { + // do nothing - skip the index and go to next one + filter(current + 1, filteredIndices, filteredMetadata, filteredPolicies, enabledStatus) + } else { + sendResponse(filteredIndices, filteredMetadata, filteredPolicies, enabledStatus) + } + } + false -> { + actionListener.onFailure(e) + } + } + } + } + ) + } + + private fun sendResponse( + indices: List = indexNames, + metadata: List = indexMetadatas, + policies: List = indexPolicyIDs, + enabledStatus: Map = enabledState, + totalIndices: Int = totalManagedIndices + ) { if (explainAll) { - actionListener.onResponse(ExplainAllResponse(indexNames, indexPolicyIDs, indexMetadatas, totalManagedIndices, enabledState)) + actionListener.onResponse(ExplainAllResponse(indices, policies, metadata, totalIndices, enabledStatus)) return } - actionListener.onResponse(ExplainResponse(indexNames, indexPolicyIDs, indexMetadatas)) + actionListener.onResponse(ExplainResponse(indices, policies, metadata)) } private fun getMetadata(response: GetResponse?): ManagedIndexMetaData? { @@ -319,13 +399,5 @@ class TransportExplainAction @Inject constructor( response.id, response.seqNo, response.primaryTerm ) } - - private fun emptyResponse(size: Int = 0) { - if (explainAll) { - actionListener.onResponse(ExplainAllResponse(emptyList(), emptyList(), emptyList(), size, emptyMap())) - return - } - actionListener.onResponse(ExplainResponse(emptyList(), emptyList(), emptyList())) - } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesAction.kt index a999d265d..a0dc572d7 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPoliciesAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class GetPoliciesAction private constructor() : ActionType(NAME, ::GetPoliciesResponse) { companion object { val INSTANCE = GetPoliciesAction() - val NAME = "cluster:admin/opendistro/ism/policy/search" + const val NAME = "cluster:admin/opendistro/ism/policy/search" } } 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 2852fafde..a82367de0 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 @@ -33,7 +33,7 @@ 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 +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.util._ID import org.opensearch.indexmanagement.util._PRIMARY_TERM import org.opensearch.indexmanagement.util._SEQ_NO @@ -72,7 +72,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) + .field(Policy.POLICY_TYPE, policy, XCONTENT_WITHOUT_TYPE_AND_USER) .endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyAction.kt index 4d0d16c5b..ef6089353 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/GetPolicyAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class GetPolicyAction private constructor() : ActionType(NAME, ::GetPolicyResponse) { companion object { val INSTANCE = GetPolicyAction() - val NAME = "cluster:admin/opendistro/ism/policy/get" + const val NAME = "cluster:admin/opendistro/ism/policy/get" } } 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 0dc31f899..4ab208eef 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 @@ -33,7 +33,7 @@ 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 +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.util._ID import org.opensearch.indexmanagement.util._PRIMARY_TERM import org.opensearch.indexmanagement.util._SEQ_NO @@ -86,7 +86,7 @@ class GetPolicyResponse : ActionResponse, ToXContentObject { .field(_SEQ_NO, seqNo) .field(_PRIMARY_TERM, primaryTerm) if (policy != null) { - builder.field(Policy.POLICY_TYPE, policy, XCONTENT_WITHOUT_TYPE) + builder.field(Policy.POLICY_TYPE, policy, XCONTENT_WITHOUT_TYPE_AND_USER) } return builder.endObject() diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPoliciesAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPoliciesAction.kt index ac3eaf3b4..5b1716f61 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPoliciesAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPoliciesAction.kt @@ -34,17 +34,19 @@ import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject -import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry -import org.opensearch.common.xcontent.XContentFactory -import org.opensearch.common.xcontent.XContentType import org.opensearch.index.IndexNotFoundException import org.opensearch.index.query.Operator import org.opensearch.index.query.QueryBuilders import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX import org.opensearch.indexmanagement.indexstatemanagement.model.Policy -import org.opensearch.indexmanagement.opensearchapi.parseWithType +import org.opensearch.indexmanagement.opensearchapi.parseFromSearchResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.addUserFilter +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.search.sort.SortBuilders import org.opensearch.search.sort.SortOrder @@ -57,17 +59,28 @@ class TransportGetPoliciesAction @Inject constructor( transportService: TransportService, val client: Client, actionFilters: ActionFilters, + val clusterService: ClusterService, + val settings: Settings, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( GetPoliciesAction.NAME, transportService, actionFilters, ::GetPoliciesRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute( task: Task, getPoliciesRequest: GetPoliciesRequest, actionListener: ActionListener ) { val params = getPoliciesRequest.searchParams + val user = buildUser(client.threadPool().threadContext) val sortBuilder = SortBuilders .fieldSort(params.sortField) @@ -76,6 +89,9 @@ class TransportGetPoliciesAction @Inject constructor( val queryBuilder = QueryBuilders.boolQuery() .must(QueryBuilders.existsQuery("policy")) + // Add user filter if enabled + addUserFilter(user, queryBuilder, filterByEnabled, "policy.user") + queryBuilder.must( QueryBuilders .queryStringQuery(params.queryString) @@ -94,33 +110,26 @@ class TransportGetPoliciesAction @Inject constructor( .source(searchSourceBuilder) .indices(INDEX_MANAGEMENT_INDEX) - client.search( - searchRequest, - object : ActionListener { - override fun onResponse(response: SearchResponse) { - val totalPolicies = response.hits.totalHits?.value ?: 0 - val policies = response.hits.hits.map { - val id = it.id - val seqNo = it.seqNo - val primaryTerm = it.primaryTerm - val xcp = XContentFactory.xContent(XContentType.JSON) - .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, it.sourceAsString) - xcp.parseWithType(id, seqNo, primaryTerm, Policy.Companion::parse) - .copy(id = id, seqNo = seqNo, primaryTerm = primaryTerm) + client.threadPool().threadContext.stashContext().use { + client.search( + searchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + val totalPolicies = response.hits.totalHits?.value ?: 0 + val policies = parseFromSearchResponse(response, xContentRegistry, Policy.Companion::parse) + actionListener.onResponse(GetPoliciesResponse(policies, totalPolicies.toInt())) } - actionListener.onResponse(GetPoliciesResponse(policies, totalPolicies.toInt())) - } - - override fun onFailure(t: Exception) { - if (t is IndexNotFoundException) { - // config index hasn't been initialized, catch this here and show empty result on Kibana - actionListener.onResponse(GetPoliciesResponse(emptyList(), 0)) - return + override fun onFailure(t: Exception) { + if (t is IndexNotFoundException) { + // config index hasn't been initialized, catch this here and show empty result on Kibana + actionListener.onResponse(GetPoliciesResponse(emptyList(), 0)) + return + } + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) } - actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) } - } - ) + ) + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPolicyAction.kt index 38df8497a..266df16fe 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/getpolicy/TransportGetPolicyAction.kt @@ -34,26 +34,42 @@ import org.opensearch.action.get.GetResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.node.NodeClient +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject -import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry -import org.opensearch.common.xcontent.XContentHelper -import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementPlugin import org.opensearch.indexmanagement.indexstatemanagement.model.Policy -import org.opensearch.indexmanagement.opensearchapi.parseWithType +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings.Companion.FILTER_BY_BACKEND_ROLES +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +import java.lang.IllegalArgumentException +@Suppress("ReturnCount") class TransportGetPolicyAction @Inject constructor( val client: NodeClient, transportService: TransportService, actionFilters: ActionFilters, + val clusterService: ClusterService, + val settings: Settings, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( GetPolicyAction.NAME, transportService, actionFilters, ::GetPolicyRequest ) { + + @Volatile private var filterByEnabled = FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: GetPolicyRequest, listener: ActionListener) { GetPolicyHandler(client, listener, request).start() } @@ -61,25 +77,27 @@ class TransportGetPolicyAction @Inject constructor( inner class GetPolicyHandler( private val client: NodeClient, private val actionListener: ActionListener, - private val request: GetPolicyRequest + private val request: GetPolicyRequest, + private val user: User? = buildUser(client.threadPool().threadContext) ) { fun start() { val getRequest = GetRequest(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX, request.policyID) .version(request.version) - .fetchSourceContext(request.fetchSrcContext) - client.get( - getRequest, - object : ActionListener { - override fun onResponse(response: GetResponse) { - onGetResponse(response) - } + client.threadPool().threadContext.stashContext().use { + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + onGetResponse(response) + } - override fun onFailure(t: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + override fun onFailure(t: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + } } - } - ) + ) + } } fun onGetResponse(response: GetResponse) { @@ -88,21 +106,24 @@ class TransportGetPolicyAction @Inject constructor( return } - var policy: Policy? = null - if (!response.isSourceEmpty) { - XContentHelper.createParser( - xContentRegistry, - LoggingDeprecationHandler.INSTANCE, - response.sourceAsBytesRef, - XContentType.JSON - ).use { xcp -> - policy = xcp.parseWithType(response.id, response.seqNo, response.primaryTerm, Policy.Companion::parse) + val policy: Policy? + try { + policy = parseFromGetResponse(response, xContentRegistry, Policy.Companion::parse) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Policy not found", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, policy.user, filterByEnabled, "policy", request.policyID, actionListener)) { + return + } else { + // if HEAD request don't return the policy + val policyResponse = if (!request.fetchSrcContext.fetchSource()) { + GetPolicyResponse(response.id, response.version, response.seqNo, response.primaryTerm, null) + } else { + GetPolicyResponse(response.id, response.version, response.seqNo, response.primaryTerm, policy) } + actionListener.onResponse(policyResponse) } - - actionListener.onResponse( - GetPolicyResponse(response.id, response.version, response.seqNo, response.primaryTerm, policy) - ) } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyAction.kt index a57878b17..1ef16d001 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/IndexPolicyAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class IndexPolicyAction private constructor() : ActionType(NAME, ::IndexPolicyResponse) { companion object { val INSTANCE = IndexPolicyAction() - val NAME = "cluster:admin/opendistro/ism/policy/write" + const val NAME = "cluster:admin/opendistro/ism/policy/write" } } 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 48f8f5331..1a263101f 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 @@ -33,6 +33,7 @@ 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.util._ID import org.opensearch.indexmanagement.util._PRIMARY_TERM import org.opensearch.indexmanagement.util._SEQ_NO @@ -91,7 +92,7 @@ class IndexPolicyResponse : ActionResponse, ToXContentObject { .field(_VERSION, version) .field(_PRIMARY_TERM, primaryTerm) .field(_SEQ_NO, seqNo) - .field(Policy.POLICY_TYPE, policy) + .field(Policy.POLICY_TYPE, policy, XCONTENT_WITHOUT_USER) .endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/TransportIndexPolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/TransportIndexPolicyAction.kt index d53dcbe3b..77393e757 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/TransportIndexPolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/indexpolicy/TransportIndexPolicyAction.kt @@ -39,20 +39,30 @@ import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.client.node.NodeClient +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.commons.authuser.User import org.opensearch.index.query.QueryBuilders import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.IndexManagementIndices import org.opensearch.indexmanagement.IndexManagementPlugin +import org.opensearch.indexmanagement.indexstatemanagement.ManagedIndexCoordinator.Companion.MAX_HITS import org.opensearch.indexmanagement.indexstatemanagement.findConflictingPolicyTemplates +import org.opensearch.indexmanagement.indexstatemanagement.findSelfConflictingTemplates +import org.opensearch.indexmanagement.indexstatemanagement.model.ISMTemplate +import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.filterNotNullValues -import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.getPolicyToTemplateMap import org.opensearch.indexmanagement.indexstatemanagement.util.ISM_TEMPLATE_FIELD import org.opensearch.indexmanagement.indexstatemanagement.validateFormat +import org.opensearch.indexmanagement.opensearchapi.parseFromSearchResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.util.IndexManagementException import org.opensearch.indexmanagement.util.IndexUtils +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.validateUserConfiguration import org.opensearch.rest.RestStatus import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.tasks.Task @@ -65,10 +75,21 @@ class TransportIndexPolicyAction @Inject constructor( transportService: TransportService, actionFilters: ActionFilters, val ismIndices: IndexManagementIndices, + val clusterService: ClusterService, + val settings: Settings, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( IndexPolicyAction.NAME, transportService, actionFilters, ::IndexPolicyRequest ) { + + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: IndexPolicyRequest, listener: ActionListener) { IndexPolicyHandler(client, listener, request).start() } @@ -76,18 +97,24 @@ class TransportIndexPolicyAction @Inject constructor( inner class IndexPolicyHandler( private val client: NodeClient, private val actionListener: ActionListener, - private val request: IndexPolicyRequest + private val request: IndexPolicyRequest, + private val user: User? = buildUser(client.threadPool().threadContext) ) { fun start() { - ismIndices.checkAndUpdateIMConfigIndex(object : ActionListener { - override fun onResponse(response: AcknowledgedResponse) { - onCreateMappingsResponse(response) + client.threadPool().threadContext.stashContext().use { + if (!validateUserConfiguration(user, filterByEnabled, actionListener)) { + return } + ismIndices.checkAndUpdateIMConfigIndex(object : ActionListener { + override fun onResponse(response: AcknowledgedResponse) { + onCreateMappingsResponse(response) + } - override fun onFailure(t: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) - } - }) + override fun onFailure(t: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + } + }) + } } private fun onCreateMappingsResponse(response: AcknowledgedResponse) { @@ -95,9 +122,9 @@ class TransportIndexPolicyAction @Inject constructor( log.info("Successfully created or updated ${IndexManagementPlugin.INDEX_MANAGEMENT_INDEX} with newest mappings.") // if there is template field, we will check - val reqTemplate = request.policy.ismTemplate - if (reqTemplate != null) { - checkTemplate(reqTemplate.indexPatterns, reqTemplate.priority) + val reqTemplates = request.policy.ismTemplate + if (reqTemplates != null) { + validateISMTemplates(reqTemplates) } else putPolicy() } else { log.error("Unable to create or update ${IndexManagementPlugin.INDEX_MANAGEMENT_INDEX} with newest mapping.") @@ -111,18 +138,28 @@ class TransportIndexPolicyAction @Inject constructor( } } - private fun checkTemplate(indexPatterns: List, priority: Int) { - val possibleEx = validateFormat(indexPatterns) + private fun validateISMTemplates(ismTemplateList: List) { + val possibleEx = validateFormat(ismTemplateList.map { it.indexPatterns }.flatten()) if (possibleEx != null) { actionListener.onFailure(possibleEx) return } + // check self overlapping + val selfOverlap = ismTemplateList.findSelfConflictingTemplates() + if (selfOverlap != null) { + val errorMessage = "New policy ${request.policyID} has an ISM template with index pattern ${selfOverlap.first} " + + "matching this policy's other ISM templates with index patterns ${selfOverlap.second}," + + " please use different priority" + actionListener.onFailure(IndexManagementException.wrap(IllegalArgumentException(errorMessage))) + return + } + val searchRequest = SearchRequest() .source( SearchSourceBuilder().query( QueryBuilders.existsQuery(ISM_TEMPLATE_FIELD) - ) + ).size(MAX_HITS) ) .indices(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX) @@ -130,14 +167,26 @@ class TransportIndexPolicyAction @Inject constructor( searchRequest, object : ActionListener { override fun onResponse(response: SearchResponse) { - val policyToTemplateMap = getPolicyToTemplateMap(response, xContentRegistry).filterNotNullValues() - val conflictingPolicyTemplates = policyToTemplateMap.findConflictingPolicyTemplates(request.policyID, indexPatterns, priority) - if (conflictingPolicyTemplates.isNotEmpty()) { - val errorMessage = "New policy ${request.policyID} has an ISM template with index pattern $indexPatterns " + - "matching existing policy templates," + - " please use a different priority than $priority" - actionListener.onFailure(IndexManagementException.wrap(IllegalArgumentException(errorMessage))) - return + val policies = parseFromSearchResponse(response, xContentRegistry, Policy.Companion::parse) + val policyToTemplateMap: Map> = + policies.map { it.id to it.ismTemplate }.toMap().filterNotNullValues() + ismTemplateList.forEach { + val conflictingPolicyTemplates = policyToTemplateMap + .findConflictingPolicyTemplates(request.policyID, it.indexPatterns, it.priority) + if (conflictingPolicyTemplates.isNotEmpty()) { + val errorMessage = + "New policy ${request.policyID} has an ISM template with index pattern ${it.indexPatterns} " + + "matching existing policy templates," + + " please use a different priority than ${it.priority}" + actionListener.onFailure( + IndexManagementException.wrap( + IllegalArgumentException( + errorMessage + ) + ) + ) + return + } } putPolicy() @@ -151,11 +200,13 @@ class TransportIndexPolicyAction @Inject constructor( } private fun putPolicy() { - request.policy.copy(schemaVersion = IndexUtils.indexManagementConfigSchemaVersion) + val policy = request.policy.copy( + schemaVersion = IndexUtils.indexManagementConfigSchemaVersion, user = this.user + ) val indexRequest = IndexRequest(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX) .setRefreshPolicy(request.refreshPolicy) - .source(request.policy.toXContent(XContentFactory.jsonBuilder())) + .source(policy.toXContent(XContentFactory.jsonBuilder())) .id(request.policyID) .timeout(IndexRequest.DEFAULT_TIMEOUT) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/ManagedIndexAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/ManagedIndexAction.kt new file mode 100644 index 000000000..257704e6e --- /dev/null +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/ManagedIndexAction.kt @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex + +import org.opensearch.action.ActionType +import org.opensearch.action.support.master.AcknowledgedResponse + +class ManagedIndexAction : ActionType(NAME, ::AcknowledgedResponse) { + companion object { + const val NAME = "indices:admin/opensearch/ism/managedindex" + val INSTANCE = ManagedIndexAction() + } +} diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/ManagedIndexRequest.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/ManagedIndexRequest.kt new file mode 100644 index 000000000..c3a094c29 --- /dev/null +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/ManagedIndexRequest.kt @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex + +import org.opensearch.action.support.broadcast.BroadcastRequest +import org.opensearch.common.io.stream.StreamInput +import java.io.IOException + +@Suppress("SpreadOperator") +class ManagedIndexRequest : BroadcastRequest { + + constructor(vararg indices: String) : super(*indices) + + @Throws(IOException::class) + constructor(sin: StreamInput) : super(sin) +} diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/TransportManagedIndexAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/TransportManagedIndexAction.kt new file mode 100644 index 000000000..bf89c2718 --- /dev/null +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/managedIndex/TransportManagedIndexAction.kt @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex + +import org.opensearch.action.ActionListener +import org.opensearch.action.support.ActionFilters +import org.opensearch.action.support.HandledTransportAction +import org.opensearch.action.support.master.AcknowledgedResponse +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.inject.Inject +import org.opensearch.tasks.Task +import org.opensearch.transport.TransportService + +/** + * This is a non operational transport action that is used by ISM to check if the user has required index permissions to manage index + */ +class TransportManagedIndexAction @Inject constructor( + transportService: TransportService, + actionFilters: ActionFilters, + val clusterService: ClusterService, +) : HandledTransportAction( + ManagedIndexAction.NAME, transportService, actionFilters, ::ManagedIndexRequest +) { + + override fun doExecute(task: Task, request: ManagedIndexRequest, listener: ActionListener) { + // Do nothing + return listener.onResponse(AcknowledgedResponse(true)) + } +} diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/RemovePolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/RemovePolicyAction.kt index 281e1fe72..63e90447e 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/RemovePolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/RemovePolicyAction.kt @@ -32,6 +32,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMS class RemovePolicyAction private constructor() : ActionType(NAME, ::ISMStatusResponse) { companion object { val INSTANCE = RemovePolicyAction() - val NAME = "cluster:admin/opendistro/ism/managedindex/remove" + const val NAME = "cluster:admin/opendistro/ism/managedindex/remove" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/TransportRemovePolicyAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/TransportRemovePolicyAction.kt index f570188ff..684a710a6 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/TransportRemovePolicyAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/removepolicy/TransportRemovePolicyAction.kt @@ -26,11 +26,13 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.removepolicy -import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException +import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionListener import org.opensearch.action.admin.cluster.state.ClusterStateRequest import org.opensearch.action.admin.cluster.state.ClusterStateResponse +import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest import org.opensearch.action.bulk.BulkRequest import org.opensearch.action.bulk.BulkResponse import org.opensearch.action.get.MultiGetRequest @@ -38,31 +40,45 @@ import org.opensearch.action.get.MultiGetResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.action.support.IndicesOptions +import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.client.node.NodeClient import org.opensearch.cluster.ClusterState import org.opensearch.cluster.block.ClusterBlockException +import org.opensearch.cluster.metadata.IndexMetadata.INDEX_BLOCKS_READ_ONLY_ALLOW_DELETE_SETTING +import org.opensearch.cluster.metadata.IndexMetadata.INDEX_READ_ONLY_SETTING +import org.opensearch.cluster.metadata.IndexMetadata.SETTING_READ_ONLY +import org.opensearch.cluster.metadata.IndexMetadata.SETTING_READ_ONLY_ALLOW_DELETE +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.commons.authuser.User import org.opensearch.index.Index import org.opensearch.index.IndexNotFoundException import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.getUuidsForClosedIndices +import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMStatusResponse +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexRequest import org.opensearch.indexmanagement.indexstatemanagement.util.FailedIndex import org.opensearch.indexmanagement.indexstatemanagement.util.deleteManagedIndexMetadataRequest import org.opensearch.indexmanagement.indexstatemanagement.util.deleteManagedIndexRequest import org.opensearch.indexmanagement.util.IndexManagementException +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService -private val log = LogManager.getLogger(TransportRemovePolicyAction::class.java) - +@Suppress("SpreadOperator") class TransportRemovePolicyAction @Inject constructor( val client: NodeClient, transportService: TransportService, - actionFilters: ActionFilters + actionFilters: ActionFilters, + val clusterService: ClusterService ) : HandledTransportAction( RemovePolicyAction.NAME, transportService, actionFilters, ::RemovePolicyRequest ) { + override fun doExecute(task: Task, request: RemovePolicyRequest, listener: ActionListener) { RemovePolicyHandler(client, listener, request).start() } @@ -70,14 +86,52 @@ class TransportRemovePolicyAction @Inject constructor( inner class RemovePolicyHandler( private val client: NodeClient, private val actionListener: ActionListener, - private val request: RemovePolicyRequest + private val request: RemovePolicyRequest, + private val user: User? = buildUser(client.threadPool().threadContext) ) { private val failedIndices: MutableList = mutableListOf() private val indicesToRemove = mutableMapOf() // uuid: name + private val indicesWithAutoManageBlock = mutableSetOf() + private val indicesWithReadOnlyBlock = mutableSetOf() + private val indicesWithReadOnlyAllowDeleteBlock = mutableSetOf() - @Suppress("SpreadOperator") fun start() { + if (user == null) { + getClusterState() + } else { + validateAndGetClusterState() + } + } + + private fun validateAndGetClusterState() { + val request = ManagedIndexRequest().indices(*request.indices.toTypedArray()) + client.execute( + ManagedIndexAction.INSTANCE, + request, + object : ActionListener { + override fun onResponse(response: AcknowledgedResponse) { + getClusterState() + } + + override fun onFailure(e: java.lang.Exception) { + actionListener.onFailure( + IndexManagementException.wrap( + when (e is OpenSearchSecurityException) { + true -> OpenSearchStatusException( + "User doesn't have required index permissions on one or more requested indices: ${e.localizedMessage}", + RestStatus.FORBIDDEN + ) + false -> e + } + ) + ) + } + } + ) + } + + private fun getClusterState() { val strictExpandOptions = IndicesOptions.strictExpand() val clusterStateRequest = ClusterStateRequest() @@ -87,24 +141,35 @@ class TransportRemovePolicyAction @Inject constructor( .local(false) .indicesOptions(strictExpandOptions) - client.admin() - .cluster() - .state( - clusterStateRequest, - object : ActionListener { - override fun onResponse(response: ClusterStateResponse) { - val indexMetadatas = response.state.metadata.indices - indexMetadatas.forEach { - indicesToRemove.putIfAbsent(it.value.indexUUID, it.key) + client.threadPool().threadContext.stashContext().use { + client.admin() + .cluster() + .state( + clusterStateRequest, + object : ActionListener { + override fun onResponse(response: ClusterStateResponse) { + val indexMetadatas = response.state.metadata.indices + indexMetadatas.forEach { + indicesToRemove.putIfAbsent(it.value.indexUUID, it.key) + if (it.value.settings.get(ManagedIndexSettings.AUTO_MANAGE.key) == "false") { + indicesWithAutoManageBlock.add(it.value.indexUUID) + } + if (it.value.settings.get(SETTING_READ_ONLY) == "true") { + indicesWithReadOnlyBlock.add(it.value.indexUUID) + } + if (it.value.settings.get(SETTING_READ_ONLY_ALLOW_DELETE) == "true") { + indicesWithReadOnlyAllowDeleteBlock.add(it.value.indexUUID) + } + } + populateLists(response.state) } - populateLists(response.state) - } - override fun onFailure(t: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + override fun onFailure(t: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + } } - } - ) + ) + } } private fun populateLists(state: ClusterState) { @@ -128,7 +193,13 @@ class TransportRemovePolicyAction @Inject constructor( val f = response.responses.first() if (f.isFailed && f.failure.failure is IndexNotFoundException) { indicesToRemove.forEach { (uuid, name) -> - failedIndices.add(FailedIndex(name, uuid, "This index does not have a policy to remove")) + failedIndices.add( + FailedIndex( + name, + uuid, + "This index does not have a policy to remove" + ) + ) } actionListener.onResponse(ISMStatusResponse(0, failedIndices)) return @@ -147,7 +218,7 @@ class TransportRemovePolicyAction @Inject constructor( } } - removeManagedIndices() + updateSettings(indicesToRemove) } override fun onFailure(t: Exception) { @@ -157,6 +228,85 @@ class TransportRemovePolicyAction @Inject constructor( ) } + /** + * try to update auto_manage setting to false before delete managed-index + * so that index will not be picked up by Coordinator background sweep process + * this wont happen for cold indices + * if update setting failed, remove managed-index and metadata will not happen + */ + @Suppress("SpreadOperator") + fun updateSettings(indices: Map) { + // indices divide to read_only, read_only_allow_delete, normal + val indicesUuidsSet = indices.keys.toSet() - indicesWithAutoManageBlock + val readOnlyIndices = indicesUuidsSet.filter { it in indicesWithReadOnlyBlock } + val readOnlyAllowDeleteIndices = (indicesUuidsSet - readOnlyIndices).filter { it in indicesWithReadOnlyAllowDeleteBlock } + val normalIndices = indicesUuidsSet - readOnlyIndices - readOnlyAllowDeleteIndices + + val updateSettingReqsList = mutableListOf() + if (readOnlyIndices.isNotEmpty()) { + updateSettingReqsList.add( + UpdateSettingsRequest().indices(*readOnlyIndices.map { indices[it] }.toTypedArray()) + .settings( + Settings.builder().put(ManagedIndexSettings.AUTO_MANAGE.key, false) + .put(INDEX_READ_ONLY_SETTING.key, true) + ) + ) + } + if (readOnlyAllowDeleteIndices.isNotEmpty()) { + updateSettingReqsList.add( + UpdateSettingsRequest().indices(*readOnlyAllowDeleteIndices.map { indices[it] }.toTypedArray()) + .settings( + Settings.builder().put(ManagedIndexSettings.AUTO_MANAGE.key, false) + .put(INDEX_BLOCKS_READ_ONLY_ALLOW_DELETE_SETTING.key, true) + ) + ) + } + if (normalIndices.isNotEmpty()) { + updateSettingReqsList.add( + UpdateSettingsRequest().indices(*normalIndices.map { indices[it] }.toTypedArray()) + .settings(Settings.builder().put(ManagedIndexSettings.AUTO_MANAGE.key, false)) + ) + } + + updateSettingCallChain(0, updateSettingReqsList) + } + + fun updateSettingCallChain(current: Int, updateSettingReqsList: List) { + if (updateSettingReqsList.isEmpty()) { + removeManagedIndices() + return + } + client.admin().indices().updateSettings( + updateSettingReqsList[current], + object : ActionListener { + override fun onResponse(response: AcknowledgedResponse) { + if (!response.isAcknowledged) { + actionListener.onFailure( + IndexManagementException.wrap( + Exception("Failed to remove policy because ISM auto_manage setting update requests are not fully acknowledged.") + ) + ) + return + } + if (current < updateSettingReqsList.size - 1) { + updateSettingCallChain(current + 1, updateSettingReqsList) + } else { + removeManagedIndices() + } + } + + override fun onFailure(t: Exception) { + val ex = ExceptionsHelper.unwrapCause(t) as Exception + actionListener.onFailure( + IndexManagementException.wrap( + Exception("Failed to remove policy because ISM auto_manage setting update requests failed with exception:", ex) + ) + ) + } + } + ) + } + @Suppress("SpreadOperator") // There is no way around dealing with java vararg without spread operator. fun removeManagedIndices() { if (indicesToRemove.isNotEmpty()) { @@ -169,7 +319,13 @@ class TransportRemovePolicyAction @Inject constructor( response.forEach { val docId = it.id // docId is indexUuid of the managed index if (it.isFailed) { - failedIndices.add(FailedIndex(indicesToRemove[docId] as String, docId, "Failed to remove policy")) + failedIndices.add( + FailedIndex( + indicesToRemove[docId] as String, + docId, + "Failed to remove policy" + ) + ) indicesToRemove.remove(docId) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/RetryFailedManagedIndexAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/RetryFailedManagedIndexAction.kt index 7bdd15f6d..f201cc9d0 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/RetryFailedManagedIndexAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/RetryFailedManagedIndexAction.kt @@ -32,6 +32,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMS class RetryFailedManagedIndexAction private constructor() : ActionType(NAME, ::ISMStatusResponse) { companion object { val INSTANCE = RetryFailedManagedIndexAction() - val NAME = "cluster:admin/opendistro/ism/managedindex/retry" + const val NAME = "cluster:admin/opendistro/ism/managedindex/retry" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/TransportRetryFailedManagedIndexAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/TransportRetryFailedManagedIndexAction.kt index 27e3780b9..af872da56 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/TransportRetryFailedManagedIndexAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/retryfailedmanagedindex/TransportRetryFailedManagedIndexAction.kt @@ -28,6 +28,8 @@ package org.opensearch.indexmanagement.indexstatemanagement.transport.action.ret import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException +import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionListener import org.opensearch.action.admin.cluster.state.ClusterStateRequest import org.opensearch.action.admin.cluster.state.ClusterStateResponse @@ -38,6 +40,7 @@ import org.opensearch.action.get.MultiGetResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.action.support.IndicesOptions +import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.update.UpdateRequest import org.opensearch.client.node.NodeClient import org.opensearch.cluster.ClusterState @@ -45,6 +48,7 @@ import org.opensearch.cluster.block.ClusterBlockException import org.opensearch.common.inject.Inject import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.commons.authuser.User import org.opensearch.index.Index import org.opensearch.index.IndexNotFoundException import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX @@ -54,15 +58,21 @@ import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.buildMg import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.getManagedIndexMetadata import org.opensearch.indexmanagement.indexstatemanagement.opensearchapi.mgetResponseToList import org.opensearch.indexmanagement.indexstatemanagement.transport.action.ISMStatusResponse +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexAction +import org.opensearch.indexmanagement.indexstatemanagement.transport.action.managedIndex.ManagedIndexRequest import org.opensearch.indexmanagement.indexstatemanagement.util.FailedIndex import org.opensearch.indexmanagement.indexstatemanagement.util.isFailed import org.opensearch.indexmanagement.indexstatemanagement.util.managedIndexMetadataID import org.opensearch.indexmanagement.indexstatemanagement.util.updateEnableManagedIndexRequest +import org.opensearch.indexmanagement.util.IndexManagementException +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService private val log = LogManager.getLogger(TransportRetryFailedManagedIndexAction::class.java) +@Suppress("SpreadOperator") class TransportRetryFailedManagedIndexAction @Inject constructor( val client: NodeClient, transportService: TransportService, @@ -77,7 +87,8 @@ class TransportRetryFailedManagedIndexAction @Inject constructor( inner class RetryFailedManagedIndexHandler( private val client: NodeClient, private val actionListener: ActionListener, - private val request: RetryFailedManagedIndexRequest + private val request: RetryFailedManagedIndexRequest, + private val user: User? = buildUser(client.threadPool().threadContext) ) { private val failedIndices: MutableList = mutableListOf() private val listOfMetadata: MutableList = mutableListOf() @@ -89,6 +100,42 @@ class TransportRetryFailedManagedIndexAction @Inject constructor( @Suppress("SpreadOperator") fun start() { + if (user == null) { + // Security plugin is not enabled + getClusterState() + } else { + validateAndGetClusterState() + } + } + + fun validateAndGetClusterState() { + val request = ManagedIndexRequest().indices(*request.indices.toTypedArray()) + client.execute( + ManagedIndexAction.INSTANCE, + request, + object : ActionListener { + override fun onResponse(response: AcknowledgedResponse) { + getClusterState() + } + + override fun onFailure(e: java.lang.Exception) { + actionListener.onFailure( + IndexManagementException.wrap( + when (e is OpenSearchSecurityException) { + true -> OpenSearchStatusException( + "User doesn't have required index permissions on one or more requested indices: ${e.localizedMessage}", + RestStatus.FORBIDDEN + ) + false -> e + } + ) + ) + } + } + ) + } + + fun getClusterState() { val strictExpandIndicesOptions = IndicesOptions.strictExpand() val clusterStateRequest = ClusterStateRequest() @@ -99,25 +146,27 @@ class TransportRetryFailedManagedIndexAction @Inject constructor( .masterNodeTimeout(request.masterTimeout) .indicesOptions(strictExpandIndicesOptions) - client.admin() - .cluster() - .state( - clusterStateRequest, - object : ActionListener { - override fun onResponse(response: ClusterStateResponse) { - clusterState = response.state - val indexMetadatas = response.state.metadata.indices - indexMetadatas.forEach { - indicesToRetry.putIfAbsent(it.value.indexUUID, it.key) + client.threadPool().threadContext.stashContext().use { + client.admin() + .cluster() + .state( + clusterStateRequest, + object : ActionListener { + override fun onResponse(response: ClusterStateResponse) { + clusterState = response.state + val indexMetadatas = response.state.metadata.indices + indexMetadatas.forEach { + indicesToRetry.putIfAbsent(it.value.indexUUID, it.key) + } + processResponse(response) } - processResponse(response) - } - override fun onFailure(t: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + override fun onFailure(t: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(t) as Exception) + } } - } - ) + ) + } } fun processResponse(clusterStateResponse: ClusterStateResponse) { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/updateindexmetadata/TransportUpdateManagedIndexMetaDataAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/updateindexmetadata/TransportUpdateManagedIndexMetaDataAction.kt index 63588cada..98ae2bf73 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/updateindexmetadata/TransportUpdateManagedIndexMetaDataAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/transport/action/updateindexmetadata/TransportUpdateManagedIndexMetaDataAction.kt @@ -31,7 +31,6 @@ import org.opensearch.action.ActionListener import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.support.master.TransportMasterNodeAction -import org.opensearch.client.Client import org.opensearch.cluster.ClusterState import org.opensearch.cluster.ClusterStateTaskConfig import org.opensearch.cluster.ClusterStateTaskExecutor @@ -53,30 +52,23 @@ import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMet import org.opensearch.threadpool.ThreadPool import org.opensearch.transport.TransportService -class TransportUpdateManagedIndexMetaDataAction : TransportMasterNodeAction { - - @Inject - constructor( - client: Client, - threadPool: ThreadPool, - clusterService: ClusterService, - transportService: TransportService, - actionFilters: ActionFilters, - indexNameExpressionResolver: IndexNameExpressionResolver - ) : super( - UpdateManagedIndexMetaDataAction.INSTANCE.name(), - transportService, - clusterService, - threadPool, - actionFilters, - Writeable.Reader { UpdateManagedIndexMetaDataRequest(it) }, - indexNameExpressionResolver - ) { - this.client = client - } +class TransportUpdateManagedIndexMetaDataAction @Inject constructor( + threadPool: ThreadPool, + clusterService: ClusterService, + transportService: TransportService, + actionFilters: ActionFilters, + indexNameExpressionResolver: IndexNameExpressionResolver +) : TransportMasterNodeAction( + UpdateManagedIndexMetaDataAction.INSTANCE.name(), + transportService, + clusterService, + threadPool, + actionFilters, + Writeable.Reader { UpdateManagedIndexMetaDataRequest(it) }, + indexNameExpressionResolver +) { private val log = LogManager.getLogger(javaClass) - private val client: Client private val executor = ManagedIndexMetaDataExecutor() override fun checkBlock(request: UpdateManagedIndexMetaDataRequest, state: ClusterState): ClusterBlockException? { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtils.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtils.kt index 8f8578ee2..46e0d55a1 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtils.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtils.kt @@ -76,7 +76,15 @@ import java.net.InetAddress import java.time.Instant import java.time.temporal.ChronoUnit -fun managedIndexConfigIndexRequest(index: String, uuid: String, policyID: String, jobInterval: Int): IndexRequest { +@Suppress("LongParameterList") +fun managedIndexConfigIndexRequest( + index: String, + uuid: String, + policyID: String, + jobInterval: Int, + policy: Policy? = null, + jobJitter: Double? +): IndexRequest { val managedIndexConfig = ManagedIndexConfig( jobName = index, index = index, @@ -86,10 +94,11 @@ fun managedIndexConfigIndexRequest(index: String, uuid: String, policyID: String jobLastUpdatedTime = Instant.now(), jobEnabledTime = Instant.now(), policyID = policyID, - policy = null, - policySeqNo = null, - policyPrimaryTerm = null, - changePolicy = null + policy = policy, + policySeqNo = policy?.seqNo, + policyPrimaryTerm = policy?.primaryTerm, + changePolicy = null, + jobJitter = jobJitter ) return IndexRequest(INDEX_MANAGEMENT_INDEX) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/RestHandlerUtils.kt b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/RestHandlerUtils.kt index 49581d430..4fd95b2b6 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/RestHandlerUtils.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/RestHandlerUtils.kt @@ -40,7 +40,10 @@ import org.opensearch.indexmanagement.opensearchapi.optionalTimeField import java.time.Instant const val WITH_TYPE = "with_type" +const val WITH_USER = "with_user" val XCONTENT_WITHOUT_TYPE = ToXContent.MapParams(mapOf(WITH_TYPE to "false")) +val XCONTENT_WITHOUT_USER = ToXContent.MapParams(mapOf(WITH_USER to "false")) +val XCONTENT_WITHOUT_TYPE_AND_USER = ToXContent.MapParams(mapOf(WITH_TYPE to "false", WITH_USER to "false")) const val FAILURES = "failures" const val FAILED_INDICES = "failed_indices" @@ -108,11 +111,10 @@ data class FailedIndex(val name: String, val uuid: String, val reason: String) : fun getPartialChangePolicyBuilder( changePolicy: ChangePolicy? ): XContentBuilder { - return XContentFactory.jsonBuilder() + val builder = XContentFactory.jsonBuilder() .startObject() .startObject(ManagedIndexConfig.MANAGED_INDEX_TYPE) .optionalTimeField(ManagedIndexConfig.LAST_UPDATED_TIME_FIELD, Instant.now()) .field(ManagedIndexConfig.CHANGE_POLICY_FIELD, changePolicy) - .endObject() - .endObject() + return builder.endObject().endObject() } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/opensearchapi/OpenSearchExtensions.kt b/src/main/kotlin/org/opensearch/indexmanagement/opensearchapi/OpenSearchExtensions.kt index 1a8859862..fb12e83f8 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/opensearchapi/OpenSearchExtensions.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/opensearchapi/OpenSearchExtensions.kt @@ -28,35 +28,49 @@ package org.opensearch.indexmanagement.opensearchapi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ThreadContextElement import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import org.opensearch.ExceptionsHelper import org.opensearch.OpenSearchException import org.opensearch.action.ActionListener import org.opensearch.action.bulk.BackoffPolicy +import org.opensearch.action.get.GetResponse +import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.DefaultShardOperationFailedException import org.opensearch.client.OpenSearchClient import org.opensearch.common.bytes.BytesReference +import org.opensearch.common.settings.Settings import org.opensearch.common.unit.TimeValue +import org.opensearch.common.util.concurrent.ThreadContext import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentFactory import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParser.Token import org.opensearch.common.xcontent.XContentParserUtils import org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.InjectSecurity +import org.opensearch.commons.authuser.User import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.indexstatemanagement.model.ISMTemplate import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.util.NO_ID +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.DEFAULT_INJECT_ROLES +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.INTERNAL_REQUEST import org.opensearch.jobscheduler.spi.utils.LockService import org.opensearch.rest.RestStatus import org.opensearch.transport.RemoteTransportException import java.io.IOException import java.time.Instant +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -92,11 +106,50 @@ fun XContentBuilder.optionalTimeField(name: String, instant: Instant?): XContent return this.timeField(name, "${name}_in_millis", instant.toEpochMilli()) } -fun XContentBuilder.optionalISMTemplateField(name: String, ismTemplate: ISMTemplate?): XContentBuilder { - if (ismTemplate == null) { +fun XContentBuilder.optionalISMTemplateField(name: String, ismTemplates: List?): XContentBuilder { + if (ismTemplates == null) { return nullField(name) } - return this.field(Policy.ISM_TEMPLATE, ismTemplate) + return this.field(Policy.ISM_TEMPLATE, ismTemplates.toTypedArray()) +} + +fun XContentBuilder.optionalUserField(name: String, user: User?): XContentBuilder { + return if (user == null) nullField(name) else this.field(name, user) +} + +/** + * Parse data from SearchResponse using the defined parser and xContentRegistry + */ +fun parseFromSearchResponse( + response: SearchResponse, + xContentRegistry: NamedXContentRegistry = NamedXContentRegistry.EMPTY, + parse: (xcp: XContentParser, id: String, seqNo: Long, primaryTerm: Long) -> T +): List { + return response.hits.hits.map { + val id = it.id + val seqNo = it.seqNo + val primaryTerm = it.primaryTerm + val xcp = XContentFactory.xContent(XContentType.JSON) + .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, it.sourceAsString) + xcp.parseWithType(id, seqNo, primaryTerm, parse) + } +} + +/** + * Parse data from GetResponse using the defined parser and xContentRegistry + */ +fun parseFromGetResponse( + response: GetResponse, + xContentRegistry: NamedXContentRegistry = NamedXContentRegistry.EMPTY, + parse: (xcp: XContentParser, id: String, seqNo: Long, primaryTerm: Long) -> T +): T { + val xcp = XContentHelper.createParser( + xContentRegistry, + LoggingDeprecationHandler.INSTANCE, + response.sourceAsBytesRef, + XContentType.JSON + ) + return xcp.parseWithType(response.id, response.seqNo, response.primaryTerm, parse) } /** @@ -199,3 +252,47 @@ fun XContentParser.parseWithType( ensureExpectedToken(Token.END_OBJECT, this.nextToken(), this) return parsed } + +class IndexManagementSecurityContext( + private val id: String, + settings: Settings, + private val threadContext: ThreadContext, + private val user: User? +) : ThreadContextElement { + + companion object Key : CoroutineContext.Key + + private val logger: Logger = LogManager.getLogger(javaClass) + override val key: CoroutineContext.Key<*> + get() = Key + val injector = InjectSecurity(id, settings, threadContext) + + /** + * Before the thread executes the coroutine we want the thread context to contain user roles so they are used when executing the code inside + * the coroutine + */ + override fun updateThreadContext(context: CoroutineContext) { + logger.debug("Setting security context in thread ${Thread.currentThread().name} for job $id") + injector.injectRoles(if (user == null) DEFAULT_INJECT_ROLES else user.roles) + injector.injectProperty(INTERNAL_REQUEST, true) + } + + /** + * Clean up the thread context before the coroutine executed by thread is suspended + */ + override fun restoreThreadContext(context: CoroutineContext, oldState: Unit) { + logger.debug("Cleaning up security context in thread ${Thread.currentThread().name} for job $id") + injector.close() + } +} + +suspend fun withClosableContext( + context: IndexManagementSecurityContext, + block: suspend CoroutineScope.() -> T +): T { + try { + return withContext(context) { block() } + } finally { + context.injector.close() + } +} diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupIndexer.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupIndexer.kt index 80c21ebb7..a10457840 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupIndexer.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupIndexer.kt @@ -30,6 +30,7 @@ import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper import org.opensearch.action.DocWriteRequest import org.opensearch.action.bulk.BackoffPolicy +import org.opensearch.action.bulk.BulkItemResponse import org.opensearch.action.bulk.BulkRequest import org.opensearch.action.bulk.BulkResponse import org.opensearch.action.index.IndexRequest @@ -55,6 +56,7 @@ import org.opensearch.search.aggregations.metrics.InternalSum import org.opensearch.search.aggregations.metrics.InternalValueCount import org.opensearch.transport.RemoteTransportException +@Suppress("ThrowsCount", "ComplexMethod") class RollupIndexer( settings: Settings, clusterService: ClusterService, @@ -77,6 +79,7 @@ class RollupIndexer( try { var requestsToRetry = convertResponseToRequests(rollup, internalComposite) var stats = RollupStats(0, 0, requestsToRetry.size.toLong(), 0, 0) + var nonRetryableFailures = mutableListOf() if (requestsToRetry.isNotEmpty()) { retryIngestPolicy.retry(logger, listOf(RestStatus.TOO_MANY_REQUESTS)) { if (it.seconds >= (Rollup.ROLLUP_LOCK_DURATION_SECONDS / 2)) { @@ -87,16 +90,26 @@ class RollupIndexer( val bulkRequest = BulkRequest().add(requestsToRetry) val bulkResponse: BulkResponse = client.suspendUntil { bulk(bulkRequest, it) } stats = stats.copy(indexTimeInMillis = stats.indexTimeInMillis + bulkResponse.took.millis) - val failedResponses = (bulkResponse.items ?: arrayOf()).filter { it.isFailed } - requestsToRetry = failedResponses.filter { it.status() == RestStatus.TOO_MANY_REQUESTS } - .map { bulkRequest.requests()[it.itemId] as IndexRequest } + val retryableFailures = mutableListOf() + (bulkResponse.items ?: arrayOf()).filter { it.isFailed }.forEach { failedResponse -> + if (failedResponse.status() == RestStatus.TOO_MANY_REQUESTS) { + retryableFailures.add(failedResponse) + } else { + nonRetryableFailures.add(failedResponse) + } + } + requestsToRetry = retryableFailures.map { retryableFailure -> bulkRequest.requests()[retryableFailure.itemId] as IndexRequest } if (requestsToRetry.isNotEmpty()) { - val retryCause = failedResponses.first { it.status() == RestStatus.TOO_MANY_REQUESTS }.failure.cause + val retryCause = retryableFailures.first().failure.cause throw ExceptionsHelper.convertToOpenSearchException(retryCause) } } } + if (nonRetryableFailures.isNotEmpty()) { + logger.error("Failed to index ${nonRetryableFailures.size} documents") + throw ExceptionsHelper.convertToOpenSearchException(nonRetryableFailures.first().failure.cause) + } return RollupIndexResult.Success(stats) } catch (e: RemoteTransportException) { logger.error(e.message, e.cause) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupMapperService.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupMapperService.kt index bc3ac8d96..c0c0bb7b0 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupMapperService.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupMapperService.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.action.admin.indices.create.CreateIndexRequest import org.opensearch.action.admin.indices.create.CreateIndexResponse import org.opensearch.action.admin.indices.mapping.get.GetMappingsRequest @@ -106,6 +107,9 @@ class RollupMapperService( val unwrappedException = ExceptionsHelper.unwrapCause(e) as Exception logger.error(errorMessage, unwrappedException) RollupJobValidationResult.Failure(errorMessage, unwrappedException) + } catch (e: OpenSearchSecurityException) { + logger.error("$errorMessage because ", e) + RollupJobValidationResult.Failure("$errorMessage - missing required cluster permissions: ${e.localizedMessage}", e) } catch (e: Exception) { logger.error("$errorMessage because ", e) RollupJobValidationResult.Failure(errorMessage, e) @@ -256,13 +260,16 @@ class RollupMapperService( val unwrappedException = ExceptionsHelper.unwrapCause(e) as Exception logger.error(errorMessage, unwrappedException) return GetMappingsResult.Failure(errorMessage, unwrappedException) + } catch (e: OpenSearchSecurityException) { + logger.error(errorMessage, e) + return GetMappingsResult.Failure("$errorMessage - missing required index permissions: ${e.localizedMessage}", e) } catch (e: Exception) { logger.error(errorMessage, e) return GetMappingsResult.Failure(errorMessage, e) } } - fun indexExists(index: String): Boolean = clusterService.state().routingTable.hasIndex(index) + private fun indexExists(index: String): Boolean = clusterService.state().routingTable.hasIndex(index) // TODO: error handling - can RemoteTransportException happen here? // TODO: The use of the master transport action UpdateRollupMappingAction will prevent @@ -288,6 +295,9 @@ class RollupMapperService( return RollupJobValidationResult.Failure(errorMessage) } return RollupJobValidationResult.Valid + } catch (e: OpenSearchSecurityException) { + logger.error("$errorMessage because ", e) + return RollupJobValidationResult.Failure("$errorMessage - missing required index permissions: ${e.localizedMessage}", e) } catch (e: Exception) { logger.error("$errorMessage because ", e) return RollupJobValidationResult.Failure(errorMessage, e) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupRunner.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupRunner.kt index b9c276e1d..2399614bf 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupRunner.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupRunner.kt @@ -41,7 +41,9 @@ import org.opensearch.common.settings.Settings import org.opensearch.common.unit.TimeValue import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.indexmanagement.indexstatemanagement.SkipExecution +import org.opensearch.indexmanagement.opensearchapi.IndexManagementSecurityContext import org.opensearch.indexmanagement.opensearchapi.suspendUntil +import org.opensearch.indexmanagement.opensearchapi.withClosableContext import org.opensearch.indexmanagement.rollup.action.get.GetRollupAction import org.opensearch.indexmanagement.rollup.action.get.GetRollupRequest import org.opensearch.indexmanagement.rollup.action.get.GetRollupResponse @@ -257,7 +259,12 @@ object RollupRunner : } } - when (val result = rollupMapperService.attemptCreateRollupTargetIndex(updatableJob, clusterConfigurationProvider.hasLegacyPlugin)) { + val result = withClosableContext( + IndexManagementSecurityContext(job.id, settings, threadPool.threadContext, job.user) + ) { + rollupMapperService.attemptCreateRollupTargetIndex(updatableJob, clusterConfigurationProvider.hasLegacyPlugin) + } + when (result) { is RollupJobValidationResult.Failure -> { setFailedMetadataAndDisableJob(updatableJob, result.message, metadata) return @@ -272,11 +279,21 @@ object RollupRunner : while (rollupSearchService.shouldProcessRollup(updatableJob, metadata)) { do { try { - val rollupResult = when (val rollupSearchResult = rollupSearchService.executeCompositeSearch(updatableJob, metadata)) { + val rollupSearchResult = withClosableContext( + IndexManagementSecurityContext(job.id, settings, threadPool.threadContext, job.user) + ) { + rollupSearchService.executeCompositeSearch(updatableJob, metadata) + } + val rollupResult = when (rollupSearchResult) { is RollupSearchResult.Success -> { val compositeRes: InternalComposite = rollupSearchResult.searchResponse.aggregations.get(updatableJob.id) metadata = metadata.incrementStats(rollupSearchResult.searchResponse, compositeRes) - when (val rollupIndexResult = rollupIndexer.indexRollups(updatableJob, compositeRes)) { + val rollupIndexResult = withClosableContext( + IndexManagementSecurityContext(job.id, settings, threadPool.threadContext, job.user) + ) { + rollupIndexer.indexRollups(updatableJob, compositeRes) + } + when (rollupIndexResult) { is RollupIndexResult.Success -> RollupResult.Success(compositeRes, rollupIndexResult.stats) is RollupIndexResult.Failure -> RollupResult.Failure(rollupIndexResult.message, rollupIndexResult.cause) } @@ -291,9 +308,13 @@ object RollupRunner : updatableJob, metadata.mergeStats(rollupResult.stats), rollupResult.internalComposite ) - updatableJob = client.suspendUntil { listener: ActionListener -> - execute(GetRollupAction.INSTANCE, GetRollupRequest(updatableJob.id, null, "_local"), listener) - }.rollup ?: throw IllegalStateException("Unable to get rollup job") + updatableJob = withClosableContext( + IndexManagementSecurityContext(job.id, settings, threadPool.threadContext, null) + ) { + client.suspendUntil { listener: ActionListener -> + execute(GetRollupAction.INSTANCE, GetRollupRequest(updatableJob.id, null, "_local"), listener) + }.rollup ?: throw IllegalStateException("Unable to get rollup job") + } } is RollupResult.Failure -> { rollupMetadataService.updateMetadata( @@ -353,10 +374,14 @@ object RollupRunner : */ private suspend fun updateRollupJob(job: Rollup, metadata: RollupMetadata): RollupJobResult { try { - val req = IndexRollupRequest(rollup = job, refreshPolicy = WriteRequest.RefreshPolicy.IMMEDIATE) - val res: IndexRollupResponse = client.suspendUntil { execute(IndexRollupAction.INSTANCE, req, it) } - // TODO: Verify the seqNo/primterm got updated - return RollupJobResult.Success(res.rollup) + return withClosableContext( + IndexManagementSecurityContext(job.id, settings, threadPool.threadContext, null) + ) { + val req = IndexRollupRequest(rollup = job, refreshPolicy = WriteRequest.RefreshPolicy.IMMEDIATE) + val res: IndexRollupResponse = client.suspendUntil { execute(IndexRollupAction.INSTANCE, req, it) } + // TODO: Verify the seqNo/primterm got updated + return@withClosableContext RollupJobResult.Success(res.rollup) + } } catch (e: Exception) { // TODO: Catching general exceptions for now, can make more granular // Set metadata to failed since update to rollup job failed @@ -377,30 +402,35 @@ object RollupRunner : // which means we always need to validate the source index on every execution? @Suppress("ReturnCount", "ComplexMethod") private suspend fun isJobValid(job: Rollup): RollupJobValidationResult { - var metadata: RollupMetadata? = null - if (job.metadataID != null) { - logger.debug("Fetching associated metadata for rollup job [${job.id}]") - metadata = when (val getMetadataResult = rollupMetadataService.getExistingMetadata(job)) { - is MetadataResult.Success -> getMetadataResult.metadata - is MetadataResult.NoMetadata -> null - is MetadataResult.Failure -> - throw RollupMetadataException("Failed to get existing rollup metadata [${job.metadataID}]", getMetadataResult.cause) + return withClosableContext( + IndexManagementSecurityContext(job.id, settings, threadPool.threadContext, job.user) + ) { + var metadata: RollupMetadata? = null + if (job.metadataID != null) { + logger.debug("Fetching associated metadata for rollup job [${job.id}]") + metadata = when (val getMetadataResult = rollupMetadataService.getExistingMetadata(job)) { + is MetadataResult.Success -> getMetadataResult.metadata + is MetadataResult.NoMetadata -> null + is MetadataResult.Failure -> + throw RollupMetadataException("Failed to get existing rollup metadata [${job.metadataID}]", getMetadataResult.cause) + } } - } - logger.debug("Validating source index [${job.sourceIndex}] for rollup job [${job.id}]") - when (val sourceIndexValidationResult = rollupMapperService.isSourceIndexValid(job)) { - is RollupJobValidationResult.Valid -> {} // No action taken when valid - else -> return sourceIndexValidationResult - } + logger.debug("Validating source index [${job.sourceIndex}] for rollup job [${job.id}]") + when (val sourceIndexValidationResult = rollupMapperService.isSourceIndexValid(job)) { + is RollupJobValidationResult.Valid -> { + } // No action taken when valid + else -> return@withClosableContext sourceIndexValidationResult + } - // we validate target index only if there is metadata document in the rollup - if (metadata != null) { - logger.debug("Attempting to create/validate target index [${job.targetIndex}] for rollup job [${job.id}]") - return rollupMapperService.attemptCreateRollupTargetIndex(job, clusterConfigurationProvider.hasLegacyPlugin) - } + // we validate target index only if there is metadata document in the rollup + if (metadata != null) { + logger.debug("Attempting to create/validate target index [${job.targetIndex}] for rollup job [${job.id}]") + return@withClosableContext rollupMapperService.attemptCreateRollupTargetIndex(job, clusterConfigurationProvider.hasLegacyPlugin) + } - return RollupJobValidationResult.Valid + return@withClosableContext RollupJobValidationResult.Valid + } } /** diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupSearchService.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupSearchService.kt index 4f4a4e095..b953387ee 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupSearchService.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/RollupSearchService.kt @@ -28,6 +28,7 @@ package org.opensearch.indexmanagement.rollup import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.action.ActionListener import org.opensearch.action.bulk.BackoffPolicy import org.opensearch.action.search.SearchPhaseExecutionException @@ -103,12 +104,12 @@ class RollupSearchService( logger.debug("Non-continuous job [${rollup.id}] is not processing next window [$metadata]") return false } else { - return hasNextFullWindow(metadata) // TODO: Behavior when next full window but 0 docs/afterkey is null + return hasNextFullWindow(rollup, metadata) // TODO: Behavior when next full window but 0 docs/afterkey is null } } - private fun hasNextFullWindow(metadata: RollupMetadata): Boolean { - return Instant.now().isAfter(metadata.continuous!!.nextWindowEndTime) // TODO: !! + private fun hasNextFullWindow(rollup: Rollup, metadata: RollupMetadata): Boolean { + return Instant.now().isAfter(metadata.continuous!!.nextWindowEndTime.plusMillis(rollup.delay ?: 0)) // TODO: !! } @Suppress("ComplexMethod") @@ -145,6 +146,9 @@ class RollupSearchService( } catch (e: MultiBucketConsumerService.TooManyBucketsException) { logger.error(e.message, e.cause) RollupSearchResult.Failure(cause = e) + } catch (e: OpenSearchSecurityException) { + logger.error(e.message, e.cause) + RollupSearchResult.Failure("Cannot search data in source index/s - missing required index permissions: ${e.localizedMessage}", e) } catch (e: Exception) { logger.error(e.message, e.cause) RollupSearchResult.Failure(cause = e) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/DeleteRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/DeleteRollupAction.kt index 26bb7b1e2..971958ce1 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/DeleteRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/DeleteRollupAction.kt @@ -32,6 +32,6 @@ import org.opensearch.action.delete.DeleteResponse class DeleteRollupAction private constructor() : ActionType(NAME, ::DeleteResponse) { companion object { val INSTANCE = DeleteRollupAction() - val NAME = "cluster:admin/opendistro/rollup/delete" + const val NAME = "cluster:admin/opendistro/rollup/delete" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/TransportDeleteRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/TransportDeleteRollupAction.kt index 7f61fc64d..7644a6c4d 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/TransportDeleteRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/delete/TransportDeleteRollupAction.kt @@ -26,28 +26,107 @@ package org.opensearch.indexmanagement.rollup.action.delete +import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionListener import org.opensearch.action.delete.DeleteRequest import org.opensearch.action.delete.DeleteResponse +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX +import org.opensearch.indexmanagement.rollup.model.Rollup +import org.opensearch.indexmanagement.rollup.util.parseRollup +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource +import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +import java.lang.Exception +@Suppress("ReturnCount") class TransportDeleteRollupAction @Inject constructor( transportService: TransportService, val client: Client, - actionFilters: ActionFilters + val clusterService: ClusterService, + val settings: Settings, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( DeleteRollupAction.NAME, transportService, actionFilters, ::DeleteRollupRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: DeleteRollupRequest, actionListener: ActionListener) { - val deleteRequest = DeleteRequest(INDEX_MANAGEMENT_INDEX, request.id()) - .setRefreshPolicy(request.refreshPolicy) - client.delete(deleteRequest, actionListener) + DeleteRollupHandler(client, actionListener, request).start() + } + + inner class DeleteRollupHandler( + private val client: Client, + private val actionListener: ActionListener, + private val request: DeleteRollupRequest, + private val user: User? = SecurityUtils.buildUser(client.threadPool().threadContext) + ) { + + fun start() { + client.threadPool().threadContext.stashContext().use { + getRollup() + } + } + + private fun getRollup() { + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.id()) + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Rollup ${request.id()} is not found", RestStatus.NOT_FOUND)) + return + } + + val rollup: Rollup? + try { + rollup = parseRollup(response, xContentRegistry) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Rollup ${request.id()} is not found", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, rollup.user, filterByEnabled, "rollup", rollup.id, actionListener)) { + return + } else { + delete() + } + } + + override fun onFailure(e: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + } + } + ) + } + + private fun delete() { + val deleteRequest = DeleteRequest(INDEX_MANAGEMENT_INDEX, request.id()) + .setRefreshPolicy(request.refreshPolicy) + client.threadPool().threadContext.stashContext().use { + client.delete(deleteRequest, actionListener) + } + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/ExplainRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/ExplainRollupAction.kt index 71c398353..fbd8019a0 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/ExplainRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/ExplainRollupAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class ExplainRollupAction private constructor() : ActionType(NAME, ::ExplainRollupResponse) { companion object { val INSTANCE = ExplainRollupAction() - val NAME = "cluster:admin/opendistro/rollup/explain" + const val NAME = "cluster:admin/opendistro/rollup/explain" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/TransportExplainRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/TransportExplainRollupAction.kt index 75c3029f1..cf0b204e5 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/TransportExplainRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/explain/TransportExplainRollupAction.kt @@ -35,7 +35,9 @@ import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.index.query.BoolQueryBuilder import org.opensearch.index.query.IdsQueryBuilder import org.opensearch.index.query.WildcardQueryBuilder @@ -45,6 +47,9 @@ import org.opensearch.indexmanagement.opensearchapi.parseWithType import org.opensearch.indexmanagement.rollup.model.ExplainRollup import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.RollupMetadata +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.addUserFilter +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.tasks.Task import org.opensearch.transport.RemoteTransportException @@ -54,11 +59,21 @@ import kotlin.Exception class TransportExplainRollupAction @Inject constructor( transportService: TransportService, val client: Client, + val settings: Settings, + val clusterService: ClusterService, actionFilters: ActionFilters ) : HandledTransportAction( ExplainRollupAction.NAME, transportService, actionFilters, ::ExplainRollupRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) @Suppress("SpreadOperator") @@ -67,78 +82,80 @@ class TransportExplainRollupAction @Inject constructor( // Instantiate concrete ids to metadata map by removing wildcard matches val idsToExplain: MutableMap = ids.filter { !it.contains("*") }.map { it to null }.toMap(mutableMapOf()) // First search is for all rollup documents that match at least one of the given rollupIDs - val searchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX) - .source( - SearchSourceBuilder().query( - BoolQueryBuilder().minimumShouldMatch(1).apply { - ids.forEach { - this.should(WildcardQueryBuilder("${Rollup.ROLLUP_TYPE}.${Rollup.ROLLUP_ID_FIELD}.keyword", "*$it*")) - } - } - ) - ) - client.search( - searchRequest, - object : ActionListener { - override fun onResponse(response: SearchResponse) { - try { - response.hits.hits.forEach { - val rollup = contentParser(it.sourceRef).parseWithType(it.id, it.seqNo, it.primaryTerm, Rollup.Companion::parse) - idsToExplain[rollup.id] = ExplainRollup(metadataID = rollup.metadataID) + val queryBuilder = BoolQueryBuilder().minimumShouldMatch(1).apply { + ids.forEach { + this.should(WildcardQueryBuilder("${Rollup.ROLLUP_TYPE}.${Rollup.ROLLUP_ID_FIELD}.keyword", "*$it*")) + } + } + val user = buildUser(client.threadPool().threadContext) + addUserFilter(user, queryBuilder, filterByEnabled, "rollup.user") + + val searchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX).source(SearchSourceBuilder().query(queryBuilder)) + + client.threadPool().threadContext.stashContext().use { + client.search( + searchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + try { + response.hits.hits.forEach { + val rollup = contentParser(it.sourceRef).parseWithType(it.id, it.seqNo, it.primaryTerm, Rollup.Companion::parse) + idsToExplain[rollup.id] = ExplainRollup(metadataID = rollup.metadataID) + } + } catch (e: Exception) { + log.error("Failed to parse explain response", e) + actionListener.onFailure(e) + return } - } catch (e: Exception) { - log.error("Failed to parse explain response", e) - actionListener.onFailure(e) - return - } - val metadataIds = idsToExplain.values.mapNotNull { it?.metadataID } - val metadataSearchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX) - .source(SearchSourceBuilder().query(IdsQueryBuilder().addIds(*metadataIds.toTypedArray()))) - client.search( - metadataSearchRequest, - object : ActionListener { - override fun onResponse(response: SearchResponse) { - try { - response.hits.hits.forEach { - val metadata = contentParser(it.sourceRef) - .parseWithType(it.id, it.seqNo, it.primaryTerm, RollupMetadata.Companion::parse) - idsToExplain.computeIfPresent(metadata.rollupID) { _, - explainRollup -> - explainRollup.copy(metadata = metadata) + val metadataIds = idsToExplain.values.mapNotNull { it?.metadataID } + val metadataSearchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX) + .source(SearchSourceBuilder().query(IdsQueryBuilder().addIds(*metadataIds.toTypedArray()))) + client.search( + metadataSearchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + try { + response.hits.hits.forEach { + val metadata = contentParser(it.sourceRef) + .parseWithType(it.id, it.seqNo, it.primaryTerm, RollupMetadata.Companion::parse) + idsToExplain.computeIfPresent(metadata.rollupID) { _, + explainRollup -> + explainRollup.copy(metadata = metadata) + } } + actionListener.onResponse(ExplainRollupResponse(idsToExplain.toMap())) + } catch (e: Exception) { + log.error("Failed to parse rollup metadata", e) + actionListener.onFailure(e) + return } - actionListener.onResponse(ExplainRollupResponse(idsToExplain.toMap())) - } catch (e: Exception) { - log.error("Failed to parse rollup metadata", e) - actionListener.onFailure(e) - return } - } - override fun onFailure(e: Exception) { - log.error("Failed to search rollup metadata", e) - when (e) { - is RemoteTransportException -> actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) - else -> actionListener.onFailure(e) + override fun onFailure(e: Exception) { + log.error("Failed to search rollup metadata", e) + when (e) { + is RemoteTransportException -> actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + else -> actionListener.onFailure(e) + } } } - } - ) - } + ) + } - override fun onFailure(e: Exception) { - log.error("Failed to search for rollups", e) - when (e) { - is ResourceNotFoundException -> { - val nonWildcardIds = ids.filter { !it.contains("*") }.map { it to null }.toMap(mutableMapOf()) - actionListener.onResponse(ExplainRollupResponse(nonWildcardIds)) + override fun onFailure(e: Exception) { + log.error("Failed to search for rollups", e) + when (e) { + is ResourceNotFoundException -> { + val nonWildcardIds = ids.filter { !it.contains("*") }.map { it to null }.toMap(mutableMapOf()) + actionListener.onResponse(ExplainRollupResponse(nonWildcardIds)) + } + is RemoteTransportException -> actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + else -> actionListener.onFailure(e) } - is RemoteTransportException -> actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) - else -> actionListener.onFailure(e) } } - } - ) + ) + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupAction.kt index 4f25a2796..627c6b9fa 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class GetRollupAction private constructor() : ActionType(NAME, ::GetRollupResponse) { companion object { val INSTANCE = GetRollupAction() - val NAME = "cluster:admin/opendistro/rollup/get" + const val NAME = "cluster:admin/opendistro/rollup/get" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupResponse.kt index f4896b99a..7c5e209e4 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupResponse.kt @@ -32,7 +32,7 @@ import org.opensearch.common.io.stream.StreamOutput import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.Rollup.Companion.ROLLUP_TYPE import org.opensearch.indexmanagement.util._ID @@ -98,7 +98,7 @@ class GetRollupResponse : ActionResponse, ToXContentObject { .field(_VERSION, version) .field(_SEQ_NO, seqNo) .field(_PRIMARY_TERM, primaryTerm) - if (rollup != null) builder.field(ROLLUP_TYPE, rollup, XCONTENT_WITHOUT_TYPE) + if (rollup != null) builder.field(ROLLUP_TYPE, rollup, XCONTENT_WITHOUT_TYPE_AND_USER) return builder.endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsAction.kt index d3033dcf1..fce50bc87 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class GetRollupsAction private constructor() : ActionType(NAME, ::GetRollupsResponse) { companion object { val INSTANCE = GetRollupsAction() - val NAME = "cluster:admin/opendistro/rollup/search" + const val NAME = "cluster:admin/opendistro/rollup/search" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsResponse.kt index 88778f7f4..fd09eea7f 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/GetRollupsResponse.kt @@ -32,7 +32,7 @@ import org.opensearch.common.io.stream.StreamOutput import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.Rollup.Companion.ROLLUP_TYPE import org.opensearch.indexmanagement.util._ID @@ -81,7 +81,7 @@ class GetRollupsResponse : ActionResponse, ToXContentObject { .field(_ID, rollup.id) .field(_SEQ_NO, rollup.seqNo) .field(_PRIMARY_TERM, rollup.primaryTerm) - .field(ROLLUP_TYPE, rollup, XCONTENT_WITHOUT_TYPE) + .field(ROLLUP_TYPE, rollup, XCONTENT_WITHOUT_TYPE_AND_USER) .endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupAction.kt index 1cc88b5cb..3c4f11a75 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupAction.kt @@ -33,14 +33,16 @@ import org.opensearch.action.get.GetResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject -import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry -import org.opensearch.common.xcontent.XContentHelper -import org.opensearch.common.xcontent.XContentType import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX -import org.opensearch.indexmanagement.opensearchapi.parseWithType import org.opensearch.indexmanagement.rollup.model.Rollup +import org.opensearch.indexmanagement.rollup.util.parseRollup +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService @@ -50,39 +52,59 @@ class TransportGetRollupAction @Inject constructor( transportService: TransportService, val client: Client, actionFilters: ActionFilters, + val settings: Settings, + val clusterService: ClusterService, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction ( GetRollupAction.NAME, transportService, actionFilters, ::GetRollupRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + + @Suppress("ReturnCount") override fun doExecute(task: Task, request: GetRollupRequest, listener: ActionListener) { - val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.id) - .fetchSourceContext(request.srcContext).preference(request.preference) - client.get( - getRequest, - object : ActionListener { - override fun onResponse(response: GetResponse) { - if (!response.isExists) { - return listener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) - } + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.id).preference(request.preference) + val user = buildUser(client.threadPool().threadContext) + client.threadPool().threadContext.stashContext().use { + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + return listener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + } - var rollup: Rollup? = null - if (!response.isSourceEmpty) { - XContentHelper.createParser( - xContentRegistry, LoggingDeprecationHandler.INSTANCE, - response.sourceAsBytesRef, XContentType.JSON - ).use { xcp -> - rollup = xcp.parseWithType(response.id, response.seqNo, response.primaryTerm, Rollup.Companion::parse) + val rollup: Rollup? + try { + rollup = parseRollup(response, xContentRegistry) + } catch (e: IllegalArgumentException) { + listener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + return + } + if (!SecurityUtils.userHasPermissionForResource(user, rollup.user, filterByEnabled, "rollup", request.id, listener)) { + return + } else { + // if HEAD request don't return the rollup + val rollupResponse = if (request.srcContext != null && !request.srcContext.fetchSource()) { + GetRollupResponse(response.id, response.version, response.seqNo, response.primaryTerm, RestStatus.OK, null) + } else { + GetRollupResponse(response.id, response.version, response.seqNo, response.primaryTerm, RestStatus.OK, rollup) + } + listener.onResponse(rollupResponse) } } - listener.onResponse(GetRollupResponse(response.id, response.version, response.seqNo, response.primaryTerm, RestStatus.OK, rollup)) - } - - override fun onFailure(e: Exception) { - listener.onFailure(e) + override fun onFailure(e: Exception) { + listener.onFailure(e) + } } - } - ) + ) + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupsAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupsAction.kt index c5cc95b48..e60cab2b3 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupsAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/get/TransportGetRollupsAction.kt @@ -34,7 +34,9 @@ import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.index.query.BoolQueryBuilder import org.opensearch.index.query.ExistsQueryBuilder @@ -43,6 +45,9 @@ import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANA import org.opensearch.indexmanagement.opensearchapi.contentParser import org.opensearch.indexmanagement.opensearchapi.parseWithType import org.opensearch.indexmanagement.rollup.model.Rollup +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.addUserFilter +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser import org.opensearch.rest.RestStatus import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.search.sort.SortOrder @@ -54,11 +59,21 @@ class TransportGetRollupsAction @Inject constructor( transportService: TransportService, val client: Client, actionFilters: ActionFilters, + val clusterService: ClusterService, + val settings: Settings, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction ( GetRollupsAction.NAME, transportService, actionFilters, ::GetRollupsRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: GetRollupsRequest, listener: ActionListener) { val searchString = request.searchString.trim() val from = request.from @@ -70,37 +85,41 @@ class TransportGetRollupsAction @Inject constructor( if (searchString.isNotEmpty()) { boolQueryBuilder.filter(WildcardQueryBuilder("${Rollup.ROLLUP_TYPE}.${Rollup.ROLLUP_ID_FIELD}.keyword", "*$searchString*")) } + val user = buildUser(client.threadPool().threadContext) + addUserFilter(user, boolQueryBuilder, filterByEnabled, "rollup.user") val searchSourceBuilder = SearchSourceBuilder().query(boolQueryBuilder).from(from).size(size).seqNoAndPrimaryTerm(true) .sort(sortField, SortOrder.fromString(sortDirection)) val searchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX).source(searchSourceBuilder) - client.search( - searchRequest, - object : ActionListener { - override fun onResponse(response: SearchResponse) { - val totalRollups = response.hits.totalHits?.value ?: 0 + client.threadPool().threadContext.stashContext().use { + client.search( + searchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + val totalRollups = response.hits.totalHits?.value ?: 0 - if (response.shardFailures.isNotEmpty()) { - val failure = response.shardFailures.reduce { s1, s2 -> if (s1.status().status > s2.status().status) s1 else s2 } - listener.onFailure(OpenSearchStatusException("Get rollups failed on some shards", failure.status(), failure.cause)) - } else { - try { - val rollups = response.hits.hits.map { - contentParser(it.sourceRef).parseWithType(it.id, it.seqNo, it.primaryTerm, Rollup.Companion::parse) - } - listener.onResponse(GetRollupsResponse(rollups, totalRollups.toInt(), RestStatus.OK)) - } catch (e: Exception) { - listener.onFailure( - OpenSearchStatusException( - "Failed to parse rollups", - RestStatus.INTERNAL_SERVER_ERROR, ExceptionsHelper.unwrapCause(e) + if (response.shardFailures.isNotEmpty()) { + val failure = response.shardFailures.reduce { s1, s2 -> if (s1.status().status > s2.status().status) s1 else s2 } + listener.onFailure(OpenSearchStatusException("Get rollups failed on some shards", failure.status(), failure.cause)) + } else { + try { + val rollups = response.hits.hits.map { + contentParser(it.sourceRef).parseWithType(it.id, it.seqNo, it.primaryTerm, Rollup.Companion::parse) + } + listener.onResponse(GetRollupsResponse(rollups, totalRollups.toInt(), RestStatus.OK)) + } catch (e: Exception) { + listener.onFailure( + OpenSearchStatusException( + "Failed to parse rollups", + RestStatus.INTERNAL_SERVER_ERROR, ExceptionsHelper.unwrapCause(e) + ) ) - ) + } } } - } - override fun onFailure(e: Exception) = listener.onFailure(e) - } - ) + override fun onFailure(e: Exception) = listener.onFailure(e) + } + ) + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupAction.kt index be2e48af1..25d0dcafe 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupAction.kt @@ -31,6 +31,6 @@ import org.opensearch.action.ActionType class IndexRollupAction private constructor() : ActionType(NAME, ::IndexRollupResponse) { companion object { val INSTANCE = IndexRollupAction() - val NAME = "cluster:admin/opendistro/rollup/index" + const val NAME = "cluster:admin/opendistro/rollup/index" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupResponse.kt index d7de18d3c..3de0a03b7 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/IndexRollupResponse.kt @@ -32,7 +32,7 @@ import org.opensearch.common.io.stream.StreamOutput import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.Rollup.Companion.ROLLUP_TYPE import org.opensearch.indexmanagement.util._ID @@ -93,7 +93,7 @@ class IndexRollupResponse : ActionResponse, ToXContentObject { .field(_VERSION, version) .field(_SEQ_NO, seqNo) .field(_PRIMARY_TERM, primaryTerm) - .field(ROLLUP_TYPE, rollup, XCONTENT_WITHOUT_TYPE) + .field(ROLLUP_TYPE, rollup, XCONTENT_WITHOUT_TYPE_AND_USER) .endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/TransportIndexRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/TransportIndexRollupAction.kt index 5a5961fb7..1a5b5126f 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/TransportIndexRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/index/TransportIndexRollupAction.kt @@ -30,6 +30,8 @@ import org.apache.logging.log4j.LogManager import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionListener import org.opensearch.action.DocWriteRequest +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse import org.opensearch.action.index.IndexRequest import org.opensearch.action.index.IndexResponse import org.opensearch.action.support.ActionFilters @@ -38,15 +40,20 @@ import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.XContentFactory.jsonBuilder +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementIndices import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX -import org.opensearch.indexmanagement.rollup.action.get.GetRollupAction -import org.opensearch.indexmanagement.rollup.action.get.GetRollupRequest -import org.opensearch.indexmanagement.rollup.action.get.GetRollupResponse import org.opensearch.indexmanagement.rollup.model.Rollup +import org.opensearch.indexmanagement.rollup.util.parseRollup +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.util.IndexUtils +import org.opensearch.indexmanagement.util.SecurityUtils +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.validateUserConfiguration import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService @@ -57,11 +64,21 @@ class TransportIndexRollupAction @Inject constructor( val client: Client, actionFilters: ActionFilters, val indexManagementIndices: IndexManagementIndices, - val clusterService: ClusterService + val clusterService: ClusterService, + val settings: Settings, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( IndexRollupAction.NAME, transportService, actionFilters, ::IndexRollupRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) override fun doExecute(task: Task, request: IndexRollupRequest, listener: ActionListener) { @@ -71,11 +88,17 @@ class TransportIndexRollupAction @Inject constructor( inner class IndexRollupHandler( private val client: Client, private val actionListener: ActionListener, - private val request: IndexRollupRequest + private val request: IndexRollupRequest, + private val user: User? = buildUser(client.threadPool().threadContext, request.rollup.user) ) { fun start() { - indexManagementIndices.checkAndUpdateIMConfigIndex(ActionListener.wrap(::onCreateMappingsResponse, actionListener::onFailure)) + client.threadPool().threadContext.stashContext().use { + if (!validateUserConfiguration(user, filterByEnabled, actionListener)) { + return + } + indexManagementIndices.checkAndUpdateIMConfigIndex(ActionListener.wrap(::onCreateMappingsResponse, actionListener::onFailure)) + } } private fun onCreateMappingsResponse(response: AcknowledgedResponse) { @@ -94,17 +117,27 @@ class TransportIndexRollupAction @Inject constructor( } private fun getRollup() { - val getReq = GetRollupRequest(request.rollup.id, null) - client.execute(GetRollupAction.INSTANCE, getReq, ActionListener.wrap(::onGetRollup, actionListener::onFailure)) + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.rollup.id) + client.get(getRequest, ActionListener.wrap(::onGetRollup, actionListener::onFailure)) } @Suppress("ReturnCount") - private fun onGetRollup(response: GetRollupResponse) { - if (response.status != RestStatus.OK) { - return actionListener.onFailure(OpenSearchStatusException("Unable to get existing rollup", response.status)) + private fun onGetRollup(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + return + } + + val rollup: Rollup? + try { + rollup = parseRollup(response, xContentRegistry) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + return + } + if (!SecurityUtils.userHasPermissionForResource(user, rollup.user, filterByEnabled, "rollup", rollup.id, actionListener)) { + return } - val rollup = response.rollup - ?: return actionListener.onFailure(OpenSearchStatusException("The current rollup is null", RestStatus.INTERNAL_SERVER_ERROR)) val modified = modifiedImmutableProperties(rollup, request.rollup) if (modified.isNotEmpty()) { return actionListener.onFailure(OpenSearchStatusException("Not allowed to modify $modified", RestStatus.BAD_REQUEST)) @@ -124,7 +157,7 @@ class TransportIndexRollupAction @Inject constructor( } private fun putRollup() { - val rollup = request.rollup.copy(schemaVersion = IndexUtils.indexManagementConfigSchemaVersion) + val rollup = request.rollup.copy(schemaVersion = IndexUtils.indexManagementConfigSchemaVersion, user = this.user) request.index(INDEX_MANAGEMENT_INDEX) .id(request.rollup.id) .source(rollup.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/StartRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/StartRollupAction.kt index 0bc17871c..318df65e7 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/StartRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/StartRollupAction.kt @@ -32,6 +32,6 @@ import org.opensearch.action.support.master.AcknowledgedResponse class StartRollupAction private constructor() : ActionType(NAME, ::AcknowledgedResponse) { companion object { val INSTANCE = StartRollupAction() - val NAME = "cluster:admin/opendistro/rollup/start" + const val NAME = "cluster:admin/opendistro/rollup/start" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/TransportStartRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/TransportStartRollupAction.kt index e023f239e..2b0f85275 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/TransportStartRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/start/TransportStartRollupAction.kt @@ -39,63 +39,91 @@ import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.update.UpdateRequest import org.opensearch.action.update.UpdateResponse import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX import org.opensearch.indexmanagement.opensearchapi.parseWithType -import org.opensearch.indexmanagement.rollup.action.get.GetRollupAction -import org.opensearch.indexmanagement.rollup.action.get.GetRollupRequest -import org.opensearch.indexmanagement.rollup.action.get.GetRollupResponse import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.RollupMetadata +import org.opensearch.indexmanagement.rollup.util.parseRollup +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +import java.lang.IllegalArgumentException import java.time.Instant +@Suppress("ReturnCount") class TransportStartRollupAction @Inject constructor( transportService: TransportService, val client: Client, - actionFilters: ActionFilters + val clusterService: ClusterService, + val settings: Settings, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( StartRollupAction.NAME, transportService, actionFilters, ::StartRollupRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) override fun doExecute(task: Task, request: StartRollupRequest, actionListener: ActionListener) { - val getReq = GetRollupRequest(request.id(), null) - client.execute( - GetRollupAction.INSTANCE, getReq, - object : ActionListener { - override fun onResponse(response: GetRollupResponse) { - val rollup = response.rollup - if (rollup == null) { - return actionListener.onFailure( - OpenSearchStatusException("Could not find rollup [${request.id()}]", RestStatus.NOT_FOUND) - ) - } + val getReq = GetRequest(INDEX_MANAGEMENT_INDEX, request.id()) + val user: User? = buildUser(client.threadPool().threadContext) + client.threadPool().threadContext.stashContext().use { + client.get( + getReq, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + return + } - if (rollup.enabled) { - log.debug("Rollup job is already enabled, checking if metadata needs to be updated") - return if (rollup.metadataID == null) { - actionListener.onResponse(AcknowledgedResponse(true)) - } else { - getRollupMetadata(rollup, actionListener) + val rollup: Rollup? + try { + rollup = parseRollup(response, xContentRegistry) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, rollup.user, filterByEnabled, "rollup", rollup.id, actionListener)) { + return + } + if (rollup.enabled) { + log.debug("Rollup job is already enabled, checking if metadata needs to be updated") + return if (rollup.metadataID == null) { + actionListener.onResponse(AcknowledgedResponse(true)) + } else { + getRollupMetadata(rollup, actionListener) + } } - } - updateRollupJob(rollup, request, actionListener) - } + updateRollupJob(rollup, request, actionListener) + } - override fun onFailure(e: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + override fun onFailure(e: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + } } - } - ) + ) + } } // TODO: Should create a transport action to update metadata diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/StopRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/StopRollupAction.kt index c08d38fd1..506b697bd 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/StopRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/StopRollupAction.kt @@ -32,6 +32,6 @@ import org.opensearch.action.support.master.AcknowledgedResponse class StopRollupAction private constructor() : ActionType(NAME, ::AcknowledgedResponse) { companion object { val INSTANCE = StopRollupAction() - val NAME = "cluster:admin/opendistro/rollup/stop" + const val NAME = "cluster:admin/opendistro/rollup/stop" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/TransportStopRollupAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/TransportStopRollupAction.kt index 0f4ec1891..04e6a8fa3 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/TransportStopRollupAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/action/stop/TransportStopRollupAction.kt @@ -39,21 +39,25 @@ import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.update.UpdateRequest import org.opensearch.action.update.UpdateResponse import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType import org.opensearch.indexmanagement.IndexManagementPlugin import org.opensearch.indexmanagement.opensearchapi.parseWithType -import org.opensearch.indexmanagement.rollup.action.get.GetRollupAction -import org.opensearch.indexmanagement.rollup.action.get.GetRollupRequest -import org.opensearch.indexmanagement.rollup.action.get.GetRollupResponse import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.RollupMetadata +import org.opensearch.indexmanagement.rollup.util.parseRollup +import org.opensearch.indexmanagement.settings.IndexManagementSettings +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +import java.lang.IllegalArgumentException import java.time.Instant /** @@ -72,39 +76,62 @@ import java.time.Instant class TransportStopRollupAction @Inject constructor( transportService: TransportService, val client: Client, - actionFilters: ActionFilters + val clusterService: ClusterService, + val settings: Settings, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( StopRollupAction.NAME, transportService, actionFilters, ::StopRollupRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) + @Suppress("ReturnCount") override fun doExecute(task: Task, request: StopRollupRequest, actionListener: ActionListener) { log.debug("Executing StopRollupAction on ${request.id()}") - val getReq = GetRollupRequest(request.id(), null) - client.execute( - GetRollupAction.INSTANCE, getReq, - object : ActionListener { - override fun onResponse(response: GetRollupResponse) { - val rollup = response.rollup - if (rollup == null) { - return actionListener.onFailure( - OpenSearchStatusException("Could not find rollup [${request.id()}]", RestStatus.NOT_FOUND) - ) - } + val getRequest = GetRequest(IndexManagementPlugin.INDEX_MANAGEMENT_INDEX, request.id()) + val user = buildUser(client.threadPool().threadContext) + client.threadPool().threadContext.stashContext().use { + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + return + } - if (rollup.metadataID != null) { - getRollupMetadata(rollup, request, actionListener) - } else { - updateRollupJob(rollup, request, actionListener) + val rollup: Rollup? + try { + rollup = parseRollup(response, xContentRegistry) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Rollup not found", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, rollup.user, filterByEnabled, "rollup", rollup.id, actionListener)) { + return + } + if (rollup.metadataID != null) { + getRollupMetadata(rollup, request, actionListener) + } else { + updateRollupJob(rollup, request, actionListener) + } } - } - override fun onFailure(e: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + override fun onFailure(e: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + } } - } - ) + ) + } } private fun getRollupMetadata(rollup: Rollup, request: StopRollupRequest, actionListener: ActionListener) { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptor.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptor.kt index 9d8f8585a..4cd68c1b4 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptor.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptor.kt @@ -77,11 +77,15 @@ class RollupInterceptor( private val logger = LogManager.getLogger(javaClass) @Volatile private var searchEnabled = RollupSettings.ROLLUP_SEARCH_ENABLED.get(settings) + @Volatile private var searchAllJobs = RollupSettings.ROLLUP_SEARCH_ALL_JOBS.get(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(RollupSettings.ROLLUP_SEARCH_ENABLED) { searchEnabled = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(RollupSettings.ROLLUP_SEARCH_ALL_JOBS) { + searchAllJobs = it + } } @Suppress("SpreadOperator") @@ -126,12 +130,9 @@ class RollupInterceptor( throw IllegalArgumentException("Could not find a rollup job that can answer this query because $issues") } - val matchedRollup = pickRollupJob(matchingRollupJobs.keys) - val fieldNameMappingTypeMap = matchingRollupJobs.getValue(matchedRollup).associateBy({ it.fieldName }, { it.mappingType }) - // only rebuild if there is necessity to rebuild if (fieldMappings.isNotEmpty()) { - request.source(request.source().rewriteSearchSourceBuilder(matchedRollup, fieldNameMappingTypeMap)) + rewriteShardSearchForRollupJobs(request, matchingRollupJobs) } } } @@ -298,4 +299,14 @@ class RollupInterceptor( DateHistogramInterval(rollup.getDateHistogram().fixedInterval).estimateMillis() } } + + private fun rewriteShardSearchForRollupJobs(request: ShardSearchRequest, matchingRollupJobs: Map>) { + val matchedRollup = pickRollupJob(matchingRollupJobs.keys) + val fieldNameMappingTypeMap = matchingRollupJobs.getValue(matchedRollup).associateBy({ it.fieldName }, { it.mappingType }) + if (searchAllJobs) { + request.source(request.source().rewriteSearchSourceBuilder(matchingRollupJobs.keys, fieldNameMappingTypeMap)) + } else { + request.source(request.source().rewriteSearchSourceBuilder(matchedRollup, fieldNameMappingTypeMap)) + } + } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollup.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollup.kt index 2203bba54..f9e7d1539 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollup.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollup.kt @@ -34,6 +34,7 @@ import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.authuser.User import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.common.model.dimension.DateHistogram import org.opensearch.indexmanagement.common.model.dimension.Dimension @@ -79,7 +80,7 @@ data class ISMRollup( return builder } - fun toRollup(sourceIndex: String, roles: List = listOf()): Rollup { + fun toRollup(sourceIndex: String, user: User? = null): Rollup { val id = sourceIndex + toString() val currentTime = Instant.now() return Rollup( @@ -95,12 +96,12 @@ data class ISMRollup( sourceIndex = sourceIndex, targetIndex = this.targetIndex, metadataID = null, - roles = roles, pageSize = pageSize, delay = null, continuous = false, dimensions = dimensions, - metrics = metrics + metrics = metrics, + user = user ) } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/Rollup.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/Rollup.kt index 09e114365..00ed128f8 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/Rollup.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/model/Rollup.kt @@ -34,14 +34,17 @@ import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParser.Token import org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken +import org.opensearch.commons.authuser.User import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.common.model.dimension.DateHistogram import org.opensearch.indexmanagement.common.model.dimension.Dimension import org.opensearch.indexmanagement.common.model.dimension.Histogram import org.opensearch.indexmanagement.common.model.dimension.Terms import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_USER import org.opensearch.indexmanagement.opensearchapi.instant import org.opensearch.indexmanagement.opensearchapi.optionalTimeField +import org.opensearch.indexmanagement.opensearchapi.optionalUserField import org.opensearch.indexmanagement.util.IndexUtils import org.opensearch.indexmanagement.util._ID import org.opensearch.jobscheduler.spi.ScheduledJobParameter @@ -58,19 +61,20 @@ data class Rollup( val primaryTerm: Long = SequenceNumbers.UNASSIGNED_PRIMARY_TERM, val enabled: Boolean, val schemaVersion: Long, - val jobSchedule: Schedule, + var jobSchedule: Schedule, val jobLastUpdatedTime: Instant, val jobEnabledTime: Instant?, val description: String, val sourceIndex: String, val targetIndex: String, val metadataID: String?, - val roles: List, + @Deprecated("Will be ignored, to check the roles use user field") val roles: List = listOf(), val pageSize: Int, val delay: Long?, val continuous: Boolean, val dimensions: List, - val metrics: List + val metrics: List, + val user: User? = null ) : ScheduledJobParameter, Writeable { init { @@ -79,12 +83,26 @@ data class Rollup( } else { require(jobEnabledTime == null) { "Job enabled time must not be present if the job is disabled" } } + // Copy the delay parameter of the job into the job scheduler for continuous jobs only + if (jobSchedule.delay != delay && continuous) { + jobSchedule = when (jobSchedule) { + is CronSchedule -> { + val cronSchedule = jobSchedule as CronSchedule + CronSchedule(cronSchedule.cronExpression, cronSchedule.timeZone, delay ?: 0) + } + is IntervalSchedule -> { + val intervalSchedule = jobSchedule as IntervalSchedule + IntervalSchedule(intervalSchedule.startTime, intervalSchedule.interval, intervalSchedule.unit, delay ?: 0) + } + else -> jobSchedule + } + } when (jobSchedule) { is CronSchedule -> { // Job scheduler already correctly throws errors for this } is IntervalSchedule -> { - require(jobSchedule.interval >= MINIMUM_JOB_INTERVAL) { "Rollup job schedule interval must be greater than 0" } + require((jobSchedule as IntervalSchedule).interval >= MINIMUM_JOB_INTERVAL) { "Rollup job schedule interval must be greater than 0" } } } require(sourceIndex != targetIndex) { "Your source and target index cannot be the same" } @@ -93,7 +111,10 @@ data class Rollup( } require(dimensions.first().type == Dimension.Type.DATE_HISTOGRAM) { "The first dimension must be a date histogram" } require(pageSize in MINIMUM_PAGE_SIZE..MAXIMUM_PAGE_SIZE) { "Page size must be between 1 and 10,000" } - if (delay != null) require(delay >= MINIMUM_DELAY) { "Delay must be non-negative if set" } + if (delay != null) { + require(delay >= MINIMUM_DELAY) { "Delay must be non-negative if set" } + require(delay <= Instant.now().toEpochMilli()) { "Delay must be less than the current unix time" } + } } override fun isEnabled() = enabled @@ -146,7 +167,10 @@ data class Rollup( } dimensionsList.toList() }, - metrics = sin.readList(::RollupMetrics) + metrics = sin.readList(::RollupMetrics), + user = if (sin.readBoolean()) { + User(sin) + } else null ) override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { @@ -162,12 +186,12 @@ data class Rollup( .field(SOURCE_INDEX_FIELD, sourceIndex) .field(TARGET_INDEX_FIELD, targetIndex) .field(METADATA_ID_FIELD, metadataID) - .field(ROLES_FIELD, roles.toTypedArray()) .field(PAGE_SIZE_FIELD, pageSize) .field(DELAY_FIELD, delay) .field(CONTINUOUS_FIELD, continuous) .field(DIMENSIONS_FIELD, dimensions.toTypedArray()) .field(RollupMetrics.METRICS_FIELD, metrics.toTypedArray()) + if (params.paramAsBoolean(WITH_USER, true)) builder.optionalUserField(USER_FIELD, user) if (params.paramAsBoolean(WITH_TYPE, true)) builder.endObject() builder.endObject() return builder @@ -205,6 +229,8 @@ data class Rollup( } } out.writeCollection(metrics) + out.writeBoolean(user != null) + user?.writeTo(out) } companion object { @@ -238,6 +264,7 @@ data class Rollup( const val ROLLUP_DOC_ID_FIELD = "$ROLLUP_TYPE.$_ID" const val ROLLUP_DOC_COUNT_FIELD = "$ROLLUP_TYPE._doc_count" const val ROLLUP_DOC_SCHEMA_VERSION_FIELD = "$ROLLUP_TYPE._$SCHEMA_VERSION_FIELD" + const val USER_FIELD = "user" @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth") @JvmStatic @@ -258,12 +285,12 @@ data class Rollup( var sourceIndex: String? = null var targetIndex: String? = null var metadataID: String? = null - val roles = mutableListOf() var pageSize: Int? = null var delay: Long? = null var continuous = false val dimensions = mutableListOf() val metrics = mutableListOf() + var user: User? = null ensureExpectedToken(Token.START_OBJECT, xcp.currentToken(), xcp) @@ -283,9 +310,10 @@ data class Rollup( TARGET_INDEX_FIELD -> targetIndex = xcp.text() METADATA_ID_FIELD -> metadataID = xcp.textOrNull() ROLES_FIELD -> { + // Parsing but not storing the field, deprecated ensureExpectedToken(Token.START_ARRAY, xcp.currentToken(), xcp) while (xcp.nextToken() != Token.END_ARRAY) { - roles.add(xcp.text()) + xcp.text() } } PAGE_SIZE_FIELD -> pageSize = xcp.intValue() @@ -303,6 +331,9 @@ data class Rollup( metrics.add(RollupMetrics.parse(xcp)) } } + USER_FIELD -> { + user = if (xcp.currentToken() == Token.VALUE_NULL) null else User.parse(xcp) + } else -> throw IllegalArgumentException("Invalid field [$fieldName] found in Rollup.") } } @@ -317,7 +348,7 @@ data class Rollup( // TODO: Make startTime public in Job Scheduler so we can just directly check the value if (seqNo == SequenceNumbers.UNASSIGNED_SEQ_NO || primaryTerm == SequenceNumbers.UNASSIGNED_PRIMARY_TERM) { if (schedule is IntervalSchedule) { - schedule = IntervalSchedule(Instant.now(), schedule.interval, schedule.unit) + schedule = IntervalSchedule(Instant.now(), schedule.interval, schedule.unit, schedule.delay ?: 0) } } return Rollup( @@ -333,12 +364,12 @@ data class Rollup( sourceIndex = requireNotNull(sourceIndex) { "Rollup source index is null" }, targetIndex = requireNotNull(targetIndex) { "Rollup target index is null" }, metadataID = metadataID, - roles = roles.toList(), pageSize = requireNotNull(pageSize) { "Rollup page size is null" }, delay = delay, continuous = continuous, dimensions = dimensions, - metrics = metrics + metrics = metrics, + user = user ) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/settings/RollupSettings.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/settings/RollupSettings.kt index c6bd99968..063724df8 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/settings/RollupSettings.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/settings/RollupSettings.kt @@ -33,6 +33,7 @@ class RollupSettings { companion object { const val DEFAULT_ROLLUP_ENABLED = true + const val DEFAULT_SEARCH_ALL_JOBS = false const val DEFAULT_ACQUIRE_LOCK_RETRY_COUNT = 3 const val DEFAULT_ACQUIRE_LOCK_RETRY_DELAY = 1000L const val DEFAULT_RENEW_LOCK_RETRY_COUNT = 3 @@ -89,6 +90,13 @@ class RollupSettings { Setting.Property.Dynamic ) + val ROLLUP_SEARCH_ALL_JOBS: Setting = Setting.boolSetting( + "plugins.rollup.search.search_all_jobs", + DEFAULT_SEARCH_ALL_JOBS, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ) + val ROLLUP_DASHBOARDS: Setting = Setting.boolSetting( "plugins.rollup.dashboards.enabled", LegacyOpenDistroRollupSettings.ROLLUP_DASHBOARDS, diff --git a/src/main/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtils.kt b/src/main/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtils.kt index ab799c90e..a1bcf2f4e 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtils.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtils.kt @@ -28,6 +28,7 @@ package org.opensearch.indexmanagement.rollup.util +import org.opensearch.action.get.GetResponse import org.opensearch.action.search.SearchRequest import org.opensearch.cluster.ClusterState import org.opensearch.cluster.metadata.IndexMetadata @@ -51,6 +52,7 @@ import org.opensearch.indexmanagement.common.model.dimension.DateHistogram import org.opensearch.indexmanagement.common.model.dimension.Dimension import org.opensearch.indexmanagement.common.model.dimension.Histogram import org.opensearch.indexmanagement.common.model.dimension.Terms +import org.opensearch.indexmanagement.opensearchapi.parseWithType import org.opensearch.indexmanagement.rollup.RollupMapperService import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.RollupFieldMapping @@ -382,10 +384,11 @@ fun Rollup.rewriteQueryBuilder(queryBuilder: QueryBuilder, fieldNameMappingTypeM } } -fun Rollup.buildRollupQuery(fieldNameMappingTypeMap: Map, oldQuery: QueryBuilder): QueryBuilder { +fun Set.buildRollupQuery(fieldNameMappingTypeMap: Map, oldQuery: QueryBuilder): QueryBuilder { val wrappedQueryBuilder = BoolQueryBuilder() - wrappedQueryBuilder.must(this.rewriteQueryBuilder(oldQuery, fieldNameMappingTypeMap)) - wrappedQueryBuilder.filter(TermQueryBuilder("rollup._id", this.id)) + wrappedQueryBuilder.must(this.first().rewriteQueryBuilder(oldQuery, fieldNameMappingTypeMap)) + wrappedQueryBuilder.should(TermsQueryBuilder("rollup._id", this.map { it.id })) + wrappedQueryBuilder.minimumShouldMatch(1) return wrappedQueryBuilder } @@ -405,9 +408,10 @@ fun Rollup.populateFieldMappings(): Set { // TODO: Not a fan of this.. but I can't find a way to overwrite the aggregations on the shallow copy or original // so we need to instantiate a new one so we can add the rewritten aggregation builders @Suppress("ComplexMethod") -fun SearchSourceBuilder.rewriteSearchSourceBuilder(job: Rollup, fieldNameMappingTypeMap: Map): SearchSourceBuilder { +fun SearchSourceBuilder.rewriteSearchSourceBuilder(jobs: Set, fieldNameMappingTypeMap: Map): SearchSourceBuilder { val ssb = SearchSourceBuilder() - this.aggregations()?.aggregatorFactories?.forEach { ssb.aggregation(job.rewriteAggregationBuilder(it)) } + // can use first() here as all jobs in the set will have a superset of the query's terms + this.aggregations()?.aggregatorFactories?.forEach { ssb.aggregation(jobs.first().rewriteAggregationBuilder(it)) } if (this.explain() != null) ssb.explain(this.explain()) if (this.ext() != null) ssb.ext(this.ext()) ssb.fetchSource(this.fetchSource()) @@ -419,7 +423,7 @@ fun SearchSourceBuilder.rewriteSearchSourceBuilder(job: Rollup, fieldNameMapping if (this.minScore() != null) ssb.minScore(this.minScore()) if (this.postFilter() != null) ssb.postFilter(this.postFilter()) ssb.profile(this.profile()) - if (this.query() != null) ssb.query(job.buildRollupQuery(fieldNameMappingTypeMap, this.query())) + if (this.query() != null) ssb.query(jobs.buildRollupQuery(fieldNameMappingTypeMap, this.query())) this.rescores()?.forEach { ssb.addRescorer(it) } this.scriptFields()?.forEach { ssb.scriptField(it.fieldName(), it.script(), it.ignoreFailure()) } if (this.searchAfter() != null) ssb.searchAfter(this.searchAfter()) @@ -438,9 +442,22 @@ fun SearchSourceBuilder.rewriteSearchSourceBuilder(job: Rollup, fieldNameMapping return ssb } +fun SearchSourceBuilder.rewriteSearchSourceBuilder(job: Rollup, fieldNameMappingTypeMap: Map): SearchSourceBuilder { + return this.rewriteSearchSourceBuilder(setOf(job), fieldNameMappingTypeMap) +} + fun Rollup.getInitialDocValues(docCount: Long): MutableMap = mutableMapOf( Rollup.ROLLUP_DOC_ID_FIELD to this.id, Rollup.ROLLUP_DOC_COUNT_FIELD to docCount, Rollup.ROLLUP_DOC_SCHEMA_VERSION_FIELD to this.schemaVersion ) + +fun parseRollup(response: GetResponse, xContentRegistry: NamedXContentRegistry = NamedXContentRegistry.EMPTY): Rollup { + val xcp = XContentHelper.createParser( + xContentRegistry, LoggingDeprecationHandler.INSTANCE, + response.sourceAsBytesRef, XContentType.JSON + ) + + return xcp.parseWithType(response.id, response.seqNo, response.primaryTerm, Rollup.Companion::parse) +} diff --git a/src/main/kotlin/org/opensearch/indexmanagement/settings/IndexManagementSettings.kt b/src/main/kotlin/org/opensearch/indexmanagement/settings/IndexManagementSettings.kt new file mode 100644 index 000000000..8d5775582 --- /dev/null +++ b/src/main/kotlin/org/opensearch/indexmanagement/settings/IndexManagementSettings.kt @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.indexmanagement.settings + +import org.opensearch.common.settings.Setting + +class IndexManagementSettings { + + companion object { + + val FILTER_BY_BACKEND_ROLES: Setting = Setting.boolSetting( + "plugins.index_management.filter_by_backend_roles", + false, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ) + } +} diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformIndexer.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformIndexer.kt index 239af8a94..0b702865f 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformIndexer.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformIndexer.kt @@ -13,10 +13,12 @@ package org.opensearch.indexmanagement.transform import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.action.DocWriteRequest import org.opensearch.action.admin.indices.create.CreateIndexRequest import org.opensearch.action.admin.indices.create.CreateIndexResponse import org.opensearch.action.bulk.BackoffPolicy +import org.opensearch.action.bulk.BulkItemResponse import org.opensearch.action.bulk.BulkRequest import org.opensearch.action.bulk.BulkResponse import org.opensearch.action.index.IndexRequest @@ -33,6 +35,7 @@ import org.opensearch.indexmanagement.util._DOC import org.opensearch.rest.RestStatus import org.opensearch.transport.RemoteTransportException +@Suppress("ComplexMethod") class TransformIndexer( settings: Settings, private val clusterService: ClusterService, @@ -65,7 +68,7 @@ class TransformIndexer( val response: CreateIndexResponse = client.admin().indices().suspendUntil { create(request, it) } if (!response.isAcknowledged) { logger.error("Failed to create the target index $index") - throw Exception() + throw TransformIndexException("Failed to create the target index") } } } @@ -74,6 +77,7 @@ class TransformIndexer( suspend fun index(docsToIndex: List>): Long { var updatableDocsToIndex = docsToIndex var indexTimeInMillis = 0L + var nonRetryableFailures = mutableListOf() try { if (updatableDocsToIndex.isNotEmpty()) { val targetIndex = updatableDocsToIndex.first().index() @@ -83,22 +87,34 @@ class TransformIndexer( val bulkRequest = BulkRequest().add(updatableDocsToIndex) val bulkResponse: BulkResponse = client.suspendUntil { bulk(bulkRequest, it) } indexTimeInMillis += bulkResponse.took.millis - - val failed = (bulkResponse.items ?: arrayOf()).filter { item -> item.isFailed } - - updatableDocsToIndex = failed.map { itemResponse -> - updatableDocsToIndex[itemResponse.itemId] as IndexRequest + val retryableFailures = mutableListOf() + (bulkResponse.items ?: arrayOf()).filter { it.isFailed }.forEach { failedResponse -> + if (failedResponse.status() == RestStatus.TOO_MANY_REQUESTS) { + retryableFailures.add(failedResponse) + } else { + nonRetryableFailures.add(failedResponse) + } + } + updatableDocsToIndex = retryableFailures.map { failure -> + updatableDocsToIndex[failure.itemId] as IndexRequest } if (updatableDocsToIndex.isNotEmpty()) { - val retryCause = failed.first().failure.cause - throw ExceptionsHelper.convertToOpenSearchException(retryCause) + throw ExceptionsHelper.convertToOpenSearchException(retryableFailures.first().failure.cause) } } } + if (nonRetryableFailures.isNotEmpty()) { + logger.error("Failed to index ${nonRetryableFailures.size} documents") + throw ExceptionsHelper.convertToOpenSearchException(nonRetryableFailures.first().failure.cause) + } return indexTimeInMillis + } catch (e: TransformIndexException) { + throw e } catch (e: RemoteTransportException) { val unwrappedException = ExceptionsHelper.unwrapCause(e) as Exception throw TransformIndexException("Failed to index the documents", unwrappedException) + } catch (e: OpenSearchSecurityException) { + throw TransformIndexException("Failed to index the documents - missing required index permissions: ${e.localizedMessage}", e) } catch (e: Exception) { throw TransformIndexException("Failed to index the documents", e) } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformRunner.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformRunner.kt index f54fce14f..f4ba7f400 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformRunner.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformRunner.kt @@ -25,7 +25,9 @@ import org.opensearch.cluster.service.ClusterService import org.opensearch.common.settings.Settings import org.opensearch.common.unit.TimeValue import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.indexmanagement.opensearchapi.IndexManagementSecurityContext import org.opensearch.indexmanagement.opensearchapi.suspendUntil +import org.opensearch.indexmanagement.opensearchapi.withClosableContext import org.opensearch.indexmanagement.transform.action.index.IndexTransformAction import org.opensearch.indexmanagement.transform.action.index.IndexTransformRequest import org.opensearch.indexmanagement.transform.action.index.IndexTransformResponse @@ -39,6 +41,7 @@ import org.opensearch.jobscheduler.spi.JobExecutionContext import org.opensearch.jobscheduler.spi.ScheduledJobParameter import org.opensearch.jobscheduler.spi.ScheduledJobRunner import org.opensearch.monitor.jvm.JvmService +import org.opensearch.threadpool.ThreadPool import java.time.Instant @Suppress("LongParameterList") @@ -56,6 +59,7 @@ object TransformRunner : private lateinit var transformSearchService: TransformSearchService private lateinit var transformIndexer: TransformIndexer private lateinit var transformValidator: TransformValidator + private lateinit var threadPool: ThreadPool fun initialize( client: Client, @@ -63,7 +67,8 @@ object TransformRunner : xContentRegistry: NamedXContentRegistry, settings: Settings, indexNameExpressionResolver: IndexNameExpressionResolver, - jvmService: JvmService + jvmService: JvmService, + threadPool: ThreadPool ): TransformRunner { this.clusterService = clusterService this.client = client @@ -73,6 +78,7 @@ object TransformRunner : this.transformMetadataService = TransformMetadataService(client, xContentRegistry) this.transformIndexer = TransformIndexer(settings, clusterService, client) this.transformValidator = TransformValidator(indexNameExpressionResolver, clusterService, client, settings, jvmService) + this.threadPool = threadPool return this } @@ -131,7 +137,7 @@ object TransformRunner : } } while (currentMetadata.afterKey != null) } catch (e: Exception) { - logger.error("Failed to execute the transform job because of exception [${e.localizedMessage}]", e) + logger.error("Failed to execute the transform job [${transform.id}] because of exception [${e.localizedMessage}]", e) currentMetadata = currentMetadata.copy( lastUpdatedAt = Instant.now(), status = TransformMetadata.Status.FAILED, @@ -148,10 +154,22 @@ object TransformRunner : } private suspend fun executeJobIteration(transform: Transform, metadata: TransformMetadata): TransformMetadata { - val validationResult = transformValidator.validate(transform) + val validationResult = withClosableContext( + IndexManagementSecurityContext(transform.id, settings, threadPool.threadContext, transform.user) + ) { + transformValidator.validate(transform) + } if (validationResult.isValid) { - val transformSearchResult = transformSearchService.executeCompositeSearch(transform, metadata.afterKey) - val indexTimeInMillis = transformIndexer.index(transformSearchResult.docsToIndex) + val transformSearchResult = withClosableContext( + IndexManagementSecurityContext(transform.id, settings, threadPool.threadContext, transform.user) + ) { + transformSearchService.executeCompositeSearch(transform, metadata.afterKey) + } + val indexTimeInMillis = withClosableContext( + IndexManagementSecurityContext(transform.id, settings, threadPool.threadContext, transform.user) + ) { + transformIndexer.index(transformSearchResult.docsToIndex) + } val afterKey = transformSearchResult.afterKey val stats = transformSearchResult.stats val updatedStats = stats.copy( @@ -175,10 +193,16 @@ object TransformRunner : transform = transform.copy(updatedAt = Instant.now()), refreshPolicy = WriteRequest.RefreshPolicy.IMMEDIATE ) - val response: IndexTransformResponse = client.suspendUntil { execute(IndexTransformAction.INSTANCE, request, it) } - return transform.copy( - seqNo = response.seqNo, - primaryTerm = response.primaryTerm - ) + return withClosableContext( + IndexManagementSecurityContext(transform.id, settings, threadPool.threadContext, null) + ) { + val response: IndexTransformResponse = client.suspendUntil { + execute(IndexTransformAction.INSTANCE, request, it) + } + return@withClosableContext transform.copy( + seqNo = response.seqNo, + primaryTerm = response.primaryTerm + ) + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformSearchService.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformSearchService.kt index 8b054845a..7145e3f0d 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformSearchService.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformSearchService.kt @@ -13,6 +13,7 @@ package org.opensearch.indexmanagement.transform import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.action.ActionListener import org.opensearch.action.bulk.BackoffPolicy import org.opensearch.action.index.IndexRequest @@ -70,7 +71,7 @@ class TransformSearchService( } suspend fun executeCompositeSearch(transform: Transform, afterKey: Map? = null): TransformSearchResult { - val errorMessage = "Failed to search data in source indices in transform job ${transform.id}" + val errorMessage = "Failed to search data in source indices" try { var retryAttempt = 0 val searchResponse = backoffPolicy.retry(logger) { @@ -90,14 +91,13 @@ class TransformSearchService( } return convertResponse(transform, searchResponse) } catch (e: TransformSearchServiceException) { - logger.error(errorMessage) throw e } catch (e: RemoteTransportException) { val unwrappedException = ExceptionsHelper.unwrapCause(e) as Exception - logger.error(errorMessage, unwrappedException) throw TransformSearchServiceException(errorMessage, unwrappedException) + } catch (e: OpenSearchSecurityException) { + throw TransformSearchServiceException("$errorMessage - missing required index permissions: ${e.localizedMessage}", e) } catch (e: Exception) { - logger.error(errorMessage, e) throw TransformSearchServiceException(errorMessage, e) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformValidator.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformValidator.kt index 99da72619..24b9bc728 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformValidator.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/TransformValidator.kt @@ -11,8 +11,8 @@ package org.opensearch.indexmanagement.transform -import org.apache.logging.log4j.LogManager import org.opensearch.ExceptionsHelper +import org.opensearch.OpenSearchSecurityException import org.opensearch.action.admin.cluster.health.ClusterHealthAction import org.opensearch.action.admin.cluster.health.ClusterHealthRequest import org.opensearch.action.admin.cluster.health.ClusterHealthResponse @@ -41,8 +41,6 @@ class TransformValidator( private val jvmService: JvmService ) { - private val logger = LogManager.getLogger(javaClass) - @Volatile private var circuitBreakerEnabled = TransformSettings.TRANSFORM_CIRCUIT_BREAKER_ENABLED.get(settings) @Volatile private var circuitBreakerJvmThreshold = TransformSettings.TRANSFORM_CIRCUIT_BREAKER_JVM_THRESHOLD.get(settings) @@ -86,8 +84,9 @@ class TransformValidator( } catch (e: RemoteTransportException) { val unwrappedException = ExceptionsHelper.unwrapCause(e) as Exception throw TransformValidationException(errorMessage, unwrappedException) + } catch (e: OpenSearchSecurityException) { + throw TransformValidationException("$errorMessage - missing required index permissions: ${e.localizedMessage}") } catch (e: Exception) { - logger.error("Failed to validate transform [${transform.id}]", e) throw TransformValidationException(errorMessage, e) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/DeleteTransformsAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/DeleteTransformsAction.kt index 8671c3e39..3d2f4a50b 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/DeleteTransformsAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/DeleteTransformsAction.kt @@ -17,6 +17,6 @@ import org.opensearch.action.bulk.BulkResponse class DeleteTransformsAction private constructor() : ActionType(NAME, ::BulkResponse) { companion object { val INSTANCE = DeleteTransformsAction() - val NAME = "cluster:admin/opendistro/transform/delete" + const val NAME = "cluster:admin/opendistro/transform/delete" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/TransportDeleteTransformsAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/TransportDeleteTransformsAction.kt index 4d6fa1904..73a6dd326 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/TransportDeleteTransformsAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/delete/TransportDeleteTransformsAction.kt @@ -11,6 +11,7 @@ package org.opensearch.indexmanagement.transform.action.delete +import org.apache.logging.log4j.LogManager import org.opensearch.OpenSearchStatusException import org.opensearch.action.ActionListener import org.opensearch.action.bulk.BulkRequest @@ -22,110 +23,157 @@ import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.action.support.WriteRequest import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.model.Transform +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource import org.opensearch.rest.RestStatus import org.opensearch.search.fetch.subphase.FetchSourceContext import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +@Suppress("ReturnCount") class TransportDeleteTransformsAction @Inject constructor( transportService: TransportService, val client: Client, + val settings: Settings, + val clusterService: ClusterService, + val xContentRegistry: NamedXContentRegistry, actionFilters: ActionFilters ) : HandledTransportAction( DeleteTransformsAction.NAME, transportService, actionFilters, ::DeleteTransformsRequest ) { + private val log = LogManager.getLogger(javaClass) + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: DeleteTransformsRequest, actionListener: ActionListener) { // TODO: if metadata id exists delete the metadata doc else just delete transform - // Use Multi-Get Request - val getRequest = MultiGetRequest() - val includes = arrayOf( - "${Transform.TRANSFORM_TYPE}.${Transform.ENABLED_FIELD}" - ) - val fetchSourceContext = FetchSourceContext(true, includes, emptyArray()) - request.ids.forEach { id -> - getRequest.add(MultiGetRequest.Item(INDEX_MANAGEMENT_INDEX, id).fetchSourceContext(fetchSourceContext)) + DeleteTransformHandler(client, request, actionListener).start() + } + + inner class DeleteTransformHandler( + val client: Client, + val request: DeleteTransformsRequest, + val actionListener: ActionListener, + val user: User? = buildUser(client.threadPool().threadContext) + ) { + + fun start() { + // Use Multi-Get Request + val getRequest = MultiGetRequest() + val fetchSourceContext = FetchSourceContext(true) + request.ids.forEach { id -> + getRequest.add(MultiGetRequest.Item(INDEX_MANAGEMENT_INDEX, id).fetchSourceContext(fetchSourceContext)) + } + + client.threadPool().threadContext.stashContext().use { + client.multiGet( + getRequest, + object : ActionListener { + override fun onResponse(response: MultiGetResponse) { + try { + // response is failed only if managed index is not present + if (response.responses.first().isFailed) { + actionListener.onFailure( + OpenSearchStatusException( + "Cluster missing system index $INDEX_MANAGEMENT_INDEX, cannot execute the request", RestStatus.BAD_REQUEST + ) + ) + return + } + + bulkDelete(response, request.ids, request.force, actionListener) + } catch (e: Exception) { + actionListener.onFailure(e) + } + } + + override fun onFailure(e: Exception) = actionListener.onFailure(e) + } + ) + } } - client.multiGet( - getRequest, - object : ActionListener { - override fun onResponse(response: MultiGetResponse) { + private fun bulkDelete(response: MultiGetResponse, ids: List, forceDelete: Boolean, actionListener: ActionListener) { + val enabledIDs = mutableListOf() + val notTransform = mutableListOf() + val noPermission = mutableListOf() + + response.responses.forEach { + if (it.response.isExists) { try { - // response is failed only if managed index is not present - if (response.responses.first().isFailed) { - actionListener.onFailure( - OpenSearchStatusException( - "Cluster missing system index $INDEX_MANAGEMENT_INDEX, cannot execute the request", RestStatus.BAD_REQUEST - ) - ) - return + val transform = parseFromGetResponse(it.response, xContentRegistry, Transform.Companion::parse) + val enabled = transform.enabled + if (enabled && !forceDelete) { + enabledIDs.add(it.id) + } + if (!userHasPermissionForResource(user, transform.user, filterByEnabled)) { + noPermission.add(it.id) } - - bulkDelete(response, request.ids, request.force, actionListener) } catch (e: Exception) { - actionListener.onFailure(e) + // if cannot parse considering not a transform + notTransform.add(it.id) } } - - override fun onFailure(e: Exception) = actionListener.onFailure(e) } - ) - } - private fun bulkDelete(response: MultiGetResponse, ids: List, forceDelete: Boolean, actionListener: ActionListener) { - val enabledIDs = mutableListOf() - val notTransform = mutableListOf() - - response.responses.forEach { - if (it.response.isExists) { - val source = it.response.source - val enabled = (source["transform"] as Map<*, *>?)?.get("enabled") as Boolean? - if (enabled == null) { - notTransform.add(it.id) - } - if (enabled == true && !forceDelete) { - enabledIDs.add(it.id) - } + if (noPermission.isNotEmpty()) { + actionListener.onFailure( + OpenSearchStatusException( + "Don't have permission to delete some/all transforms in [${request.ids}]", RestStatus.FORBIDDEN + ) + ) + return } - } - if (notTransform.isNotEmpty()) { - actionListener.onFailure( - OpenSearchStatusException( - "$notTransform IDs are not transforms!", RestStatus.BAD_REQUEST + if (notTransform.isNotEmpty()) { + actionListener.onFailure( + OpenSearchStatusException( + "Cannot find transforms $notTransform", RestStatus.BAD_REQUEST + ) ) - ) - return - } + return + } - if (enabledIDs.isNotEmpty()) { - actionListener.onFailure( - OpenSearchStatusException( - "$enabledIDs transform(s) are enabled, please disable them before deleting them or set force flag", RestStatus.CONFLICT + if (enabledIDs.isNotEmpty()) { + actionListener.onFailure( + OpenSearchStatusException( + "$enabledIDs transform(s) are enabled, please disable them before deleting them or set force flag", RestStatus.CONFLICT + ) ) - ) - return - } + return + } - val bulkDeleteRequest = BulkRequest() - bulkDeleteRequest.refreshPolicy = WriteRequest.RefreshPolicy.IMMEDIATE - for (id in ids) { - bulkDeleteRequest.add(DeleteRequest(INDEX_MANAGEMENT_INDEX, id)) - } + val bulkDeleteRequest = BulkRequest() + bulkDeleteRequest.refreshPolicy = WriteRequest.RefreshPolicy.IMMEDIATE + for (id in ids) { + bulkDeleteRequest.add(DeleteRequest(INDEX_MANAGEMENT_INDEX, id)) + } - client.bulk( - bulkDeleteRequest, - object : ActionListener { - override fun onResponse(response: BulkResponse) { - actionListener.onResponse(response) - } + client.bulk( + bulkDeleteRequest, + object : ActionListener { + override fun onResponse(response: BulkResponse) { + actionListener.onResponse(response) + } - override fun onFailure(e: Exception) = actionListener.onFailure(e) - } - ) + override fun onFailure(e: Exception) = actionListener.onFailure(e) + } + ) + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/ExplainTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/ExplainTransformAction.kt index b24c721ef..f64b61018 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/ExplainTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/ExplainTransformAction.kt @@ -16,6 +16,6 @@ import org.opensearch.action.ActionType class ExplainTransformAction private constructor() : ActionType(NAME, ::ExplainTransformResponse) { companion object { val INSTANCE = ExplainTransformAction() - val NAME = "cluster:admin/opendistro/transform/explain" + const val NAME = "cluster:admin/opendistro/transform/explain" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/TransportExplainTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/TransportExplainTransformAction.kt index 7b8417cf8..9020e9ee1 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/TransportExplainTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/explain/TransportExplainTransformAction.kt @@ -20,8 +20,10 @@ import org.opensearch.action.search.SearchResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.bytes.BytesReference import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper @@ -32,9 +34,12 @@ import org.opensearch.index.query.IdsQueryBuilder import org.opensearch.index.query.WildcardQueryBuilder import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX import org.opensearch.indexmanagement.opensearchapi.parseWithType +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.model.ExplainTransform import org.opensearch.indexmanagement.transform.model.Transform import org.opensearch.indexmanagement.transform.model.TransformMetadata +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.addUserFilter +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.tasks.Task import org.opensearch.transport.RemoteTransportException @@ -44,11 +49,21 @@ class TransportExplainTransformAction @Inject constructor( transportService: TransportService, val client: Client, actionFilters: ActionFilters, + val clusterService: ClusterService, + val settings: Settings, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( ExplainTransformAction.NAME, transportService, actionFilters, ::ExplainTransformRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) @Suppress("SpreadOperator") @@ -57,78 +72,81 @@ class TransportExplainTransformAction @Inject constructor( // Instantiate concrete ids to metadata map by removing wildcard matches val idsToExplain: MutableMap = ids.filter { !it.contains("*") } .map { it to null }.toMap(mutableMapOf()) - val searchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX) - .source( - SearchSourceBuilder().seqNoAndPrimaryTerm(true).query( - BoolQueryBuilder().minimumShouldMatch(1).apply { - ids.forEach { - this.should(WildcardQueryBuilder("${ Transform.TRANSFORM_TYPE}.${Transform.TRANSFORM_ID_FIELD}.keyword", "*$it*")) - } - } - ) - ) - client.search( - searchRequest, - object : ActionListener { - override fun onResponse(response: SearchResponse) { - try { - response.hits.hits.forEach { - val transform = contentParser(it.sourceRef).parseWithType(it.id, it.seqNo, it.primaryTerm, Transform.Companion::parse) - idsToExplain[transform.id] = ExplainTransform(metadataID = transform.metadataId) + val queryBuilder = BoolQueryBuilder().minimumShouldMatch(1).apply { + ids.forEach { + this.should(WildcardQueryBuilder("${ Transform.TRANSFORM_TYPE}.${Transform.TRANSFORM_ID_FIELD}.keyword", "*$it*")) + } + } + val user = buildUser(client.threadPool().threadContext) + addUserFilter(user, queryBuilder, filterByEnabled, "transform.user") + + val searchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX).source(SearchSourceBuilder().seqNoAndPrimaryTerm(true).query(queryBuilder)) + + client.threadPool().threadContext.stashContext().use { + client.search( + searchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + try { + response.hits.hits.forEach { + val transform = contentParser(it.sourceRef).parseWithType(it.id, it.seqNo, it.primaryTerm, Transform.Companion::parse) + idsToExplain[transform.id] = ExplainTransform(metadataID = transform.metadataId) + } + } catch (e: Exception) { + log.error("Failed to parse explain response", e) + actionListener.onFailure(e) + return } - } catch (e: Exception) { - log.error("Failed to parse explain response", e) - actionListener.onFailure(e) - return - } - val metadataIds = idsToExplain.values.mapNotNull { it?.metadataID } - val metadataSearchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX) - .source(SearchSourceBuilder().query(IdsQueryBuilder().addIds(*metadataIds.toTypedArray()))) - client.search( - metadataSearchRequest, - object : ActionListener { - override fun onResponse(response: SearchResponse) { - try { - response.hits.hits.forEach { - val metadata = contentParser(it.sourceRef) - .parseWithType(it.id, it.seqNo, it.primaryTerm, TransformMetadata.Companion::parse) - idsToExplain.computeIfPresent(metadata.transformId) { _, explainTransform -> - explainTransform.copy(metadata = metadata) + val metadataIds = idsToExplain.values.mapNotNull { it?.metadataID } + val metadataSearchRequest = SearchRequest(INDEX_MANAGEMENT_INDEX) + .source(SearchSourceBuilder().query(IdsQueryBuilder().addIds(*metadataIds.toTypedArray()))) + client.search( + metadataSearchRequest, + object : ActionListener { + override fun onResponse(response: SearchResponse) { + try { + response.hits.hits.forEach { + val metadata = contentParser(it.sourceRef) + .parseWithType(it.id, it.seqNo, it.primaryTerm, TransformMetadata.Companion::parse) + idsToExplain.computeIfPresent(metadata.transformId) { _, explainTransform -> + explainTransform.copy(metadata = metadata) + } } + actionListener.onResponse(ExplainTransformResponse(idsToExplain.toMap())) + } catch (e: Exception) { + log.error("Failed to parse transform metadata", e) + actionListener.onFailure(e) + return } - actionListener.onResponse(ExplainTransformResponse(idsToExplain.toMap())) - } catch (e: Exception) { - log.error("Failed to parse transform metadata", e) - actionListener.onFailure(e) - return } - } - override fun onFailure(e: Exception) { - log.error("Failed to search transform metadata", e) - when (e) { - is RemoteTransportException -> actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as java.lang.Exception) - else -> actionListener.onFailure(e) + override fun onFailure(e: Exception) { + log.error("Failed to search transform metadata", e) + when (e) { + is RemoteTransportException -> + actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as java.lang.Exception) + else -> actionListener.onFailure(e) + } } } - } - ) - } + ) + } - override fun onFailure(e: Exception) { - log.error("Failed to search for transforms", e) - when (e) { - is ResourceNotFoundException -> { - val nonWildcardIds = ids.filter { !it.contains("*") }.map { it to null }.toMap(mutableMapOf()) - actionListener.onResponse(ExplainTransformResponse(nonWildcardIds)) + override fun onFailure(e: Exception) { + log.error("Failed to search for transforms", e) + when (e) { + is ResourceNotFoundException -> { + val nonWildcardIds = ids.filter { !it.contains("*") }.map { it to null }.toMap(mutableMapOf()) + actionListener.onResponse(ExplainTransformResponse(nonWildcardIds)) + } + is RemoteTransportException -> actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as java.lang.Exception) + else -> actionListener.onFailure(e) } - is RemoteTransportException -> actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as java.lang.Exception) - else -> actionListener.onFailure(e) } } - } - ) + ) + } } private fun contentParser(bytesReference: BytesReference): XContentParser { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformAction.kt index 4a6575fa9..90ffac372 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformAction.kt @@ -16,6 +16,6 @@ import org.opensearch.action.ActionType class GetTransformAction private constructor() : ActionType(NAME, ::GetTransformResponse) { companion object { val INSTANCE = GetTransformAction() - val NAME = "cluster:admin/opendistro/transform/get" + const val NAME = "cluster:admin/opendistro/transform/get" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformResponse.kt index 50c968c7a..81149a1e2 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformResponse.kt @@ -17,7 +17,7 @@ import org.opensearch.common.io.stream.StreamOutput import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.transform.model.Transform import org.opensearch.indexmanagement.transform.model.Transform.Companion.TRANSFORM_TYPE import org.opensearch.indexmanagement.util._ID @@ -66,7 +66,7 @@ class GetTransformResponse( .field(_VERSION, version) .field(_SEQ_NO, seqNo) .field(_PRIMARY_TERM, primaryTerm) - if (transform != null) builder.field(TRANSFORM_TYPE, transform, XCONTENT_WITHOUT_TYPE) + if (transform != null) builder.field(TRANSFORM_TYPE, transform, XCONTENT_WITHOUT_TYPE_AND_USER) return builder.endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsAction.kt index 11c8f3942..514187d0a 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsAction.kt @@ -16,6 +16,6 @@ import org.opensearch.action.ActionType class GetTransformsAction private constructor() : ActionType(NAME, ::GetTransformsResponse) { companion object { val INSTANCE = GetTransformsAction() - val NAME = "cluster:admin/opendistro/transform/get_transforms" + const val NAME = "cluster:admin/opendistro/transform/get_transforms" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsResponse.kt index 2b4d982f8..f47c6ba24 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/GetTransformsResponse.kt @@ -17,7 +17,7 @@ import org.opensearch.common.io.stream.StreamOutput import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.transform.model.Transform import org.opensearch.indexmanagement.transform.model.Transform.Companion.TRANSFORM_TYPE import org.opensearch.indexmanagement.util._ID @@ -55,7 +55,7 @@ class GetTransformsResponse( .field(_ID, transform.id) .field(_SEQ_NO, transform.seqNo) .field(_PRIMARY_TERM, transform.primaryTerm) - .field(TRANSFORM_TYPE, transform, XCONTENT_WITHOUT_TYPE) + .field(TRANSFORM_TYPE, transform, XCONTENT_WITHOUT_TYPE_AND_USER) .endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformAction.kt index 976daa890..227ba1ea5 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformAction.kt @@ -19,84 +19,88 @@ import org.opensearch.action.get.GetResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject -import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry -import org.opensearch.common.xcontent.XContentHelper -import org.opensearch.common.xcontent.XContentType import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX -import org.opensearch.indexmanagement.opensearchapi.parseWithType +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.model.Transform +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource import org.opensearch.rest.RestStatus -import org.opensearch.search.fetch.subphase.FetchSourceContext import org.opensearch.tasks.Task import org.opensearch.transport.TransportService class TransportGetTransformAction @Inject constructor( transportService: TransportService, val client: Client, + val settings: Settings, + val clusterService: ClusterService, actionFilters: ActionFilters, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction ( GetTransformAction.NAME, transportService, actionFilters, ::GetTransformRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + + @Suppress("ReturnCount") override fun doExecute(task: Task, request: GetTransformRequest, listener: ActionListener) { - val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.id) - .fetchSourceContext(request.srcContext).preference(request.preference) - client.get( - getRequest, - object : ActionListener { - override fun onResponse(response: GetResponse) { - if (!response.isExists) { - listener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) - return - } + val user = buildUser(client.threadPool().threadContext) + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.id).preference(request.preference) + client.threadPool().threadContext.stashContext().use { + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + listener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return + } - if (response.isSourceEmpty && getRequest.fetchSourceContext() != FetchSourceContext.DO_NOT_FETCH_SOURCE) { - listener.onFailure(OpenSearchStatusException("Missing transform data", RestStatus.INTERNAL_SERVER_ERROR)) - return - } else if (response.isSourceEmpty) { - // For HEAD requests only - listener.onResponse( - GetTransformResponse( - response.id, - response.version, - response.seqNo, - response.primaryTerm, - RestStatus.OK, - null - ) - ) - return - } + try { + val transform: Transform? + try { + transform = parseFromGetResponse(response, xContentRegistry, Transform.Companion::parse) + } catch (e: IllegalArgumentException) { + listener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, transform.user, filterByEnabled, "transform", request.id, listener)) { + return + } - try { - val contentParser = XContentHelper.createParser( - xContentRegistry, - LoggingDeprecationHandler.INSTANCE, response.sourceAsBytesRef, XContentType.JSON - ) - val transform = contentParser.parseWithType( - response.id, response.seqNo, - response.primaryTerm, Transform.Companion::parse - ) - listener.onResponse( - GetTransformResponse( - response.id, response.version, response.seqNo, - response.primaryTerm, RestStatus.OK, transform + // if HEAD request don't return the transform + val transformResponse = if (request.srcContext != null && !request.srcContext.fetchSource()) { + GetTransformResponse(response.id, response.version, response.seqNo, response.primaryTerm, RestStatus.OK, null) + } else { + GetTransformResponse(response.id, response.version, response.seqNo, response.primaryTerm, RestStatus.OK, transform) + } + listener.onResponse(transformResponse) + } catch (e: Exception) { + listener.onFailure( + OpenSearchStatusException( + "Failed to parse transform", + RestStatus.INTERNAL_SERVER_ERROR, + ExceptionsHelper.unwrapCause(e) + ) ) - ) - } catch (e: Exception) { - listener.onFailure( - OpenSearchStatusException("Failed to parse transform", RestStatus.INTERNAL_SERVER_ERROR, ExceptionsHelper.unwrapCause(e)) - ) + } } - } - override fun onFailure(e: Exception) { - listener.onFailure(e) + override fun onFailure(e: Exception) { + listener.onFailure(e) + } } - } - ) + ) + } } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformsAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformsAction.kt index 08c473d70..ed3cd6689 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformsAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/get/TransportGetTransformsAction.kt @@ -16,8 +16,10 @@ import org.opensearch.action.ActionResponse import org.opensearch.action.support.ActionFilters import org.opensearch.action.support.HandledTransportAction import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.bytes.BytesReference import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper @@ -26,7 +28,10 @@ import org.opensearch.common.xcontent.XContentType import org.opensearch.index.query.BoolQueryBuilder import org.opensearch.index.query.ExistsQueryBuilder import org.opensearch.index.query.WildcardQueryBuilder +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.model.Transform +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.addUserFilter +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser import org.opensearch.indexmanagement.util.getJobs import org.opensearch.search.builder.SearchSourceBuilder import org.opensearch.search.sort.SortOrder @@ -36,12 +41,22 @@ import org.opensearch.transport.TransportService class TransportGetTransformsAction @Inject constructor( transportService: TransportService, val client: Client, + val settings: Settings, + val clusterService: ClusterService, actionFilters: ActionFilters, val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction ( GetTransformsAction.NAME, transportService, actionFilters, ::GetTransformsRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + override fun doExecute(task: Task, request: GetTransformsRequest, listener: ActionListener) { val searchString = request.searchString.trim() val from = request.from @@ -53,16 +68,20 @@ class TransportGetTransformsAction @Inject constructor( if (searchString.isNotEmpty()) { boolQueryBuilder.filter(WildcardQueryBuilder("${Transform.TRANSFORM_TYPE}.${Transform.TRANSFORM_ID_FIELD}.keyword", "*$searchString*")) } + val user = buildUser(client.threadPool().threadContext) + addUserFilter(user, boolQueryBuilder, filterByEnabled, "transform.user") val searchSourceBuilder = SearchSourceBuilder().query(boolQueryBuilder).from(from).size(size).seqNoAndPrimaryTerm(true) .sort(sortField, SortOrder.fromString(sortDirection)) - getJobs( - client, - searchSourceBuilder, - listener as ActionListener, - Transform.TRANSFORM_TYPE, - ::contentParser - ) + client.threadPool().threadContext.stashContext().use { + getJobs( + client, + searchSourceBuilder, + listener as ActionListener, + Transform.TRANSFORM_TYPE, + ::contentParser + ) + } } private fun contentParser(bytesReference: BytesReference): XContentParser { diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformAction.kt index e66e4744b..64228d055 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformAction.kt @@ -16,6 +16,6 @@ import org.opensearch.action.ActionType class IndexTransformAction private constructor() : ActionType(NAME, ::IndexTransformResponse) { companion object { val INSTANCE = IndexTransformAction() - val NAME = "cluster:admin/opendistro/transform/index" + const val NAME = "cluster:admin/opendistro/transform/index" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformResponse.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformResponse.kt index 456efb40f..996f8f172 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformResponse.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/IndexTransformResponse.kt @@ -17,7 +17,7 @@ import org.opensearch.common.io.stream.StreamOutput import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.ToXContentObject import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.XCONTENT_WITHOUT_TYPE_AND_USER import org.opensearch.indexmanagement.transform.model.Transform import org.opensearch.indexmanagement.transform.model.Transform.Companion.TRANSFORM_TYPE import org.opensearch.indexmanagement.util._ID @@ -63,7 +63,7 @@ class IndexTransformResponse( .field(_VERSION, version) .field(_SEQ_NO, seqNo) .field(_PRIMARY_TERM, primaryTerm) - .field(TRANSFORM_TYPE, transform, XCONTENT_WITHOUT_TYPE) + .field(TRANSFORM_TYPE, transform, XCONTENT_WITHOUT_TYPE_AND_USER) .endObject() } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/TransportIndexTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/TransportIndexTransformAction.kt index 204028d8a..c8829441f 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/TransportIndexTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/index/TransportIndexTransformAction.kt @@ -18,6 +18,8 @@ import org.opensearch.action.DocWriteRequest import org.opensearch.action.admin.indices.mapping.get.GetMappingsAction import org.opensearch.action.admin.indices.mapping.get.GetMappingsRequest import org.opensearch.action.admin.indices.mapping.get.GetMappingsResponse +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse import org.opensearch.action.index.IndexRequest import org.opensearch.action.index.IndexResponse import org.opensearch.action.support.ActionFilters @@ -28,31 +30,47 @@ import org.opensearch.client.Client import org.opensearch.cluster.metadata.IndexNameExpressionResolver import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.XContentFactory.jsonBuilder +import org.opensearch.commons.authuser.User import org.opensearch.indexmanagement.IndexManagementIndices import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.TransformValidator -import org.opensearch.indexmanagement.transform.action.get.GetTransformAction -import org.opensearch.indexmanagement.transform.action.get.GetTransformRequest -import org.opensearch.indexmanagement.transform.action.get.GetTransformResponse import org.opensearch.indexmanagement.transform.model.Transform import org.opensearch.indexmanagement.util.IndexUtils +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.validateUserConfiguration import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService +@Suppress("SpreadOperator") class TransportIndexTransformAction @Inject constructor( transportService: TransportService, val client: Client, actionFilters: ActionFilters, val indexManagementIndices: IndexManagementIndices, val indexNameExpressionResolver: IndexNameExpressionResolver, - val clusterService: ClusterService + val clusterService: ClusterService, + val settings: Settings, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( IndexTransformAction.NAME, transportService, actionFilters, ::IndexTransformRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) override fun doExecute(task: Task, request: IndexTransformRequest, listener: ActionListener) { @@ -62,11 +80,19 @@ class TransportIndexTransformAction @Inject constructor( inner class IndexTransformHandler( private val client: Client, private val actionListener: ActionListener, - private val request: IndexTransformRequest + private val request: IndexTransformRequest, + private val user: User? = buildUser(client.threadPool().threadContext, request.transform.user) ) { fun start() { - indexManagementIndices.checkAndUpdateIMConfigIndex(ActionListener.wrap(::onConfigIndexAcknowledgedResponse, actionListener::onFailure)) + client.threadPool().threadContext.stashContext().use { + if (!validateUserConfiguration(user, filterByEnabled, actionListener)) { + return + } + indexManagementIndices.checkAndUpdateIMConfigIndex( + ActionListener.wrap(::onConfigIndexAcknowledgedResponse, actionListener::onFailure) + ) + } } private fun onConfigIndexAcknowledgedResponse(response: AcknowledgedResponse) { @@ -85,17 +111,27 @@ class TransportIndexTransformAction @Inject constructor( } private fun updateTransform() { - val getReq = GetTransformRequest(request.transform.id, null, null) - client.execute(GetTransformAction.INSTANCE, getReq, ActionListener.wrap(::onGetTransform, actionListener::onFailure)) + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.transform.id) + client.get(getRequest, ActionListener.wrap(::onGetTransform, actionListener::onFailure)) } @Suppress("ReturnCount") - private fun onGetTransform(response: GetTransformResponse) { - if (response.status != RestStatus.OK) { - return actionListener.onFailure(OpenSearchStatusException("Unable to get existing transform", response.status)) + private fun onGetTransform(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return + } + + val transform: Transform? + try { + transform = parseFromGetResponse(response, xContentRegistry, Transform.Companion::parse) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return + } + if (!userHasPermissionForResource(user, transform.user, filterByEnabled, "transform", transform.id, actionListener)) { + return } - val transform = response.transform - ?: return actionListener.onFailure(OpenSearchStatusException("The current transform is null", RestStatus.INTERNAL_SERVER_ERROR)) val modified = modifiedImmutableProperties(transform, request.transform) if (modified.isNotEmpty()) { return actionListener.onFailure(OpenSearchStatusException("Not allowed to modify $modified", RestStatus.BAD_REQUEST)) @@ -115,7 +151,7 @@ class TransportIndexTransformAction @Inject constructor( } private fun putTransform() { - val transform = request.transform.copy(schemaVersion = IndexUtils.indexManagementConfigSchemaVersion) + val transform = request.transform.copy(schemaVersion = IndexUtils.indexManagementConfigSchemaVersion, user = this.user) request.index(INDEX_MANAGEMENT_INDEX) .id(request.transform.id) .source(transform.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) @@ -164,7 +200,7 @@ class TransportIndexTransformAction @Inject constructor( override fun onResponse(response: GetMappingsResponse) { val issues = validateMappings(concreteIndices.toList(), response, request.transform) if (issues.isNotEmpty()) { - val errorMessage = "${issues.joinToString(" ")}" + val errorMessage = issues.joinToString(" ") actionListener.onFailure(OpenSearchStatusException(errorMessage, RestStatus.BAD_REQUEST)) return } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/PreviewTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/PreviewTransformAction.kt index 3b567034f..71334178d 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/PreviewTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/PreviewTransformAction.kt @@ -16,6 +16,6 @@ import org.opensearch.action.ActionType class PreviewTransformAction private constructor() : ActionType(NAME, ::PreviewTransformResponse) { companion object { val INSTANCE = PreviewTransformAction() - val NAME = "cluster:admin/opendistro/transform/preview" + const val NAME = "cluster:admin/opendistro/transform/preview" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/TransportPreviewTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/TransportPreviewTransformAction.kt index 7b35e9108..b049b85db 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/TransportPreviewTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/preview/TransportPreviewTransformAction.kt @@ -43,6 +43,7 @@ class TransportPreviewTransformAction @Inject constructor( PreviewTransformAction.NAME, transportService, actionFilters, ::PreviewTransformRequest ) { + @Suppress("SpreadOperator") override fun doExecute(task: Task, request: PreviewTransformRequest, listener: ActionListener) { val transform = request.transform @@ -60,7 +61,7 @@ class TransportPreviewTransformAction @Inject constructor( override fun onResponse(response: GetMappingsResponse) { val issues = validateMappings(concreteIndices.toList(), response, transform) if (issues.isNotEmpty()) { - val errorMessage = "${issues.joinToString(" ")}" + val errorMessage = issues.joinToString(" ") listener.onFailure(OpenSearchStatusException(errorMessage, RestStatus.BAD_REQUEST)) return } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/StartTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/StartTransformAction.kt index 8576cc5bb..5b6a1ef5e 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/StartTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/StartTransformAction.kt @@ -17,6 +17,6 @@ import org.opensearch.action.support.master.AcknowledgedResponse class StartTransformAction private constructor() : ActionType(NAME, ::AcknowledgedResponse) { companion object { val INSTANCE = StartTransformAction() - val NAME = "cluster:admin/opendistro/transform/start" + const val NAME = "cluster:admin/opendistro/transform/start" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/TransportStartTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/TransportStartTransformAction.kt index 47b12b6e1..ec12967f0 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/TransportStartTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/start/TransportStartTransformAction.kt @@ -24,62 +24,90 @@ import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.update.UpdateRequest import org.opensearch.action.update.UpdateResponse import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse import org.opensearch.indexmanagement.opensearchapi.parseWithType -import org.opensearch.indexmanagement.transform.action.get.GetTransformAction -import org.opensearch.indexmanagement.transform.action.get.GetTransformRequest -import org.opensearch.indexmanagement.transform.action.get.GetTransformResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.model.Transform import org.opensearch.indexmanagement.transform.model.TransformMetadata +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService import java.time.Instant +@Suppress("ReturnCount") class TransportStartTransformAction @Inject constructor( transportService: TransportService, val client: Client, - actionFilters: ActionFilters + val settings: Settings, + val clusterService: ClusterService, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( StartTransformAction.NAME, transportService, actionFilters, ::StartTransformRequest ) { + + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) override fun doExecute(task: Task, request: StartTransformRequest, actionListener: ActionListener) { - val getReq = GetTransformRequest(request.id()) - client.execute( - GetTransformAction.INSTANCE, getReq, - object : ActionListener { - override fun onResponse(response: GetTransformResponse) { - val transform = response.transform - if (transform == null) { - return actionListener.onFailure( - OpenSearchStatusException("Could not find transform [${request.id()}]", RestStatus.NOT_FOUND) - ) - } + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.id()) + val user = buildUser(client.threadPool().threadContext) + client.threadPool().threadContext.stashContext().use { + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return + } - if (transform.enabled) { - log.debug("Transform job is already enabled, checking if metadata needs to be updated") - return if (transform.metadataId == null) { - actionListener.onResponse(AcknowledgedResponse(true)) - } else { - retrieveAndUpdateTransformMetadata(transform, actionListener) + val transform: Transform? + try { + transform = parseFromGetResponse(response, xContentRegistry, Transform.Companion::parse) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return } - } - updateTransformJob(transform, request, actionListener) - } + if (!userHasPermissionForResource(user, transform.user, filterByEnabled, "transform", transform.id, actionListener)) { + return + } + if (transform.enabled) { + log.debug("Transform job is already enabled, checking if metadata needs to be updated") + return if (transform.metadataId == null) { + actionListener.onResponse(AcknowledgedResponse(true)) + } else { + retrieveAndUpdateTransformMetadata(transform, actionListener) + } + } - override fun onFailure(e: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + updateTransformJob(transform, request, actionListener) + } + + override fun onFailure(e: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + } } - } - ) + ) + } } private fun updateTransformJob( diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/StopTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/StopTransformAction.kt index 22c8e9976..dce46ebff 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/StopTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/StopTransformAction.kt @@ -17,6 +17,6 @@ import org.opensearch.action.support.master.AcknowledgedResponse class StopTransformAction private constructor() : ActionType(NAME, ::AcknowledgedResponse) { companion object { val INSTANCE = StopTransformAction() - val NAME = "cluster:admin/opendistro/transform/stop" + const val NAME = "cluster:admin/opendistro/transform/stop" } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/TransportStopTransformAction.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/TransportStopTransformAction.kt index a12ab3c06..483282b8a 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/TransportStopTransformAction.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/action/stop/TransportStopTransformAction.kt @@ -24,18 +24,22 @@ import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.action.update.UpdateRequest import org.opensearch.action.update.UpdateResponse import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType import org.opensearch.indexmanagement.IndexManagementPlugin +import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX +import org.opensearch.indexmanagement.opensearchapi.parseFromGetResponse import org.opensearch.indexmanagement.opensearchapi.parseWithType -import org.opensearch.indexmanagement.transform.action.get.GetTransformAction -import org.opensearch.indexmanagement.transform.action.get.GetTransformRequest -import org.opensearch.indexmanagement.transform.action.get.GetTransformResponse +import org.opensearch.indexmanagement.settings.IndexManagementSettings import org.opensearch.indexmanagement.transform.model.Transform import org.opensearch.indexmanagement.transform.model.TransformMetadata +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.buildUser +import org.opensearch.indexmanagement.util.SecurityUtils.Companion.userHasPermissionForResource import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService @@ -57,39 +61,62 @@ import java.time.Instant class TransportStopTransformAction @Inject constructor( transportService: TransportService, val client: Client, - actionFilters: ActionFilters + val settings: Settings, + val clusterService: ClusterService, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( StopTransformAction.NAME, transportService, actionFilters, ::StopTransformRequest ) { + @Volatile private var filterByEnabled = IndexManagementSettings.FILTER_BY_BACKEND_ROLES.get(settings) + + init { + clusterService.clusterSettings.addSettingsUpdateConsumer(IndexManagementSettings.FILTER_BY_BACKEND_ROLES) { + filterByEnabled = it + } + } + private val log = LogManager.getLogger(javaClass) override fun doExecute(task: Task, request: StopTransformRequest, actionListener: ActionListener) { log.debug("Executing StopTransformAction on ${request.id()}") - val getReq = GetTransformRequest(request.id(), null) - client.execute( - GetTransformAction.INSTANCE, getReq, - object : ActionListener { - override fun onResponse(response: GetTransformResponse) { - val transform = response.transform - if (transform == null) { - return actionListener.onFailure( - OpenSearchStatusException("Could not find transform [${request.id()}]", RestStatus.NOT_FOUND) - ) - } + val getRequest = GetRequest(INDEX_MANAGEMENT_INDEX, request.id()) + val user = buildUser(client.threadPool().threadContext) + client.threadPool().threadContext.stashContext().use { + client.get( + getRequest, + object : ActionListener { + override fun onResponse(response: GetResponse) { + if (!response.isExists) { + actionListener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return + } - if (transform.metadataId != null) { - retrieveAndUpdateTransformMetadata(transform, request, actionListener) - } else { - updateTransformJob(transform, request, actionListener) + val transform: Transform? + try { + transform = parseFromGetResponse(response, xContentRegistry, Transform.Companion::parse) + } catch (e: IllegalArgumentException) { + actionListener.onFailure(OpenSearchStatusException("Transform not found", RestStatus.NOT_FOUND)) + return + } + + if (!userHasPermissionForResource(user, transform.user, filterByEnabled, "transform", transform.id, actionListener)) { + return + } + if (transform.metadataId != null) { + retrieveAndUpdateTransformMetadata(transform, request, actionListener) + } else { + updateTransformJob(transform, request, actionListener) + } } - } - override fun onFailure(e: Exception) { - actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + override fun onFailure(e: Exception) { + actionListener.onFailure(ExceptionsHelper.unwrapCause(e) as Exception) + } } - } - ) + ) + } } private fun retrieveAndUpdateTransformMetadata( diff --git a/src/main/kotlin/org/opensearch/indexmanagement/transform/model/Transform.kt b/src/main/kotlin/org/opensearch/indexmanagement/transform/model/Transform.kt index 945540a3e..ef233cb2d 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/transform/model/Transform.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/transform/model/Transform.kt @@ -23,6 +23,7 @@ import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParser.Token import org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken import org.opensearch.common.xcontent.XContentType +import org.opensearch.commons.authuser.User import org.opensearch.index.query.AbstractQueryBuilder import org.opensearch.index.query.MatchAllQueryBuilder import org.opensearch.index.query.QueryBuilder @@ -32,8 +33,10 @@ import org.opensearch.indexmanagement.common.model.dimension.Dimension import org.opensearch.indexmanagement.common.model.dimension.Histogram import org.opensearch.indexmanagement.common.model.dimension.Terms import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_TYPE +import org.opensearch.indexmanagement.indexstatemanagement.util.WITH_USER import org.opensearch.indexmanagement.opensearchapi.instant import org.opensearch.indexmanagement.opensearchapi.optionalTimeField +import org.opensearch.indexmanagement.opensearchapi.optionalUserField import org.opensearch.indexmanagement.util.IndexUtils import org.opensearch.jobscheduler.spi.ScheduledJobParameter import org.opensearch.jobscheduler.spi.schedule.CronSchedule @@ -58,10 +61,11 @@ data class Transform( val sourceIndex: String, val dataSelectionQuery: QueryBuilder = MatchAllQueryBuilder(), val targetIndex: String, - val roles: List, + @Deprecated("Will be ignored, to check the roles use user field") val roles: List = emptyList(), val pageSize: Int, val groups: List, - val aggregations: AggregatorFactories.Builder = AggregatorFactories.builder() + val aggregations: AggregatorFactories.Builder = AggregatorFactories.builder(), + val user: User? = null ) : ScheduledJobParameter, Writeable { init { @@ -107,10 +111,10 @@ data class Transform( .field(SOURCE_INDEX_FIELD, sourceIndex) .field(DATA_SELECTION_QUERY_FIELD, dataSelectionQuery) .field(TARGET_INDEX_FIELD, targetIndex) - .field(ROLES_FIELD, roles.toTypedArray()) .field(PAGE_SIZE_FIELD, pageSize) .field(GROUPS_FIELD, groups.toTypedArray()) .field(AGGREGATIONS_FIELD, aggregations) + if (params.paramAsBoolean(WITH_USER, true)) builder.optionalUserField(USER_FIELD, user) if (params.paramAsBoolean(WITH_TYPE, true)) builder.endObject() builder.endObject() return builder @@ -147,6 +151,8 @@ data class Transform( } } out.writeOptionalWriteable(aggregations) + out.writeBoolean(user != null) + user?.writeTo(out) } fun convertToDoc(docCount: Long, includeId: Boolean = true): MutableMap { @@ -199,7 +205,10 @@ data class Transform( } dimensionList.toList() }, - aggregations = requireNotNull(sin.readOptionalWriteable { AggregatorFactories.Builder(it) }) { "Aggregations cannot be null" } + aggregations = requireNotNull(sin.readOptionalWriteable { AggregatorFactories.Builder(it) }) { "Aggregations cannot be null" }, + user = if (sin.readBoolean()) { + User(sin) + } else null ) companion object { @@ -231,8 +240,9 @@ data class Transform( const val MINIMUM_JOB_INTERVAL = 1 const val TRANSFORM_DOC_ID_FIELD = "$TRANSFORM_TYPE._id" const val TRANSFORM_DOC_COUNT_FIELD = "$TRANSFORM_TYPE._doc_count" + const val USER_FIELD = "user" - @Suppress("LongMethod") + @Suppress("ComplexMethod", "LongMethod") @JvmStatic @JvmOverloads fun parse( @@ -251,10 +261,10 @@ data class Transform( var dataSelectionQuery: QueryBuilder = MatchAllQueryBuilder() var targetIndex: String? = null var metadataId: String? = null - val roles = mutableListOf() var pageSize: Int? = null val groups = mutableListOf() var aggregations: AggregatorFactories.Builder = AggregatorFactories.builder() + var user: User? = null ensureExpectedToken(Token.START_OBJECT, xcp.currentToken(), xcp) @@ -287,9 +297,10 @@ data class Transform( TARGET_INDEX_FIELD -> targetIndex = xcp.text() METADATA_ID_FIELD -> metadataId = xcp.textOrNull() ROLES_FIELD -> { + // Parsing but not storing the field, deprecated ensureExpectedToken(Token.START_ARRAY, xcp.currentToken(), xcp) while (xcp.nextToken() != Token.END_ARRAY) { - roles.add(xcp.text()) + xcp.text() } } PAGE_SIZE_FIELD -> pageSize = xcp.intValue() @@ -300,6 +311,9 @@ data class Transform( } } AGGREGATIONS_FIELD -> aggregations = AggregatorFactories.parseAggregators(xcp) + USER_FIELD -> { + user = if (xcp.currentToken() == Token.VALUE_NULL) null else User.parse(xcp) + } else -> throw IllegalArgumentException("Invalid field [$fieldName] found in Transforms.") } } @@ -335,10 +349,10 @@ data class Transform( sourceIndex = requireNotNull(sourceIndex) { "Transform source index is null" }, dataSelectionQuery = dataSelectionQuery, targetIndex = requireNotNull(targetIndex) { "Transform target index is null" }, - roles = roles, pageSize = requireNotNull(pageSize) { "Transform page size is null" }, groups = groups, - aggregations = aggregations + aggregations = aggregations, + user = user ) } } diff --git a/src/main/kotlin/org/opensearch/indexmanagement/util/DummyFileForLicenseCheck.java b/src/main/kotlin/org/opensearch/indexmanagement/util/DummyFileForLicenseCheck.java new file mode 100644 index 000000000..4a9a3dc52 --- /dev/null +++ b/src/main/kotlin/org/opensearch/indexmanagement/util/DummyFileForLicenseCheck.java @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.indexmanagement.util; + +/* + * Need to have a java file, so licenseHeaders task can function + */ +public class DummyFileForLicenseCheck { +} diff --git a/src/main/kotlin/org/opensearch/indexmanagement/util/ScheduledJobUtils.kt b/src/main/kotlin/org/opensearch/indexmanagement/util/ScheduledJobUtils.kt index c7d3e1405..e1ab18187 100644 --- a/src/main/kotlin/org/opensearch/indexmanagement/util/ScheduledJobUtils.kt +++ b/src/main/kotlin/org/opensearch/indexmanagement/util/ScheduledJobUtils.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + /* * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. * diff --git a/src/main/kotlin/org/opensearch/indexmanagement/util/SecurityUtils.kt b/src/main/kotlin/org/opensearch/indexmanagement/util/SecurityUtils.kt new file mode 100644 index 000000000..169ed5754 --- /dev/null +++ b/src/main/kotlin/org/opensearch/indexmanagement/util/SecurityUtils.kt @@ -0,0 +1,135 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.indexmanagement.util + +import org.opensearch.OpenSearchStatusException +import org.opensearch.action.ActionListener +import org.opensearch.common.util.concurrent.ThreadContext +import org.opensearch.commons.ConfigConstants +import org.opensearch.commons.authuser.User +import org.opensearch.index.query.BoolQueryBuilder +import org.opensearch.index.query.ExistsQueryBuilder +import org.opensearch.index.query.TermsQueryBuilder +import org.opensearch.rest.RestStatus + +@Suppress("ReturnCount") +class SecurityUtils { + companion object { + const val INTERNAL_REQUEST = "index_management_plugin_internal_user" + const val ADMIN_ROLE = "all_access" + val DEFAULT_INJECT_ROLES: List = listOf("all_access", "AmazonES_all_access") + + /** + * Helper method to build the user object either from the threadContext or from the requested user. + */ + fun buildUser(threadContext: ThreadContext, requestedUser: User? = null): User? { + if (threadContext.getTransient(INTERNAL_REQUEST) != null && threadContext.getTransient(INTERNAL_REQUEST)) { + // received internal request + return requestedUser + } + val injectedUser: User? = User.parse(threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT)) + return if (injectedUser == null) { + null + } else { + User(injectedUser.name, injectedUser.backendRoles, injectedUser.roles, injectedUser.customAttNames) + } + } + + /** + * If filterBy is enabled and security is disabled or if filter by is enabled and backend role are empty + * we should prevent users from scheduling new jobs + */ + fun validateUserConfiguration(user: User?, filterEnabled: Boolean, actionListener: ActionListener): Boolean { + if (filterEnabled) { + if (user == null) { + actionListener.onFailure( + IndexManagementException.wrap( + OpenSearchStatusException( + "Filter by user backend roles in IndexManagement is not supported with security disabled", + RestStatus.FORBIDDEN + ) + ) + ) + return false + } else if (user.backendRoles.isEmpty()) { + actionListener.onFailure( + IndexManagementException.wrap( + OpenSearchStatusException("User doesn't have backend roles configured. Contact administrator", RestStatus.FORBIDDEN) + ) + ) + return false + } + } + return true + } + + /** + * Check if the requested user has permission on the resource + */ + fun userHasPermissionForResource( + requestedUser: User?, + resourceUser: User?, + filterEnabled: Boolean = false, + resourceName: String, + resourceId: String, + actionListener: ActionListener + ): Boolean { + if (!userHasPermissionForResource(requestedUser, resourceUser, filterEnabled)) { + actionListener.onFailure( + IndexManagementException.wrap( + OpenSearchStatusException("Do not have permission for $resourceName [$resourceId]", RestStatus.FORBIDDEN) + ) + ) + return false + } + + return true + } + + /** + * Check if the requested user has permission on the resource + */ + fun userHasPermissionForResource( + requestedUser: User?, + resourceUser: User?, + filterEnabled: Boolean = false + ): Boolean { + // Will not filter if filter is not enabled or stored user is null or requested user is null or if the user is admin + if (!filterEnabled || resourceUser == null || requestedUser == null || requestedUser.roles.contains(ADMIN_ROLE)) { + return true + } + + val resourceBackendRoles = resourceUser.backendRoles + val requestedBackendRoles = requestedUser.backendRoles + + return !(resourceBackendRoles == null || requestedBackendRoles == null || resourceBackendRoles.intersect(requestedBackendRoles).isEmpty()) + } + + /** + * Add user filter to search requests + */ + fun addUserFilter(user: User?, queryBuilder: BoolQueryBuilder, filterEnabled: Boolean = false, filterPathPrefix: String) { + if (!filterEnabled || user == null || user.roles.contains(ADMIN_ROLE)) { + return + } + + val filterQuery = BoolQueryBuilder().should( + TermsQueryBuilder("$filterPathPrefix.backend_roles.keyword", user.backendRoles) + ).should( + BoolQueryBuilder().mustNot( + ExistsQueryBuilder(filterPathPrefix) + ) + ) + queryBuilder.filter(filterQuery) + } + } +} diff --git a/src/main/resources/mappings/opendistro-ism-config.json b/src/main/resources/mappings/opendistro-ism-config.json index f3eda3c04..51d329a12 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": 10 + "schema_version": 12 }, "dynamic": "strict", "properties": { @@ -456,6 +456,43 @@ "format": "strict_date_time||epoch_millis" } } + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, @@ -510,6 +547,43 @@ }, "is_safe": { "type": "boolean" + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, @@ -526,6 +600,9 @@ "start_time": { "type": "date", "format": "strict_date_time||epoch_millis" + }, + "schedule_delay": { + "type": "long" } } }, @@ -536,10 +613,16 @@ }, "timezone": { "type": "keyword" + }, + "schedule_delay": { + "type": "long" } } } } + }, + "jitter": { + "type": "double" } } }, @@ -718,6 +801,9 @@ "start_time": { "type": "date", "format": "strict_date_time||epoch_millis" + }, + "schedule_delay": { + "type": "long" } } }, @@ -728,6 +814,9 @@ }, "timezone": { "type": "keyword" + }, + "schedule_delay": { + "type": "long" } } } @@ -851,6 +940,43 @@ } } } + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, @@ -935,6 +1061,9 @@ "start_time": { "type": "date", "format": "strict_date_time||epoch_millis" + }, + "schedule_delay": { + "type": "long" } } }, @@ -945,6 +1074,9 @@ }, "timezone": { "type": "keyword" + }, + "schedule_delay": { + "type": "long" } } } @@ -1039,6 +1171,43 @@ "data_selection_query": { "type": "object", "enabled": false + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, diff --git a/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementIndicesIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementIndicesIT.kt index 21996f1f8..ef39b3498 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementIndicesIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementIndicesIT.kt @@ -133,7 +133,6 @@ class IndexManagementIndicesIT : IndexStateManagementRestTestCase() { val managedIndexConfig = getExistingManagedIndexConfig(index) assertNull("Change policy is not null", managedIndexConfig.changePolicy) - assertNull("Policy has already initialized", managedIndexConfig.policy) assertEquals("Policy id does not match", policy.id, managedIndexConfig.policyID) val mapping = "{" + indexManagementMappings.trimStart('{').trimEnd('}') diff --git a/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt index e25c5c282..428d60ea7 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementRestTestCase.kt @@ -46,7 +46,7 @@ import javax.management.remote.JMXServiceURL abstract class IndexManagementRestTestCase : ODFERestTestCase() { - val configSchemaVersion = 10 + val configSchemaVersion = 12 val historySchemaVersion = 3 // 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/IndexManagementSettingsTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementSettingsTests.kt index 0fbeb8c2d..61ff8dbe0 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementSettingsTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/IndexManagementSettingsTests.kt @@ -103,6 +103,7 @@ class IndexManagementSettingsTests : OpenSearchTestCase() { ManagedIndexSettings.COORDINATOR_BACKOFF_MILLIS, ManagedIndexSettings.ALLOW_LIST, ManagedIndexSettings.SNAPSHOT_DENY_LIST, + ManagedIndexSettings.JITTER, RollupSettings.ROLLUP_INGEST_BACKOFF_COUNT, RollupSettings.ROLLUP_INGEST_BACKOFF_MILLIS, RollupSettings.ROLLUP_SEARCH_BACKOFF_COUNT, @@ -110,6 +111,7 @@ class IndexManagementSettingsTests : OpenSearchTestCase() { RollupSettings.ROLLUP_INDEX, RollupSettings.ROLLUP_ENABLED, RollupSettings.ROLLUP_SEARCH_ENABLED, + RollupSettings.ROLLUP_SEARCH_ALL_JOBS, RollupSettings.ROLLUP_DASHBOARDS ) ) @@ -172,6 +174,7 @@ class IndexManagementSettingsTests : OpenSearchTestCase() { assertEquals(ManagedIndexSettings.SNAPSHOT_DENY_LIST.get(settings), listOf("1")) assertEquals(RollupSettings.ROLLUP_ENABLED.get(settings), false) assertEquals(RollupSettings.ROLLUP_SEARCH_ENABLED.get(settings), false) + assertEquals(RollupSettings.ROLLUP_SEARCH_ALL_JOBS.get(settings), false) assertEquals(RollupSettings.ROLLUP_INGEST_BACKOFF_MILLIS.get(settings), TimeValue.timeValueMillis(1)) assertEquals(RollupSettings.ROLLUP_INGEST_BACKOFF_COUNT.get(settings), 1) assertEquals(RollupSettings.ROLLUP_SEARCH_BACKOFF_MILLIS.get(settings), TimeValue.timeValueMillis(1)) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/TestHelpers.kt b/src/test/kotlin/org/opensearch/indexmanagement/TestHelpers.kt index 7e5977b90..c75a308c2 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/TestHelpers.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/TestHelpers.kt @@ -33,6 +33,7 @@ import org.opensearch.client.RequestOptions import org.opensearch.client.Response import org.opensearch.client.RestClient import org.opensearch.client.WarningsHandler +import org.opensearch.commons.authuser.User import org.opensearch.jobscheduler.spi.schedule.CronSchedule import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule import org.opensearch.jobscheduler.spi.schedule.Schedule @@ -66,6 +67,19 @@ fun randomIntervalSchedule(): IntervalSchedule = IntervalSchedule(randomInstant( fun randomSchedule(): Schedule = if (OpenSearchRestTestCase.randomBoolean()) randomIntervalSchedule() else randomCronSchedule() +private fun randomStringList(): List { + val data = mutableListOf() + for (i in 1..OpenSearchRestTestCase.randomIntBetween(10, 10)) { + data.add(OpenSearchRestTestCase.randomAlphaOfLength(10)) + } + + return data +} + +fun randomUser(): User { + return User(OpenSearchRestTestCase.randomAlphaOfLength(10), randomStringList(), randomStringList(), randomStringList()) +} + /** * Wrapper for [RestClient.performRequest] which was deprecated in ES 6.5 and is used in tests. This provides * a single place to suppress deprecation warnings. This will probably need further work when the API is removed entirely diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementIntegTestCase.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementIntegTestCase.kt index 9f7935b19..5d8bc7ece 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementIntegTestCase.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementIntegTestCase.kt @@ -28,6 +28,7 @@ package org.opensearch.indexmanagement.indexstatemanagement import org.apache.http.entity.ContentType import org.apache.http.entity.StringEntity +import org.junit.Before import org.opensearch.OpenSearchParseException import org.opensearch.action.ActionRequest import org.opensearch.action.ActionResponse @@ -54,6 +55,7 @@ import org.opensearch.indexmanagement.indexstatemanagement.model.Policy import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.PolicyRetryInfoMetaData import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.StateMetaData import org.opensearch.indexmanagement.indexstatemanagement.resthandler.RestExplainAction +import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.transport.action.explain.ExplainAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.explain.TransportExplainAction import org.opensearch.indexmanagement.indexstatemanagement.transport.action.updateindexmetadata.TransportUpdateManagedIndexMetaDataAction @@ -73,6 +75,11 @@ import java.time.Duration import java.time.Instant abstract class IndexStateManagementIntegTestCase : OpenSearchIntegTestCase() { + @Before + fun disableIndexStateManagementJitter() { + // jitter would add a test-breaking delay to the integration tests + updateIndexStateManagementJitterSetting(0.0) + } protected val isMixedNodeRegressionTest = System.getProperty("cluster.mixed", "false")!!.toBoolean() @@ -357,4 +364,8 @@ abstract class IndexStateManagementIntegTestCase : OpenSearchIntegTestCase() { ) assertEquals("Request failed", RestStatus.OK, res.restStatus()) } + + protected fun updateIndexStateManagementJitterSetting(value: Double?) { + updateClusterSetting(ManagedIndexSettings.JITTER.key, value.toString(), false) + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementRestTestCase.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementRestTestCase.kt index 07487e85b..c92498ef4 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementRestTestCase.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/IndexStateManagementRestTestCase.kt @@ -31,6 +31,7 @@ import org.apache.http.HttpHeaders import org.apache.http.entity.ContentType.APPLICATION_JSON import org.apache.http.entity.StringEntity import org.apache.http.message.BasicHeader +import org.junit.Before import org.opensearch.OpenSearchParseException import org.opensearch.action.get.GetResponse import org.opensearch.action.search.SearchResponse @@ -92,6 +93,15 @@ import java.util.Locale abstract class IndexStateManagementRestTestCase : IndexManagementRestTestCase() { + val explainResponseOpendistroPolicyIdSetting = "index.opendistro.index_state_management.policy_id" + val explainResponseOpenSearchPolicyIdSetting = "index.plugins.index_state_management.policy_id" + + @Before + protected fun disableIndexStateManagementJitter() { + // jitter would add a test-breaking delay to the integration tests + updateIndexStateManagementJitterSetting(0.0) + } + protected fun createPolicy( policy: Policy, policyId: String = OpenSearchTestCase.randomAlphaOfLength(10), @@ -244,12 +254,6 @@ abstract class IndexStateManagementRestTestCase : IndexManagementRestTestCase() client().makeRequest("POST", "/_opendistro/_ism/remove/$index") } - @Suppress("UNCHECKED_CAST") - protected fun getPolicyFromIndex(index: String): String? { - val indexSettings = getIndexSettings(index) as Map>> - return indexSettings[index]!!["settings"]!![ManagedIndexSettings.POLICY_ID.key] as? String - } - protected fun getPolicyIDOfManagedIndex(index: String): String? { val managedIndex = getManagedIndexConfig(index) return managedIndex?.policyID @@ -271,6 +275,10 @@ abstract class IndexStateManagementRestTestCase : IndexManagementRestTestCase() assertEquals("Request failed", RestStatus.OK, res.restStatus()) } + protected fun updateIndexStateManagementJitterSetting(value: Double) { + updateClusterSetting(ManagedIndexSettings.JITTER.key, value.toString(), false) + } + protected fun updateIndexSetting( index: String, key: String, @@ -507,6 +515,30 @@ abstract class IndexStateManagementRestTestCase : IndexManagementRestTestCase() return (indexSettings[indexName]!!["settings"]!!["index.priority"] as String).toInt() } + @Suppress("UNCHECKED_CAST") + protected fun getIndexAutoManageSetting(indexName: String): Boolean? { + val indexSettings = getIndexSettings(indexName) as Map>> + val autoManageSetting = indexSettings[indexName]!!["settings"]!!["index.plugins.index_state_management.auto_manage"] + if (autoManageSetting != null) return (autoManageSetting as String).toBoolean() + return null + } + + @Suppress("UNCHECKED_CAST") + protected fun getIndexReadOnlySetting(indexName: String): Boolean? { + val indexSettings = getIndexSettings(indexName) as Map>> + val readOnlySetting = indexSettings[indexName]!!["settings"]!![IndexMetadata.SETTING_READ_ONLY] + if (readOnlySetting != null) return (readOnlySetting as String).toBoolean() + return null + } + + @Suppress("UNCHECKED_CAST") + protected fun getIndexReadOnlyAllowDeleteSetting(indexName: String): Boolean? { + val indexSettings = getIndexSettings(indexName) as Map>> + val readOnlyAllowDeleteSetting = indexSettings[indexName]!!["settings"]!![IndexMetadata.SETTING_READ_ONLY_ALLOW_DELETE] + if (readOnlyAllowDeleteSetting != null) return (readOnlyAllowDeleteSetting as String).toBoolean() + return null + } + @Suppress("UNCHECKED_CAST") protected fun getUuid(indexName: String): String { val indexSettings = getIndexSettings(indexName) as Map>> @@ -579,6 +611,16 @@ abstract class IndexStateManagementRestTestCase : IndexManagementRestTestCase() return metadata } + protected fun rolloverIndex(index: String) { + val response = client().performRequest( + Request( + "POST", + "/$index/_rollover" + ) + ) + assertEquals(response.statusLine.statusCode, RestStatus.OK.status) + } + protected fun createRepository( repository: String ) { @@ -692,13 +734,13 @@ abstract class IndexStateManagementRestTestCase : IndexManagementRestTestCase() protected fun assertSnapshotExists( repository: String, snapshot: String - ) = require(getSnapshotsList(repository).any { element -> (element as Map)["id"]!!.contains(snapshot) }) { "No snapshot found with id: $snapshot" } + ) = require(getSnapshotsList(repository).any { element -> (element as Map)["id"]!!.startsWith(snapshot) }) { "No snapshot found with id: $snapshot" } @Suppress("UNCHECKED_CAST") protected fun assertSnapshotFinishedWithSuccess( repository: String, snapshot: String - ) = require(getSnapshotsList(repository).any { element -> (element as Map)["id"]!!.contains(snapshot) && "SUCCESS" == element["status"] }) { "Snapshot didn't finish with success." } + ) = require(getSnapshotsList(repository).any { element -> (element as Map)["id"]!!.startsWith(snapshot) && "SUCCESS" == element["status"] }) { "Snapshot didn't finish with success." } /** * Compares responses returned by APIs such as those defined in [RetryFailedManagedIndexAction] and [RestAddPolicyAction] diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/MetadataRegressionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/MetadataRegressionIT.kt index 46ec87d7a..792461acf 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/MetadataRegressionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/MetadataRegressionIT.kt @@ -63,8 +63,10 @@ class MetadataRegressionIT : IndexStateManagementIntegTestCase() { fun cleanClusterSetting() { // need to clean up otherwise will throw error updateClusterSetting(ManagedIndexSettings.METADATA_SERVICE_ENABLED.key, null, false) + updateIndexStateManagementJitterSetting(null) } + @AwaitsFix(bugUrl = "https://github.com/opensearch-project/index-management/issues/176") fun `test move metadata service`() { updateClusterSetting(ManagedIndexSettings.METADATA_SERVICE_ENABLED.key, "false") updateClusterSetting(ManagedIndexSettings.METADATA_SERVICE_ENABLED.key, "true") @@ -126,18 +128,10 @@ class MetadataRegressionIT : IndexStateManagementIntegTestCase() { logger.info("metadata has moved") val managedIndexConfig = getExistingManagedIndexConfig(indexName) - // Change the start time so the job will trigger in 2 seconds, this will trigger the first initialization of the policy + // Change the start time so the job will trigger in 2 seconds, since there is metadata and policy with the index there is no initialization updateManagedIndexConfigStartTime(managedIndexConfig) waitFor { assertEquals(policyID, getExplainManagedIndexMetaData(indexName).policyID) } - waitFor { - assertEquals( - "Successfully initialized policy: ${policy.id}", - getExplainManagedIndexMetaData(indexName).info?.get("message") - ) - } - - updateManagedIndexConfigStartTime(managedIndexConfig) waitFor { assertEquals( "Index did not set number_of_replicas to ${actionConfig.numOfReplicas}", @@ -147,6 +141,7 @@ class MetadataRegressionIT : IndexStateManagementIntegTestCase() { } } + @AwaitsFix(bugUrl = "https://github.com/opensearch-project/index-management/issues/176") fun `test job can continue run from cluster state metadata`() { /** * new version of ISM plugin can handle metadata in cluster state @@ -229,6 +224,7 @@ class MetadataRegressionIT : IndexStateManagementIntegTestCase() { } } + @AwaitsFix(bugUrl = "https://github.com/opensearch-project/index-management/issues/176") fun `test new node skip execution when old node exist in cluster`() { Assume.assumeTrue(isMixedNodeRegressionTest) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt index 3cbef7a25..68f64e664 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/TestHelpers.kt @@ -79,7 +79,7 @@ fun randomPolicy( lastUpdatedTime: Instant = Instant.now().truncatedTo(ChronoUnit.MILLIS), errorNotification: ErrorNotification? = randomErrorNotification(), states: List = List(OpenSearchRestTestCase.randomIntBetween(1, 10)) { randomState() }, - ismTemplate: ISMTemplate? = null + ismTemplate: List? = null ): Policy { return Policy( id = id, schemaVersion = schemaVersion, lastUpdatedTime = lastUpdatedTime, @@ -275,7 +275,8 @@ fun randomManagedIndexConfig( enabledTime: Instant? = if (enabled) Instant.now().truncatedTo(ChronoUnit.MILLIS) else null, policyID: String = OpenSearchRestTestCase.randomAlphaOfLength(10), policy: Policy? = randomPolicy(), - changePolicy: ChangePolicy? = randomChangePolicy() + changePolicy: ChangePolicy? = randomChangePolicy(), + jitter: Double? = 0.0 ): ManagedIndexConfig { return ManagedIndexConfig( jobName = name, @@ -289,7 +290,8 @@ fun randomManagedIndexConfig( policySeqNo = policy?.seqNo, policyPrimaryTerm = policy?.primaryTerm, policy = policy?.copy(seqNo = SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm = SequenceNumbers.UNASSIGNED_PRIMARY_TERM), - changePolicy = changePolicy + changePolicy = changePolicy, + jobJitter = jitter ) } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ActionRetryIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ActionRetryIT.kt index 6f91cbad3..55a00f30a 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ActionRetryIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/ActionRetryIT.kt @@ -32,7 +32,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmet import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.PolicyRetryInfoMetaData import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.StateMetaData import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.StepMetaData -import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.step.Step import org.opensearch.indexmanagement.indexstatemanagement.step.rollover.AttemptRolloverStep import org.opensearch.indexmanagement.waitFor @@ -159,7 +158,8 @@ class ActionRetryIT : IndexStateManagementRestTestCase() { assertPredicatesOnMetaData( listOf( indexName to listOf( - ManagedIndexSettings.POLICY_ID.key to policyID::equals, + explainResponseOpendistroPolicyIdSetting to policyID::equals, + explainResponseOpenSearchPolicyIdSetting to policyID::equals, ManagedIndexMetaData.INDEX to managedIndexConfig.index::equals, ManagedIndexMetaData.INDEX_UUID to managedIndexConfig.indexUuid::equals, ManagedIndexMetaData.POLICY_ID to managedIndexConfig.policyID::equals, diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/NotificationActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/NotificationActionIT.kt index a80a4f4e8..0d4e233e0 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/NotificationActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/NotificationActionIT.kt @@ -85,7 +85,7 @@ class NotificationActionIT : IndexStateManagementRestTestCase() { createPolicy(policy, policyID) createIndex(indexName, policyID) - createIndex(notificationIndex) + createIndex(notificationIndex, null) val managedIndexConfig = getExistingManagedIndexConfig(indexName) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RolloverActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RolloverActionIT.kt index 2fd1e89ed..6073f2031 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RolloverActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RolloverActionIT.kt @@ -42,6 +42,7 @@ import org.opensearch.indexmanagement.indexstatemanagement.model.action.Rollover import org.opensearch.indexmanagement.indexstatemanagement.randomErrorNotification import org.opensearch.indexmanagement.indexstatemanagement.resthandler.RestRetryFailedManagedIndexAction import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings +import org.opensearch.indexmanagement.indexstatemanagement.step.Step import org.opensearch.indexmanagement.indexstatemanagement.step.rollover.AttemptRolloverStep import org.opensearch.indexmanagement.makeRequest import org.opensearch.indexmanagement.waitFor @@ -364,7 +365,7 @@ class RolloverActionIT : IndexStateManagementRestTestCase() { errorNotification = randomErrorNotification(), defaultState = states[0].name, states = states, - ismTemplate = ISMTemplate(listOf(dataStreamName), 100, Instant.now().truncatedTo(ChronoUnit.MILLIS)) + ismTemplate = listOf(ISMTemplate(listOf(dataStreamName), 100, Instant.now().truncatedTo(ChronoUnit.MILLIS))) ) createPolicy(policy, policyID) @@ -420,7 +421,7 @@ class RolloverActionIT : IndexStateManagementRestTestCase() { errorNotification = randomErrorNotification(), defaultState = states[0].name, states = states, - ismTemplate = ISMTemplate(listOf(dataStreamName), 100, Instant.now().truncatedTo(ChronoUnit.MILLIS)) + ismTemplate = listOf(ISMTemplate(listOf(dataStreamName), 100, Instant.now().truncatedTo(ChronoUnit.MILLIS))) ) createPolicy(policy, policyID) @@ -496,4 +497,46 @@ class RolloverActionIT : IndexStateManagementRestTestCase() { val secondIndexName = DataStream.getDefaultBackingIndexName(dataStreamName, 2L) Assert.assertTrue("New rollover index does not exist.", indexExists(secondIndexName)) } + + @Suppress("UNCHECKED_CAST") + fun `test rollover from outside ISM doesn't fail ISM job`() { + val aliasName = "${testIndexName}_alias" + val indexNameBase = "${testIndexName}_index" + val firstIndex = "$indexNameBase-1" + val policyID = "${testIndexName}_testPolicyName_1" + val actionConfig = RolloverActionConfig(null, null, null, 0) + val states = listOf(State(name = "RolloverAction", actions = listOf(actionConfig), transitions = listOf())) + val policy = Policy( + id = policyID, + description = "$testIndexName description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + + createPolicy(policy, policyID) + // create index defaults + createIndex(firstIndex, policyID, aliasName) + + val managedIndexConfig = getExistingManagedIndexConfig(firstIndex) + + // Change the start time so the job will trigger in 2 seconds, this will trigger the first initialization of the policy + updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { assertEquals(policyID, getExplainManagedIndexMetaData(firstIndex).policyID) } + + // Rollover the alias manually before ISM tries to roll it over + rolloverIndex(aliasName) + + // Need to speed up to second execution where it will trigger the first execution of the action + updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { + val info = getExplainManagedIndexMetaData(firstIndex).info as Map + val stepMetadata = getExplainManagedIndexMetaData(firstIndex).stepMetaData + assertEquals("Index should succeed if already rolled over.", AttemptRolloverStep.getAlreadyRolledOverMessage(firstIndex, aliasName), info["message"]) + assertEquals("Index should succeed if already rolled over.", Step.StepStatus.COMPLETED, stepMetadata?.stepStatus) + } + assertTrue("New rollover index does not exist.", indexExists("$indexNameBase-000002")) + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RollupActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RollupActionIT.kt index 7a899dfcc..559554f26 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RollupActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/RollupActionIT.kt @@ -126,7 +126,7 @@ class RollupActionIT : IndexStateManagementRestTestCase() { errorNotification = randomErrorNotification(), defaultState = states[0].name, states = states, - ismTemplate = ISMTemplate(listOf(dataStreamName), 100, Instant.now().truncatedTo(ChronoUnit.MILLIS)) + ismTemplate = listOf(ISMTemplate(listOf(dataStreamName), 100, Instant.now().truncatedTo(ChronoUnit.MILLIS))) ) createPolicy(policy, policyID) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotActionIT.kt index 9cf77bca5..ddb826b0c 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/action/SnapshotActionIT.kt @@ -77,8 +77,82 @@ class SnapshotActionIT : IndexStateManagementRestTestCase() { // Need to wait two cycles for wait for snapshot step updateManagedIndexConfigStartTime(managedIndexConfig) - waitFor { assertSnapshotExists(repository, snapshot) } - waitFor { assertSnapshotFinishedWithSuccess(repository, snapshot) } + waitFor { assertSnapshotExists(repository, "snapshot") } + waitFor { assertSnapshotFinishedWithSuccess(repository, "snapshot") } + } + + fun `test basic with templated snapshot name`() { + val indexName = "${testIndexName}_index_basic" + val policyID = "${testIndexName}_policy_basic" + val repository = "repository" + val actionConfig = SnapshotActionConfig(repository, "{{ctx.index}}", 0) + val states = listOf( + State("Snapshot", listOf(actionConfig), listOf()) + ) + + createRepository(repository) + + val policy = Policy( + id = policyID, + description = "$testIndexName description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + createPolicy(policy, policyID) + createIndex(indexName, policyID) + + val managedIndexConfig = getExistingManagedIndexConfig(indexName) + + // Change the start time so the job will trigger in 2 seconds. + updateManagedIndexConfigStartTime(managedIndexConfig) + + waitFor { assertEquals(policyID, getExplainManagedIndexMetaData(indexName).policyID) } + + // Need to wait two cycles for wait for snapshot step + updateManagedIndexConfigStartTime(managedIndexConfig) + + waitFor { assertSnapshotExists(repository, indexName) } + waitFor { assertSnapshotFinishedWithSuccess(repository, indexName) } + } + + fun `test basic with invalid templated snapshot name default to indexName`() { + val indexName = "${testIndexName}_index_basic" + val policyID = "${testIndexName}_policy_basic" + val repository = "repository" + val actionConfig = SnapshotActionConfig(repository, "{{ctx.someField}}", 0) + val states = listOf( + State("Snapshot", listOf(actionConfig), listOf()) + ) + + createRepository(repository) + + val policy = Policy( + id = policyID, + description = "$testIndexName description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + createPolicy(policy, policyID) + createIndex(indexName, policyID) + + val managedIndexConfig = getExistingManagedIndexConfig(indexName) + + // Change the start time so the job will trigger in 2 seconds. + updateManagedIndexConfigStartTime(managedIndexConfig) + + waitFor { assertEquals(policyID, getExplainManagedIndexMetaData(indexName).policyID) } + + // Need to wait two cycles for wait for snapshot step + updateManagedIndexConfigStartTime(managedIndexConfig) + + waitFor { assertSnapshotExists(repository, indexName) } + waitFor { assertSnapshotFinishedWithSuccess(repository, indexName) } } fun `test successful wait for snapshot step`() { @@ -130,6 +204,55 @@ class SnapshotActionIT : IndexStateManagementRestTestCase() { waitFor { assertSnapshotFinishedWithSuccess(repository, snapshot) } } + fun `test successful wait for snapshot step - empty snapshot name`() { + val indexName = "${testIndexName}_index_success" + val policyID = "${testIndexName}_policy_success" + val repository = "repository" + val snapshot = "-" + val actionConfig = SnapshotActionConfig(repository, "", 0) + val states = listOf( + State("Snapshot", listOf(actionConfig), listOf()) + ) + + createRepository(repository) + + val policy = Policy( + id = policyID, + description = "$testIndexName description", + schemaVersion = 1L, + lastUpdatedTime = Instant.now().truncatedTo(ChronoUnit.MILLIS), + errorNotification = randomErrorNotification(), + defaultState = states[0].name, + states = states + ) + createPolicy(policy, policyID) + createIndex(indexName, policyID) + + val managedIndexConfig = getExistingManagedIndexConfig(indexName) + + // Change the start time so the job will initialize the policy + updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { assertEquals(policyID, getExplainManagedIndexMetaData(indexName).policyID) } + + // Change the start time so attempt snapshot step with execute + updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { assertEquals(AttemptSnapshotStep.getSuccessMessage(indexName), getExplainManagedIndexMetaData(indexName).info?.get("message")) } + + // Change the start time so wait for snapshot step will execute + updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { assertEquals(WaitForSnapshotStep.getSuccessMessage(indexName), getExplainManagedIndexMetaData(indexName).info?.get("message")) } + + // verify we set snapshotName in action properties + waitFor { + assert( + getExplainManagedIndexMetaData(indexName).actionMetaData?.actionProperties?.snapshotName?.contains(snapshot) == true + ) + } + + waitFor { assertSnapshotExists(repository, snapshot) } + waitFor { assertSnapshotFinishedWithSuccess(repository, snapshot) } + } + fun `test failed wait for snapshot step`() { val indexName = "${testIndexName}_index_failed" val policyID = "${testIndexName}_policy_failed" @@ -188,8 +311,7 @@ class SnapshotActionIT : IndexStateManagementRestTestCase() { val indexName = "${testIndexName}_index_blocked" val policyID = "${testIndexName}_policy_basic" val repository = "hello-world" - val snapshot = "snapshot" - val actionConfig = SnapshotActionConfig(repository, snapshot, 0) + val actionConfig = SnapshotActionConfig(repository, "snapshot", 0) val states = listOf( State("Snapshot", listOf(actionConfig), listOf()) ) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorIT.kt index e63966dad..0da2b71ef 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorIT.kt @@ -27,8 +27,6 @@ package org.opensearch.indexmanagement.indexstatemanagement.coordinator import org.opensearch.client.ResponseException -import org.opensearch.common.xcontent.XContentType -import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX import org.opensearch.indexmanagement.indexstatemanagement.IndexStateManagementRestTestCase import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexConfig import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData @@ -49,11 +47,13 @@ import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus import java.time.Instant import java.time.temporal.ChronoUnit +import kotlin.test.assertFailsWith class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { fun `test creating index with valid policy_id`() { - val (index, policyID) = createIndex(policyID = "some_policy") + val policy = createRandomPolicy() + val (index, policyID) = createIndex(policyID = policy.id) waitFor { val managedIndexConfig = getManagedIndexConfig(index) assertNotNull("Did not create ManagedIndexConfig", managedIndexConfig) @@ -64,29 +64,15 @@ class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { } } - @Suppress("UNCHECKED_CAST") - fun `test first time add policy to index will create config index with correct mappings`() { - createIndex() - waitFor { - val response = client().makeRequest("GET", "/$INDEX_MANAGEMENT_INDEX/_mapping") - val parserMap = createParser( - XContentType.JSON.xContent(), - response.entity.content - ).map() as Map>> - val mappingsMap = parserMap[INDEX_MANAGEMENT_INDEX]?.getValue("mappings")!! - - val expected = createParser( - XContentType.JSON.xContent(), - javaClass.classLoader.getResource("mappings/opendistro-ism-config.json").readText() - ) - - val expectedMap = expected.map() - assertEquals("Mappings are different", expectedMap, mappingsMap) + fun `test first time add policy to index will fail without an existing policy`() { + assertFailsWith(Exception::class, "add policy is expected to fail when called with non existent policy") { + createIndex() } } fun `test deleting index will remove managed-index`() { - val (index) = createIndex(policyID = "some_policy") + val policy = createRandomPolicy() + val (index) = createIndex(policyID = policy.id) waitFor { val afterCreateConfig = getManagedIndexConfig(index) assertNotNull("Did not create ManagedIndexConfig", afterCreateConfig) @@ -100,8 +86,8 @@ class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { } fun `test managed index metadata is cleaned up after removing policy`() { - val policyID = "some_policy" - val (index) = createIndex(policyID = policyID) + val policy = createRandomPolicy() + val (index) = createIndex(policyID = policy.id) val managedIndexConfig = getExistingManagedIndexConfig(index) @@ -112,7 +98,7 @@ class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { // Verify ManagedIndexMetaData contains information waitFor { assertPredicatesOnMetaData( - listOf(index to listOf(ManagedIndexMetaData.POLICY_ID to policyID::equals)), + listOf(index to listOf(ManagedIndexMetaData.POLICY_ID to policy.id::equals)), getExplainMap(index), false ) @@ -127,7 +113,9 @@ class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { assertPredicatesOnMetaData( listOf( index to listOf( - ManagedIndexSettings.POLICY_ID.key to fun(policyID: Any?): Boolean = + explainResponseOpendistroPolicyIdSetting to fun(policyID: Any?): Boolean = + policyID == null, + explainResponseOpenSearchPolicyIdSetting to fun(policyID: Any?): Boolean = policyID == null ) ), @@ -138,8 +126,8 @@ class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { } fun `test managed-index metadata is cleaned up after index deleted`() { - val policyID = "some_policy" - val (index) = createIndex(policyID = policyID) + val policy = createRandomPolicy() + val (index) = createIndex(policyID = policy.id) val managedIndexConfig = getExistingManagedIndexConfig(index) @@ -150,7 +138,7 @@ class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { // Verify ManagedIndexMetaData contains information waitFor { assertPredicatesOnMetaData( - listOf(index to listOf(ManagedIndexMetaData.POLICY_ID to policyID::equals)), + listOf(index to listOf(ManagedIndexMetaData.POLICY_ID to policy.id::equals)), getExplainMap(index), false ) @@ -269,7 +257,9 @@ class ManagedIndexCoordinatorIT : IndexStateManagementRestTestCase() { } // TODO seen version conflict flaky failure here - // could be same reason as the test failure in ChangePolicyActionIT + logger.info("Config we use on update: $enabledManagedIndexConfig") + logger.info("Latest config: ${getExistingManagedIndexConfig(indexName)}") + // seems the config from above waitFor, after that, config got updated again? updateManagedIndexConfigStartTime(enabledManagedIndexConfig) waitFor { diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorTests.kt index 015eed2c6..0723b86b3 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/coordinator/ManagedIndexCoordinatorTests.kt @@ -31,8 +31,6 @@ import org.mockito.Mockito import org.opensearch.Version import org.opensearch.client.Client import org.opensearch.cluster.OpenSearchAllocationTestCase -import org.opensearch.cluster.metadata.IndexMetadata -import org.opensearch.cluster.metadata.IndexMetadata.SETTING_INDEX_UUID import org.opensearch.cluster.node.DiscoveryNode import org.opensearch.cluster.service.ClusterService import org.opensearch.common.settings.ClusterSettings @@ -80,6 +78,7 @@ class ManagedIndexCoordinatorTests : OpenSearchAllocationTestCase() { val settingSet = hashSetOf>() settingSet.addAll(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) settingSet.add(ManagedIndexSettings.SWEEP_PERIOD) + settingSet.add(ManagedIndexSettings.JITTER) settingSet.add(ManagedIndexSettings.JOB_INTERVAL) settingSet.add(ManagedIndexSettings.INDEX_STATE_MANAGEMENT_ENABLED) settingSet.add(ManagedIndexSettings.METADATA_SERVICE_ENABLED) @@ -139,18 +138,6 @@ class ManagedIndexCoordinatorTests : OpenSearchAllocationTestCase() { Mockito.verify(threadPool, Mockito.times(2)).scheduleWithFixedDelay(Mockito.any(), Mockito.any(), Mockito.anyString()) } - private fun createIndexMetaData(indexName: String, replicaNumber: Int, shardNumber: Int, policyID: String?): IndexMetadata.Builder { - val defaultSettings = Settings.builder() - .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) - .put(ManagedIndexSettings.POLICY_ID.key, policyID) - .put(SETTING_INDEX_UUID, randomAlphaOfLength(20)) - .build() - return IndexMetadata.Builder(indexName) - .settings(defaultSettings) - .numberOfReplicas(replicaNumber) - .numberOfShards(shardNumber) - } - private fun any(): T { Mockito.any() return uninitialized() diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/ISMTemplateRestAPIIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/ISMTemplateRestAPIIT.kt index 4adbe2a6e..b05db6628 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/ISMTemplateRestAPIIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/ISMTemplateRestAPIIT.kt @@ -35,7 +35,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.model.State import org.opensearch.indexmanagement.indexstatemanagement.model.action.ReadOnlyActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomErrorNotification import org.opensearch.indexmanagement.indexstatemanagement.randomPolicy -import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.util.INDEX_HIDDEN import org.opensearch.indexmanagement.randomInstant import org.opensearch.indexmanagement.waitFor @@ -55,7 +54,7 @@ class ISMTemplateRestAPIIT : IndexStateManagementRestTestCase() { fun `test add template with invalid index pattern`() { try { val ismTemp = ISMTemplate(listOf(" "), 100, randomInstant()) - createPolicy(randomPolicy(ismTemplate = ismTemp), policyID1) + createPolicy(randomPolicy(ismTemplate = listOf(ismTemp)), policyID1) fail("Expect a failure") } catch (e: ResponseException) { assertEquals("Unexpected RestStatus", RestStatus.BAD_REQUEST, e.response.restStatus()) @@ -65,14 +64,28 @@ class ISMTemplateRestAPIIT : IndexStateManagementRestTestCase() { } } + fun `test add template with self-overlapping index pattern`() { + try { + val ismTemp = ISMTemplate(listOf("ab*"), 100, randomInstant()) + val ismTemp2 = ISMTemplate(listOf("abc*"), 100, randomInstant()) + createPolicy(randomPolicy(ismTemplate = listOf(ismTemp, ismTemp2)), policyID1) + fail("Expect a failure") + } catch (e: ResponseException) { + assertEquals("Unexpected RestStatus", RestStatus.BAD_REQUEST, e.response.restStatus()) + val actualMessage = e.response.asMap()["error"] as Map + val expectedReason = "New policy $policyID1 has an ISM template with index pattern [ab*] matching this policy's other ISM templates with index patterns [abc*], please use different priority" + assertEquals(expectedReason, actualMessage["reason"]) + } + } + fun `test add template with overlapping index pattern`() { try { val ismTemp = ISMTemplate(listOf("log*"), 100, randomInstant()) val ismTemp2 = ISMTemplate(listOf("abc*"), 100, randomInstant()) val ismTemp3 = ISMTemplate(listOf("*"), 100, randomInstant()) - createPolicy(randomPolicy(ismTemplate = ismTemp), policyID1) - createPolicy(randomPolicy(ismTemplate = ismTemp2), policyID2) - createPolicy(randomPolicy(ismTemplate = ismTemp3), policyID3) + createPolicy(randomPolicy(ismTemplate = listOf(ismTemp)), policyID1) + createPolicy(randomPolicy(ismTemplate = listOf(ismTemp2)), policyID2) + createPolicy(randomPolicy(ismTemplate = listOf(ismTemp3)), policyID3) fail("Expect a failure") } catch (e: ResponseException) { assertEquals("Unexpected RestStatus", RestStatus.BAD_REQUEST, e.response.restStatus()) @@ -105,7 +118,7 @@ class ISMTemplateRestAPIIT : IndexStateManagementRestTestCase() { errorNotification = randomErrorNotification(), defaultState = states[0].name, states = states, - ismTemplate = ismTemp + ismTemplate = listOf(ismTemp) ) createPolicy(policy, policyID) @@ -121,7 +134,12 @@ class ISMTemplateRestAPIIT : IndexStateManagementRestTestCase() { // only index create after template can be managed assertPredicatesOnMetaData( - listOf(indexName1 to listOf(ManagedIndexSettings.POLICY_ID.key to fun(policyID: Any?): Boolean = policyID == null)), + listOf( + indexName1 to listOf( + explainResponseOpendistroPolicyIdSetting to fun(policyID: Any?): Boolean = policyID == null, + explainResponseOpenSearchPolicyIdSetting to fun(policyID: Any?): Boolean = policyID == null + ) + ), getExplainMap(indexName1), true ) @@ -129,7 +147,12 @@ class ISMTemplateRestAPIIT : IndexStateManagementRestTestCase() { // hidden index will not be manage assertPredicatesOnMetaData( - listOf(indexName1 to listOf(ManagedIndexSettings.POLICY_ID.key to fun(policyID: Any?): Boolean = policyID == null)), + listOf( + indexName1 to listOf( + explainResponseOpendistroPolicyIdSetting to fun(policyID: Any?): Boolean = policyID == null, + explainResponseOpenSearchPolicyIdSetting to fun(policyID: Any?): Boolean = policyID == null + ) + ), getExplainMap(indexName1), true ) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestAddPolicyActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestAddPolicyActionIT.kt index 68ee2e8e8..cfe8baeb5 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestAddPolicyActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestAddPolicyActionIT.kt @@ -63,13 +63,14 @@ class RestAddPolicyActionIT : IndexStateManagementRestTestCase() { fun `test closed index`() { val index = "movies" + val policy = createRandomPolicy() createIndex(index, null) closeIndex(index) val response = client().makeRequest( POST.toString(), "${RestAddPolicyAction.ADD_POLICY_BASE_URI}/$index", - StringEntity("{ \"policy_id\": \"somePolicy\" }", APPLICATION_JSON) + StringEntity("{ \"policy_id\": \"${policy.id}\" }", APPLICATION_JSON) ) assertEquals("Unexpected RestStatus", RestStatus.OK, response.restStatus()) val actualMessage = response.asMap() @@ -118,16 +119,17 @@ class RestAddPolicyActionIT : IndexStateManagementRestTestCase() { fun `test index list`() { val indexOne = "movies_1" val indexTwo = "movies_2" - + val policy = createRandomPolicy() + val newPolicy = createRandomPolicy() createIndex(indexOne, null) - createIndex(indexTwo, "somePolicy") + createIndex(indexTwo, policy.id) closeIndex(indexOne) val response = client().makeRequest( POST.toString(), "${RestAddPolicyAction.ADD_POLICY_BASE_URI}/$indexOne,$indexTwo", - StringEntity("{ \"policy_id\": \"someOtherPolicy\" }", APPLICATION_JSON) + StringEntity("{ \"policy_id\": \"${newPolicy.id}\" }", APPLICATION_JSON) ) assertEquals("Unexpected RestStatus", RestStatus.OK, response.restStatus()) val actualMessage = response.asMap() @@ -156,9 +158,10 @@ class RestAddPolicyActionIT : IndexStateManagementRestTestCase() { val indexOne = "movies_1" val indexTwo = "movies_2" val indexThree = "movies_3" - + val policy = createRandomPolicy() + val newPolicy = createRandomPolicy() createIndex(indexOne, null) - createIndex(indexTwo, "somePolicy") + createIndex(indexTwo, policy.id) createIndex(indexThree, null) closeIndex(indexOne) @@ -166,7 +169,7 @@ class RestAddPolicyActionIT : IndexStateManagementRestTestCase() { val response = client().makeRequest( POST.toString(), "${RestAddPolicyAction.ADD_POLICY_BASE_URI}/$indexPattern*", - StringEntity("{ \"policy_id\": \"someOtherPolicy\" }", APPLICATION_JSON) + StringEntity("{ \"policy_id\": \"${newPolicy.id}\" }", APPLICATION_JSON) ) assertEquals("Unexpected RestStatus", RestStatus.OK, response.restStatus()) val actualMessage = response.asMap() @@ -191,7 +194,7 @@ class RestAddPolicyActionIT : IndexStateManagementRestTestCase() { // Check if indexThree had policy set waitFor { - assertEquals("someOtherPolicy", getPolicyIDOfManagedIndex(indexThree)) + assertEquals(newPolicy.id, getPolicyIDOfManagedIndex(indexThree)) } } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestChangePolicyActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestChangePolicyActionIT.kt index 5c450520c..882053c83 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestChangePolicyActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestChangePolicyActionIT.kt @@ -29,6 +29,7 @@ package org.opensearch.indexmanagement.indexstatemanagement.resthandler import org.junit.Before import org.opensearch.client.ResponseException import org.opensearch.common.settings.Settings +import org.opensearch.indexmanagement.IndexManagementPlugin.Companion.INDEX_MANAGEMENT_INDEX import org.opensearch.indexmanagement.indexstatemanagement.IndexStateManagementRestTestCase import org.opensearch.indexmanagement.indexstatemanagement.model.ChangePolicy import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexConfig @@ -46,7 +47,6 @@ import org.opensearch.indexmanagement.indexstatemanagement.randomPolicy import org.opensearch.indexmanagement.indexstatemanagement.randomReplicaCountActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomState import org.opensearch.indexmanagement.indexstatemanagement.resthandler.RestChangePolicyAction.Companion.INDEX_NOT_MANAGED -import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.step.rollover.AttemptRolloverStep import org.opensearch.indexmanagement.indexstatemanagement.util.FAILED_INDICES import org.opensearch.indexmanagement.indexstatemanagement.util.FAILURES @@ -108,6 +108,7 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { } fun `test nonexistent ism config index`() { + if (indexExists(INDEX_MANAGEMENT_INDEX)) deleteIndex(INDEX_MANAGEMENT_INDEX) try { val changePolicy = ChangePolicy("some_id", null, emptyList(), false) client().makeRequest( @@ -210,7 +211,6 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { val managedIndexConfig = getExistingManagedIndexConfig(index) assertNull("Change policy is not null", managedIndexConfig.changePolicy) - assertNull("Policy has already initialized", managedIndexConfig.policy) assertEquals("Policy id does not match", policy.id, managedIndexConfig.policyID) // If we try to change the policy now, it hasn't actually run and has no ManagedIndexMetaData yet so it should succeed @@ -316,7 +316,6 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { val managedIndexConfig = getExistingManagedIndexConfig(index) assertNull("Change policy is not null", managedIndexConfig.changePolicy) - assertNull("Policy already initialized", managedIndexConfig.policy) assertEquals("Policy id does not match", policy.id, managedIndexConfig.policyID) // speed up to first execution where we initialize the policy on the job @@ -340,7 +339,8 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { assertPredicatesOnMetaData( listOf( index to listOf( - ManagedIndexSettings.POLICY_ID.key to policy.id::equals, + explainResponseOpendistroPolicyIdSetting to policy.id::equals, + explainResponseOpenSearchPolicyIdSetting to policy.id::equals, ManagedIndexMetaData.INDEX to executedManagedIndexConfig.index::equals, ManagedIndexMetaData.INDEX_UUID to executedManagedIndexConfig.indexUuid::equals, ManagedIndexMetaData.POLICY_ID to executedManagedIndexConfig.policyID::equals, @@ -384,7 +384,8 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { assertPredicatesOnMetaData( listOf( index to listOf( - ManagedIndexSettings.POLICY_ID.key to policy.id::equals, + explainResponseOpendistroPolicyIdSetting to policy.id::equals, + explainResponseOpenSearchPolicyIdSetting to policy.id::equals, ManagedIndexMetaData.INDEX to executedManagedIndexConfig.index::equals, ManagedIndexMetaData.INDEX_UUID to executedManagedIndexConfig.indexUuid::equals, ManagedIndexMetaData.POLICY_ID to executedManagedIndexConfig.policyID::equals, @@ -422,7 +423,8 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { assertPredicatesOnMetaData( listOf( index to listOf( - ManagedIndexSettings.POLICY_ID.key to newPolicy.id::equals, + explainResponseOpendistroPolicyIdSetting to newPolicy.id::equals, + explainResponseOpenSearchPolicyIdSetting to newPolicy.id::equals, ManagedIndexMetaData.INDEX to changedManagedIndexConfig.index::equals, ManagedIndexMetaData.INDEX_UUID to changedManagedIndexConfig.indexUuid::equals, ManagedIndexMetaData.POLICY_ID to changedManagedIndexConfig.policyID::equals, @@ -466,11 +468,15 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { // speed up to third execution where we transition to second state updateManagedIndexConfigStartTime(firstManagedIndexConfig) + logger.info("time before check") waitFor { - getExplainManagedIndexMetaData(firstIndex).let { - assertEquals(it.copy(stateMetaData = it.stateMetaData?.copy(name = secondState.name)), it) - } +// getExplainManagedIndexMetaData(firstIndex).let { +// assertEquals(it.copy(stateMetaData = it.stateMetaData?.copy(name = secondState.name)), it) +// } + assertEquals(secondState.name, getExplainManagedIndexMetaData(firstIndex).stateMetaData?.name) + logger.info("Explain firstIndex before change policy: ${getExplainManagedIndexMetaData(firstIndex)}") } + logger.info("time after check") // create second index val (secondIndex) = createIndex("second_index", policy.id) @@ -493,7 +499,10 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { FAILED_INDICES to emptyList(), UPDATED_INDICES to 1 ) - assertAffectedIndicesResponseIsEqual(expectedResponse, response.asMap()) + // TODO flaky part, log for more info + val responseMap = response.asMap() + logger.info("Change policy response: $responseMap") + assertAffectedIndicesResponseIsEqual(expectedResponse, responseMap) waitFor { // The first managed index should not have a change policy added to it as it should of been filtered out from the states filter @@ -516,7 +525,6 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { val managedIndexConfig = getExistingManagedIndexConfig(index) assertNull("Change policy is not null", managedIndexConfig.changePolicy) - assertNull("Policy has already initialized", managedIndexConfig.policy) assertEquals("Policy id does not match", policy.id, managedIndexConfig.policyID) // if we try to change policy now, it'll have no ManagedIndexMetaData yet and should succeed @@ -633,65 +641,4 @@ class RestChangePolicyActionIT : IndexStateManagementRestTestCase() { ) } } - - fun `test changing failed init policy`() { - /* - * 1. User adds nonexistent policy_id - * 2. ManagedIndexConfig fails because policy does not exist - * 3. Calls ChangePolicy API to switch to new policy and then calls Retry API - * 4. ChangePolicy should be successful with the new policy initialized - * */ - - val indexName = "${testIndexName}_safe_2" - val policy = createRandomPolicy() - val (index) = createIndex(indexName, "doesnt_exist_policy_id") - - val managedIndexConfig = getExistingManagedIndexConfig(index) - - // Change the start time so the job will trigger in 2 seconds and fail initializing policy - updateManagedIndexConfigStartTime(managedIndexConfig) - waitFor { - assertEquals(true, getExplainManagedIndexMetaData(index).policyRetryInfo?.failed) - assertNull(getExistingManagedIndexConfig(index).policy) - } - - val changePolicy = ChangePolicy(policy.id, null, emptyList(), false) - val response = client().makeRequest( - RestRequest.Method.POST.toString(), - "${RestChangePolicyAction.CHANGE_POLICY_BASE_URI}/$index", emptyMap(), changePolicy.toHttpEntity() - ) - val expectedResponse = mapOf( - FAILURES to false, - FAILED_INDICES to emptyList(), - UPDATED_INDICES to 1 - ) - assertAffectedIndicesResponseIsEqual(expectedResponse, response.asMap()) - - // the change policy REST API should set the ChangePolicy on the config job - waitFor { assertEquals(policy.id, getManagedIndexConfigByDocId(managedIndexConfig.id)?.changePolicy?.policyID) } - - // retry failed index - val retryResponse = client().makeRequest( - RestRequest.Method.POST.toString(), - "${RestRetryFailedManagedIndexAction.RETRY_BASE_URI}/$indexName" - ) - assertEquals("Unexpected RestStatus", RestStatus.OK, retryResponse.restStatus()) - val expectedErrorMessage = mapOf( - UPDATED_INDICES to 1, - FAILURES to false, - FAILED_INDICES to emptyList>() - ) - assertAffectedIndicesResponseIsEqual(expectedErrorMessage, retryResponse.asMap()) - - // speed up to next execution where we should again attempt to init policy, but use the change policy which should exist - updateManagedIndexConfigStartTime(managedIndexConfig) - - waitFor { - assertNull(getManagedIndexConfigByDocId(managedIndexConfig.id)?.changePolicy) - assertNotNull(getManagedIndexConfigByDocId(managedIndexConfig.id)?.policy) - assertEquals(policy.id, getManagedIndexConfigByDocId(managedIndexConfig.id)?.policyID) - assertEquals(policy.id, getExplainManagedIndexMetaData(indexName).policyID) - assertEquals("Successfully initialized policy: ${policy.id}", getExplainManagedIndexMetaData(indexName).info?.get("message")) - } - } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestExplainActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestExplainActionIT.kt index 5abc31002..41ecd585c 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestExplainActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestExplainActionIT.kt @@ -26,12 +26,16 @@ package org.opensearch.indexmanagement.indexstatemanagement.resthandler +import org.opensearch.indexmanagement.IndexManagementPlugin import org.opensearch.indexmanagement.indexstatemanagement.IndexStateManagementRestTestCase +import org.opensearch.indexmanagement.indexstatemanagement.model.ChangePolicy import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.PolicyRetryInfoMetaData import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.StateMetaData -import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings +import org.opensearch.indexmanagement.makeRequest import org.opensearch.indexmanagement.waitFor +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestStatus import java.time.Instant import java.util.Locale @@ -44,7 +48,8 @@ class RestExplainActionIT : IndexStateManagementRestTestCase() { createIndex(indexName, null) val expected = mapOf( indexName to mapOf( - ManagedIndexSettings.POLICY_ID.key to null + explainResponseOpendistroPolicyIdSetting to null, + explainResponseOpenSearchPolicyIdSetting to null ) ) assertResponseMap(expected, getExplainMap(indexName)) @@ -69,13 +74,15 @@ class RestExplainActionIT : IndexStateManagementRestTestCase() { val expected = mapOf( indexName1 to mapOf( - ManagedIndexSettings.POLICY_ID.key to policy.id, + explainResponseOpendistroPolicyIdSetting to policy.id, + explainResponseOpenSearchPolicyIdSetting to policy.id, "index" to indexName1, "index_uuid" to getUuid(indexName1), "policy_id" to policy.id ), indexName2 to mapOf( - ManagedIndexSettings.POLICY_ID.key to null + explainResponseOpendistroPolicyIdSetting to null, + explainResponseOpenSearchPolicyIdSetting to null ) ) waitFor { @@ -93,7 +100,8 @@ class RestExplainActionIT : IndexStateManagementRestTestCase() { val expected = mapOf( indexName1 to mapOf( - ManagedIndexSettings.POLICY_ID.key to policy.id, + explainResponseOpendistroPolicyIdSetting to policy.id, + explainResponseOpenSearchPolicyIdSetting to policy.id, "index" to indexName1, "index_uuid" to getUuid(indexName1), "policy_id" to policy.id, @@ -116,19 +124,22 @@ class RestExplainActionIT : IndexStateManagementRestTestCase() { createIndex(indexName3, null) val expected = mapOf( indexName1 to mapOf( - ManagedIndexSettings.POLICY_ID.key to policy.id, + explainResponseOpendistroPolicyIdSetting to policy.id, + explainResponseOpenSearchPolicyIdSetting to policy.id, "index" to indexName1, "index_uuid" to getUuid(indexName1), "policy_id" to policy.id ), indexName2 to mapOf( - ManagedIndexSettings.POLICY_ID.key to policy.id, + explainResponseOpendistroPolicyIdSetting to policy.id, + explainResponseOpenSearchPolicyIdSetting to policy.id, "index" to indexName2, "index_uuid" to getUuid(indexName2), "policy_id" to policy.id ), indexName3 to mapOf( - ManagedIndexSettings.POLICY_ID.key to null + explainResponseOpendistroPolicyIdSetting to null, + explainResponseOpenSearchPolicyIdSetting to null ) ) waitFor { @@ -151,7 +162,8 @@ class RestExplainActionIT : IndexStateManagementRestTestCase() { assertPredicatesOnMetaData( listOf( indexName to listOf( - ManagedIndexSettings.POLICY_ID.key to policy.id::equals, + explainResponseOpendistroPolicyIdSetting to policy.id::equals, + explainResponseOpenSearchPolicyIdSetting to policy.id::equals, ManagedIndexMetaData.INDEX to managedIndexConfig.index::equals, ManagedIndexMetaData.INDEX_UUID to managedIndexConfig.indexUuid::equals, ManagedIndexMetaData.POLICY_ID to managedIndexConfig.policyID::equals, @@ -174,22 +186,34 @@ class RestExplainActionIT : IndexStateManagementRestTestCase() { fun `test failed policy`() { val indexName = "${testIndexName}_melon" - val policyID = "${testIndexName}_does_not_exist" - createIndex(indexName, policyID) + val policy = createRandomPolicy() + createIndex(indexName, policy.id) + val newPolicy = createRandomPolicy() + val changePolicy = ChangePolicy(newPolicy.id, null, emptyList(), false) + client().makeRequest( + RestRequest.Method.POST.toString(), + "${RestChangePolicyAction.CHANGE_POLICY_BASE_URI}/$indexName", emptyMap(), changePolicy.toHttpEntity() + ) + val deletePolicyResponse = client().makeRequest( + RestRequest.Method.DELETE.toString(), + "${IndexManagementPlugin.LEGACY_POLICY_BASE_URI}/${changePolicy.policyID}" + ) + assertEquals("Unexpected RestStatus", RestStatus.OK, deletePolicyResponse.restStatus()) val managedIndexConfig = getExistingManagedIndexConfig(indexName) // change the start time so the job will trigger in 2 seconds. updateManagedIndexConfigStartTime(managedIndexConfig) waitFor { - val expectedInfoString = mapOf("message" to "Fail to load policy: $policyID").toString() + val expectedInfoString = mapOf("message" to "Fail to load policy: ${changePolicy.policyID}").toString() assertPredicatesOnMetaData( listOf( indexName to listOf( - ManagedIndexSettings.POLICY_ID.key to policyID::equals, + explainResponseOpendistroPolicyIdSetting to policy.id::equals, + explainResponseOpenSearchPolicyIdSetting to policy.id::equals, ManagedIndexMetaData.INDEX to managedIndexConfig.index::equals, ManagedIndexMetaData.INDEX_UUID to managedIndexConfig.indexUuid::equals, - ManagedIndexMetaData.POLICY_ID to managedIndexConfig.policyID::equals, + ManagedIndexMetaData.POLICY_ID to newPolicy.id::equals, PolicyRetryInfoMetaData.RETRY_INFO to fun(retryInfoMetaDataMap: Any?): Boolean = assertRetryInfoEquals(PolicyRetryInfoMetaData(true, 0), retryInfoMetaDataMap), ManagedIndexMetaData.INFO to fun(info: Any?): Boolean = expectedInfoString == info.toString() diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRemovePolicyActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRemovePolicyActionIT.kt index bc556ef39..7af04bf35 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRemovePolicyActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRemovePolicyActionIT.kt @@ -27,7 +27,9 @@ package org.opensearch.indexmanagement.indexstatemanagement.resthandler import org.opensearch.client.ResponseException +import org.opensearch.cluster.metadata.IndexMetadata import org.opensearch.indexmanagement.indexstatemanagement.IndexStateManagementRestTestCase +import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings import org.opensearch.indexmanagement.indexstatemanagement.util.FAILED_INDICES import org.opensearch.indexmanagement.indexstatemanagement.util.FAILURES import org.opensearch.indexmanagement.indexstatemanagement.util.UPDATED_INDICES @@ -61,7 +63,8 @@ class RestRemovePolicyActionIT : IndexStateManagementRestTestCase() { fun `test closed index`() { val index = "movies" - createIndex(index, "somePolicy") + val policy = createRandomPolicy() + createIndex(index, policy.id) closeIndex(index) val response = client().makeRequest( @@ -114,8 +117,8 @@ class RestRemovePolicyActionIT : IndexStateManagementRestTestCase() { fun `test index list`() { val indexOne = "movies_1" val indexTwo = "movies_2" - - createIndex(indexOne, "somePolicy") + val policy = createRandomPolicy() + createIndex(indexOne, policy.id) createIndex(indexTwo, null) closeIndex(indexOne) @@ -151,10 +154,10 @@ class RestRemovePolicyActionIT : IndexStateManagementRestTestCase() { val indexOne = "movies_1" val indexTwo = "movies_2" val indexThree = "movies_3" - - createIndex(indexOne, "somePolicy") + val policy = createRandomPolicy() + createIndex(indexOne, policy.id) createIndex(indexTwo, null) - createIndex(indexThree, "somePolicy") + createIndex(indexThree, policy.id) closeIndex(indexOne) @@ -188,4 +191,49 @@ class RestRemovePolicyActionIT : IndexStateManagementRestTestCase() { assertEquals(null, getPolicyIDOfManagedIndex(indexThree)) } } + + fun `test remove policy on read only index update auto_manage setting`() { + val index1 = "read_only_index" + val index2 = "read_only_allow_delete_index" + val index3 = "normal_index" + val index4 = "auto_manage_false_index" + val indexPattern = "*index" + val policy = createRandomPolicy() + createIndex(index1, policy.id) + createIndex(index2, policy.id) + createIndex(index3, policy.id) + createIndex(index4, policy.id) + updateIndexSetting(index1, IndexMetadata.SETTING_READ_ONLY, "true") + updateIndexSetting(index2, IndexMetadata.SETTING_READ_ONLY_ALLOW_DELETE, "true") + updateIndexSetting(index4, ManagedIndexSettings.AUTO_MANAGE.key, "false") + + val response = client().makeRequest( + POST.toString(), + "${RestRemovePolicyAction.REMOVE_POLICY_BASE_URI}/$indexPattern" + ) + assertEquals("Unexpected RestStatus", RestStatus.OK, response.restStatus()) + val actualMessage = response.asMap() + val expectedMessage = mapOf( + UPDATED_INDICES to 4, + FAILURES to false, + FAILED_INDICES to emptyList() + ) + assertAffectedIndicesResponseIsEqual(expectedMessage, actualMessage) + + waitFor { + assertEquals("auto manage setting not false after removing policy for index $index1", false, getIndexAutoManageSetting(index1)) + assertEquals("read only allow delete setting changed after removing policy for index $index1", null, getIndexReadOnlyAllowDeleteSetting(index1)) + assertEquals("auto manage setting not false after removing policy for index $index2", false, getIndexAutoManageSetting(index2)) + assertEquals("read only setting changed after removing policy for index $index2", null, getIndexReadOnlySetting(index2)) + assertEquals("auto manage setting not false after removing policy for index $index3", false, getIndexAutoManageSetting(index3)) + assertEquals("read only setting changed after removing policy for index $index3", null, getIndexReadOnlySetting(index3)) + assertEquals("read only allow delete setting changed after removing policy for index $index3", null, getIndexReadOnlyAllowDeleteSetting(index3)) + assertEquals("auto manage setting not false after removing policy for index $index4", false, getIndexAutoManageSetting(index3)) + assertEquals("read only setting changed after removing policy for index $index4", null, getIndexReadOnlySetting(index3)) + assertEquals("read only allow delete setting changed after removing policy for index $index4", null, getIndexReadOnlyAllowDeleteSetting(index3)) + } + + // otherwise, test cleanup cannot delete this index + updateIndexSetting(index1, IndexMetadata.SETTING_READ_ONLY, "false") + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRetryFailedManagedIndexActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRetryFailedManagedIndexActionIT.kt index 0bc390bd1..6f286a538 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRetryFailedManagedIndexActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/resthandler/RestRetryFailedManagedIndexActionIT.kt @@ -29,10 +29,12 @@ package org.opensearch.indexmanagement.indexstatemanagement.resthandler import org.opensearch.client.ResponseException import org.opensearch.indexmanagement.indexstatemanagement.IndexStateManagementRestTestCase import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData +import org.opensearch.indexmanagement.indexstatemanagement.model.action.AllocationActionConfig import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.ActionMetaData import org.opensearch.indexmanagement.indexstatemanagement.randomForceMergeActionConfig import org.opensearch.indexmanagement.indexstatemanagement.randomPolicy import org.opensearch.indexmanagement.indexstatemanagement.randomState +import org.opensearch.indexmanagement.indexstatemanagement.step.Step import org.opensearch.indexmanagement.indexstatemanagement.util.FAILED_INDICES import org.opensearch.indexmanagement.indexstatemanagement.util.FAILURES import org.opensearch.indexmanagement.indexstatemanagement.util.UPDATED_INDICES @@ -73,8 +75,9 @@ class RestRetryFailedManagedIndexActionIT : IndexStateManagementRestTestCase() { val indexName1 = "${indexName}_1" val indexName2 = "${indexName}_2" val indexName3 = "${testIndexName}_some_other_test" + val policy = createRandomPolicy() createIndex(indexName, null) - createIndex(indexName1, "somePolicy") + createIndex(indexName1, policy.id) createIndex(indexName2, null) createIndex(indexName3, null) @@ -108,9 +111,10 @@ class RestRetryFailedManagedIndexActionIT : IndexStateManagementRestTestCase() { val indexName1 = "${indexName}_1" val indexName2 = "${indexName}_2" val indexName3 = "${testIndexName}_some_other_test_2" + val policy = createRandomPolicy() createIndex(indexName, null) createIndex(indexName1, null) - createIndex(indexName2, "somePolicy") + createIndex(indexName2, policy.id) createIndex(indexName3, null) val response = client().makeRequest( @@ -169,7 +173,8 @@ class RestRetryFailedManagedIndexActionIT : IndexStateManagementRestTestCase() { fun `test index has no metadata`() { val indexName = "${testIndexName}_players" - createIndex(indexName, "somePolicy") + val policy = createRandomPolicy() + createIndex(indexName, policy.id) val response = client().makeRequest( RestRequest.Method.POST.toString(), @@ -224,11 +229,29 @@ class RestRetryFailedManagedIndexActionIT : IndexStateManagementRestTestCase() { fun `test index failed`() { val indexName = "${testIndexName}_blueberry" - createIndex(indexName, "invalid_policy") + val states = listOf( + randomState().copy( + transitions = listOf(), + actions = listOf(AllocationActionConfig(require = mapOf("..//" to "value"), exclude = emptyMap(), include = emptyMap(), index = 0)) + ) + ) + val invalidPolicy = randomPolicy().copy( + states = states, + defaultState = states[0].name + ) + createPolicy(invalidPolicy, invalidPolicy.id) + createIndex(indexName, invalidPolicy.id) val managedIndexConfig = getExistingManagedIndexConfig(indexName) // change the start time so the job will trigger in 2 seconds. updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { assertEquals(invalidPolicy.id, getExplainManagedIndexMetaData(indexName).policyID) } + + // Change the start time so we attempt allocation that is intended to fail + updateManagedIndexConfigStartTime(managedIndexConfig) + waitFor { + assertEquals(Step.StepStatus.FAILED, getExplainManagedIndexMetaData(indexName).stepMetaData?.stepStatus) + } waitFor { val response = client().makeRequest( diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerIT.kt index 9e7251f9c..51c88cfb1 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerIT.kt @@ -107,20 +107,41 @@ class ManagedIndexRunnerIT : IndexStateManagementRestTestCase() { val managedIndexConfig = getExistingManagedIndexConfig(indexName) assertEquals( - "Created managed index did not default to ${ManagedIndexSettings.DEFAULT_JOB_INTERVAL}minutes", + "Created managed index did not default to ${ManagedIndexSettings.DEFAULT_JOB_INTERVAL} minutes", ManagedIndexSettings.DEFAULT_JOB_INTERVAL, (managedIndexConfig.jobSchedule as IntervalSchedule).interval ) // init policy updateManagedIndexConfigStartTime(managedIndexConfig) - waitFor { assertEquals(createdPolicy.id, getManagedIndexConfigByDocId(managedIndexConfig.id)?.policyID) } + waitFor { + assertEquals(createdPolicy.id, getManagedIndexConfigByDocId(managedIndexConfig.id)?.policyID) + val currInterval = (getManagedIndexConfigByDocId(managedIndexConfig.id)?.jobSchedule as? IntervalSchedule)?.interval + assertEquals("Managed index was not created with default job interval", ManagedIndexSettings.DEFAULT_JOB_INTERVAL, currInterval) + } // change cluster job interval setting to 2 (minutes) - updateClusterSetting(ManagedIndexSettings.JOB_INTERVAL.key, "2") + val newJobInterval = 2 + updateClusterSetting(ManagedIndexSettings.JOB_INTERVAL.key, newJobInterval.toString()) - // fast forward to next execution where at the end we should change the job interval time - updateManagedIndexConfigStartTime(managedIndexConfig) - waitFor { (getManagedIndexConfigByDocId(managedIndexConfig.id)?.jobSchedule as? IntervalSchedule)?.interval == 2 } + // Create a new index and policy to check if they have the updated interval + val newIndexName = indexName + "new" + val newCreatedPolicy = createRandomPolicy() + createIndex(newIndexName, newCreatedPolicy.id) + + val newManagedIndexConfig = getExistingManagedIndexConfig(newIndexName) + + assertEquals( + "New managed index did not have updated job schedule interval", + newJobInterval, (newManagedIndexConfig.jobSchedule as IntervalSchedule).interval + ) + + // init new policy + updateManagedIndexConfigStartTime(newManagedIndexConfig) + waitFor { + assertEquals(newCreatedPolicy.id, getManagedIndexConfigByDocId(newManagedIndexConfig.id)?.policyID) + val currInterval = (getManagedIndexConfigByDocId(newManagedIndexConfig.id)?.jobSchedule as? IntervalSchedule)?.interval + assertEquals("Failed to update ManagedIndexConfig interval", newJobInterval, currInterval) + } } fun `test allow list fails execution`() { @@ -171,4 +192,42 @@ class ManagedIndexRunnerIT : IndexStateManagementRestTestCase() { updateManagedIndexConfigStartTime(managedIndexConfig) waitFor { assertEquals("Attempted to execute action=read_only which is not allowed.", getExplainManagedIndexMetaData(indexName).info?.get("message")) } } + + fun `test jitter changing`() { + val indexName = "jitter_index_" + + val createdPolicy = createRandomPolicy() + createIndex(indexName, createdPolicy.id) + + val managedIndexConfig = getExistingManagedIndexConfig(indexName) + assertEquals( + "Created managed index did not default to 0.0", 0.0, managedIndexConfig.jitter + ) + + waitFor { + assertEquals(createdPolicy.id, getManagedIndexConfigByDocId(managedIndexConfig.id)?.policyID) + val currJitter = getManagedIndexConfigByDocId(managedIndexConfig.id)?.jitter + assertEquals("Managed index was not created with 0.0 jitter", 0.0, currJitter) + } + + // change jitter to 0.5 + val newJitter = 0.5 + updateIndexStateManagementJitterSetting(newJitter) + + // Create a new index and policy to check if they have the updated jitter + val newIndexName = indexName + "new" + val newCreatedPolicy = createRandomPolicy() + createIndex(newIndexName, newCreatedPolicy.id) + + val newManagedIndexConfig = getExistingManagedIndexConfig(newIndexName) + assertEquals( + "New managed index did not have updated jitter", newJitter, newManagedIndexConfig.jitter + ) + + waitFor { + assertEquals(newCreatedPolicy.id, getManagedIndexConfigByDocId(newManagedIndexConfig.id)?.policyID) + val currJitter = getManagedIndexConfigByDocId(newManagedIndexConfig.id)?.jitter + assertEquals("Failed to update ManagedIndexConfig jitter", newJitter, currJitter) + } + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerTests.kt index 74ea950cd..d4cc24abb 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/runner/ManagedIndexRunnerTests.kt @@ -82,6 +82,7 @@ class ManagedIndexRunnerTests : OpenSearchTestCase() { val settingSet = hashSetOf>() settingSet.addAll(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) settingSet.add(ManagedIndexSettings.SWEEP_PERIOD) + settingSet.add(ManagedIndexSettings.JITTER) settingSet.add(ManagedIndexSettings.JOB_INTERVAL) settingSet.add(ManagedIndexSettings.INDEX_STATE_MANAGEMENT_ENABLED) settingSet.add(ManagedIndexSettings.ALLOW_LIST) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/AttemptSnapshotStepTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/AttemptSnapshotStepTests.kt index 1c5cc258c..76eceb443 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/AttemptSnapshotStepTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/AttemptSnapshotStepTests.kt @@ -14,6 +14,7 @@ package org.opensearch.indexmanagement.indexstatemanagement.step import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doAnswer import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.runBlocking @@ -27,12 +28,15 @@ import org.opensearch.cluster.service.ClusterService import org.opensearch.common.settings.ClusterSettings import org.opensearch.common.settings.Settings import org.opensearch.indexmanagement.indexstatemanagement.model.ManagedIndexMetaData -import org.opensearch.indexmanagement.indexstatemanagement.model.action.SnapshotActionConfig import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.ActionMetaData import org.opensearch.indexmanagement.indexstatemanagement.model.managedindexmetadata.ActionProperties +import org.opensearch.indexmanagement.indexstatemanagement.randomSnapshotActionConfig import org.opensearch.indexmanagement.indexstatemanagement.settings.ManagedIndexSettings.Companion.SNAPSHOT_DENY_LIST import org.opensearch.indexmanagement.indexstatemanagement.step.snapshot.AttemptSnapshotStep +import org.opensearch.ingest.TestTemplateService.MockTemplateScript import org.opensearch.rest.RestStatus +import org.opensearch.script.ScriptService +import org.opensearch.script.TemplateScript import org.opensearch.snapshots.ConcurrentSnapshotExecutionException import org.opensearch.test.OpenSearchTestCase import org.opensearch.transport.RemoteTransportException @@ -40,12 +44,14 @@ import org.opensearch.transport.RemoteTransportException class AttemptSnapshotStepTests : OpenSearchTestCase() { private val clusterService: ClusterService = mock() - private val config = SnapshotActionConfig("repo", "snapshot-name", 0) + private val scriptService: ScriptService = mock() + private val config = randomSnapshotActionConfig("repo", "snapshot-name") private val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(AttemptSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) @Before fun settings() { whenever(clusterService.clusterSettings).doReturn(ClusterSettings(Settings.EMPTY, setOf(SNAPSHOT_DENY_LIST))) + whenever(scriptService.compile(any(), eq(TemplateScript.CONTEXT))).doReturn(MockTemplateScript.Factory("snapshot-name")) } fun `test snapshot response when block`() { @@ -54,7 +60,7 @@ class AttemptSnapshotStepTests : OpenSearchTestCase() { whenever(response.status()).doReturn(RestStatus.ACCEPTED) runBlocking { - val step = AttemptSnapshotStep(clusterService, client, config, metadata) + val step = AttemptSnapshotStep(clusterService, scriptService, client, config, metadata) step.execute() val updatedManagedIndexMetaData = step.getUpdatedManagedIndexMetaData(metadata) assertEquals("Step status is not COMPLETED", Step.StepStatus.COMPLETED, updatedManagedIndexMetaData.stepMetaData?.stepStatus) @@ -62,7 +68,7 @@ class AttemptSnapshotStepTests : OpenSearchTestCase() { whenever(response.status()).doReturn(RestStatus.OK) runBlocking { - val step = AttemptSnapshotStep(clusterService, client, config, metadata) + val step = AttemptSnapshotStep(clusterService, scriptService, client, config, metadata) step.execute() val updatedManagedIndexMetaData = step.getUpdatedManagedIndexMetaData(metadata) assertEquals("Step status is not COMPLETED", Step.StepStatus.COMPLETED, updatedManagedIndexMetaData.stepMetaData?.stepStatus) @@ -70,7 +76,7 @@ class AttemptSnapshotStepTests : OpenSearchTestCase() { whenever(response.status()).doReturn(RestStatus.INTERNAL_SERVER_ERROR) runBlocking { - val step = AttemptSnapshotStep(clusterService, client, config, metadata) + val step = AttemptSnapshotStep(clusterService, scriptService, client, config, metadata) step.execute() val updatedManagedIndexMetaData = step.getUpdatedManagedIndexMetaData(metadata) assertEquals("Step status is not FAILED", Step.StepStatus.FAILED, updatedManagedIndexMetaData.stepMetaData?.stepStatus) @@ -81,7 +87,7 @@ class AttemptSnapshotStepTests : OpenSearchTestCase() { val exception = IllegalArgumentException("example") val client = getClient(getAdminClient(getClusterAdminClient(null, exception))) runBlocking { - val step = AttemptSnapshotStep(clusterService, client, config, metadata) + val step = AttemptSnapshotStep(clusterService, scriptService, client, config, metadata) step.execute() val updatedManagedIndexMetaData = step.getUpdatedManagedIndexMetaData(metadata) assertEquals("Step status is not FAILED", Step.StepStatus.FAILED, updatedManagedIndexMetaData.stepMetaData?.stepStatus) @@ -93,7 +99,7 @@ class AttemptSnapshotStepTests : OpenSearchTestCase() { val exception = ConcurrentSnapshotExecutionException("repo", "other-snapshot", "concurrent snapshot in progress") val client = getClient(getAdminClient(getClusterAdminClient(null, exception))) runBlocking { - val step = AttemptSnapshotStep(clusterService, client, config, metadata) + val step = AttemptSnapshotStep(clusterService, scriptService, client, config, metadata) step.execute() val updatedManagedIndexMetaData = step.getUpdatedManagedIndexMetaData(metadata) assertEquals("Step status is not CONDITION_NOT_MET", Step.StepStatus.CONDITION_NOT_MET, updatedManagedIndexMetaData.stepMetaData?.stepStatus) @@ -105,7 +111,7 @@ class AttemptSnapshotStepTests : OpenSearchTestCase() { val exception = RemoteTransportException("rte", ConcurrentSnapshotExecutionException("repo", "other-snapshot", "concurrent snapshot in progress")) val client = getClient(getAdminClient(getClusterAdminClient(null, exception))) runBlocking { - val step = AttemptSnapshotStep(clusterService, client, config, metadata) + val step = AttemptSnapshotStep(clusterService, scriptService, client, config, metadata) step.execute() val updatedManagedIndexMetaData = step.getUpdatedManagedIndexMetaData(metadata) assertEquals("Step status is not CONDITION_NOT_MET", Step.StepStatus.CONDITION_NOT_MET, updatedManagedIndexMetaData.stepMetaData?.stepStatus) @@ -117,7 +123,7 @@ class AttemptSnapshotStepTests : OpenSearchTestCase() { val exception = RemoteTransportException("rte", IllegalArgumentException("some error")) val client = getClient(getAdminClient(getClusterAdminClient(null, exception))) runBlocking { - val step = AttemptSnapshotStep(clusterService, client, config, metadata) + val step = AttemptSnapshotStep(clusterService, scriptService, client, config, metadata) step.execute() val updatedManagedIndexMetaData = step.getUpdatedManagedIndexMetaData(metadata) assertEquals("Step status is not FAILED", Step.StepStatus.FAILED, updatedManagedIndexMetaData.stepMetaData?.stepStatus) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/WaitForSnapshotStepTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/WaitForSnapshotStepTests.kt index 55eafd85d..5952a68ce 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/WaitForSnapshotStepTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/step/WaitForSnapshotStepTests.kt @@ -53,13 +53,14 @@ import org.opensearch.transport.RemoteTransportException class WaitForSnapshotStepTests : OpenSearchTestCase() { private val clusterService: ClusterService = mock() + val snapshot = "snapshot-name" fun `test snapshot missing snapshot name in action properties`() { val exception = IllegalArgumentException("not used") val client = getClient(getAdminClient(getClusterAdminClient(null, exception))) runBlocking { val emptyActionProperties = ActionProperties() - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, emptyActionProperties), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -70,7 +71,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { runBlocking { val nullActionProperties = null - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, nullActionProperties), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -89,7 +90,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { whenever(snapshotStatus.state).doReturn(SnapshotsInProgress.State.INIT) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -100,7 +101,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { whenever(snapshotStatus.state).doReturn(SnapshotsInProgress.State.STARTED) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -111,7 +112,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { whenever(snapshotStatus.state).doReturn(SnapshotsInProgress.State.SUCCESS) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -122,7 +123,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { whenever(snapshotStatus.state).doReturn(SnapshotsInProgress.State.ABORTED) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -133,7 +134,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { whenever(snapshotStatus.state).doReturn(SnapshotsInProgress.State.FAILED) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -151,7 +152,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { val client = getClient(getAdminClient(getClusterAdminClient(response, null))) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -165,7 +166,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { val exception = IllegalArgumentException("example") val client = getClient(getAdminClient(getClusterAdminClient(null, exception))) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() @@ -179,7 +180,7 @@ class WaitForSnapshotStepTests : OpenSearchTestCase() { val exception = RemoteTransportException("rte", IllegalArgumentException("nested")) val client = getClient(getAdminClient(getClusterAdminClient(null, exception))) runBlocking { - val config = SnapshotActionConfig("repo", "snapshot-name", 0) + val config = SnapshotActionConfig("repo", snapshot, 0) val metadata = ManagedIndexMetaData("test", "indexUuid", "policy_id", null, null, null, null, null, null, ActionMetaData(WaitForSnapshotStep.name, 1, 0, false, 0, null, ActionProperties(snapshotName = "snapshot-name")), null, null, null) val step = WaitForSnapshotStep(clusterService, client, config, metadata) step.execute() diff --git a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtilsTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtilsTests.kt index ebb9821a3..8b58b0b01 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtilsTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/indexstatemanagement/util/ManagedIndexUtilsTests.kt @@ -60,7 +60,7 @@ class ManagedIndexUtilsTests : OpenSearchTestCase() { val index = randomAlphaOfLength(10) val uuid = randomAlphaOfLength(10) val policyID = randomAlphaOfLength(10) - val createRequest = managedIndexConfigIndexRequest(index, uuid, policyID, 5) + val createRequest = managedIndexConfigIndexRequest(index, uuid, policyID, 5, jobJitter = 0.0) assertNotNull("IndexRequest not created", createRequest) assertEquals("Incorrect ism index used in request", INDEX_MANAGEMENT_INDEX, createRequest.index()) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/RollupRestTestCase.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/RollupRestTestCase.kt index a234f6902..20e3b382c 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/RollupRestTestCase.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/RollupRestTestCase.kt @@ -48,6 +48,7 @@ import org.opensearch.indexmanagement.common.model.dimension.Dimension import org.opensearch.indexmanagement.makeRequest import org.opensearch.indexmanagement.rollup.model.Rollup import org.opensearch.indexmanagement.rollup.model.RollupMetadata +import org.opensearch.indexmanagement.rollup.settings.RollupSettings import org.opensearch.indexmanagement.util._ID import org.opensearch.indexmanagement.util._PRIMARY_TERM import org.opensearch.indexmanagement.util._SEQ_NO @@ -233,4 +234,20 @@ abstract class RollupRestTestCase : IndexManagementRestTestCase() { assertEquals("Request failed", RestStatus.OK, response.restStatus()) } + + protected fun updateSearchAllJobsClusterSetting(value: Boolean) { + val formattedValue = "\"${value}\"" + val request = """ + { + "persistent": { + "${RollupSettings.ROLLUP_SEARCH_ALL_JOBS.key}": $formattedValue + } + } + """.trimIndent() + val res = client().makeRequest( + "PUT", "_cluster/settings", emptyMap(), + StringEntity(request, APPLICATION_JSON) + ) + assertEquals("Request failed", RestStatus.OK, res.restStatus()) + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/TestHelpers.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/TestHelpers.kt index 24c688713..dc9062acd 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/TestHelpers.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/TestHelpers.kt @@ -36,6 +36,7 @@ import org.opensearch.indexmanagement.common.model.dimension.Terms import org.opensearch.indexmanagement.opensearchapi.string import org.opensearch.indexmanagement.randomInstant import org.opensearch.indexmanagement.randomSchedule +import org.opensearch.indexmanagement.randomUser import org.opensearch.indexmanagement.rollup.actionfilter.ISMFieldCapabilities import org.opensearch.indexmanagement.rollup.actionfilter.ISMFieldCapabilitiesIndexResponse import org.opensearch.indexmanagement.rollup.actionfilter.ISMFieldCapabilitiesResponse @@ -128,10 +129,11 @@ fun randomRollup(): Rollup { metadataID = if (OpenSearchRestTestCase.randomBoolean()) null else OpenSearchRestTestCase.randomAlphaOfLength(10), roles = OpenSearchRestTestCase.randomList(10) { OpenSearchRestTestCase.randomAlphaOfLength(10) }, pageSize = OpenSearchRestTestCase.randomIntBetween(1, 10000), - delay = OpenSearchRestTestCase.randomNonNegativeLong(), + delay = 0, continuous = OpenSearchRestTestCase.randomBoolean(), dimensions = randomRollupDimensions(), - metrics = OpenSearchRestTestCase.randomList(20, ::randomRollupMetrics).distinctBy { it.targetField } + metrics = OpenSearchRestTestCase.randomList(20, ::randomRollupMetrics).distinctBy { it.targetField }, + user = randomUser() ) } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptorIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptorIT.kt index 1d51e0699..872cccb0a 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptorIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/interceptor/RollupInterceptorIT.kt @@ -663,4 +663,151 @@ class RollupInterceptorIT : RollupRestTestCase() { rollupAggRes.getValue("min_passenger_count")["value"] ) } + + fun `test rollup search all jobs`() { + generateNYCTaxiData("source_rollup_search_all_jobs_1") + generateNYCTaxiData("source_rollup_search_all_jobs_2") + val targetIndex = "target_rollup_search_all_jobs" + val rollupHourly = Rollup( + id = "hourly_basic_term_query_rollup_search_all", + enabled = true, + schemaVersion = 1L, + jobSchedule = IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES), + jobLastUpdatedTime = Instant.now(), + jobEnabledTime = Instant.now(), + description = "basic search test", + sourceIndex = "source_rollup_search_all_jobs_1", + targetIndex = targetIndex, + metadataID = null, + roles = emptyList(), + pageSize = 10, + delay = 0, + continuous = false, + dimensions = listOf( + DateHistogram(sourceField = "tpep_pickup_datetime", fixedInterval = "1h"), + Terms("RatecodeID", "RatecodeID"), + Terms("PULocationID", "PULocationID") + ), + metrics = listOf( + RollupMetrics( + sourceField = "passenger_count", targetField = "passenger_count", + metrics = listOf( + Sum(), Min(), Max(), + ValueCount(), Average() + ) + ), + RollupMetrics(sourceField = "total_amount", targetField = "total_amount", metrics = listOf(Max(), Min())) + ) + ).let { createRollup(it, it.id) } + + updateRollupStartTime(rollupHourly) + + waitFor { + val rollupJob = getRollup(rollupId = rollupHourly.id) + assertNotNull("Rollup job doesn't have metadata set", rollupJob.metadataID) + val rollupMetadata = getRollupMetadata(rollupJob.metadataID!!) + assertEquals("Rollup is not finished", RollupMetadata.Status.FINISHED, rollupMetadata.status) + } + + val rollupMinutely = Rollup( + id = "minutely_basic_term_query_rollup_search_all", + enabled = true, + schemaVersion = 1L, + jobSchedule = IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES), + jobLastUpdatedTime = Instant.now(), + jobEnabledTime = Instant.now(), + description = "basic search test", + sourceIndex = "source_rollup_search_all_jobs_2", + targetIndex = targetIndex, + metadataID = null, + roles = emptyList(), + pageSize = 10, + delay = 0, + continuous = false, + dimensions = listOf( + DateHistogram(sourceField = "tpep_pickup_datetime", fixedInterval = "1m"), + Terms("RatecodeID", "RatecodeID") + ), + metrics = listOf( + RollupMetrics( + sourceField = "passenger_count", targetField = "passenger_count", + metrics = listOf( + Sum(), Min(), Max(), + ValueCount(), Average() + ) + ), + RollupMetrics(sourceField = "total_amount", targetField = "total_amount", metrics = listOf(Max(), Min())) + ) + ).let { createRollup(it, it.id) } + + updateRollupStartTime(rollupMinutely) + + waitFor { + val rollupJob = getRollup(rollupId = rollupMinutely.id) + assertNotNull("Rollup job doesn't have metadata set", rollupJob.metadataID) + val rollupMetadata = getRollupMetadata(rollupJob.metadataID!!) + assertEquals("Rollup is not finished", RollupMetadata.Status.FINISHED, rollupMetadata.status) + } + + refreshAllIndices() + + val req = """ + { + "size": 0, + "query": { + "term": { "RatecodeID": 1 } + }, + "aggs": { + "sum_passenger_count": { "sum": { "field": "passenger_count" } }, + "max_passenger_count": { "max": { "field": "passenger_count" } }, + "value_count_passenger_count": { "value_count": { "field": "passenger_count" } } + } + } + """.trimIndent() + val rawRes1 = client().makeRequest("POST", "/source_rollup_search_all_jobs_1/_search", emptyMap(), StringEntity(req, ContentType.APPLICATION_JSON)) + assertTrue(rawRes1.restStatus() == RestStatus.OK) + val rawRes2 = client().makeRequest("POST", "/source_rollup_search_all_jobs_2/_search", emptyMap(), StringEntity(req, ContentType.APPLICATION_JSON)) + assertTrue(rawRes2.restStatus() == RestStatus.OK) + val rollupResSingle = client().makeRequest("POST", "/$targetIndex/_search", emptyMap(), StringEntity(req, ContentType.APPLICATION_JSON)) + assertTrue(rollupResSingle.restStatus() == RestStatus.OK) + val rawAgg1Res = rawRes1.asMap()["aggregations"] as Map> + val rawAgg2Res = rawRes2.asMap()["aggregations"] as Map> + val rollupAggResSingle = rollupResSingle.asMap()["aggregations"] as Map> + + // When the cluster setting to search all jobs is off, the aggregations will be the same for searching a single job as for searching both + assertEquals( + "Searching single rollup job and rollup target index did not return the same max results", + rawAgg1Res.getValue("max_passenger_count")["value"], rollupAggResSingle.getValue("max_passenger_count")["value"] + ) + assertEquals( + "Searching single rollup job and rollup target index did not return the same sum results", + rawAgg1Res.getValue("sum_passenger_count")["value"], rollupAggResSingle.getValue("sum_passenger_count")["value"] + ) + val trueAggCount = rawAgg1Res.getValue("value_count_passenger_count")["value"] as Int + rawAgg2Res.getValue("value_count_passenger_count")["value"] as Int + assertEquals( + "Searching single rollup job and rollup target index did not return the same value count results", + rawAgg1Res.getValue("value_count_passenger_count")["value"], rollupAggResSingle.getValue("value_count_passenger_count")["value"] + ) + + val trueAggSum = rawAgg1Res.getValue("sum_passenger_count")["value"] as Double + rawAgg2Res.getValue("sum_passenger_count")["value"] as Double + updateSearchAllJobsClusterSetting(true) + + val rollupResAll = client().makeRequest("POST", "/$targetIndex/_search", emptyMap(), StringEntity(req, ContentType.APPLICATION_JSON)) + assertTrue(rollupResAll.restStatus() == RestStatus.OK) + val rollupAggResAll = rollupResAll.asMap()["aggregations"] as Map> + + // With search all jobs setting on, the sum, and value_count will now be equal to the sum of the single job search results + assertEquals( + "Searching single rollup job and rollup target index did not return the same sum results", + rawAgg1Res.getValue("max_passenger_count")["value"], rollupAggResAll.getValue("max_passenger_count")["value"] + ) + assertEquals( + "Searching rollup target index did not return the sum for all of the rollup jobs on the index", + trueAggSum, rollupAggResAll.getValue("sum_passenger_count")["value"] + ) + assertEquals( + "Searching rollup target index did not return the value count for all of the rollup jobs on the index", + trueAggCount, rollupAggResAll.getValue("value_count_passenger_count")["value"] + ) + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollupTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollupTests.kt index e47284791..3ea80443a 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollupTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/ISMRollupTests.kt @@ -11,13 +11,13 @@ package org.opensearch.indexmanagement.rollup.model -import org.apache.commons.codec.digest.DigestUtils import org.opensearch.index.seqno.SequenceNumbers import org.opensearch.indexmanagement.rollup.randomDateHistogram import org.opensearch.indexmanagement.rollup.randomISMRollup import org.opensearch.indexmanagement.rollup.randomTerms import org.opensearch.indexmanagement.util.IndexUtils import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule +import org.opensearch.notification.repackage.org.apache.commons.codec.digest.DigestUtils import org.opensearch.test.OpenSearchTestCase import java.time.temporal.ChronoUnit import kotlin.test.assertFailsWith diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/RollupTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/RollupTests.kt index 3018d8dd0..3e8611a6e 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/RollupTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/RollupTests.kt @@ -27,6 +27,7 @@ package org.opensearch.indexmanagement.rollup.model import org.opensearch.indexmanagement.randomInstant +import org.opensearch.indexmanagement.randomSchedule import org.opensearch.indexmanagement.rollup.randomDateHistogram import org.opensearch.indexmanagement.rollup.randomRollup import org.opensearch.indexmanagement.rollup.randomTerms @@ -95,9 +96,30 @@ class RollupTests : OpenSearchTestCase() { assertFailsWith(IllegalArgumentException::class, "Delay was negative") { randomRollup().copy(delay = -1) } + assertFailsWith(IllegalArgumentException::class, "Delay was too high") { + randomRollup().copy(delay = Long.MAX_VALUE) + } // These should successfully parse without exceptions randomRollup().copy(delay = 0) randomRollup().copy(delay = 930490) + randomRollup().copy(delay = null) + } + + fun `test delay applies to continuous rollups only`() { + // Continuous rollup schedule matches delay + val newDelay: Long = 500 + val continuousRollup = randomRollup().copy( + delay = newDelay, + continuous = true + ) + assertEquals(newDelay, continuousRollup.jobSchedule.delay) + // Non continuous rollup schedule should have null delay + val nonContinuousRollup = randomRollup().copy( + jobSchedule = randomSchedule(), + delay = newDelay, + continuous = false + ) + assertNull(nonContinuousRollup.jobSchedule.delay) } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/WriteableTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/WriteableTests.kt index cde75ffc2..02efc6fa7 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/WriteableTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/WriteableTests.kt @@ -128,7 +128,7 @@ class WriteableTests : OpenSearchTestCase() { } fun `test rollup as stream`() { - val rollup = randomRollup() + val rollup = randomRollup().copy(delay = randomLongBetween(0, 60000000)) val out = BytesStreamOutput().also { rollup.writeTo(it) } val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) val streamedRollup = Rollup(sin) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/XContentTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/XContentTests.kt index f759521ed..e4473e69c 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/XContentTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/model/XContentTests.kt @@ -126,18 +126,20 @@ class XContentTests : OpenSearchTestCase() { } fun `test rollup parsing with type`() { - val rollup = randomRollup() + val rollup = randomRollup().copy(delay = randomLongBetween(0, 60000000)) val rollupString = rollup.toJsonString() val parser = parserWithType(rollupString) val parsedRollup = parser.parseWithType(rollup.id, rollup.seqNo, rollup.primaryTerm, Rollup.Companion::parse) - assertEquals("Round tripping Rollup with type doesn't work", rollup, parsedRollup) + // roles are deprecated and not populated in toXContent and parsed as part of parse + assertEquals("Round tripping Rollup with type doesn't work", rollup.copy(roles = listOf()), parsedRollup) } fun `test rollup parsing without type`() { - val rollup = randomRollup() + val rollup = randomRollup().copy(delay = randomLongBetween(0, 60000000)) val rollupString = rollup.toJsonString(XCONTENT_WITHOUT_TYPE) val parsedRollup = Rollup.parse(parser(rollupString), rollup.id, rollup.seqNo, rollup.primaryTerm) - assertEquals("Round tripping Rollup without type doesn't work", rollup, parsedRollup) + // roles are deprecated and not populated in toXContent and parsed as part of parse + assertEquals("Round tripping Rollup without type doesn't work", rollup.copy(roles = listOf()), parsedRollup) } fun `test ism rollup parsing`() { diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestDeleteRollupActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestDeleteRollupActionIT.kt index 4c8704ae5..010d08b7d 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestDeleteRollupActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestDeleteRollupActionIT.kt @@ -50,9 +50,13 @@ class RestDeleteRollupActionIT : RollupRestTestCase() { @Throws(Exception::class) fun `test deleting a rollup that doesn't exist in existing config index`() { - createRandomRollup() - val res = client().makeRequest("DELETE", "$ROLLUP_JOBS_BASE_URI/foobarbaz") - assertEquals("Was not not_found response", "not_found", res.asMap()["result"]) + try { + createRandomRollup() + client().makeRequest("DELETE", "$ROLLUP_JOBS_BASE_URI/foobarbaz") + fail("expected 404 ResponseException") + } catch (e: ResponseException) { + assertEquals(RestStatus.NOT_FOUND, e.response.restStatus()) + } } @Throws(Exception::class) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestGetRollupActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestGetRollupActionIT.kt index 12c057084..1981869b4 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestGetRollupActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/resthandler/RestGetRollupActionIT.kt @@ -48,7 +48,11 @@ class RestGetRollupActionIT : RollupRestTestCase() { rollup = rollup.copy( schemaVersion = indexedRollup.schemaVersion, jobLastUpdatedTime = indexedRollup.jobLastUpdatedTime, - jobSchedule = indexedRollup.jobSchedule + jobSchedule = indexedRollup.jobSchedule, + // roles are deprecated and will not be stored or returned + roles = listOf(), + // user information is hidden and not returned + user = null ) assertEquals("Indexed and retrieved rollup differ", rollup, indexedRollup) } @@ -94,7 +98,8 @@ class RestGetRollupActionIT : RollupRestTestCase() { assertEquals(testRollup.targetIndex, innerRollup["target_index"] as String) assertEquals(testRollup.sourceIndex, innerRollup["source_index"] as String) assertEquals(testRollup.metadataID, innerRollup["metadata_id"] as String?) - assertEquals(testRollup.roles, innerRollup["roles"] as List) + assertNull(innerRollup["roles"]) + assertNull(innerRollup["user"]) assertEquals(testRollup.pageSize, innerRollup["page_size"] as Int) assertEquals(testRollup.description, innerRollup["description"] as String) assertEquals(testRollup.delay, (innerRollup["delay"] as Number?)?.toLong()) diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/runner/RollupRunnerIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/runner/RollupRunnerIT.kt index 1099be775..8be74d424 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/runner/RollupRunnerIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/runner/RollupRunnerIT.kt @@ -570,6 +570,185 @@ class RollupRunnerIT : RollupRestTestCase() { } } + // Tests that a continuous rollup will not be processed until the end of the interval plus delay passes + fun `test delaying continuous execution`() { + val indexName = "test_index_runner_eighth" + val delay: Long = 15000 + // Define rollup + var rollup = randomRollup().copy( + enabled = true, + jobSchedule = IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES), + jobEnabledTime = Instant.now(), + sourceIndex = indexName, + metadataID = null, + continuous = true, + delay = delay, + dimensions = listOf( + randomCalendarDateHistogram().copy( + calendarInterval = "5s" + ) + ) + ) + + // Create source index + createRollupSourceIndex(rollup) + // Add a document using the rollup's DateHistogram source field to ensure a metadata document is created + putDateDocumentInSourceIndex(rollup) + + // Create rollup job + rollup = createRollup(rollup = rollup, rollupId = rollup.id) + + var nextExecutionTime = rollup.schedule.getNextExecutionTime(null).toEpochMilli() + val expectedExecutionTime = rollup.jobEnabledTime!!.plusMillis(delay).toEpochMilli() + val delayIsCorrect = ((expectedExecutionTime - nextExecutionTime) > -500) && ((expectedExecutionTime - nextExecutionTime) < 500) + assertTrue("Delay was not correctly applied", delayIsCorrect) + + waitFor { + // Wait until half a second before the intended execution time + assertTrue(Instant.now().toEpochMilli() >= nextExecutionTime - 500) + // Still should not have run at this point + assertFalse("Target rollup index was created before the delay should allow", indexExists(rollup.targetIndex)) + } + val rollupMetadata = waitFor { + assertTrue("Target rollup index was not created", indexExists(rollup.targetIndex)) + val rollupJob = getRollup(rollupId = rollup.id) + assertNotNull("Rollup job doesn't have metadata set", rollupJob.metadataID) + val rollupMetadata = getRollupMetadata(rollupJob.metadataID!!) + assertNotNull("Rollup metadata not found", rollupMetadata) + rollupMetadata + } + nextExecutionTime = rollup.schedule.getNextExecutionTime(null).toEpochMilli() + val nextExecutionOffset = (nextExecutionTime - Instant.now().toEpochMilli()) - 60000 + val nextExecutionIsCorrect = nextExecutionOffset < 5000 && nextExecutionOffset > -5000 + assertTrue("Next execution time not updated correctly", nextExecutionIsCorrect) + val nextWindowStartTime: Instant = rollupMetadata.continuous!!.nextWindowStartTime + val nextWindowEndTime: Instant = rollupMetadata.continuous!!.nextWindowEndTime + // Assert that after the window was updated, it falls approximately around 'now' + assertTrue("Rollup window start time is incorrect", nextWindowStartTime.plusMillis(delay).minusMillis(1000) < Instant.now()) + assertTrue("Rollup window end time is incorrect", nextWindowEndTime.plusMillis(delay).plusMillis(1000) > Instant.now()) + + // window length should be 5 seconds + val expectedWindowEnd = nextWindowStartTime.plusMillis(5000) + assertEquals("Rollup window length applied incorrectly", expectedWindowEnd, nextWindowEndTime) + } + + fun `test non continuous delay does nothing`() { + generateNYCTaxiData("source_runner_ninth") + + // Setting the delay to this time so most of the data records would be excluded if delay were applied + val goalDateMS: Long = Instant.parse("2018-11-30T00:00:00Z").toEpochMilli() + val testDelay: Long = Instant.now().toEpochMilli() - goalDateMS + val rollup = Rollup( + id = "non_continuous_delay_stats_check", + schemaVersion = 1L, + enabled = true, + jobSchedule = IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES), + jobLastUpdatedTime = Instant.now(), + jobEnabledTime = Instant.now(), + description = "basic delay test", + sourceIndex = "source_runner_ninth", + targetIndex = "target_runner_ninth", + metadataID = null, + roles = emptyList(), + pageSize = 100, + delay = testDelay, + continuous = false, + dimensions = listOf(DateHistogram(sourceField = "tpep_pickup_datetime", fixedInterval = "1h")), + metrics = listOf( + RollupMetrics(sourceField = "passenger_count", targetField = "passenger_count", metrics = listOf(Sum(), Min(), Max(), ValueCount(), Average())) + ) + ).let { createRollup(it, it.id) } + + val now = Instant.now() + val intervalMillis = (rollup.schedule as IntervalSchedule).interval * 60 * 1000 + val nextExecutionTime = rollup.schedule.getNextExecutionTime(now).toEpochMilli() + val remainder = intervalMillis - ((now.toEpochMilli() - rollup.jobEnabledTime!!.toEpochMilli()) % intervalMillis) + val expectedExecutionTime = now.toEpochMilli() + remainder + val delayIsCorrect = ((expectedExecutionTime - nextExecutionTime) > -500) && ((expectedExecutionTime - nextExecutionTime) < 500) + assertTrue("Non continuous execution time was not correct", delayIsCorrect) + + updateRollupStartTime(rollup) + + val finishedRollup = waitFor { + val rollupJob = getRollup(rollupId = rollup.id) + assertNotNull("Rollup job doesn't have metadata set", rollupJob.metadataID) + val rollupMetadata = getRollupMetadata(rollupJob.metadataID!!) + assertEquals("Rollup is not finished $rollupMetadata", RollupMetadata.Status.FINISHED, rollupMetadata.status) + rollupJob + } + + refreshAllIndices() + + // No data should be excluded as the delay should not have been included + val rollupMetadataID = finishedRollup.metadataID!! + val rollupMetadata = getRollupMetadata(rollupMetadataID) + // These values would not match up with a delay + assertEquals("Did not have 2 pages processed", 2L, rollupMetadata.stats.pagesProcessed) + // This is a non-continuous job that rolls up every document of which there are 5k + assertEquals("Did not have 5000 documents processed", 5000L, rollupMetadata.stats.documentsProcessed) + // Based on the very first document using the tpep_pickup_datetime date field and an hourly rollup there + // should be 10 buckets with data in them which means 10 rollup documents + assertEquals("Did not have 10 rollups indexed", 10L, rollupMetadata.stats.rollupsIndexed) + // These are hard to test.. just assert they are more than 0 + assertTrue("Did not spend time indexing", rollupMetadata.stats.indexTimeInMillis > 0L) + assertTrue("Did not spend time searching", rollupMetadata.stats.searchTimeInMillis > 0L) + } + + // Tests that the continuous delay excludes recent data correctly + fun `test continuous delay exclusion period`() { + generateNYCTaxiData("source_runner_tenth") + + // Setting the delay to this time so most of the data records are excluded + val goalDateMS: Long = Instant.parse("2018-11-30T00:00:00Z").toEpochMilli() + val testDelay: Long = Instant.now().toEpochMilli() - goalDateMS + val rollup = Rollup( + id = "continuous_delay_stats_check", + schemaVersion = 1L, + enabled = true, + jobSchedule = IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES), + jobLastUpdatedTime = Instant.now(), + jobEnabledTime = Instant.now(), + description = "basic delay test", + sourceIndex = "source_runner_tenth", + targetIndex = "target_runner_tenth", + metadataID = null, + roles = emptyList(), + pageSize = 100, + delay = testDelay, + continuous = true, + dimensions = listOf(DateHistogram(sourceField = "tpep_pickup_datetime", fixedInterval = "1h")), + metrics = listOf( + RollupMetrics(sourceField = "passenger_count", targetField = "passenger_count", metrics = listOf(Sum(), Min(), Max(), ValueCount(), Average())) + ) + ).let { createRollup(it, it.id) } + + updateRollupStartTime(rollup, Instant.now().minusMillis(testDelay).minusMillis(55000).toEpochMilli()) + + val finishedRollup = waitFor { + val rollupJob = getRollup(rollupId = rollup.id) + assertNotNull("Rollup job doesn't have metadata set", rollupJob.metadataID) + val rollupMetadata = getRollupMetadata(rollupJob.metadataID!!) + assertEquals("Rollup is not started $rollupMetadata", RollupMetadata.Status.STARTED, rollupMetadata.status) + assertTrue("Continuous rollup did not process history", rollupMetadata.continuous!!.nextWindowEndTime.toEpochMilli() > goalDateMS) + rollupJob + } + + refreshAllIndices() + + val rollupMetadataID = finishedRollup.metadataID!! + val rollupMetadata = getRollupMetadata(rollupMetadataID) + // These numbers seem arbitrary, but match the case when the continuous rollup stops processing at 2018-11-30 + assertEquals("Did not have 35 pages processed", 35, rollupMetadata.stats.pagesProcessed) + // This is a continuous job that rolls up documents before 2018-11-30, of which there are 4 + assertEquals("Did not have 4 documents processed", 4, rollupMetadata.stats.documentsProcessed) + // Based on the very first document using the tpep_pickup_datetime date field and a 1 hour rollup there + // should be 2 buckets with data in them which means 2 rollup documents + assertEquals("Did not have 2 rollups indexed", 2, rollupMetadata.stats.rollupsIndexed) + // These are hard to test.. just assert they are more than 0 + assertTrue("Did not spend time indexing", rollupMetadata.stats.indexTimeInMillis > 0L) + assertTrue("Did not spend time searching", rollupMetadata.stats.searchTimeInMillis > 0L) + } + // TODO: Test scenarios: // - Source index deleted after first execution // * If this is with a source index pattern and the underlying indices are recreated but with different data diff --git a/src/test/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtilsTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtilsTests.kt index 05bc7a59b..094e87a27 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtilsTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/rollup/util/RollupUtilsTests.kt @@ -35,10 +35,25 @@ import org.opensearch.index.query.RangeQueryBuilder import org.opensearch.index.query.TermQueryBuilder import org.opensearch.index.query.TermsQueryBuilder import org.opensearch.index.search.MatchQuery +import org.opensearch.indexmanagement.common.model.dimension.DateHistogram +import org.opensearch.indexmanagement.common.model.dimension.Dimension +import org.opensearch.indexmanagement.common.model.dimension.Histogram +import org.opensearch.indexmanagement.common.model.dimension.Terms +import org.opensearch.indexmanagement.opensearchapi.convertToMap import org.opensearch.indexmanagement.rollup.model.RollupFieldMapping +import org.opensearch.indexmanagement.rollup.model.RollupMetrics +import org.opensearch.indexmanagement.rollup.randomAverage +import org.opensearch.indexmanagement.rollup.randomMax +import org.opensearch.indexmanagement.rollup.randomMin import org.opensearch.indexmanagement.rollup.randomRollup +import org.opensearch.indexmanagement.rollup.randomSum import org.opensearch.indexmanagement.rollup.randomTermQuery +import org.opensearch.indexmanagement.rollup.randomValueCount +import org.opensearch.indexmanagement.transform.randomAggregationBuilder +import org.opensearch.search.aggregations.metrics.AvgAggregationBuilder +import org.opensearch.search.aggregations.metrics.ValueCountAggregationBuilder import org.opensearch.test.OpenSearchTestCase +import org.opensearch.test.rest.OpenSearchRestTestCase class RollupUtilsTests : OpenSearchTestCase() { @@ -168,16 +183,32 @@ class RollupUtilsTests : OpenSearchTestCase() { fun `test buildRollupQuery`() { val rollup = randomRollup() val queryBuilder = MatchAllQueryBuilder() - val actual = rollup.buildRollupQuery(mapOf(), queryBuilder) as BoolQueryBuilder - val expectedFilter = TermQueryBuilder("rollup._id", rollup.id) - assertTrue(actual.should().isEmpty()) + val actual = setOf(rollup).buildRollupQuery(mapOf(), queryBuilder) as BoolQueryBuilder + val expectedShould = TermsQueryBuilder("rollup._id", rollup.id) assertTrue(actual.mustNot().isEmpty()) - assertFalse(actual.filter().isEmpty()) - assertFalse(actual.must().isEmpty()) + assertTrue(actual.filter().isEmpty()) + assertEquals("1", actual.minimumShouldMatch()) assertEquals(1, actual.must().size) assertEquals(rollup.rewriteQueryBuilder(queryBuilder, mapOf()), actual.must().first()) - assertEquals(1, actual.filter().size) - assertEquals(expectedFilter, actual.filter().first()) + assertEquals(1, actual.should().size) + assertEquals(1, (actual.should()[0] as TermsQueryBuilder).values().size) + assertEquals(expectedShould, actual.should().first()) + } + + fun `test buildRollupQuery multiple`() { + var rollups = setOf(randomRollup(), randomRollup(), randomRollup(), randomRollup(), randomRollup()) + rollups = OpenSearchRestTestCase.randomSubsetOf(randomIntBetween(2, 5), rollups).toSet() + val queryBuilder = MatchAllQueryBuilder() + val actual = rollups.buildRollupQuery(mapOf(), queryBuilder) as BoolQueryBuilder + val expectedShould = TermsQueryBuilder("rollup._id", rollups.map { it.id }) + assertTrue(actual.mustNot().isEmpty()) + assertTrue(actual.filter().isEmpty()) + assertEquals("1", actual.minimumShouldMatch()) + assertEquals(1, actual.must().size) + assertEquals(rollups.first().rewriteQueryBuilder(queryBuilder, mapOf()), actual.must().first()) + assertEquals(1, actual.should().size) + assertEquals(rollups.size, (actual.should()[0] as TermsQueryBuilder).values().size) + assertEquals(expectedShould, actual.should().first()) } fun `test rewriteQueryBuilder match phrase query`() { @@ -194,4 +225,30 @@ class RollupUtilsTests : OpenSearchTestCase() { assertEquals(matchPhraseQuery.value(), actual.value()) assertNull(actual.analyzer()) } + + fun `test rewriteAggregationBuilder`() { + var rollup = randomRollup() + val aggBuilder = randomAggregationBuilder() + val aggField = ((aggBuilder.convertToMap()[aggBuilder.name] as Map<*, *>).values.first() as Map<*, *>).values.first() as String + val newDims = mutableListOf() + // Make rollup dimensions and metrics contain the aggregation field name and aggregation metrics + rollup.dimensions.forEach { + val dimToAdd = when (it) { + is DateHistogram -> it.copy(sourceField = aggField, targetField = aggField) + is Terms -> it.copy(sourceField = aggField, targetField = aggField) + is Histogram -> it.copy(sourceField = aggField, targetField = aggField) + else -> it + } + newDims.add(dimToAdd) + } + val newMetrics = mutableListOf(RollupMetrics(aggField, aggField, listOf(randomAverage(), randomMax(), randomMin(), randomSum(), randomValueCount()))) + rollup = rollup.copy(dimensions = newDims, metrics = newMetrics) + val rewrittenAgg = rollup.rewriteAggregationBuilder(aggBuilder) + assertEquals("Rewritten aggregation builder does not have the same name", aggBuilder.name, rewrittenAgg.name) + if (aggBuilder is AvgAggregationBuilder || aggBuilder is ValueCountAggregationBuilder) { + assertEquals("Rewritten aggregation builder is not the correct type", "scripted_metric", rewrittenAgg.type) + } else { + assertEquals("Rewritten aggregation builder is not the correct type", aggBuilder.type, rewrittenAgg.type) + } + } } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/transform/TestHelpers.kt b/src/test/kotlin/org/opensearch/indexmanagement/transform/TestHelpers.kt index 3dd5144d9..bccd613bb 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/transform/TestHelpers.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/transform/TestHelpers.kt @@ -21,6 +21,7 @@ import org.opensearch.indexmanagement.common.model.dimension.Dimension import org.opensearch.indexmanagement.opensearchapi.string import org.opensearch.indexmanagement.randomInstant import org.opensearch.indexmanagement.randomSchedule +import org.opensearch.indexmanagement.randomUser import org.opensearch.indexmanagement.rollup.randomAfterKey import org.opensearch.indexmanagement.rollup.randomDimension import org.opensearch.indexmanagement.transform.model.ExplainTransform @@ -89,7 +90,8 @@ fun randomTransform(): Transform { roles = OpenSearchRestTestCase.randomList(10) { OpenSearchRestTestCase.randomAlphaOfLength(10) }, pageSize = OpenSearchRestTestCase.randomIntBetween(1, 10000), groups = randomGroups(), - aggregations = randomAggregationFactories() + aggregations = randomAggregationFactories(), + user = randomUser() ) } diff --git a/src/test/kotlin/org/opensearch/indexmanagement/transform/model/XContentTests.kt b/src/test/kotlin/org/opensearch/indexmanagement/transform/model/XContentTests.kt index a734a221f..cfcceb031 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/transform/model/XContentTests.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/transform/model/XContentTests.kt @@ -49,7 +49,8 @@ class XContentTests : OpenSearchTestCase() { val transform = randomTransform() val transformString = transform.toJsonString(XCONTENT_WITHOUT_TYPE) val parsedTransform = Transform.parse(parser(transformString), transform.id, transform.seqNo, transform.primaryTerm) - assertEquals("Round tripping Transform without type doesn't work", transform, parsedTransform) + // roles are deprecated and not populated in toXContent and parsed as part of parse + assertEquals("Round tripping Transform without type doesn't work", transform.copy(roles = listOf()), parsedTransform) } fun `test transform parsing with type`() { @@ -57,7 +58,8 @@ class XContentTests : OpenSearchTestCase() { val transformString = transform.toJsonString() val parser = parserWithType(transformString) val parsedTransform = parser.parseWithType(transform.id, transform.seqNo, transform.primaryTerm, Transform.Companion::parse) - assertEquals("Round tripping Transform with type doesn't work", transform, parsedTransform) + // roles are deprecated and not populated in toXContent and parsed as part of parse + assertEquals("Round tripping Transform with type doesn't work", transform.copy(roles = listOf()), parsedTransform) } fun `test transform parsing should ignore metadata id and startTime if its newly created transform`() { diff --git a/src/test/kotlin/org/opensearch/indexmanagement/transform/resthandler/RestGetTransformActionIT.kt b/src/test/kotlin/org/opensearch/indexmanagement/transform/resthandler/RestGetTransformActionIT.kt index 06c1d1891..82a30a7ee 100644 --- a/src/test/kotlin/org/opensearch/indexmanagement/transform/resthandler/RestGetTransformActionIT.kt +++ b/src/test/kotlin/org/opensearch/indexmanagement/transform/resthandler/RestGetTransformActionIT.kt @@ -36,7 +36,11 @@ class RestGetTransformActionIT : TransformRestTestCase() { schemaVersion = indexedTransform.schemaVersion, updatedAt = indexedTransform.updatedAt, jobSchedule = indexedTransform.jobSchedule, - metadataId = null + metadataId = null, + // Roles are deprecated and will not be returned + roles = listOf(), + // User information is not returned as part of REST output + user = null ) assertEquals("Indexed and retrieved transform differ", transform, indexedTransform) } @@ -80,7 +84,8 @@ class RestGetTransformActionIT : TransformRestTestCase() { assertEquals(testTransform.description, innerTransform["description"] as String) assertEquals(testTransform.sourceIndex, innerTransform["source_index"] as String) assertEquals(testTransform.targetIndex, innerTransform["target_index"] as String) - assertEquals(testTransform.roles, innerTransform["roles"] as List) + assertNull(innerTransform["roles"]) + assertNull(innerTransform["user"]) assertEquals(testTransform.pageSize, innerTransform["page_size"] as Int) assertEquals(testTransform.groups.size, (innerTransform["groups"] as List).size) } diff --git a/src/test/resources/index-management/opendistro-index-management-1.13.0.0.zip b/src/test/resources/index-management/opendistro-index-management-1.13.0.0.zip deleted file mode 100644 index 1a09c750d..000000000 Binary files a/src/test/resources/index-management/opendistro-index-management-1.13.0.0.zip and /dev/null differ diff --git a/src/test/resources/job-scheduler/opensearch-job-scheduler-1.0.0.0.zip b/src/test/resources/job-scheduler/opensearch-job-scheduler-1.0.0.0.zip deleted file mode 100644 index ba9cb7c61..000000000 Binary files a/src/test/resources/job-scheduler/opensearch-job-scheduler-1.0.0.0.zip and /dev/null differ diff --git a/src/test/resources/job-scheduler/opensearch-job-scheduler-1.2.0.0-SNAPSHOT.zip b/src/test/resources/job-scheduler/opensearch-job-scheduler-1.2.0.0-SNAPSHOT.zip new file mode 100644 index 000000000..43e0c7fae Binary files /dev/null and b/src/test/resources/job-scheduler/opensearch-job-scheduler-1.2.0.0-SNAPSHOT.zip differ diff --git a/src/test/resources/lang-mustache/lang-mustache-1.0.0.zip b/src/test/resources/lang-mustache/lang-mustache-1.0.0.zip new file mode 100644 index 000000000..83be078eb Binary files /dev/null and b/src/test/resources/lang-mustache/lang-mustache-1.0.0.zip differ diff --git a/src/test/resources/mappings/cached-opendistro-ism-config.json b/src/test/resources/mappings/cached-opendistro-ism-config.json index f3eda3c04..51d329a12 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": 10 + "schema_version": 12 }, "dynamic": "strict", "properties": { @@ -456,6 +456,43 @@ "format": "strict_date_time||epoch_millis" } } + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, @@ -510,6 +547,43 @@ }, "is_safe": { "type": "boolean" + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, @@ -526,6 +600,9 @@ "start_time": { "type": "date", "format": "strict_date_time||epoch_millis" + }, + "schedule_delay": { + "type": "long" } } }, @@ -536,10 +613,16 @@ }, "timezone": { "type": "keyword" + }, + "schedule_delay": { + "type": "long" } } } } + }, + "jitter": { + "type": "double" } } }, @@ -718,6 +801,9 @@ "start_time": { "type": "date", "format": "strict_date_time||epoch_millis" + }, + "schedule_delay": { + "type": "long" } } }, @@ -728,6 +814,9 @@ }, "timezone": { "type": "keyword" + }, + "schedule_delay": { + "type": "long" } } } @@ -851,6 +940,43 @@ } } } + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, @@ -935,6 +1061,9 @@ "start_time": { "type": "date", "format": "strict_date_time||epoch_millis" + }, + "schedule_delay": { + "type": "long" } } }, @@ -945,6 +1074,9 @@ }, "timezone": { "type": "keyword" + }, + "schedule_delay": { + "type": "long" } } } @@ -1039,6 +1171,43 @@ "data_selection_query": { "type": "object", "enabled": false + }, + "user": { + "properties": { + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "backend_roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "roles": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + }, + "custom_attribute_names": { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword" + } + } + } + } } } }, diff --git a/src/test/resources/security/opensearch-security-1.0.0.0.zip b/src/test/resources/security/opensearch-security-1.0.0.0.zip deleted file mode 100644 index 1f763dc4d..000000000 Binary files a/src/test/resources/security/opensearch-security-1.0.0.0.zip and /dev/null differ diff --git a/worksheets/notifications/create.http b/worksheets/notifications/create.http new file mode 100644 index 000000000..6334841d4 --- /dev/null +++ b/worksheets/notifications/create.http @@ -0,0 +1,21 @@ +POST localhost:9200/_plugins/_notifications/configs +Content-Type: application/json + +{ + "config":{ + "name":"this is a sample config name", + "description":"this is a sample config description", + "config_type":"chime", + "feature_list":[ + "index_management" + ], + "is_enabled":true, + "chime":{ + "url":"https://hooks.chime.aws/incomingwebhooks/5685915d-985f-4e1e-9087-f20da57dff13?token=RjRpb0pUZU98MXxFSVl2MUdRVUlNZEE5UnU1M0ZTMERBVHdhSVhZMmpsSlM3aThHbXhFMEFV" + } + } +} + +### + +GET localhost:9200/_opensearch/_notifications/feature/test/Lk6qg_noBMj6vY9S6g3Jz?feature=index_management \ No newline at end of file