From 6731e8bb0bf48991e091b4c2e3016d06b171a3d7 Mon Sep 17 00:00:00 2001 From: Petar Dzepina Date: Sat, 12 Nov 2022 01:33:25 +0100 Subject: [PATCH 1/5] added support for index pattern as param to CreateIndexMappings Signed-off-by: Petar Dzepina --- .../mapper/MapperService.java | 50 +++++- .../securityanalytics/mapper/MapperUtils.java | 22 ++- .../TransportCreateIndexMappingsAction.java | 5 - .../mapper/MapperRestApiIT.java | 152 +++++++++++++++++- .../mapper/MapperUtilsTests.java | 6 +- 5 files changed, 209 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java b/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java index b356e5b54..52ce060d2 100644 --- a/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java +++ b/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java @@ -5,16 +5,17 @@ package org.opensearch.securityanalytics.mapper; -import java.util.Locale; +import java.util.Collection; +import java.util.Optional; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchStatusException; -import org.opensearch.ResourceNotFoundException; import org.opensearch.action.ActionListener; import org.opensearch.action.admin.indices.mapping.get.GetMappingsRequest; import org.opensearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.opensearch.action.support.GroupedActionListener; import org.opensearch.action.support.master.AcknowledgedResponse; import org.opensearch.client.IndicesAdminClient; import org.opensearch.cluster.metadata.MappingMetadata; @@ -74,10 +75,51 @@ public void onFailure(Exception e) { private void createMappingActionContinuation(ImmutableOpenMap indexMappings, String ruleTopic, String aliasMappings, boolean partial, ActionListener actionListener) { + int numOfIndices = indexMappings.size(); + + GroupedActionListener doCreateMappingActionsListener = new GroupedActionListener(new ActionListener>() { + @Override + public void onResponse(Collection response) { + // We will return ack==false if one of the requests returned that + // else return ack==true + Optional notAckd = response.stream().filter(e -> e.isAcknowledged() == false).findFirst(); + AcknowledgedResponse ack = new AcknowledgedResponse( + notAckd.isPresent() ? false : true + ); + actionListener.onResponse(ack); + } + + @Override + public void onFailure(Exception e) { + actionListener.onFailure( + new SecurityAnalyticsException( + "Failed applying mappings to index", RestStatus.INTERNAL_SERVER_ERROR, e) + ); + } + }, numOfIndices); + + indexMappings.forEach(iter -> { + String indexName = iter.key; + MappingMetadata mappingMetadata = iter.value; + // Try to apply mapping to index + doCreateMapping(indexName, mappingMetadata, ruleTopic, aliasMappings, partial, doCreateMappingActionsListener); + }); + } + + /** + * Applies alias mappings to index. + * @param indexName Index name + * @param mappingMetadata Index mappings + * @param ruleTopic Rule topic spcifying specific alias templates + * @param aliasMappings User-supplied alias mappings + * @param partial Partial flag indicating if we should apply mappings partially, in case source index doesn't have all paths specified in alias mappings + * @param actionListener actionListener used to return response/error + */ + private void doCreateMapping(String indexName, MappingMetadata mappingMetadata, String ruleTopic, String aliasMappings, boolean partial, ActionListener actionListener) { + PutMappingRequest request; try { - String indexName = indexMappings.iterator().next().key; String aliasMappingsJSON; // aliasMappings parameter has higher priority then ruleTopic if (aliasMappings != null) { @@ -86,7 +128,7 @@ private void createMappingActionContinuation(ImmutableOpenMap missingPathsInIndex = MapperUtils.validateIndexMappings(indexMappings, aliasMappingsJSON); + List missingPathsInIndex = MapperUtils.validateIndexMappings(indexName, mappingMetadata, aliasMappingsJSON); if(missingPathsInIndex.size() > 0) { // If user didn't allow partial apply, we should error out here diff --git a/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java b/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java index 2b3389928..90282d0b2 100644 --- a/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java +++ b/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java @@ -6,6 +6,7 @@ package org.opensearch.securityanalytics.mapper; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import org.apache.commons.lang3.tuple.Pair; import org.opensearch.cluster.metadata.MappingMetadata; @@ -89,24 +90,22 @@ public void onError(String error) { *
  • Alias mappings have to have property type=alias and path property has to exist *
  • Paths from alias mappings should exists in index mappings * - * @param indexMappings Index Mappings to which alias mappings will be applied - * @param aliasMappingsJSON Alias Mappings as JSON string + * @param indexName Source index name + * @param mappingMetadata Source index mapping to which alias mappings will be applied + * @param aliasMappingsJSON Alias mappings as JSON string * @return list of alias mappings paths which are missing in index mappings * */ - public static List validateIndexMappings(ImmutableOpenMap indexMappings, String aliasMappingsJSON) throws IOException { + public static List validateIndexMappings(String indexName, MappingMetadata mappingMetadata, String aliasMappingsJSON) throws IOException { // Check if index's mapping is empty - if (isIndexMappingsEmpty(indexMappings)) { - throw new IllegalArgumentException("Index mappings are empty"); + if (isIndexMappingsEmpty(mappingMetadata)) { + throw new IllegalArgumentException(String.format(Locale.ROOT, "Mappings for index [%s] are empty", indexName)); } // Get all paths (field names) to which we're going to apply aliases List paths = getAllPathsFromAliasMappings(aliasMappingsJSON); // Traverse Index Mappings and extract all fields(paths) - String indexName = indexMappings.iterator().next().key; - MappingMetadata mappingMetadata = indexMappings.get(indexName); - List flatFields = getAllNonAliasFieldsFromIndex(mappingMetadata); // Return list of paths from Alias Mappings which are missing in Index Mappings return paths.stream() @@ -164,11 +163,8 @@ public static List getAllNonAliasFieldsFromIndex(MappingMetadata mapping return mappingsTraverser.extractFlatNonAliasFields(); } - public static boolean isIndexMappingsEmpty(ImmutableOpenMap indexMappings) { - if (indexMappings.iterator().hasNext()) { - return indexMappings.iterator().next().value.getSourceAsMap().size() == 0; - } - throw new IllegalArgumentException("Invalid Index Mappings"); + public static boolean isIndexMappingsEmpty(MappingMetadata mappingMetadata) { + return mappingMetadata.getSourceAsMap().size() == 0; } public static Map getAliasMappingsWithFilter( diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java index bef747909..162c45c16 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportCreateIndexMappingsAction.java @@ -43,11 +43,6 @@ public TransportCreateIndexMappingsAction( protected void doExecute(Task task, CreateIndexMappingsRequest request, ActionListener actionListener) { this.threadPool.getThreadContext().stashContext(); - IndexMetadata index = clusterService.state().metadata().index(request.getIndexName()); - if (index == null) { - actionListener.onFailure(new IllegalStateException("Could not find index [" + request.getIndexName() + "]")); - return; - } mapperService.createMappingAction( request.getIndexName(), request.getRuleTopic(), diff --git a/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java index 08fb730e3..ccfba3c10 100644 --- a/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java @@ -242,7 +242,7 @@ public void testCreateIndexMappingsIndexMappingsEmpty() throws IOException { try { client().performRequest(request); } catch (ResponseException e) { - assertTrue(e.getMessage().contains("Index mappings are empty")); + assertTrue(e.getMessage().contains("Mappings for index [my_index_alias_fail_1] are empty")); } } @@ -290,6 +290,156 @@ public void testGetMappingsViewSuccess() throws IOException { assertEquals(2, unmappedFieldAliases.size()); } + public void testCreateMappings_withIndexPattern_success() throws IOException { + String indexName1 = "test_index_1"; + String indexName2 = "test_index_2"; + String indexPattern = "test_index*"; + + createIndex(indexName1, Settings.EMPTY, null); + createIndex(indexName2, Settings.EMPTY, null); + + client().performRequest(new Request("POST", "_refresh")); + + // Insert sample doc + String sampleDoc = "{" + + " \"netflow.source_ipv4_address\":\"10.50.221.10\"," + + " \"netflow.destination_transport_port\":1234," + + " \"netflow.destination_ipv4_address\":\"10.53.111.14\"," + + " \"netflow.source_transport_port\":4444" + + "}"; + + indexDoc(indexName1, "1", sampleDoc); + indexDoc(indexName2, "1", sampleDoc); + + client().performRequest(new Request("POST", "_refresh")); + + // Execute CreateMappingsAction to add alias mapping for index + Request request = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + request.setJsonEntity( + "{ \"index_name\":\"" + indexPattern + "\"," + + " \"rule_topic\":\"netflow\", " + + " \"partial\":true" + + "}" + ); + Response response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } + + public void testCreateMappings_withIndexPattern_differentMappings_success() throws IOException { + String indexName1 = "test_index_1"; + String indexName2 = "test_index_2"; + String indexPattern = "test_index*"; + + createIndex(indexName1, Settings.EMPTY, null); + createIndex(indexName2, Settings.EMPTY, null); + + client().performRequest(new Request("POST", "_refresh")); + + // Insert sample docs + String sampleDoc1 = "{" + + " \"netflow.source_ipv4_address\":\"10.50.221.10\"," + + " \"netflow.destination_transport_port\":1234," + + " \"netflow.source_transport_port\":4444" + + "}"; + String sampleDoc2 = "{" + + " \"netflow.destination_transport_port\":1234," + + " \"netflow.destination_ipv4_address\":\"10.53.111.14\"" + + "}"; + indexDoc(indexName1, "1", sampleDoc1); + indexDoc(indexName2, "1", sampleDoc2); + + client().performRequest(new Request("POST", "_refresh")); + + // Execute CreateMappingsAction to add alias mapping for index + Request request = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + request.setJsonEntity( + "{ \"index_name\":\"" + indexPattern + "\"," + + " \"rule_topic\":\"netflow\", " + + " \"partial\":true" + + "}" + ); + Response response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } + + public void testCreateMappings_withIndexPattern_oneNoMatches_success() throws IOException { + String indexName1 = "test_index_1"; + String indexName2 = "test_index_2"; + String indexPattern = "test_index*"; + + createIndex(indexName1, Settings.EMPTY, null); + createIndex(indexName2, Settings.EMPTY, null); + + client().performRequest(new Request("POST", "_refresh")); + + // Insert sample docs + String sampleDoc1 = "{" + + " \"netflow.source_ipv4_address\":\"10.50.221.10\"," + + " \"netflow.destination_transport_port\":1234," + + " \"netflow.source_transport_port\":4444" + + "}"; + String sampleDoc2 = "{" + + " \"netflow11.destination33_transport_port\":1234," + + " \"netflow11.destination33_ipv4_address\":\"10.53.111.14\"" + + "}"; + indexDoc(indexName1, "1", sampleDoc1); + indexDoc(indexName2, "1", sampleDoc2); + + client().performRequest(new Request("POST", "_refresh")); + + // Execute CreateMappingsAction to add alias mapping for index + Request request = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + request.setJsonEntity( + "{ \"index_name\":\"" + indexPattern + "\"," + + " \"rule_topic\":\"netflow\", " + + " \"partial\":true" + + "}" + ); + Response response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } + + public void testCreateMappings_withIndexPattern_oneNoMappings_failure() throws IOException { + String indexName1 = "test_index_1"; + String indexName2 = "test_index_2"; + String indexPattern = "test_index*"; + + createIndex(indexName1, Settings.EMPTY, null); + createIndex(indexName2, Settings.EMPTY, null); + + client().performRequest(new Request("POST", "_refresh")); + + // Insert sample docs + String sampleDoc1 = "{" + + " \"netflow.source_ipv4_address\":\"10.50.221.10\"," + + " \"netflow.destination_transport_port\":1234," + + " \"netflow.source_transport_port\":4444" + + "}"; + indexDoc(indexName1, "1", sampleDoc1); + + client().performRequest(new Request("POST", "_refresh")); + + // Execute CreateMappingsAction to add alias mapping for index + Request request = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + request.setJsonEntity( + "{ \"index_name\":\"" + indexPattern + "\"," + + " \"rule_topic\":\"netflow\", " + + " \"partial\":true" + + "}" + ); + try { + client().performRequest(request); + fail("expected 500 failure!"); + } catch (ResponseException e) { + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getResponse().getStatusLine().getStatusCode()); + } + + } + private void createSampleIndex(String indexName) throws IOException { String indexMapping = " \"properties\": {" + diff --git a/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java b/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java index f9af17e1d..76beb8b96 100644 --- a/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java +++ b/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java @@ -31,7 +31,7 @@ public void testValidateIndexMappingsMissingSome() throws IOException { MappingMetadata mappingMetadata = new MappingMetadata(MapperService.SINGLE_MAPPING_NAME, root); mappings.put("my_index", mappingMetadata); - List missingFields = MapperUtils.validateIndexMappings(mappings.build(), MapperTopicStore.aliasMappings("test123")); + List missingFields = MapperUtils.validateIndexMappings("my_index", mappingMetadata, MapperTopicStore.aliasMappings("test123")); assertEquals(3, missingFields.size()); } @@ -44,7 +44,7 @@ public void testValidateIndexMappingsEmptyMappings() throws IOException { MappingMetadata mappingMetadata = new MappingMetadata(MapperService.SINGLE_MAPPING_NAME, root); mappings.put("my_index", mappingMetadata); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> MapperUtils.validateIndexMappings(mappings.build(), MapperTopicStore.aliasMappings("test123"))); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> MapperUtils.validateIndexMappings("my_index", mappingMetadata, MapperTopicStore.aliasMappings("test123"))); assertTrue(e.getMessage().contains("Index mappings are empty")); } @@ -60,7 +60,7 @@ public void testValidateIndexMappingsNoMissing() throws IOException { MappingMetadata mappingMetadata = new MappingMetadata(MapperService.SINGLE_MAPPING_NAME, root); mappings.put("my_index", mappingMetadata); - List missingFields = MapperUtils.validateIndexMappings(mappings.build(), MapperTopicStore.aliasMappings("test123")); + List missingFields = MapperUtils.validateIndexMappings("my_index", mappingMetadata, MapperTopicStore.aliasMappings("test123")); assertEquals(0, missingFields.size()); } From 601f60d82a182b1e55cf3bb5f8e721baf4cce465 Mon Sep 17 00:00:00 2001 From: Petar Dzepina Date: Fri, 6 Jan 2023 02:25:36 +0100 Subject: [PATCH 2/5] initial commit Signed-off-by: Petar Dzepina --- .../SecurityAnalyticsPlugin.java | 2 +- .../mapper/MapperService.java | 82 +++++- .../TransportGetMappingsViewAction.java | 13 +- .../securityanalytics/util/IndexUtils.java | 37 +++ .../SecurityAnalyticsRestTestCase.java | 47 ++++ .../mapper/MapperRestApiIT.java | 260 +++++++++++++++++- .../mapper/MapperServiceTests.java | 4 +- 7 files changed, 426 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java b/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java index 4ccf26a33..2faf7f08c 100644 --- a/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java +++ b/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java @@ -121,7 +121,7 @@ public Collection createComponents(Client client, Supplier repositoriesServiceSupplier) { detectorIndices = new DetectorIndices(client.admin(), clusterService, threadPool); ruleTopicIndices = new RuleTopicIndices(client, clusterService); - mapperService = new MapperService(client.admin().indices()); + mapperService = new MapperService(client.admin().indices(), clusterService); ruleIndices = new RuleIndices(client, clusterService, threadPool); return List.of(detectorIndices, ruleTopicIndices, ruleIndices, mapperService); } diff --git a/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java b/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java index 52ce060d2..67189e309 100644 --- a/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java +++ b/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java @@ -12,13 +12,17 @@ import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchStatusException; import org.opensearch.action.ActionListener; +import org.opensearch.action.admin.indices.get.GetIndexRequest; +import org.opensearch.action.admin.indices.get.GetIndexResponse; import org.opensearch.action.admin.indices.mapping.get.GetMappingsRequest; import org.opensearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest; import org.opensearch.action.support.GroupedActionListener; import org.opensearch.action.support.master.AcknowledgedResponse; import org.opensearch.client.IndicesAdminClient; +import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.MappingMetadata; +import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.collect.ImmutableOpenMap; import org.opensearch.common.xcontent.XContentType; import org.opensearch.rest.RestStatus; @@ -31,6 +35,7 @@ import java.util.Map; import java.util.stream.Collectors; import org.opensearch.securityanalytics.action.GetMappingsViewResponse; +import org.opensearch.securityanalytics.util.IndexUtils; import org.opensearch.securityanalytics.util.SecurityAnalyticsException; @@ -40,13 +45,15 @@ public class MapperService { private static final Logger log = LogManager.getLogger(MapperService.class); + private ClusterService clusterService; IndicesAdminClient indicesClient; public MapperService() {} - public MapperService(IndicesAdminClient indicesClient) { + public MapperService(IndicesAdminClient indicesClient, ClusterService clusterService) { this.indicesClient = indicesClient; + this.clusterService = clusterService; } void setIndicesAdminClient(IndicesAdminClient client) { @@ -93,7 +100,8 @@ public void onResponse(Collection response) { public void onFailure(Exception e) { actionListener.onFailure( new SecurityAnalyticsException( - "Failed applying mappings to index", RestStatus.INTERNAL_SERVER_ERROR, e) + "Failed applying mappings to index", RestStatus.INTERNAL_SERVER_ERROR, e + ) ); } }, numOfIndices); @@ -280,7 +288,34 @@ public void getMappingsViewAction( String mapperTopic, ActionListener actionListener ) { - GetMappingsRequest getMappingsRequest = new GetMappingsRequest().indices(indexName); + try { + // We are returning mappings view for only 1 index: writeIndex or latest from the pattern + resolveConcreteIndex(indexName, new ActionListener<>() { + @Override + public void onResponse(String concreteIndex) { + doGetMappingsView(mapperTopic, actionListener, concreteIndex); + } + + @Override + public void onFailure(Exception e) { + actionListener.onFailure(e); + } + }); + + + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Constructs Mappings View of index + * @param mapperTopic Mapper Topic describing set of alias mappings + * @param actionListener Action Listener + * @param concreteIndex Concrete Index name for which we're computing Mappings View + */ + private void doGetMappingsView(String mapperTopic, ActionListener actionListener, String concreteIndex) { + GetMappingsRequest getMappingsRequest = new GetMappingsRequest().indices(concreteIndex); indicesClient.getMappings(getMappingsRequest, new ActionListener<>() { @Override public void onResponse(GetMappingsResponse getMappingsResponse) { @@ -333,4 +368,45 @@ public void onFailure(Exception e) { } }); } + + /** + * Given index name, resolves it to single concrete index, depending on what initial indexName is. + * In case of Datastream or Alias, WriteIndex would be returned. In case of index pattern, newest index by creation date would be returned. + * @param indexName Datastream, Alias, index patter or concrete index + * @param actionListener Action Listener + * @throws IOException + */ + private void resolveConcreteIndex(String indexName, ActionListener actionListener) throws IOException { + + indicesClient.getIndex((new GetIndexRequest()).indices(indexName), new ActionListener<>() { + @Override + public void onResponse(GetIndexResponse getIndexResponse) { + String[] indices = getIndexResponse.indices(); + if (indices.length == 0) { + actionListener.onFailure( + SecurityAnalyticsException.wrap( + new IllegalArgumentException("Invalid index name: [" + indexName + "]") + ) + ); + } else if (indices.length == 1) { + actionListener.onResponse(indices[0]); + } else if (indices.length > 1) { + String writeIndex = IndexUtils.getWriteIndex(indexName, MapperService.this.clusterService.state()); + if (writeIndex != null) { + actionListener.onResponse(writeIndex); + } else { + actionListener.onResponse( + IndexUtils.getNewestIndexByCreationDate(indices, MapperService.this.clusterService.state()) + ); + } + } + } + + @Override + public void onFailure(Exception e) { + actionListener.onFailure(e); + } + }); + + } } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java index 2ec636d4e..319bc5bca 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetMappingsViewAction.java @@ -47,17 +47,6 @@ public TransportGetMappingsViewAction( @Override protected void doExecute(Task task, GetMappingsViewRequest request, ActionListener actionListener) { this.threadPool.getThreadContext().stashContext(); - IndexMetadata index = clusterService.state().metadata().index(request.getIndexName()); - if (index == null) { - actionListener.onFailure( - SecurityAnalyticsException.wrap( - new OpenSearchStatusException( - "Could not find index [" + request.getIndexName() + "]", RestStatus.NOT_FOUND - ) - ) - ); - return; - } - mapperService.getMappingsViewAction(request.getIndexName(), request.getRuleTopic(), actionListener); + this.mapperService.getMappingsViewAction(request.getIndexName(), request.getRuleTopic(), actionListener); } } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/util/IndexUtils.java b/src/main/java/org/opensearch/securityanalytics/util/IndexUtils.java index 1bd1ff00f..aa1455470 100644 --- a/src/main/java/org/opensearch/securityanalytics/util/IndexUtils.java +++ b/src/main/java/org/opensearch/securityanalytics/util/IndexUtils.java @@ -4,11 +4,13 @@ */ package org.opensearch.securityanalytics.util; +import java.util.SortedMap; import org.opensearch.action.ActionListener; import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest; import org.opensearch.action.support.master.AcknowledgedResponse; import org.opensearch.client.IndicesAdminClient; import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.common.xcontent.NamedXContentRegistry; @@ -105,4 +107,39 @@ public static void updateIndexMapping( } } } + + public static boolean isDataStream(String name, ClusterState clusterState) { + return clusterState.getMetadata().dataStreams().containsKey(name); + } + public static boolean isAlias(String indexName, ClusterState clusterState) { + return clusterState.getMetadata().hasAlias(indexName); + } + public static String getWriteIndex(String indexName, ClusterState clusterState) { + if(isAlias(indexName, clusterState) || isDataStream(indexName, clusterState)) { + IndexMetadata metadata = clusterState.getMetadata() + .getIndicesLookup() + .get(indexName).getWriteIndex(); + if (metadata != null) { + return metadata.getIndex().getName(); + } + } + return null; + } + + public static String getNewestIndexByCreationDate(String[] concreteIndices, ClusterState clusterState) { + final SortedMap lookup = clusterState.getMetadata().getIndicesLookup(); + long maxCreationDate = Long.MIN_VALUE; + String newestIndex = null; + for (String indexName : concreteIndices) { + IndexAbstraction index = lookup.get(indexName); + IndexMetadata indexMetadata = clusterState.getMetadata().index(indexName); + if(index != null && index.getType() == IndexAbstraction.Type.CONCRETE_INDEX) { + if (indexMetadata.getCreationDate() > maxCreationDate) { + maxCreationDate = indexMetadata.getCreationDate(); + newestIndex = indexName; + } + } + } + return newestIndex; + } } \ No newline at end of file diff --git a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java index 9855f0c94..140d7cf3d 100644 --- a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java +++ b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java @@ -151,6 +151,17 @@ protected String createTestIndex(RestClient client, String index, String mapping return index; } + protected String createDocumentWithNFields(int numOfFields) { + StringBuilder doc = new StringBuilder(); + doc.append("{"); + for(int i = 0; i < numOfFields - 1; i++) { + doc.append("\"id").append(i).append("\": 5,"); + } + doc.append("\"last_field\": 100 }"); + + return doc.toString(); + } + protected Response makeRequest(RestClient client, String method, String endpoint, Map params, HttpEntity entity, Header... headers) throws IOException { Request request = new Request(method, endpoint); @@ -1211,6 +1222,25 @@ public List getAlertIndices(String detectorType) throws IOException { return indices; } + public List getQueryIndices(String detectorType) throws IOException { + Response response = client().performRequest(new Request("GET", "/_cat/indices/" + DetectorMonitorConfig.getRuleIndex(detectorType) + "*?format=json")); + XContentParser xcp = createParser(XContentType.JSON.xContent(), response.getEntity().getContent()); + List responseList = xcp.list(); + List indices = new ArrayList<>(); + for (Object o : responseList) { + if (o instanceof Map) { + ((Map) o).forEach((BiConsumer) + (o1, o2) -> { + if (o1.equals("index")) { + indices.add((String) o2); + } + }); + } + } + return indices; + } + + public List getFindingIndices(String detectorType) throws IOException { Response response = client().performRequest(new Request("GET", "/_cat/indices/" + DetectorMonitorConfig.getAllFindingsIndicesPattern(detectorType) + "?format=json")); XContentParser xcp = createParser(XContentType.JSON.xContent(), response.getEntity().getContent()); @@ -1291,4 +1321,21 @@ protected void createNetflowLogIndex(String indexName) throws IOException { response = client().performRequest(new Request("POST", "_refresh")); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); } + + + private Map getIndexAPI(String index) throws IOException { + Response resp = makeRequest(client(), "GET", "/" + index + "?expand_wildcards=all", Collections.emptyMap(), null); + return asMap(resp); + } + + private Map getIndexSettingsAPI(String index) throws IOException { + Response resp = makeRequest(client(), "GET", "/" + index + "/_settings?expand_wildcards=all", Collections.emptyMap(), null); + Map respMap = asMap(resp); + return respMap; + } + + protected void doRollover(String datastreamName) throws IOException { + Response response = makeRequest(client(), "POST", datastreamName + "/_rollover", Collections.emptyMap(), null); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } } \ No newline at end of file diff --git a/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java index 431b89564..8ebc9f9fa 100644 --- a/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java @@ -4,7 +4,11 @@ */ package org.opensearch.securityanalytics.mapper; +import java.util.Collections; +import java.util.Optional; import org.apache.http.HttpStatus; +import org.apache.http.entity.StringEntity; +import org.apache.http.message.BasicHeader; import org.opensearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Request; @@ -289,6 +293,162 @@ public void testGetMappingsViewSuccess() throws IOException { assertEquals(2, unmappedFieldAliases.size()); } + public void testGetMappingsView_alias_without_writeindex_Success() throws IOException { + + String testIndexName1 = "get_mappings_view_index11"; + String testIndexName2 = "get_mappings_view_index22"; + String indexAlias = "index_alias"; + createSampleIndex(testIndexName1, Settings.EMPTY, "\"" + indexAlias + "\":{}"); + createSampleIndex(testIndexName2, Settings.EMPTY, "\"" + indexAlias + "\":{}"); + indexDoc(testIndexName2, "987654", "{ \"extra_field\": 12345 }"); + + // Execute CreateMappingsAction to add alias mapping for index + Request request = new Request("GET", SecurityAnalyticsPlugin.MAPPINGS_VIEW_BASE_URI); + // both req params and req body are supported + request.addParameter("index_name", indexAlias); + request.addParameter("rule_topic", "netflow"); + Response response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + Map respMap = responseAsMap(response); + // Verify alias mappings + Map props = (Map) respMap.get("properties"); + assertEquals(4, props.size()); + assertTrue(props.containsKey("source.ip")); + assertTrue(props.containsKey("destination.ip")); + assertTrue(props.containsKey("source.port")); + assertTrue(props.containsKey("destination.port")); + // Verify unmapped index fields + List unmappedIndexFields = (List) respMap.get("unmapped_index_fields"); + assertEquals(7, unmappedIndexFields.size()); + // Verify that we got Mappings View of concrete index testIndexName2 because it is newest of all under this alias + Optional extraField = unmappedIndexFields.stream().filter(e -> e.equals("extra_field")).findFirst(); + assertTrue(extraField.isPresent()); + // Verify unmapped field aliases + List unmappedFieldAliases = (List) respMap.get("unmapped_field_aliases"); + assertEquals(2, unmappedFieldAliases.size()); + } + + public void testGetMappingsView_alias_with_writeindex_Success() throws IOException { + + String testIndexName1 = "get_mappings_view_index11"; + String testIndexName2 = "get_mappings_view_index22"; + String indexAlias = "index_alias"; + + createSampleIndex(testIndexName2, Settings.EMPTY, "\"" + indexAlias + "\":{}"); + createSampleIndex(testIndexName1, Settings.EMPTY, "\"" + indexAlias + "\":{ \"is_write_index\":true }"); + + // Add extra field by inserting doc to index #1 to differentiate two easier + indexDoc(testIndexName1, "987654", "{ \"extra_field\": 12345 }"); + + // Execute CreateMappingsAction to add alias mapping for index + Request request = new Request("GET", SecurityAnalyticsPlugin.MAPPINGS_VIEW_BASE_URI); + // both req params and req body are supported + request.addParameter("index_name", indexAlias); + request.addParameter("rule_topic", "netflow"); + Response response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + Map respMap = responseAsMap(response); + // Verify alias mappings + Map props = (Map) respMap.get("properties"); + assertEquals(4, props.size()); + assertTrue(props.containsKey("source.ip")); + assertTrue(props.containsKey("destination.ip")); + assertTrue(props.containsKey("source.port")); + assertTrue(props.containsKey("destination.port")); + // Verify unmapped index fields + List unmappedIndexFields = (List) respMap.get("unmapped_index_fields"); + assertEquals(7, unmappedIndexFields.size()); + // Verify that we got Mappings View of concrete index testIndexName2 because it is newest of all under this alias + Optional extraField = unmappedIndexFields.stream().filter(e -> e.equals("extra_field")).findFirst(); + assertTrue(extraField.isPresent()); + // Verify unmapped field aliases + List unmappedFieldAliases = (List) respMap.get("unmapped_field_aliases"); + assertEquals(2, unmappedFieldAliases.size()); + } + + public void testGetMappingsView_datastream_one_backing_index_Success() throws IOException { + + String datastreamName = "my_data_stream"; + createSampleDatastream(datastreamName); + // Execute GetMappingsViewAction to add alias mapping for index + Request request = new Request("GET", SecurityAnalyticsPlugin.MAPPINGS_VIEW_BASE_URI); + // both req params and req body are supported + request.addParameter("index_name", datastreamName); + request.addParameter("rule_topic", "netflow"); + Response response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + Map respMap = responseAsMap(response); + // Verify alias mappings + Map props = (Map) respMap.get("properties"); + assertEquals(4, props.size()); + assertTrue(props.containsKey("source.ip")); + assertTrue(props.containsKey("destination.ip")); + assertTrue(props.containsKey("source.port")); + assertTrue(props.containsKey("destination.port")); + // Verify unmapped index fields + List unmappedIndexFields = (List) respMap.get("unmapped_index_fields"); + assertEquals(7, unmappedIndexFields.size()); + // Verify unmapped field aliases + List unmappedFieldAliases = (List) respMap.get("unmapped_field_aliases"); + assertEquals(2, unmappedFieldAliases.size()); + + deleteDatastream(datastreamName); + } + + public void testGetMappingsView_datastream_two_backing_index_Success() throws IOException { + + String datastreamName = "my_data_stream"; + createSampleDatastream(datastreamName); + + // Modify index template to change mappings and then rollover + String indexMapping = + " \"properties\": {" + + " \"@timestamp\": {" + + " \"type\": \"date\"" + + " }," + + " \"netflow.source_ipv4_address\": {" + + " \"type\": \"ip\"" + + " }" + + "}"; + + String indexTemplateRequest = "{\n" + + " \"index_patterns\": [\"" + datastreamName + "*\"],\n" + + " \"data_stream\": { },\n" + + " \"template\": {\n" + + " \"mappings\" : {" + indexMapping + "}\n" + + " }," + + " \"priority\": 500\n" + + "}"; + + + Response response = makeRequest(client(), "PUT", "_index_template/" + datastreamName + "-template", Collections.emptyMap(), + new StringEntity(indexTemplateRequest), new BasicHeader("Content-Type", "application/json")); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + + doRollover(datastreamName); + + // Execute GetMappingsViewAction to add alias mapping for index + Request request = new Request("GET", SecurityAnalyticsPlugin.MAPPINGS_VIEW_BASE_URI); + // both req params and req body are supported + request.addParameter("index_name", datastreamName); + request.addParameter("rule_topic", "netflow"); + response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + Map respMap = responseAsMap(response); + // Verify alias mappings + Map props = (Map) respMap.get("properties"); + assertEquals(1, props.size()); + assertTrue(props.containsKey("source.ip")); + // Verify unmapped index fields + List unmappedIndexFields = (List) respMap.get("unmapped_index_fields"); + assertEquals(1, unmappedIndexFields.size()); + // Verify unmapped field aliases + List unmappedFieldAliases = (List) respMap.get("unmapped_field_aliases"); + assertEquals(5, unmappedFieldAliases.size()); + + deleteDatastream(datastreamName); + } + public void testCreateMappings_withIndexPattern_success() throws IOException { String indexName1 = "test_index_1"; String indexName2 = "test_index_2"; @@ -440,6 +600,10 @@ public void testCreateMappings_withIndexPattern_oneNoMappings_failure() throws I } private void createSampleIndex(String indexName) throws IOException { + createSampleIndex(indexName, null, null); + } + + private void createSampleIndex(String indexName, Settings settings, String aliases) throws IOException { String indexMapping = " \"properties\": {" + " \"netflow.source_ipv4_address\": {" + @@ -491,7 +655,7 @@ private void createSampleIndex(String indexName) throws IOException { "}" + " }"; - createIndex(indexName, Settings.EMPTY, indexMapping); + createIndex(indexName, settings, indexMapping, aliases); // Insert sample doc String sampleDoc = "{" + @@ -511,6 +675,100 @@ private void createSampleIndex(String indexName) throws IOException { assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); } + private void createSampleDatastream(String datastreamName) throws IOException { + String indexMapping = + " \"properties\": {" + + " \"@timestamp\": {" + + " \"type\": \"date\"" + + " }," + + " \"netflow.source_ipv4_address\": {" + + " \"type\": \"ip\"" + + " }," + + " \"netflow.destination_transport_port\": {" + + " \"type\": \"integer\"" + + " }," + + " \"netflow.destination_ipv4_address\": {" + + " \"type\": \"ip\"" + + " }," + + " \"netflow.source_transport_port\": {" + + " \"type\": \"integer\"" + + " }," + + " \"netflow.event.stop\": {" + + " \"type\": \"integer\"" + + " }," + + " \"dns.event.stop\": {" + + " \"type\": \"integer\"" + + " }," + + " \"ipx.event.stop\": {" + + " \"type\": \"integer\"" + + " }," + + " \"plain1\": {" + + " \"type\": \"integer\"" + + " }," + + " \"user\":{" + + " \"type\":\"nested\"," + + " \"properties\":{" + + " \"first\":{" + + " \"type\":\"text\"," + + " \"fields\":{" + + " \"keyword\":{" + + " \"type\":\"keyword\"," + + " \"ignore_above\":256" + + "}" + + "}" + + "}," + + " \"last\":{" + + "\"type\":\"text\"," + + "\"fields\":{" + + " \"keyword\":{" + + " \"type\":\"keyword\"," + + " \"ignore_above\":256" + + "}" + + "}" + + "}" + + "}" + + "}" + + " }"; + + + // Create index template + String indexTemplateRequest = "{\n" + + " \"index_patterns\": [\"" + datastreamName + "*\"],\n" + + " \"data_stream\": { },\n" + + " \"template\": {\n" + + " \"mappings\" : {" + indexMapping + "}\n" + + " }," + + " \"priority\": 500\n" + + "}"; + + + Response response = makeRequest(client(), "PUT", "_index_template/" + datastreamName + "-template", Collections.emptyMap(), + new StringEntity(indexTemplateRequest), new BasicHeader("Content-Type", "application/json")); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + // Insert sample doc + String sampleDoc = "{" + + " \"@timestamp\":\"2023-05-06T16:21:15.000Z\"," + + " \"netflow.source_ipv4_address\":\"10.50.221.10\"," + + " \"netflow.destination_transport_port\":1234," + + " \"netflow.destination_ipv4_address\":\"10.53.111.14\"," + + " \"netflow.source_transport_port\":4444" + + "}"; + + // Index doc + Request indexRequest = new Request("POST", datastreamName + "/_doc?refresh=wait_for"); + indexRequest.setJsonEntity(sampleDoc); + response = client().performRequest(indexRequest); + assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode()); + // Refresh everything + response = client().performRequest(new Request("POST", "_refresh")); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } + + private void deleteDatastream(String datastreamName) throws IOException { + Request indexRequest = new Request("DELETE", "_data_stream/" + datastreamName); + Response response = client().performRequest(indexRequest); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } private final String DNS_SAMPLE = "dns-sample.json"; private final String CLOUDTRAIL_SAMPLE = "cloudtrail-sample.json"; diff --git a/src/test/java/org/opensearch/securityanalytics/mapper/MapperServiceTests.java b/src/test/java/org/opensearch/securityanalytics/mapper/MapperServiceTests.java index e98486fdd..d1a33d7fa 100644 --- a/src/test/java/org/opensearch/securityanalytics/mapper/MapperServiceTests.java +++ b/src/test/java/org/opensearch/securityanalytics/mapper/MapperServiceTests.java @@ -59,7 +59,7 @@ public void onResponse(AcknowledgedResponse acknowledgedResponse) { @Override public void onFailure(Exception e) { - assertTrue(e.getMessage().equals("Alias mappings are missing path for alias: [srcport]")); + assertTrue(e.getCause().getMessage().equals("Alias mappings are missing path for alias: [srcport]")); } }); } @@ -99,7 +99,7 @@ public void onResponse(AcknowledgedResponse acknowledgedResponse) { @Override public void onFailure(Exception e) { - assertTrue(e.getMessage().contains("Duplicate field 'srcaddr'")); + assertTrue(e.getCause().getMessage().contains("Duplicate field 'srcaddr'")); } }); } From 254d15586dfa6f864334bb2119571314dda9ed13 Mon Sep 17 00:00:00 2001 From: Petar Dzepina Date: Fri, 6 Jan 2023 02:30:30 +0100 Subject: [PATCH 3/5] added one more test Signed-off-by: Petar Dzepina --- .../mapper/MapperRestApiIT.java | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java index 8ebc9f9fa..3e9e4f021 100644 --- a/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/mapper/MapperRestApiIT.java @@ -293,6 +293,41 @@ public void testGetMappingsViewSuccess() throws IOException { assertEquals(2, unmappedFieldAliases.size()); } + public void testGetMappingsView_index_pattern_two_indices_Success() throws IOException { + + String testIndexName1 = "get_mappings_view_index11"; + String testIndexName2 = "get_mappings_view_index22"; + String indexPattern = "get_mappings_view_index*"; + createSampleIndex(testIndexName1); + createSampleIndex(testIndexName2); + indexDoc(testIndexName2, "987654", "{ \"extra_field\": 12345 }"); + + // Execute CreateMappingsAction to add alias mapping for index + Request request = new Request("GET", SecurityAnalyticsPlugin.MAPPINGS_VIEW_BASE_URI); + // both req params and req body are supported + request.addParameter("index_name", indexPattern); + request.addParameter("rule_topic", "netflow"); + Response response = client().performRequest(request); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + Map respMap = responseAsMap(response); + // Verify alias mappings + Map props = (Map) respMap.get("properties"); + assertEquals(4, props.size()); + assertTrue(props.containsKey("source.ip")); + assertTrue(props.containsKey("destination.ip")); + assertTrue(props.containsKey("source.port")); + assertTrue(props.containsKey("destination.port")); + // Verify unmapped index fields + List unmappedIndexFields = (List) respMap.get("unmapped_index_fields"); + assertEquals(7, unmappedIndexFields.size()); + // Verify that we got Mappings View of concrete index testIndexName2 because it is newest of all under this alias + Optional extraField = unmappedIndexFields.stream().filter(e -> e.equals("extra_field")).findFirst(); + assertTrue(extraField.isPresent()); + // Verify unmapped field aliases + List unmappedFieldAliases = (List) respMap.get("unmapped_field_aliases"); + assertEquals(2, unmappedFieldAliases.size()); + } + public void testGetMappingsView_alias_without_writeindex_Success() throws IOException { String testIndexName1 = "get_mappings_view_index11"; @@ -600,7 +635,7 @@ public void testCreateMappings_withIndexPattern_oneNoMappings_failure() throws I } private void createSampleIndex(String indexName) throws IOException { - createSampleIndex(indexName, null, null); + createSampleIndex(indexName, Settings.EMPTY, null); } private void createSampleIndex(String indexName, Settings settings, String aliases) throws IOException { From 1c474b208eda1336fd1261a23542a25878267f53 Mon Sep 17 00:00:00 2001 From: Petar Dzepina Date: Fri, 6 Jan 2023 02:57:12 +0100 Subject: [PATCH 4/5] forbidden method fix Signed-off-by: Petar Dzepina --- .../org/opensearch/securityanalytics/mapper/MapperService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java b/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java index 67189e309..c24556605 100644 --- a/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java +++ b/src/main/java/org/opensearch/securityanalytics/mapper/MapperService.java @@ -304,7 +304,7 @@ public void onFailure(Exception e) { } catch (IOException e) { - e.printStackTrace(); + throw SecurityAnalyticsException.wrap(e); } } From 1626aa8945dfc303001bb152475c85ea2a4691f0 Mon Sep 17 00:00:00 2001 From: Petar Dzepina Date: Fri, 6 Jan 2023 19:37:55 +0100 Subject: [PATCH 5/5] fixed unit test Signed-off-by: Petar Dzepina --- .../opensearch/securityanalytics/mapper/MapperUtilsTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java b/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java index 13a35c4dd..92cf6c568 100644 --- a/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java +++ b/src/test/java/org/opensearch/securityanalytics/mapper/MapperUtilsTests.java @@ -45,7 +45,7 @@ public void testValidateIndexMappingsEmptyMappings() throws IOException { mappings.put("my_index", mappingMetadata); IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> MapperUtils.validateIndexMappings("my_index", mappingMetadata, MapperTopicStore.aliasMappings("test123"))); - assertTrue(e.getMessage().contains("Index mappings are empty")); + assertTrue(e.getMessage().contains("Mappings for index [my_index] are empty")); } public void testValidateIndexMappingsNoMissing() throws IOException {