From 0103403c828ade38d9931ac9eb36926288329eb3 Mon Sep 17 00:00:00 2001 From: Bo Zhang <bzhangam@amazon.com> Date: Tue, 25 Mar 2025 12:52:00 -0700 Subject: [PATCH] Introduce mapping transformer to allow transform mappings during index create/update or index template create/update. Signed-off-by: Bo Zhang <bzhangam@amazon.com> --- CHANGELOG.md | 3 +- .../create/TransportCreateIndexAction.java | 51 +++++--- .../put/TransportPutMappingAction.java | 15 ++- .../TransportPutComponentTemplateAction.java | 49 ++++++-- ...sportPutComposableIndexTemplateAction.java | 48 ++++++-- .../put/TransportPutIndexTemplateAction.java | 51 ++++---- .../MetadataIndexTemplateService.java | 4 + .../opensearch/cluster/metadata/Template.java | 6 +- .../index/mapper/MappingTransformer.java | 37 ++++++ .../mapper/MappingTransformerRegistry.java | 113 +++++++++++++++++ .../main/java/org/opensearch/node/Node.java | 7 +- .../org/opensearch/plugins/MapperPlugin.java | 11 ++ .../TransportCreateIndexActionTests.java | 107 ++++++++++++++++ .../put/TransportPutMappingActionTests.java | 110 +++++++++++++++++ ...nsportPutComponentTemplateActionTests.java | 111 +++++++++++++++++ ...PutComposableIndexTemplateActionTests.java | 110 +++++++++++++++++ .../TransportPutIndexTemplateActionTests.java | 105 ++++++++++++++++ .../MappingTransformerRegistryTests.java | 116 ++++++++++++++++++ .../indices/cluster/ClusterStateChanges.java | 20 ++- .../snapshots/SnapshotResiliencyTests.java | 12 +- 20 files changed, 1016 insertions(+), 70 deletions(-) create mode 100644 server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java create mode 100644 server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java create mode 100644 server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java create mode 100644 server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f8f9347402d..1567550c3a727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - [Rule Based Auto-tagging] Add rule schema for auto tagging ([#17238](https://github.com/opensearch-project/OpenSearch/pull/17238)) - Renaming the node role search to warm ([#17573](https://github.com/opensearch-project/OpenSearch/pull/17573)) - Introduce a new search node role to hold search only shards ([#17620](https://github.com/opensearch-project/OpenSearch/pull/17620)) -- Fix systemd integTest on deb regarding path ownership check ([#17641](https://github.com/opensearch-project/OpenSearch/pull/17641)) +- Fix systemd integTest on deb regarding path ownership check ([#17641](https://github.com/opensearch-project/OpenSearch/pull/17641)) - Add dfs transformation function in XContentMapValues ([#17612](https://github.com/opensearch-project/OpenSearch/pull/17612)) - Added Kinesis support as a plugin for the pull-based ingestion ([#17615](https://github.com/opensearch-project/OpenSearch/pull/17615) +- Introduce mapping transformer to allow transform mappings during index create/update or index template create/update ([#17635](https://github.com/opensearch-project/OpenSearch/pull/17635)) ### Changed - Migrate BC libs to their FIPS counterparts ([#14912](https://github.com/opensearch-project/OpenSearch/pull/14912)) diff --git a/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java b/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java index 38dc1a418ec8b..dd0a815a5c497 100644 --- a/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java +++ b/server/src/main/java/org/opensearch/action/admin/indices/create/TransportCreateIndexAction.java @@ -43,6 +43,7 @@ import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -56,6 +57,7 @@ public class TransportCreateIndexAction extends TransportClusterManagerNodeAction<CreateIndexRequest, CreateIndexResponse> { private final MetadataCreateIndexService createIndexService; + private final MappingTransformerRegistry mappingTransformerRegistry; @Inject public TransportCreateIndexAction( @@ -64,7 +66,9 @@ public TransportCreateIndexAction( ThreadPool threadPool, MetadataCreateIndexService createIndexService, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver + IndexNameExpressionResolver indexNameExpressionResolver, + MappingTransformerRegistry mappingTransformerRegistry + ) { super( CreateIndexAction.NAME, @@ -76,6 +80,7 @@ public TransportCreateIndexAction( indexNameExpressionResolver ); this.createIndexService = createIndexService; + this.mappingTransformerRegistry = mappingTransformerRegistry; } @Override @@ -112,25 +117,31 @@ protected void clusterManagerOperation( } final String indexName = indexNameExpressionResolver.resolveDateMathExpression(request.index()); - final CreateIndexClusterStateUpdateRequest updateRequest = new CreateIndexClusterStateUpdateRequest( - cause, - indexName, - request.index() - ).ackTimeout(request.timeout()) - .clusterManagerNodeTimeout(request.clusterManagerNodeTimeout()) - .settings(request.settings()) - .mappings(request.mappings()) - .aliases(request.aliases()) - .context(request.context()) - .waitForActiveShards(request.waitForActiveShards()); - - createIndexService.createIndex( - updateRequest, - ActionListener.map( - listener, - response -> new CreateIndexResponse(response.isAcknowledged(), response.isShardsAcknowledged(), indexName) - ) - ); + + final String finalCause = cause; + final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMappings -> { + final CreateIndexClusterStateUpdateRequest updateRequest = new CreateIndexClusterStateUpdateRequest( + finalCause, + indexName, + request.index() + ).ackTimeout(request.timeout()) + .clusterManagerNodeTimeout(request.clusterManagerNodeTimeout()) + .settings(request.settings()) + .mappings(transformedMappings) + .aliases(request.aliases()) + .context(request.context()) + .waitForActiveShards(request.waitForActiveShards()); + + createIndexService.createIndex( + updateRequest, + ActionListener.map( + listener, + response -> new CreateIndexResponse(response.isAcknowledged(), response.isShardsAcknowledged(), indexName) + ) + ); + }, listener::onFailure); + + mappingTransformerRegistry.applyTransformers(request.mappings(), null, mappingTransformListener); } } diff --git a/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java b/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java index ed936822bfdcd..4c37592714185 100644 --- a/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java +++ b/server/src/main/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingAction.java @@ -50,7 +50,9 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.index.Index; +import org.opensearch.core.xcontent.MediaTypeRegistry; import org.opensearch.index.IndexNotFoundException; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -72,6 +74,7 @@ public class TransportPutMappingAction extends TransportClusterManagerNodeAction private final MetadataMappingService metadataMappingService; private final RequestValidators<PutMappingRequest> requestValidators; + private final MappingTransformerRegistry mappingTransformerRegistry; @Inject public TransportPutMappingAction( @@ -81,7 +84,8 @@ public TransportPutMappingAction( final MetadataMappingService metadataMappingService, final ActionFilters actionFilters, final IndexNameExpressionResolver indexNameExpressionResolver, - final RequestValidators<PutMappingRequest> requestValidators + final RequestValidators<PutMappingRequest> requestValidators, + final MappingTransformerRegistry mappingTransformerRegistry ) { super( PutMappingAction.NAME, @@ -94,6 +98,7 @@ public TransportPutMappingAction( ); this.metadataMappingService = metadataMappingService; this.requestValidators = Objects.requireNonNull(requestValidators); + this.mappingTransformerRegistry = mappingTransformerRegistry; } @Override @@ -132,7 +137,13 @@ protected void clusterManagerOperation( listener.onFailure(maybeValidationException.get()); return; } - performMappingUpdate(concreteIndices, request, listener, metadataMappingService); + + final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMapping -> { + request.source(transformedMapping, MediaTypeRegistry.JSON); + performMappingUpdate(concreteIndices, request, listener, metadataMappingService); + }, listener::onFailure); + + mappingTransformerRegistry.applyTransformers(request.source(), null, mappingTransformListener); } catch (IndexNotFoundException ex) { logger.debug(() -> new ParameterizedMessage("failed to put mappings on indices [{}]", Arrays.asList(request.indices())), ex); throw ex; diff --git a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java index 66e2fe5c535db..a8002ec01d8cb 100644 --- a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java +++ b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateAction.java @@ -44,16 +44,20 @@ import org.opensearch.cluster.metadata.MetadataIndexTemplateService; import org.opensearch.cluster.metadata.Template; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.compress.CompressedXContent; import org.opensearch.common.inject.Inject; import org.opensearch.common.settings.IndexScopedSettings; import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; import java.io.IOException; +import reactor.util.annotation.NonNull; + /** * An action for putting a single component template into the cluster state * @@ -65,6 +69,7 @@ public class TransportPutComponentTemplateAction extends TransportClusterManager private final MetadataIndexTemplateService indexTemplateService; private final IndexScopedSettings indexScopedSettings; + private final MappingTransformerRegistry mappingTransformerRegistry; @Inject public TransportPutComponentTemplateAction( @@ -74,7 +79,8 @@ public TransportPutComponentTemplateAction( MetadataIndexTemplateService indexTemplateService, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, - IndexScopedSettings indexScopedSettings + IndexScopedSettings indexScopedSettings, + MappingTransformerRegistry mappingTransformerRegistry ) { super( PutComponentTemplateAction.NAME, @@ -87,6 +93,7 @@ public TransportPutComponentTemplateAction( ); this.indexTemplateService = indexTemplateService; this.indexScopedSettings = indexScopedSettings; + this.mappingTransformerRegistry = mappingTransformerRegistry; } @Override @@ -121,13 +128,37 @@ protected void clusterManagerOperation( template = new Template(settings, template.mappings(), template.aliases()); componentTemplate = new ComponentTemplate(template, componentTemplate.version(), componentTemplate.metadata()); } - indexTemplateService.putComponentTemplate( - request.cause(), - request.create(), - request.name(), - request.clusterManagerNodeTimeout(), - componentTemplate, - listener - ); + + final ActionListener<String> mappingTransformListener = getMappingTransformListener(request, listener, componentTemplate); + + transformMapping(template, mappingTransformListener); + } + + private ActionListener<String> getMappingTransformListener( + @NonNull final PutComponentTemplateAction.Request request, + @NonNull final ActionListener<AcknowledgedResponse> listener, + @NonNull final ComponentTemplate componentTemplate + ) { + return ActionListener.wrap(transformedMappings -> { + if (transformedMappings != null && componentTemplate.template() != null) { + componentTemplate.template().setMappings(new CompressedXContent(transformedMappings)); + } + indexTemplateService.putComponentTemplate( + request.cause(), + request.create(), + request.name(), + request.clusterManagerNodeTimeout(), + componentTemplate, + listener + ); + }, listener::onFailure); + } + + private void transformMapping(final Template template, @NonNull final ActionListener<String> mappingTransformListener) { + if (template == null || template.mappings() == null) { + mappingTransformListener.onResponse(null); + } else { + mappingTransformerRegistry.applyTransformers(template.mappings().string(), null, mappingTransformListener); + } } } diff --git a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java index a5c3590a0a6d7..5554a540a57f2 100644 --- a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java +++ b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateAction.java @@ -41,15 +41,20 @@ import org.opensearch.cluster.metadata.ComposableIndexTemplate; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.metadata.MetadataIndexTemplateService; +import org.opensearch.cluster.metadata.Template; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.compress.CompressedXContent; import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; import java.io.IOException; +import reactor.util.annotation.NonNull; + /** * An action for putting a composable index template into the cluster state * @@ -60,6 +65,7 @@ public class TransportPutComposableIndexTemplateAction extends TransportClusterM AcknowledgedResponse> { private final MetadataIndexTemplateService indexTemplateService; + private final MappingTransformerRegistry mappingTransformerRegistry; @Inject public TransportPutComposableIndexTemplateAction( @@ -68,7 +74,8 @@ public TransportPutComposableIndexTemplateAction( ThreadPool threadPool, MetadataIndexTemplateService indexTemplateService, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver + IndexNameExpressionResolver indexNameExpressionResolver, + MappingTransformerRegistry mappingTransformerRegistry ) { super( PutComposableIndexTemplateAction.NAME, @@ -80,6 +87,7 @@ public TransportPutComposableIndexTemplateAction( indexNameExpressionResolver ); this.indexTemplateService = indexTemplateService; + this.mappingTransformerRegistry = mappingTransformerRegistry; } @Override @@ -103,15 +111,35 @@ protected void clusterManagerOperation( final PutComposableIndexTemplateAction.Request request, final ClusterState state, final ActionListener<AcknowledgedResponse> listener + ) throws IOException { + final ComposableIndexTemplate indexTemplate = request.indexTemplate(); + + final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMappings -> { + if (transformedMappings != null && indexTemplate.template() != null) { + indexTemplate.template().setMappings(new CompressedXContent(transformedMappings)); + } + indexTemplateService.putIndexTemplateV2( + request.cause(), + request.create(), + request.name(), + request.clusterManagerNodeTimeout(), + indexTemplate, + listener + ); + }, listener::onFailure); + + transformMapping(indexTemplate, mappingTransformListener); + } + + private void transformMapping( + @NonNull final ComposableIndexTemplate indexTemplate, + @NonNull final ActionListener<String> mappingTransformListener ) { - ComposableIndexTemplate indexTemplate = request.indexTemplate(); - indexTemplateService.putIndexTemplateV2( - request.cause(), - request.create(), - request.name(), - request.clusterManagerNodeTimeout(), - indexTemplate, - listener - ); + final Template template = indexTemplate.template(); + if (template == null || template.mappings() == null) { + mappingTransformListener.onResponse(null); + } else { + mappingTransformerRegistry.applyTransformers(template.mappings().string(), null, mappingTransformListener); + } } } diff --git a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java index b9f27c00d0d98..2467fc0346313 100644 --- a/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java +++ b/server/src/main/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java @@ -49,6 +49,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; @@ -65,6 +66,7 @@ public class TransportPutIndexTemplateAction extends TransportClusterManagerNode private final MetadataIndexTemplateService indexTemplateService; private final IndexScopedSettings indexScopedSettings; + private final MappingTransformerRegistry mappingTransformerRegistry; @Inject public TransportPutIndexTemplateAction( @@ -74,7 +76,8 @@ public TransportPutIndexTemplateAction( MetadataIndexTemplateService indexTemplateService, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, - IndexScopedSettings indexScopedSettings + IndexScopedSettings indexScopedSettings, + MappingTransformerRegistry mappingTransformerRegistry ) { super( PutIndexTemplateAction.NAME, @@ -87,6 +90,7 @@ public TransportPutIndexTemplateAction( ); this.indexTemplateService = indexTemplateService; this.indexScopedSettings = indexScopedSettings; + this.mappingTransformerRegistry = mappingTransformerRegistry; } @Override @@ -118,28 +122,33 @@ protected void clusterManagerOperation( final Settings.Builder templateSettingsBuilder = Settings.builder(); templateSettingsBuilder.put(request.settings()).normalizePrefix(IndexMetadata.INDEX_SETTING_PREFIX); indexScopedSettings.validate(templateSettingsBuilder.build(), true); // templates must be consistent with regards to dependencies - indexTemplateService.putTemplate( - new MetadataIndexTemplateService.PutRequest(cause, request.name()).patterns(request.patterns()) - .order(request.order()) - .settings(templateSettingsBuilder.build()) - .mappings(request.mappings()) - .aliases(request.aliases()) - .create(request.create()) - .clusterManagerTimeout(request.clusterManagerNodeTimeout()) - .version(request.version()), + final String finalCause = cause; + final ActionListener<String> mappingTransformListener = ActionListener.wrap(transformedMappings -> { + indexTemplateService.putTemplate( + new MetadataIndexTemplateService.PutRequest(finalCause, request.name()).patterns(request.patterns()) + .order(request.order()) + .settings(templateSettingsBuilder.build()) + .mappings(transformedMappings) + .aliases(request.aliases()) + .create(request.create()) + .clusterManagerTimeout(request.clusterManagerNodeTimeout()) + .version(request.version()), - new MetadataIndexTemplateService.PutListener() { - @Override - public void onResponse(MetadataIndexTemplateService.PutResponse response) { - listener.onResponse(new AcknowledgedResponse(response.acknowledged())); - } + new MetadataIndexTemplateService.PutListener() { + @Override + public void onResponse(MetadataIndexTemplateService.PutResponse response) { + listener.onResponse(new AcknowledgedResponse(response.acknowledged())); + } - @Override - public void onFailure(Exception e) { - logger.debug(() -> new ParameterizedMessage("failed to put template [{}]", request.name()), e); - listener.onFailure(e); + @Override + public void onFailure(Exception e) { + logger.debug(() -> new ParameterizedMessage("failed to put template [{}]", request.name()), e); + listener.onFailure(e); + } } - } - ); + ); + }, listener::onFailure); + + mappingTransformerRegistry.applyTransformers(request.mappings(), null, mappingTransformListener); } } diff --git a/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java index b032ade720612..de106d14c6fd9 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java +++ b/server/src/main/java/org/opensearch/cluster/metadata/MetadataIndexTemplateService.java @@ -1754,6 +1754,10 @@ public PutRequest mappings(String mappings) { return this; } + public String getMappings() { + return mappings; + } + public PutRequest aliases(Set<Alias> aliases) { this.aliases.addAll(aliases); return this; diff --git a/server/src/main/java/org/opensearch/cluster/metadata/Template.java b/server/src/main/java/org/opensearch/cluster/metadata/Template.java index bd110c6af8975..be29a73e9c0ad 100644 --- a/server/src/main/java/org/opensearch/cluster/metadata/Template.java +++ b/server/src/main/java/org/opensearch/cluster/metadata/Template.java @@ -94,7 +94,7 @@ public class Template extends AbstractDiffable<Template> implements ToXContentOb @Nullable private final Settings settings; @Nullable - private final CompressedXContent mappings; + private CompressedXContent mappings; @Nullable private final Map<String, AliasMetadata> aliases; @@ -218,4 +218,8 @@ private static Map<String, Object> reduceMapping(Map<String, Object> mapping) { return mapping; } } + + public void setMappings(CompressedXContent mappings) { + this.mappings = mappings; + } } diff --git a/server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java b/server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java new file mode 100644 index 0000000000000..82e48accd08a1 --- /dev/null +++ b/server/src/main/java/org/opensearch/index/mapper/MappingTransformer.java @@ -0,0 +1,37 @@ +/* + * 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. + */ + +package org.opensearch.index.mapper; + +import org.opensearch.core.action.ActionListener; + +import java.util.Map; + +import reactor.util.annotation.NonNull; + +/** + * A transformer to allow plugins to implement logic to transform the index mapping during + * index creation/update and index template creation/update on transport layer. + * + */ +public interface MappingTransformer { + default void transform( + final Map<String, Object> mapping, + final TransformContext context, + @NonNull final ActionListener<Void> listener + ) { + listener.onResponse(null); + } + + /** + * Context for mapping transform. For now, we don't need any context, but it's defined for future scalability. + * It can be used to provide the info like we are transforming the mapping for what transport action. Or provide + * index setting info to help transform the mapping. + */ + class TransformContext {} +} diff --git a/server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java b/server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java new file mode 100644 index 0000000000000..192699edf5e4f --- /dev/null +++ b/server/src/main/java/org/opensearch/index/mapper/MappingTransformerRegistry.java @@ -0,0 +1,113 @@ +/* + * 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. + */ + +package org.opensearch.index.mapper; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.plugins.MapperPlugin; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import reactor.util.annotation.NonNull; + +/** + * This class collects all registered mapping transformers and applies them when needed. + */ +public class MappingTransformerRegistry { + private final List<MappingTransformer> transformers; + private final NamedXContentRegistry xContentRegistry; + protected final Logger logger = LogManager.getLogger(getClass()); + + public MappingTransformerRegistry( + @NonNull final List<MapperPlugin> mapperPlugins, + @NonNull final NamedXContentRegistry xContentRegistry + ) { + this.xContentRegistry = xContentRegistry; + this.transformers = new ArrayList<>(); + // Collect transformers from all MapperPlugins + for (MapperPlugin plugin : mapperPlugins) { + transformers.addAll(plugin.getMappingTransformers()); + } + } + + private void applyNext( + @NonNull final Map<String, Object> mapping, + final MappingTransformer.TransformContext context, + @NonNull final AtomicInteger index, + @NonNull final ActionListener<String> listener + ) { + if (index.get() == transformers.size()) { + try { + final XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.map(mapping); + listener.onResponse(builder.toString()); + } catch (IOException e) { + listener.onFailure(new RuntimeException("Failed to parse the transformed mapping [" + mapping + "]", e)); + } + return; + } + + final MappingTransformer transformer = transformers.get(index.getAndIncrement()); + logger.debug("Applying mapping transformer: {}", transformer.getClass().getName()); + transformer.transform(mapping, context, new ActionListener<>() { + @Override + public void onResponse(Void unused) { + logger.debug("Completed transformation: {}", transformer.getClass().getName()); + applyNext(mapping, context, index, listener); + } + + @Override + public void onFailure(Exception e) { + logger.error("Transformer {} failed: {}", transformer.getClass().getName(), e.getMessage()); + listener.onFailure(e); + } + }); + } + + /** + * Applies all registered transformers to the provided mapping which is in string format. + * @param mappingString The index mapping to transform. + * @param context Context for mapping transform + * @param mappingTransformListener A listener for mapping transform + */ + public void applyTransformers( + final String mappingString, + final MappingTransformer.TransformContext context, + @NonNull final ActionListener<String> mappingTransformListener + ) { + if (transformers.isEmpty() || mappingString == null) { + mappingTransformListener.onResponse(mappingString); + return; + } + + Map<String, Object> mappingMap; + try { + mappingMap = MapperService.parseMapping(xContentRegistry, mappingString); + } catch (IOException e) { + mappingTransformListener.onFailure( + new IllegalArgumentException( + "Failed to transform the mappings because failed to parse the mappings [" + mappingString + "]", + e + ) + ); + return; + } + + final AtomicInteger index = new AtomicInteger(0); + applyNext(mappingMap, context, index, mappingTransformListener); + } +} diff --git a/server/src/main/java/org/opensearch/node/Node.java b/server/src/main/java/org/opensearch/node/Node.java index 8037f90653d89..fadc33515bc27 100644 --- a/server/src/main/java/org/opensearch/node/Node.java +++ b/server/src/main/java/org/opensearch/node/Node.java @@ -153,6 +153,7 @@ import org.opensearch.index.analysis.AnalysisRegistry; import org.opensearch.index.compositeindex.CompositeIndexSettings; import org.opensearch.index.engine.EngineFactory; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.index.recovery.RemoteStoreRestoreService; import org.opensearch.index.remote.RemoteIndexPathUploader; import org.opensearch.index.remote.RemoteStoreStatsTrackerFactory; @@ -762,7 +763,8 @@ protected Node( clusterManagerMetrics ); modules.add(clusterModule); - IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class)); + final List<MapperPlugin> mapperPlugins = pluginsService.filterPlugins(MapperPlugin.class); + IndicesModule indicesModule = new IndicesModule(mapperPlugins); modules.add(indicesModule); SearchModule searchModule = new SearchModule(settings, pluginsService.filterPlugins(SearchPlugin.class)); @@ -1433,6 +1435,8 @@ protected Node( resourcesToClose.add(persistentTasksClusterService); final PersistentTasksService persistentTasksService = new PersistentTasksService(clusterService, threadPool, client); + final MappingTransformerRegistry mappingTransformerRegistry = new MappingTransformerRegistry(mapperPlugins, xContentRegistry); + modules.add(b -> { b.bind(Node.class).toInstance(this); b.bind(NodeService.class).toInstance(nodeService); @@ -1534,6 +1538,7 @@ protected Node( b.bind(SegmentReplicationStatsTracker.class).toInstance(segmentReplicationStatsTracker); b.bind(SearchRequestOperationsCompositeListenerFactory.class).toInstance(searchRequestOperationsCompositeListenerFactory); b.bind(SegmentReplicator.class).toInstance(segmentReplicator); + b.bind(MappingTransformerRegistry.class).toInstance(mappingTransformerRegistry); taskManagerClientOptional.ifPresent(value -> b.bind(TaskManagerClient.class).toInstance(value)); }); diff --git a/server/src/main/java/org/opensearch/plugins/MapperPlugin.java b/server/src/main/java/org/opensearch/plugins/MapperPlugin.java index ff6daf1b720f6..ef5b3bf2f574c 100644 --- a/server/src/main/java/org/opensearch/plugins/MapperPlugin.java +++ b/server/src/main/java/org/opensearch/plugins/MapperPlugin.java @@ -33,9 +33,11 @@ package org.opensearch.plugins; import org.opensearch.index.mapper.Mapper; +import org.opensearch.index.mapper.MappingTransformer; import org.opensearch.index.mapper.MetadataFieldMapper; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; @@ -90,4 +92,13 @@ default Function<String, Predicate<String>> getFieldFilter() { * get field mappings and field capabilities API will return every field that's present in the mappings. */ Function<String, Predicate<String>> NOOP_FIELD_FILTER = index -> NOOP_FIELD_PREDICATE; + + /** + * Returns mapper transformer implementations added by this plugin. + * + */ + default List<MappingTransformer> getMappingTransformers() { + return Collections.emptyList(); + } + } diff --git a/server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java new file mode 100644 index 0000000000000..5b5c8e5954157 --- /dev/null +++ b/server/src/test/java/org/opensearch/action/admin/indices/create/TransportCreateIndexActionTests.java @@ -0,0 +1,107 @@ +/* + * 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. + */ + +package org.opensearch.action.admin.indices.create; + +import org.opensearch.action.support.ActionFilter; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.MetadataCreateIndexService; +import org.opensearch.core.action.ActionListener; +import org.opensearch.index.mapper.MappingTransformerRegistry; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; +import org.junit.Before; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TransportCreateIndexActionTests extends OpenSearchTestCase { + @Mock + private TransportService transportService; + @Mock + private ActionFilters actionFilters; + @Mock + private ThreadPool threadPool; + @Mock + private MetadataCreateIndexService createIndexService; + @Mock + private IndexNameExpressionResolver indexNameExpressionResolver; + + @Mock + private MappingTransformerRegistry mappingTransformerRegistry; + + @Mock + private ClusterState clusterState; + + @Mock + private ActionListener<CreateIndexResponse> responseListener; + + private TransportCreateIndexAction action; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + + ActionFilter[] emptyActionFilters = new ActionFilter[] {}; + when(actionFilters.filters()).thenReturn(emptyActionFilters); + action = new TransportCreateIndexAction( + transportService, + null, // ClusterService not needed for this test + threadPool, + createIndexService, + actionFilters, + indexNameExpressionResolver, + mappingTransformerRegistry + ); + } + + public void testClusterManagerOperation_usesTransformedMapping() { + when(indexNameExpressionResolver.resolveDateMathExpression(any())).thenReturn("test-index"); + + // Arrange: Create a test request + final CreateIndexRequest request = new CreateIndexRequest("test-index"); + request.mapping("{\"properties\": {\"field\": {\"type\": \"text\"}}}"); + + // Mock transformed mapping result + final String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}"; + + // Capture ActionListener passed to applyTransformers + final ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class); + doNothing().when(mappingTransformerRegistry).applyTransformers( + anyString(), + any(), + listenerCaptor.capture() + ); + + // Act: Call the method + action.clusterManagerOperation(request, clusterState, responseListener); + + // Simulate transformation completion + listenerCaptor.getValue().onResponse(transformedMapping); + + // Assert: Capture request sent to createIndexService + ArgumentCaptor<CreateIndexClusterStateUpdateRequest> updateRequestCaptor = + ArgumentCaptor.forClass(CreateIndexClusterStateUpdateRequest.class); + verify(createIndexService, times(1)).createIndex(updateRequestCaptor.capture(), any()); + + // Ensure transformed mapping is passed correctly + CreateIndexClusterStateUpdateRequest capturedRequest = updateRequestCaptor.getValue(); + assertEquals(transformedMapping, capturedRequest.mappings()); + } +} diff --git a/server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java new file mode 100644 index 0000000000000..f1ed0a09230bf --- /dev/null +++ b/server/src/test/java/org/opensearch/action/admin/indices/mapping/put/TransportPutMappingActionTests.java @@ -0,0 +1,110 @@ +/* + * 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. + */ + +package org.opensearch.action.admin.indices.mapping.put; + +import org.opensearch.action.RequestValidators; +import org.opensearch.action.support.ActionFilter; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.clustermanager.AcknowledgedResponse; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.MetadataMappingService; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.MediaTypeRegistry; +import org.opensearch.index.mapper.MappingTransformerRegistry; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; +import org.junit.Before; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TransportPutMappingActionTests extends OpenSearchTestCase { + @Mock + private TransportService transportService; + @Mock + private ActionFilters actionFilters; + @Mock + private ThreadPool threadPool; + @Mock + private IndexNameExpressionResolver indexNameExpressionResolver; + + @Mock + private MetadataMappingService metadataMappingService; + + @Mock + private MappingTransformerRegistry mappingTransformerRegistry; + + @Mock + private RequestValidators<PutMappingRequest> requestValidators; + + @Mock + private ClusterState clusterState; + + @Mock + private ActionListener<AcknowledgedResponse> responseListener; + + private TransportPutMappingAction action; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + + ActionFilter[] emptyActionFilters = new ActionFilter[] {}; + when(actionFilters.filters()).thenReturn(emptyActionFilters); + action = new TransportPutMappingAction( + transportService, + null, // ClusterService not needed for this test + threadPool, + metadataMappingService, + actionFilters, + indexNameExpressionResolver, + requestValidators, + mappingTransformerRegistry + ); + } + + public void testClusterManagerOperation_transformedMappingUsed() { + // Arrange: Create a test request + final PutMappingRequest request = new PutMappingRequest("index"); + final String originalMapping = "{\"properties\": {\"field\": {\"type\": \"text\"}}}"; + request.source(originalMapping, MediaTypeRegistry.JSON); + + String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}"; + + // Mock the transformer registry to return the transformed mapping + ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class); + doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture()); + + // Act: Call the method + action.clusterManagerOperation(request, clusterState, responseListener); + + // Simulate transformation completion + listenerCaptor.getValue().onResponse(transformedMapping); + + // Assert: Verify the transformed mapping is passed to metadataMappingService + ArgumentCaptor<PutMappingClusterStateUpdateRequest> updateRequestCaptor = ArgumentCaptor.forClass( + PutMappingClusterStateUpdateRequest.class + ); + verify(metadataMappingService, times(1)).putMapping(updateRequestCaptor.capture(), any()); + + // Ensure the transformed mapping is used correctly + PutMappingClusterStateUpdateRequest capturedRequest = updateRequestCaptor.getValue(); + assertEquals(transformedMapping, capturedRequest.source()); + } +} diff --git a/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java new file mode 100644 index 0000000000000..1c84eac7cd093 --- /dev/null +++ b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComponentTemplateActionTests.java @@ -0,0 +1,111 @@ +/* + * 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. + */ + +package org.opensearch.action.admin.indices.template.put; + +import org.opensearch.action.support.ActionFilter; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.clustermanager.AcknowledgedResponse; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.ComponentTemplate; +import org.opensearch.cluster.metadata.MetadataIndexTemplateService; +import org.opensearch.cluster.metadata.Template; +import org.opensearch.common.compress.CompressedXContent; +import org.opensearch.core.action.ActionListener; +import org.opensearch.index.mapper.MappingTransformerRegistry; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; +import org.junit.Before; + +import java.io.IOException; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TransportPutComponentTemplateActionTests extends OpenSearchTestCase { + @Mock + private TransportService transportService; + @Mock + private ActionFilters actionFilters; + @Mock + private ThreadPool threadPool; + @Mock + private MappingTransformerRegistry mappingTransformerRegistry; + @Mock + private MetadataIndexTemplateService indexTemplateService; + @Mock + private ActionListener<AcknowledgedResponse> responseListener; + @Mock + private ClusterState clusterState; + + private TransportPutComponentTemplateAction action; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + + ActionFilter[] emptyActionFilters = new ActionFilter[] {}; + when(actionFilters.filters()).thenReturn(emptyActionFilters); + action = new TransportPutComponentTemplateAction( + transportService, + null, // ClusterService not needed for this test + threadPool, + indexTemplateService, + actionFilters, + null, // IndexNameExpressionResolver not needed + null, // IndexScopedSettings not needed + mappingTransformerRegistry + ); + } + + public void testClusterManagerOperation_mappingTransformationApplied() throws IOException { + // Arrange: Create a test request and mock dependencies + PutComponentTemplateAction.Request request = new PutComponentTemplateAction.Request("test"); + ComponentTemplate componentTemplate = mock(ComponentTemplate.class); + Template template = mock(Template.class); + when(componentTemplate.template()).thenReturn(template); + when(template.mappings()).thenReturn(new CompressedXContent("{\"properties\": {\"field\": {\"type\": \"text\"}}}")); + request.componentTemplate(componentTemplate); + + String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}"; + + // Mock mapping transformer to return transformed mapping + ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class); + doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture()); + + // Act: Call the method + action.clusterManagerOperation(request, clusterState, responseListener); + + // Simulate mapping transformation + listenerCaptor.getValue().onResponse(transformedMapping); + + // Assert: Verify the transformed mappings are set correctly + verify(template, times(1)).setMappings(new CompressedXContent(transformedMapping)); + + // Verify that indexTemplateService.putComponentTemplate is called + verify(indexTemplateService, times(1)).putComponentTemplate( + eq(request.cause()), + eq(request.create()), + eq(request.name()), + eq(request.clusterManagerNodeTimeout()), + eq(componentTemplate), + eq(responseListener) + ); + } +} diff --git a/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java new file mode 100644 index 0000000000000..db58639ee6cdc --- /dev/null +++ b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutComposableIndexTemplateActionTests.java @@ -0,0 +1,110 @@ +/* + * 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. + */ + +package org.opensearch.action.admin.indices.template.put; + +import org.opensearch.action.support.ActionFilter; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.clustermanager.AcknowledgedResponse; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.ComposableIndexTemplate; +import org.opensearch.cluster.metadata.MetadataIndexTemplateService; +import org.opensearch.cluster.metadata.Template; +import org.opensearch.common.compress.CompressedXContent; +import org.opensearch.core.action.ActionListener; +import org.opensearch.index.mapper.MappingTransformerRegistry; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; +import org.junit.Before; + +import java.io.IOException; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TransportPutComposableIndexTemplateActionTests extends OpenSearchTestCase { + @Mock + private TransportService transportService; + @Mock + private ActionFilters actionFilters; + @Mock + private ThreadPool threadPool; + + @Mock + private MappingTransformerRegistry mappingTransformerRegistry; + @Mock + private MetadataIndexTemplateService indexTemplateService; + @Mock + private ActionListener<AcknowledgedResponse> responseListener; + @Mock + private ClusterState clusterState; + + private TransportPutComposableIndexTemplateAction action; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + + ActionFilter[] emptyActionFilters = new ActionFilter[] {}; + when(actionFilters.filters()).thenReturn(emptyActionFilters); + action = new TransportPutComposableIndexTemplateAction( + transportService, + null, // ClusterService not needed for this test + threadPool, + indexTemplateService, + actionFilters, + null, // IndexNameExpressionResolver not needed + mappingTransformerRegistry + ); + } + + public void testClusterManagerOperation_mappingTransformationApplied() throws IOException { + // Arrange: Create a test request and mock dependencies + PutComposableIndexTemplateAction.Request request = new PutComposableIndexTemplateAction.Request("test"); + ComposableIndexTemplate indexTemplate = mock(ComposableIndexTemplate.class); + Template template = mock(Template.class); + String transformedMapping = "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}"; + when(indexTemplate.template()).thenReturn(template); + when(template.mappings()).thenReturn(new CompressedXContent("{\"properties\": {\"field\": {\"type\": \"text\"}}}")); + request.indexTemplate(indexTemplate); + + // Mock mapping transformer to return transformed mapping + ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class); + doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture()); + + // Act: Call the method + action.clusterManagerOperation(request, clusterState, responseListener); + + // Simulate mapping transformation + listenerCaptor.getValue().onResponse(transformedMapping); + + // Assert: Verify the transformed mappings are set correctly + verify(template, times(1)).setMappings(new CompressedXContent(transformedMapping)); + + // Verify that indexTemplateService.putIndexTemplateV2 is called + verify(indexTemplateService, times(1)).putIndexTemplateV2( + eq(request.cause()), + eq(request.create()), + eq(request.name()), + eq(request.clusterManagerNodeTimeout()), + eq(indexTemplate), + eq(responseListener) + ); + } +} diff --git a/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java new file mode 100644 index 0000000000000..4ba5cf066cd66 --- /dev/null +++ b/server/src/test/java/org/opensearch/action/admin/indices/template/put/TransportPutIndexTemplateActionTests.java @@ -0,0 +1,105 @@ +/* + * 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. + */ + +package org.opensearch.action.admin.indices.template.put; + +import org.opensearch.action.support.ActionFilter; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.clustermanager.AcknowledgedResponse; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.MetadataIndexTemplateService; +import org.opensearch.common.settings.IndexScopedSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.MediaTypeRegistry; +import org.opensearch.index.mapper.MappingTransformerRegistry; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportService; +import org.junit.Before; + +import java.util.Collections; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TransportPutIndexTemplateActionTests extends OpenSearchTestCase { + @Mock + private TransportService transportService; + @Mock + private ActionFilters actionFilters; + @Mock + private ThreadPool threadPool; + + @Mock + private MappingTransformerRegistry mappingTransformerRegistry; + @Mock + private MetadataIndexTemplateService indexTemplateService; + @Mock + private ActionListener<AcknowledgedResponse> responseListener; + @Mock + private ClusterState clusterState; + + private final IndexScopedSettings indexScopedSettings = new IndexScopedSettings(Settings.EMPTY, Collections.emptySet()); + + private TransportPutIndexTemplateAction action; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + + ActionFilter[] emptyActionFilters = new ActionFilter[] {}; + when(actionFilters.filters()).thenReturn(emptyActionFilters); + action = new TransportPutIndexTemplateAction( + transportService, + null, // ClusterService not needed for this test + threadPool, + indexTemplateService, + actionFilters, + null, // IndexNameExpressionResolver not needed + indexScopedSettings, + mappingTransformerRegistry + ); + } + + public void testClusterManagerOperation_mappingTransformationApplied() { + // Arrange: Mock the request and response + PutIndexTemplateRequest request = new PutIndexTemplateRequest("test"); + String originalMappings = "{\"properties\":{\"field\":{\"type\":\"text\"}}}"; + request.mapping(originalMappings, MediaTypeRegistry.JSON); + + // Mock the transformed mapping + String transformedMappings = "{\"properties\":{\"field\":{\"type\":\"keyword\"}}}"; + + // Mock mapping transformer to return transformed mapping + ArgumentCaptor<ActionListener<String>> listenerCaptor = ArgumentCaptor.forClass(ActionListener.class); + doNothing().when(mappingTransformerRegistry).applyTransformers(anyString(), any(), listenerCaptor.capture()); + + // Act: Call the method + action.clusterManagerOperation(request, clusterState, responseListener); + + // Simulate mapping transformation + listenerCaptor.getValue().onResponse(transformedMappings); + + // Assert: Verify that the transformed mappings are passed to the template service + ArgumentCaptor<MetadataIndexTemplateService.PutRequest> putRequestCaptor = ArgumentCaptor.forClass( + MetadataIndexTemplateService.PutRequest.class + ); + verify(indexTemplateService).putTemplate(putRequestCaptor.capture(), any()); + + // Verify the transformed mappings are set in the PutRequest + assertEquals(transformedMappings, putRequestCaptor.getValue().getMappings()); + } +} diff --git a/server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java b/server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java new file mode 100644 index 0000000000000..4c5ad09858921 --- /dev/null +++ b/server/src/test/java/org/opensearch/index/mapper/MappingTransformerRegistryTests.java @@ -0,0 +1,116 @@ +/* + * 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. + */ + +package org.opensearch.index.mapper; + +import joptsimple.internal.Strings; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.plugins.MapperPlugin; +import org.opensearch.test.OpenSearchTestCase; +import org.junit.Before; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MappingTransformerRegistryTests extends OpenSearchTestCase { + @Mock + private MapperPlugin mapperPlugin; + @Mock + private NamedXContentRegistry xContentRegistry; + @Mock + private ActionListener<String> listener; + @Mock + private MappingTransformer transformer; + + private MappingTransformerRegistry registry; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + when(mapperPlugin.getMappingTransformers()).thenReturn(List.of(transformer)); + registry = new MappingTransformerRegistry(List.of(mapperPlugin), xContentRegistry); + // Mock dummy transformer behavior which does not modify the mapping + doAnswer(invocation -> { + final ActionListener<Void> actionListener = invocation.getArgument(2); + actionListener.onResponse(null); + return null; + }).when(transformer).transform(any(), any(), any()); + } + + public void testApplyTransformers_whenNoTransformers_returnMappingDirectly() { + final String mapping = Strings.EMPTY; + final MappingTransformerRegistry mappingTransformerRegistry = new MappingTransformerRegistry( + Collections.emptyList(), + xContentRegistry + ); + + mappingTransformerRegistry.applyTransformers(mapping, null, listener); + + verify(listener, Mockito.times(1)).onResponse(mapping); + } + + public void testApplyTransformers_WithNullMapping_ShouldReturnNull() { + registry.applyTransformers(null, null, listener); + + verify(listener).onResponse(null); + } + + public void testApplyTransformers_WithTransformerApplied() throws IOException { + final String inputMappingString = "{\"field\": \"value\"}"; + final String expectedTransformedMappingString = "{\"field\":\"transformedValue\"}"; + + doAnswer(invocation -> { + Map<String, Object> mapping = invocation.getArgument(0); + assertEquals(mapping.get("field"), "value"); + mapping.put("field", "transformedValue"); // Simulating transformation + ActionListener<Void> actionListener = invocation.getArgument(2); + actionListener.onResponse(null); + return null; + }).when(transformer).transform(any(), any(), any()); + + registry.applyTransformers(inputMappingString, null, listener); + + ArgumentCaptor<String> responseCaptor = ArgumentCaptor.forClass(String.class); + verify(listener).onResponse(responseCaptor.capture()); + + String transformedMapping = responseCaptor.getValue(); + assertEquals(expectedTransformedMappingString, transformedMapping); + } + + public void testApplyTransformers_WhenTransformerFails_ShouldCallOnFailure() { + final String inputMappingString = "{\"field\": \"value\"}"; + final String errorMsg = "Transformation failed"; + + doAnswer(invocation -> { + ActionListener<Void> actionListener = invocation.getArgument(2); + actionListener.onFailure(new RuntimeException(errorMsg)); + return null; + }).when(transformer).transform(any(), any(), any()); + + registry.applyTransformers(inputMappingString, null, listener); + + ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class); + verify(listener).onFailure(exceptionCaptor.capture()); + + assertTrue(exceptionCaptor.getValue() instanceof RuntimeException); + assertTrue(exceptionCaptor.getValue().getMessage().contains(errorMsg)); + } +} diff --git a/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java b/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java index 63758cb44dc5a..613c6dc92a2e2 100644 --- a/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java +++ b/server/src/test/java/org/opensearch/indices/cluster/ClusterStateChanges.java @@ -53,7 +53,6 @@ import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.DestructiveOperations; -import org.opensearch.action.support.PlainActionFuture; import org.opensearch.action.support.clustermanager.ClusterManagerNodeRequest; import org.opensearch.action.support.clustermanager.TransportClusterManagerNodeAction; import org.opensearch.action.support.clustermanager.TransportClusterManagerNodeActionUtils; @@ -97,6 +96,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.settings.SettingsModule; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.action.ActionResponse; import org.opensearch.core.index.Index; import org.opensearch.core.xcontent.NamedXContentRegistry; @@ -104,6 +104,7 @@ import org.opensearch.env.TestEnvironment; import org.opensearch.index.IndexService; import org.opensearch.index.mapper.MapperService; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.index.shard.IndexEventListener; import org.opensearch.indices.DefaultRemoteStoreSettings; import org.opensearch.indices.IndicesService; @@ -196,6 +197,8 @@ public ClusterStateChanges(NamedXContentRegistry xContentRegistry, ThreadPool th Environment environment = TestEnvironment.newEnvironment(SETTINGS); Transport transport = mock(Transport.class); // it's not used + MappingTransformerRegistry mappingTransformerRegistry = new MappingTransformerRegistry(List.of(), xContentRegistry); + // mocks clusterService = mock(ClusterService.class); Metadata metadata = Metadata.builder().build(); @@ -372,7 +375,8 @@ public IndexMetadata upgradeIndexMetadata(IndexMetadata indexMetadata, Version m threadPool, createIndexService, actionFilters, - indexNameExpressionResolver + indexNameExpressionResolver, + mappingTransformerRegistry ); repositoriesService = new RepositoriesService( @@ -511,7 +515,17 @@ private <Request extends ClusterManagerNodeRequest<Request>, Response extends Ac masterNodeAction, request, clusterState, - new PlainActionFuture<>() + new ActionListener<Response>() { + @Override + public void onResponse(Response response) { + + } + + @Override + public void onFailure(Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } ); } catch (Exception e) { throw new RuntimeException(e); diff --git a/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java index 580b507292ea8..8f565d6d3abfb 100644 --- a/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/opensearch/snapshots/SnapshotResiliencyTests.java @@ -187,6 +187,7 @@ import org.opensearch.index.SegmentReplicationPressureService; import org.opensearch.index.SegmentReplicationStatsTracker; import org.opensearch.index.analysis.AnalysisRegistry; +import org.opensearch.index.mapper.MappingTransformerRegistry; import org.opensearch.index.remote.RemoteStorePressureService; import org.opensearch.index.remote.RemoteStoreStatsTrackerFactory; import org.opensearch.index.seqno.GlobalCheckpointSyncAction; @@ -1932,6 +1933,8 @@ private final class TestClusterNode { private Coordinator coordinator; private RemoteStoreNodeService remoteStoreNodeService; + private final MappingTransformerRegistry mappingTransformerRegistry; + private Map<ActionType, TransportAction> actions = new HashMap<>(); TestClusterNode(DiscoveryNode node) throws IOException { @@ -2193,6 +2196,9 @@ public void onFailure(final Exception e) { DefaultRemoteStoreSettings.INSTANCE, null ); + + mappingTransformerRegistry = new MappingTransformerRegistry(List.of(), namedXContentRegistry); + actions.put( CreateIndexAction.INSTANCE, new TransportCreateIndexAction( @@ -2201,7 +2207,8 @@ public void onFailure(final Exception e) { threadPool, metadataCreateIndexService, actionFilters, - indexNameExpressionResolver + indexNameExpressionResolver, + mappingTransformerRegistry ) ); final MetadataDeleteIndexService metadataDeleteIndexService = new MetadataDeleteIndexService( @@ -2307,7 +2314,8 @@ public void onFailure(final Exception e) { metadataMappingService, actionFilters, indexNameExpressionResolver, - new RequestValidators<>(Collections.emptyList()) + new RequestValidators<>(Collections.emptyList()), + mappingTransformerRegistry ) ); actions.put(