Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
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;
Expand All @@ -34,8 +36,7 @@ public class CrossProjectIndexExpressionsRewriter {

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[] MATCH_ALL = new String[] { Metadata.ALL };
private static final String EXCLUSION = "-";
private static final String DATE_MATH = "<";

Expand Down Expand Up @@ -63,82 +64,112 @@ public static Map<String, List<String>> rewriteIndexExpressions(
}
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<String> linkedProjectNames = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
Map<String, List<String>> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
for (String resource : indices) {
if (canonicalExpressionsMap.containsKey(resource)) {
final Set<String> allProjectAliases = getAllProjectAliases(originProject, linkedProjects);
final String originProjectAlias = originProject != null ? originProject.projectAlias() : null;
final Map<String, List<String>> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
for (String indexExpression : indices) {
if (canonicalExpressionsMap.containsKey(indexExpression)) {
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<String> 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<String> canonicalExpressions = rewriteUnqualified(resource, originProject, linkedProjects);
canonicalExpressionsMap.put(resource, canonicalExpressions);
logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, canonicalExpressions);
}
canonicalExpressionsMap.put(
indexExpression,
rewriteIndexExpression(indexExpression, originProjectAlias, allProjectAliases).all()
);
}
return canonicalExpressionsMap;
}

private static List<String> rewriteUnqualified(
/**
* 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 LocalWithRemoteExpressions rewriteIndexExpression(
String indexExpression,
@Nullable ProjectRoutingInfo origin,
List<ProjectRoutingInfo> projects
@Nullable String originProjectAlias,
Set<String> allProjectAliases
) {
List<String> canonicalExpressions = new ArrayList<>();
if (origin != null) {
canonicalExpressions.add(indexExpression); // adding the original indexExpression for the _origin cluster.
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 LocalWithRemoteExpressions 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);
}
for (ProjectRoutingInfo targetProject : projects) {
canonicalExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProject.projectAlias(), indexExpression));
return rewrittenExpression;
}

private static Set<String> getAllProjectAliases(@Nullable ProjectRoutingInfo originProject, List<ProjectRoutingInfo> linkedProjects) {
assert originProject != null || linkedProjects.isEmpty() == false
: "either origin project or linked projects must be in project target set";

final Set<String> allProjectAliases = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
if (originProject != null) {
allProjectAliases.add(originProject.projectAlias());
}
return canonicalExpressions;
return Collections.unmodifiableSet(allProjectAliases);
}

private static List<String> rewriteQualified(
String requestedProjectAlias,
private static LocalWithRemoteExpressions rewriteUnqualifiedExpression(
String indexExpression,
@Nullable ProjectRoutingInfo originProject,
@Nullable String originAlias,
Set<String> allProjectAliases
) {
if (originProject != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
String localExpression = null;
final List<String> 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 LocalWithRemoteExpressions(localExpression, rewrittenExpressions);
}

private static LocalWithRemoteExpressions rewriteQualifiedExpression(
String resource,
@Nullable String originProjectAlias,
Set<String> 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 List.of(indexExpression);
return new LocalWithRemoteExpressions(indexExpression);
}

if (originProject == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
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 {
if (originProject != null) {
allProjectAliases.add(originProject.projectAlias());
}
List<String> resourcesMatchingAliases = new ArrayList<>();
List<String> allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames(
allProjectAliases,
requestedProjectAlias
Expand All @@ -148,15 +179,17 @@ private static List<String> rewriteQualified(
throw new NoMatchingProjectException(requestedProjectAlias);
}

String localExpression = null;
final List<String> resourcesMatchingLinkedProjectAliases = new ArrayList<>();
for (String project : allProjectsMatchingAlias) {
if (originProject != null && project.equals(originProject.projectAlias())) {
resourcesMatchingAliases.add(indexExpression);
if (project.equals(originProjectAlias)) {
localExpression = indexExpression;
} else {
resourcesMatchingAliases.add(RemoteClusterAware.buildRemoteIndexName(project, indexExpression));
resourcesMatchingLinkedProjectAliases.add(RemoteClusterAware.buildRemoteIndexName(project, indexExpression));
}
}

return resourcesMatchingAliases;
return new LocalWithRemoteExpressions(localExpression, resourcesMatchingLinkedProjectAliases);
} catch (NoSuchRemoteClusterException ex) {
logger.debug(ex.getMessage(), ex);
throw new NoMatchingProjectException(requestedProjectAlias);
Expand All @@ -173,7 +206,6 @@ private static void maybeThrowOnUnsupportedResource(String resource) {
}
if (IndexNameExpressionResolver.hasSelectorSuffix(resource)) {
throw new IllegalArgumentException("Selectors are not currently supported but was found in the expression [" + resource + "]");

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.core.Nullable;

import java.util.ArrayList;
import java.util.List;

/**
* A container for a local expression and a list of remote expressions.
*/
public record LocalWithRemoteExpressions(@Nullable String localExpression, List<String> remoteExpressions) {
public LocalWithRemoteExpressions(String localExpression) {
this(localExpression, List.of());
}

List<String> all() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will disappear once we've removed public static Map<String, List<String>> rewriteIndexExpressions in favor of calling rewriteIndexExpression inside the IndexAbstractionResolver

if (localExpression == null) {
return remoteExpressions;
}
List<String> all = new ArrayList<>();
all.add(localExpression);
all.addAll(remoteExpressions);
return List.copyOf(all);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,8 @@ public void testEmptyExpressionShouldMatchAll() {
requestedResources
);

assertThat(canonical.keySet(), containsInAnyOrder("*"));
assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*"));
assertThat(canonical.keySet(), containsInAnyOrder("_all"));
assertThat(canonical.get("_all"), containsInAnyOrder("P1:_all", "P2:_all", "_all"));
}

public void testNullExpressionShouldMatchAll() {
Expand All @@ -364,8 +364,8 @@ public void testNullExpressionShouldMatchAll() {

Map<String, List<String>> canonical = CrossProjectIndexExpressionsRewriter.rewriteIndexExpressions(origin, linked, null);

assertThat(canonical.keySet(), containsInAnyOrder("*"));
assertThat(canonical.get("*"), containsInAnyOrder("P1:*", "P2:*", "*"));
assertThat(canonical.keySet(), containsInAnyOrder("_all"));
assertThat(canonical.get("_all"), containsInAnyOrder("P1:_all", "P2:_all", "_all"));
}

public void testWildcardExpressionShouldMatchAll() {
Expand Down