diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java index 98a0846ad9fd1..a46eac0c31a74 100644 --- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -80,7 +80,7 @@ import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_UUID_NA_VALUE; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName; -import static org.elasticsearch.search.crossproject.CrossProjectIndexExpressionsRewriter.NO_MATCHING_PROJECT_EXCEPTION_VERSION; +import static org.elasticsearch.search.crossproject.IndexExpressionsRewriter.NO_MATCHING_PROJECT_EXCEPTION_VERSION; /** * A base class for all elasticsearch exceptions. diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java b/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java deleted file mode 100644 index c83b1731a1cb9..0000000000000 --- a/server/src/main/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriter.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.search.crossproject; - -import org.elasticsearch.TransportVersion; -import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.logging.LogManager; -import org.elasticsearch.logging.Logger; -import org.elasticsearch.transport.NoSuchRemoteClusterException; -import org.elasticsearch.transport.RemoteClusterAware; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Utility class for rewriting cross-project index expressions. - * Provides methods that can rewrite qualified and unqualified index expressions to canonical CCS. - */ -public class CrossProjectIndexExpressionsRewriter { - public static TransportVersion NO_MATCHING_PROJECT_EXCEPTION_VERSION = TransportVersion.fromName("no_matching_project_exception"); - - private static final Logger logger = LogManager.getLogger(CrossProjectIndexExpressionsRewriter.class); - private static final String ORIGIN_PROJECT_KEY = "_origin"; - private static final String WILDCARD = "*"; - private static final String[] MATCH_ALL = new String[] { WILDCARD }; - private static final String EXCLUSION = "-"; - private static final String DATE_MATH = "<"; - - /** - * Rewrites index expressions for cross-project search requests. - * Handles qualified and unqualified expressions and match-all cases will also hand exclusions in the future. - * - * @param originProject the _origin project with its alias - * @param linkedProjects the list of linked and available projects to consider for a request - * @param originalIndices the array of index expressions to be rewritten to canonical CCS - * @return a map from original index expressions to lists of canonical index expressions - * @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions - * @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing - */ - public static Map> rewriteIndexExpressions( - ProjectRoutingInfo originProject, - List linkedProjects, - final String[] originalIndices - ) { - final String[] indices; - if (originalIndices == null || originalIndices.length == 0) { // handling of match all cases besides _all and `*` - indices = MATCH_ALL; - } else { - indices = originalIndices; - } - assert false == IndexNameExpressionResolver.isNoneExpression(indices) - : "expression list is *,-* which effectively means a request that requests no indices"; - assert originProject != null || linkedProjects.isEmpty() == false - : "either origin project or linked projects must be in project target set"; - - Set linkedProjectNames = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet()); - Map> canonicalExpressionsMap = new LinkedHashMap<>(indices.length); - for (String resource : indices) { - if (canonicalExpressionsMap.containsKey(resource)) { - continue; - } - maybeThrowOnUnsupportedResource(resource); - - boolean isQualified = RemoteClusterAware.isRemoteIndexName(resource); - if (isQualified) { - // handing of qualified expressions - String[] splitResource = RemoteClusterAware.splitIndexName(resource); - assert splitResource.length == 2 - : "Expected two strings (project and indexExpression) for a qualified resource [" - + resource - + "], but found [" - + splitResource.length - + "]"; - String projectAlias = splitResource[0]; - assert projectAlias != null : "Expected a project alias for a qualified resource but was null"; - String indexExpression = splitResource[1]; - maybeThrowOnUnsupportedResource(indexExpression); - - List canonicalExpressions = rewriteQualified(projectAlias, indexExpression, originProject, linkedProjectNames); - - canonicalExpressionsMap.put(resource, canonicalExpressions); - logger.debug("Rewrote qualified expression [{}] to [{}]", resource, canonicalExpressions); - } else { - // un-qualified expression, i.e. flat-world - List canonicalExpressions = rewriteUnqualified(resource, originProject, linkedProjects); - canonicalExpressionsMap.put(resource, canonicalExpressions); - logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, canonicalExpressions); - } - } - return canonicalExpressionsMap; - } - - private static List rewriteUnqualified( - String indexExpression, - @Nullable ProjectRoutingInfo origin, - List projects - ) { - List canonicalExpressions = new ArrayList<>(); - if (origin != null) { - canonicalExpressions.add(indexExpression); // adding the original indexExpression for the _origin cluster. - } - for (ProjectRoutingInfo targetProject : projects) { - canonicalExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProject.projectAlias(), indexExpression)); - } - return canonicalExpressions; - } - - private static List rewriteQualified( - String requestedProjectAlias, - String indexExpression, - @Nullable ProjectRoutingInfo originProject, - Set allProjectAliases - ) { - if (originProject != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) { - // handling case where we have a qualified expression like: _origin:indexName - return List.of(indexExpression); - } - - if (originProject == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) { - // handling case where we have a qualified expression like: _origin:indexName but no _origin project is set - throw new NoMatchingProjectException(requestedProjectAlias); - } - - try { - if (originProject != null) { - allProjectAliases.add(originProject.projectAlias()); - } - List resourcesMatchingAliases = new ArrayList<>(); - List allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames( - allProjectAliases, - requestedProjectAlias - ); - - if (allProjectsMatchingAlias.isEmpty()) { - throw new NoMatchingProjectException(requestedProjectAlias); - } - - for (String project : allProjectsMatchingAlias) { - if (originProject != null && project.equals(originProject.projectAlias())) { - resourcesMatchingAliases.add(indexExpression); - } else { - resourcesMatchingAliases.add(RemoteClusterAware.buildRemoteIndexName(project, indexExpression)); - } - } - - return resourcesMatchingAliases; - } catch (NoSuchRemoteClusterException ex) { - logger.debug(ex.getMessage(), ex); - throw new NoMatchingProjectException(requestedProjectAlias); - } - } - - private static void maybeThrowOnUnsupportedResource(String resource) { - // TODO To be handled in future PR. - if (resource.startsWith(EXCLUSION)) { - throw new IllegalArgumentException("Exclusions are not currently supported but was found in the expression [" + resource + "]"); - } - if (resource.startsWith(DATE_MATH)) { - throw new IllegalArgumentException("Date math are not currently supported but was found in the expression [" + resource + "]"); - } - if (IndexNameExpressionResolver.hasSelectorSuffix(resource)) { - throw new IllegalArgumentException("Selectors are not currently supported but was found in the expression [" + resource + "]"); - - } - } -} diff --git a/server/src/main/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriter.java b/server/src/main/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriter.java new file mode 100644 index 0000000000000..9d21cb01d0255 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriter.java @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.search.crossproject; + +import org.elasticsearch.TransportVersion; +import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import org.elasticsearch.transport.NoSuchRemoteClusterException; +import org.elasticsearch.transport.RemoteClusterAware; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class for rewriting cross-project index expressions. + * Provides methods that can rewrite qualified and unqualified index expressions to canonical CCS. + */ +public class IndexExpressionsRewriter { + public static TransportVersion NO_MATCHING_PROJECT_EXCEPTION_VERSION = TransportVersion.fromName("no_matching_project_exception"); + + private static final Logger logger = LogManager.getLogger(IndexExpressionsRewriter.class); + private static final String ORIGIN_PROJECT_KEY = "_origin"; + private static final String[] MATCH_ALL = new String[] { Metadata.ALL }; + private static final String EXCLUSION = "-"; + private static final String DATE_MATH = "<"; + + /** + * Rewrites index expressions for cross-project search requests. + * Handles qualified and unqualified expressions and match-all cases will also hand exclusions in the future. + * + * @param originProject the _origin project with its alias + * @param linkedProjects the list of linked and available projects to consider for a request + * @param originalIndices the array of index expressions to be rewritten to canonical CCS + * @return a map from original index expressions to lists of canonical index expressions + * @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions + * @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing + */ + public static Map rewriteIndexExpressions( + ProjectRoutingInfo originProject, + List linkedProjects, + final String[] originalIndices + ) { + final String[] indices; + if (originalIndices == null || originalIndices.length == 0) { // handling of match all cases besides _all and `*` + indices = MATCH_ALL; + } else { + indices = originalIndices; + } + assert false == IndexNameExpressionResolver.isNoneExpression(indices) + : "expression list is *,-* which effectively means a request that requests no indices"; + + final Set allProjectAliases = getAllProjectAliases(originProject, linkedProjects); + final String originProjectAlias = originProject != null ? originProject.projectAlias() : null; + final Map canonicalExpressionsMap = new LinkedHashMap<>(indices.length); + for (String indexExpression : indices) { + if (canonicalExpressionsMap.containsKey(indexExpression)) { + continue; + } + canonicalExpressionsMap.put(indexExpression, rewriteIndexExpression(indexExpression, originProjectAlias, allProjectAliases)); + } + return canonicalExpressionsMap; + } + + /** + * Rewrites an index expression for cross-project search requests. + * @param indexExpression the index expression to be rewritten to canonical CCS + * @param originProjectAlias the alias of the origin project (can be null if it was excluded by project routing). It's passed + * additionally to allProjectAliases because the origin project requires special handling: + * it can match on its actual alias and on the special alias "_origin". Any expression matched by the origin + * project also cannot be qualified with its actual alias in the final rewritten expression. + * @param allProjectAliases the list of all project aliases (linked and origin) consider for a request + * @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions + * @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing + */ + public static IndexRewriteResult rewriteIndexExpression( + String indexExpression, + @Nullable String originProjectAlias, + Set allProjectAliases + ) { + assert originProjectAlias != null || allProjectAliases.isEmpty() == false + : "either origin project or linked projects must be in project target set"; + + maybeThrowOnUnsupportedResource(indexExpression); + + final boolean isQualified = RemoteClusterAware.isRemoteIndexName(indexExpression); + final IndexRewriteResult rewrittenExpression; + if (isQualified) { + rewrittenExpression = rewriteQualifiedExpression(indexExpression, originProjectAlias, allProjectAliases); + logger.debug("Rewrote qualified expression [{}] to [{}]", indexExpression, rewrittenExpression); + } else { + rewrittenExpression = rewriteUnqualifiedExpression(indexExpression, originProjectAlias, allProjectAliases); + logger.debug("Rewrote unqualified expression [{}] to [{}]", indexExpression, rewrittenExpression); + } + return rewrittenExpression; + } + + private static Set getAllProjectAliases(@Nullable ProjectRoutingInfo originProject, List linkedProjects) { + assert originProject != null || linkedProjects.isEmpty() == false + : "either origin project or linked projects must be in project target set"; + + final Set allProjectAliases = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet()); + if (originProject != null) { + allProjectAliases.add(originProject.projectAlias()); + } + return Collections.unmodifiableSet(allProjectAliases); + } + + private static IndexRewriteResult rewriteUnqualifiedExpression( + String indexExpression, + @Nullable String originAlias, + Set allProjectAliases + ) { + String localExpression = null; + final List rewrittenExpressions = new ArrayList<>(); + if (originAlias != null) { + localExpression = indexExpression; // adding the original indexExpression for the _origin cluster. + } + for (String targetProjectAlias : allProjectAliases) { + if (false == targetProjectAlias.equals(originAlias)) { + rewrittenExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProjectAlias, indexExpression)); + } + } + return new IndexRewriteResult(localExpression, rewrittenExpressions); + } + + private static IndexRewriteResult rewriteQualifiedExpression( + String resource, + @Nullable String originProjectAlias, + Set allProjectAliases + ) { + String[] splitResource = RemoteClusterAware.splitIndexName(resource); + assert splitResource.length == 2 + : "Expected two strings (project and indexExpression) for a qualified resource [" + + resource + + "], but found [" + + splitResource.length + + "]"; + String requestedProjectAlias = splitResource[0]; + assert requestedProjectAlias != null : "Expected a project alias for a qualified resource but was null"; + String indexExpression = splitResource[1]; + maybeThrowOnUnsupportedResource(indexExpression); + + if (originProjectAlias != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) { + // handling case where we have a qualified expression like: _origin:indexName + return new IndexRewriteResult(indexExpression); + } + + if (originProjectAlias == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) { + // handling case where we have a qualified expression like: _origin:indexName but no _origin project is set + throw new NoMatchingProjectException(requestedProjectAlias); + } + + try { + List allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames( + allProjectAliases, + requestedProjectAlias + ); + + if (allProjectsMatchingAlias.isEmpty()) { + throw new NoMatchingProjectException(requestedProjectAlias); + } + + String localExpression = null; + final List resourcesMatchingLinkedProjectAliases = new ArrayList<>(); + for (String project : allProjectsMatchingAlias) { + if (project.equals(originProjectAlias)) { + localExpression = indexExpression; + } else { + resourcesMatchingLinkedProjectAliases.add(RemoteClusterAware.buildRemoteIndexName(project, indexExpression)); + } + } + + return new IndexRewriteResult(localExpression, resourcesMatchingLinkedProjectAliases); + } catch (NoSuchRemoteClusterException ex) { + logger.debug(ex.getMessage(), ex); + throw new NoMatchingProjectException(requestedProjectAlias); + } + } + + private static void maybeThrowOnUnsupportedResource(String resource) { + // TODO To be handled in future PR. + if (resource.startsWith(EXCLUSION)) { + throw new IllegalArgumentException("Exclusions are not currently supported but was found in the expression [" + resource + "]"); + } + if (resource.startsWith(DATE_MATH)) { + throw new IllegalArgumentException("Date math are not currently supported but was found in the expression [" + resource + "]"); + } + if (IndexNameExpressionResolver.hasSelectorSuffix(resource)) { + throw new IllegalArgumentException("Selectors are not currently supported but was found in the expression [" + resource + "]"); + } + } + + /** + * A container for a local expression and a list of remote expressions. + */ + public record IndexRewriteResult(@Nullable String localExpression, List remoteExpressions) { + public IndexRewriteResult(String localExpression) { + this(localExpression, List.of()); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriterTests.java b/server/src/test/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriterTests.java similarity index 59% rename from server/src/test/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriterTests.java rename to server/src/test/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriterTests.java index 95285c06d1a4f..41f0d02f66af5 100644 --- a/server/src/test/java/org/elasticsearch/search/crossproject/CrossProjectIndexExpressionsRewriterTests.java +++ b/server/src/test/java/org/elasticsearch/search/crossproject/IndexExpressionsRewriterTests.java @@ -12,13 +12,15 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.cluster.metadata.ProjectId; import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matcher; +import java.util.ArrayList; import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.containsInAnyOrder; -public class CrossProjectIndexExpressionsRewriterTests extends ESTestCase { +public class IndexExpressionsRewriterTests extends ESTestCase { public void testFlatOnlyRewrite() { ProjectRoutingInfo origin = createRandomProjectWithAlias("P0"); @@ -29,15 +31,14 @@ public void testFlatOnlyRewrite() { ); String[] requestedResources = new String[] { "logs*", "metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("logs*", "metrics*")); - assertThat(canonical.get("logs*"), containsInAnyOrder("logs*", "P1:logs*", "P2:logs*", "P3:logs*")); - assertThat(canonical.get("metrics*"), containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("logs*", "metrics*")); + assertIndexRewriteResultsContains(actual.get("logs*"), containsInAnyOrder("logs*", "P1:logs*", "P2:logs*", "P3:logs*")); + assertIndexRewriteResultsContains( + actual.get("metrics*"), + containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*") + ); } public void testFlatAndQualifiedRewrite() { @@ -49,15 +50,14 @@ public void testFlatAndQualifiedRewrite() { ); String[] requestedResources = new String[] { "P1:logs*", "metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("P1:logs*", "metrics*")); - assertThat(canonical.get("P1:logs*"), containsInAnyOrder("P1:logs*")); - assertThat(canonical.get("metrics*"), containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("P1:logs*", "metrics*")); + assertIndexRewriteResultsContains(actual.get("P1:logs*"), containsInAnyOrder("P1:logs*")); + assertIndexRewriteResultsContains( + actual.get("metrics*"), + containsInAnyOrder("metrics*", "P1:metrics*", "P2:metrics*", "P3:metrics*") + ); } public void testQualifiedOnlyRewrite() { @@ -69,15 +69,11 @@ public void testQualifiedOnlyRewrite() { ); String[] requestedResources = new String[] { "P1:logs*", "P2:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("P1:logs*", "P2:metrics*")); - assertThat(canonical.get("P1:logs*"), containsInAnyOrder("P1:logs*")); - assertThat(canonical.get("P2:metrics*"), containsInAnyOrder("P2:metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("P1:logs*", "P2:metrics*")); + assertIndexRewriteResultsContains(actual.get("P1:logs*"), containsInAnyOrder("P1:logs*")); + assertIndexRewriteResultsContains(actual.get("P2:metrics*"), containsInAnyOrder("P2:metrics*")); } public void testOriginQualifiedOnlyRewrite() { @@ -89,15 +85,11 @@ public void testOriginQualifiedOnlyRewrite() { ); String[] requestedResources = new String[] { "_origin:logs*", "_origin:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*")); - assertThat(canonical.get("_origin:logs*"), containsInAnyOrder("logs*")); - assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*")); + assertIndexRewriteResultsContains(actual.get("_origin:logs*"), containsInAnyOrder("logs*")); + assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*")); } public void testOriginQualifiedOnlyRewriteWithNoLikedProjects() { @@ -105,15 +97,11 @@ public void testOriginQualifiedOnlyRewriteWithNoLikedProjects() { List linked = List.of(); String[] requestedResources = new String[] { "_origin:logs*", "_origin:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*")); - assertThat(canonical.get("_origin:logs*"), containsInAnyOrder("logs*")); - assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("_origin:logs*", "_origin:metrics*")); + assertIndexRewriteResultsContains(actual.get("_origin:logs*"), containsInAnyOrder("logs*")); + assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*")); } public void testOriginWithDifferentAliasQualifiedOnlyRewrite() { @@ -130,15 +118,11 @@ public void testOriginWithDifferentAliasQualifiedOnlyRewrite() { String metricResource = aliasForOrigin + ":" + metricsIndexAlias; String[] requestedResources = new String[] { logResource, metricResource }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder(logResource, metricResource)); - assertThat(canonical.get(logResource), containsInAnyOrder(logIndexAlias)); - assertThat(canonical.get(metricResource), containsInAnyOrder(metricsIndexAlias)); + assertThat(actual.keySet(), containsInAnyOrder(logResource, metricResource)); + assertIndexRewriteResultsContains(actual.get(logResource), containsInAnyOrder(logIndexAlias)); + assertIndexRewriteResultsContains(actual.get(metricResource), containsInAnyOrder(metricsIndexAlias)); } public void testQualifiedLinkedAndOriginRewrite() { @@ -150,15 +134,11 @@ public void testQualifiedLinkedAndOriginRewrite() { ); String[] requestedResources = new String[] { "P1:logs*", "_origin:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("P1:logs*", "_origin:metrics*")); - assertThat(canonical.get("P1:logs*"), containsInAnyOrder("P1:logs*")); - assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("P1:logs*", "_origin:metrics*")); + assertIndexRewriteResultsContains(actual.get("P1:logs*"), containsInAnyOrder("P1:logs*")); + assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*")); } public void testQualifiedStartsWithProjectWildcardRewrite() { @@ -171,14 +151,10 @@ public void testQualifiedStartsWithProjectWildcardRewrite() { ); String[] requestedResources = new String[] { "Q*:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("Q*:metrics*")); - assertThat(canonical.get("Q*:metrics*"), containsInAnyOrder("Q1:metrics*", "Q2:metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("Q*:metrics*")); + assertIndexRewriteResultsContains(actual.get("Q*:metrics*"), containsInAnyOrder("Q1:metrics*", "Q2:metrics*")); } public void testQualifiedEndsWithProjectWildcardRewrite() { @@ -191,14 +167,10 @@ public void testQualifiedEndsWithProjectWildcardRewrite() { ); String[] requestedResources = new String[] { "*1:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("*1:metrics*")); - assertThat(canonical.get("*1:metrics*"), containsInAnyOrder("P1:metrics*", "Q1:metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("*1:metrics*")); + assertIndexRewriteResultsContains(actual.get("*1:metrics*"), containsInAnyOrder("P1:metrics*", "Q1:metrics*")); } public void testOriginProjectMatchingTwice() { @@ -206,15 +178,11 @@ public void testOriginProjectMatchingTwice() { List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2")); String[] requestedResources = new String[] { "P0:metrics*", "_origin:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("P0:metrics*", "_origin:metrics*")); - assertThat(canonical.get("P0:metrics*"), containsInAnyOrder("metrics*")); - assertThat(canonical.get("_origin:metrics*"), containsInAnyOrder("metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("P0:metrics*", "_origin:metrics*")); + assertIndexRewriteResultsContains(actual.get("P0:metrics*"), containsInAnyOrder("metrics*")); + assertIndexRewriteResultsContains(actual.get("_origin:metrics*"), containsInAnyOrder("metrics*")); } public void testUnderscoreWildcardShouldNotMatchOrigin() { @@ -222,14 +190,10 @@ public void testUnderscoreWildcardShouldNotMatchOrigin() { List linked = List.of(createRandomProjectWithAlias("_P1"), createRandomProjectWithAlias("_P2")); String[] requestedResources = new String[] { "_*:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("_*:metrics*")); - assertThat(canonical.get("_*:metrics*"), containsInAnyOrder("_P1:metrics*", "_P2:metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("_*:metrics*")); + assertIndexRewriteResultsContains(actual.get("_*:metrics*"), containsInAnyOrder("_P1:metrics*", "_P2:metrics*")); } public void testDuplicateInputShouldProduceSingleOutput() { @@ -243,14 +207,10 @@ public void testDuplicateInputShouldProduceSingleOutput() { String indexPattern = "Q*:metrics*"; String[] requestedResources = new String[] { indexPattern, indexPattern }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder(indexPattern)); - assertThat(canonical.get(indexPattern), containsInAnyOrder("Q1:metrics*", "Q2:metrics*")); + assertThat(actual.keySet(), containsInAnyOrder(indexPattern)); + assertIndexRewriteResultsContains(actual.get(indexPattern), containsInAnyOrder("Q1:metrics*", "Q2:metrics*")); } public void testProjectWildcardNotMatchingAnythingShouldThrow() { @@ -265,7 +225,7 @@ public void testProjectWildcardNotMatchingAnythingShouldThrow() { expectThrows( ResourceNotFoundException.class, - () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) + () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) ); } @@ -282,7 +242,7 @@ public void testRewritingShouldThrowOnIndexExclusions() { expectThrows( IllegalArgumentException.class, - () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) + () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) ); } @@ -299,7 +259,7 @@ public void testRewritingShouldThrowOnIndexSelectors() { expectThrows( IllegalArgumentException.class, - () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) + () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) ); } @@ -313,14 +273,13 @@ public void testWildcardOnlyProjectRewrite() { ); String[] requestedResources = new String[] { "*:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("*:metrics*")); - assertThat(canonical.get("*:metrics*"), containsInAnyOrder("P1:metrics*", "P2:metrics*", "Q1:metrics*", "Q2:metrics*", "metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("*:metrics*")); + assertIndexRewriteResultsContains( + actual.get("*:metrics*"), + containsInAnyOrder("P1:metrics*", "P2:metrics*", "Q1:metrics*", "Q2:metrics*", "metrics*") + ); } public void testWildcardMatchesOnlyOriginProject() { @@ -333,14 +292,10 @@ public void testWildcardMatchesOnlyOriginProject() { ); String[] requestedResources = new String[] { "alias*:metrics*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("alias*:metrics*")); - assertThat(canonical.get("alias*:metrics*"), containsInAnyOrder("metrics*")); + assertThat(actual.keySet(), containsInAnyOrder("alias*:metrics*")); + assertIndexRewriteResultsContains(actual.get("alias*:metrics*"), containsInAnyOrder("metrics*")); } public void testEmptyExpressionShouldMatchAll() { @@ -348,24 +303,20 @@ public void testEmptyExpressionShouldMatchAll() { List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2")); String[] requestedResources = new String[] {}; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("*")); - assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*")); + assertThat(actual.keySet(), containsInAnyOrder("_all")); + assertIndexRewriteResultsContains(actual.get("_all"), containsInAnyOrder("P1:_all", "P2:_all", "_all")); } public void testNullExpressionShouldMatchAll() { ProjectRoutingInfo origin = createRandomProjectWithAlias("P0"); List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2")); - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, null); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, null); - assertThat(canonical.keySet(), containsInAnyOrder("*")); - assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*")); + assertThat(actual.keySet(), containsInAnyOrder("_all")); + assertIndexRewriteResultsContains(actual.get("_all"), containsInAnyOrder("P1:_all", "P2:_all", "_all")); } public void testWildcardExpressionShouldMatchAll() { @@ -373,14 +324,10 @@ public void testWildcardExpressionShouldMatchAll() { List linked = List.of(createRandomProjectWithAlias("P1"), createRandomProjectWithAlias("P2")); String[] requestedResources = new String[] { "*" }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder("*")); - assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*")); + assertThat(actual.keySet(), containsInAnyOrder("*")); + assertIndexRewriteResultsContains(actual.get("*"), containsInAnyOrder("P1:*", "P2:*", "*")); } public void test_ALLExpressionShouldMatchAll() { @@ -389,14 +336,10 @@ public void test_ALLExpressionShouldMatchAll() { String all = randomBoolean() ? "_ALL" : "_all"; String[] requestedResources = new String[] { all }; - Map> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions( - origin, - linked, - requestedResources - ); + var actual = IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources); - assertThat(canonical.keySet(), containsInAnyOrder(all)); - assertThat(canonical.get(all), containsInAnyOrder("P1:" + all, "P2:" + all, all)); + assertThat(actual.keySet(), containsInAnyOrder(all)); + assertIndexRewriteResultsContains(actual.get(all), containsInAnyOrder("P1:" + all, "P2:" + all, all)); } public void testRewritingShouldThrowIfNotProjectMatchExpression() { @@ -411,7 +354,7 @@ public void testRewritingShouldThrowIfNotProjectMatchExpression() { expectThrows( NoMatchingProjectException.class, - () -> CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) + () -> IndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, requestedResources) ); } @@ -424,4 +367,21 @@ private ProjectRoutingInfo createRandomProjectWithAlias(String alias) { ProjectTags projectTags = new ProjectTags(tags); return new ProjectRoutingInfo(projectId, type, alias, org, projectTags); } + + private static void assertIndexRewriteResultsContains( + IndexExpressionsRewriter.IndexRewriteResult actual, + Matcher> iterableMatcher + ) { + assertThat(resultAsList(actual), iterableMatcher); + } + + private static List resultAsList(IndexExpressionsRewriter.IndexRewriteResult result) { + if (result.localExpression() == null) { + return result.remoteExpressions(); + } + List all = new ArrayList<>(); + all.add(result.localExpression()); + all.addAll(result.remoteExpressions()); + return List.copyOf(all); + } }