Skip to content

Commit 742312a

Browse files
committed
Move rewriter
1 parent a6df704 commit 742312a

File tree

3 files changed

+626
-0
lines changed

3 files changed

+626
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.search.crossproject;
11+
12+
import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver;
13+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
14+
import org.elasticsearch.core.Nullable;
15+
import org.elasticsearch.logging.LogManager;
16+
import org.elasticsearch.logging.Logger;
17+
import org.elasticsearch.transport.NoSuchRemoteClusterException;
18+
import org.elasticsearch.transport.RemoteClusterAware;
19+
20+
import java.util.ArrayList;
21+
import java.util.LinkedHashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import java.util.stream.Collectors;
26+
27+
/**
28+
* Utility class for rewriting cross-project index expressions.
29+
* Provides methods that can rewrite qualified and unqualified index expressions to canonical CCS.
30+
*/
31+
public class CrossProjectIndexExpressionsRewriter {
32+
private static final Logger logger = LogManager.getLogger(CrossProjectIndexExpressionsRewriter.class);
33+
private static final String ORIGIN_PROJECT_KEY = "_origin";
34+
private static final String WILDCARD = "*";
35+
private static final String[] MATCH_ALL = new String[] { WILDCARD };
36+
private static final String EXCLUSION = "-";
37+
private static final String DATE_MATH = "<";
38+
39+
/**
40+
* Rewrites index expressions for cross-project search requests.
41+
* Handles qualified and unqualified expressions and match-all cases will also hand exclusions in the future.
42+
*
43+
* @param originProject the _origin project with its alias
44+
* @param linkedProjects the list of linked and available projects to consider for a request
45+
* @param originalIndices the array of index expressions to be rewritten to canonical CCS
46+
* @return a map from original index expressions to lists of canonical index expressions
47+
* @throws IllegalArgumentException if exclusions, date math or selectors are present in the index expressions
48+
* @throws NoMatchingProjectException if a qualified resource cannot be resolved because a project is missing
49+
*/
50+
public static Map<String, List<String>> rewriteIndexExpressions(
51+
ProjectRoutingInfo originProject,
52+
List<ProjectRoutingInfo> linkedProjects,
53+
final String[] originalIndices
54+
) {
55+
final String[] indices;
56+
if (originalIndices == null || originalIndices.length == 0) { // handling of match all cases besides _all and `*`
57+
indices = MATCH_ALL;
58+
} else {
59+
indices = originalIndices;
60+
}
61+
assert false == IndexNameExpressionResolver.isNoneExpression(indices)
62+
: "expression list is *,-* which effectively means a request that requests no indices";
63+
assert originProject != null || linkedProjects.isEmpty() == false
64+
: "either origin project or linked projects must be in project target set";
65+
66+
Set<String> linkedProjectNames = linkedProjects.stream().map(ProjectRoutingInfo::projectAlias).collect(Collectors.toSet());
67+
Map<String, List<String>> canonicalExpressionsMap = new LinkedHashMap<>(indices.length);
68+
for (String resource : indices) {
69+
if (canonicalExpressionsMap.containsKey(resource)) {
70+
continue;
71+
}
72+
maybeThrowOnUnsupportedResource(resource);
73+
74+
boolean isQualified = RemoteClusterAware.isRemoteIndexName(resource);
75+
if (isQualified) {
76+
// handing of qualified expressions
77+
String[] splitResource = RemoteClusterAware.splitIndexName(resource);
78+
assert splitResource.length == 2
79+
: "Expected two strings (project and indexExpression) for a qualified resource ["
80+
+ resource
81+
+ "], but found ["
82+
+ splitResource.length
83+
+ "]";
84+
String projectAlias = splitResource[0];
85+
assert projectAlias != null : "Expected a project alias for a qualified resource but was null";
86+
String indexExpression = splitResource[1];
87+
maybeThrowOnUnsupportedResource(indexExpression);
88+
89+
List<String> canonicalExpressions = rewriteQualified(projectAlias, indexExpression, originProject, linkedProjectNames);
90+
91+
canonicalExpressionsMap.put(resource, canonicalExpressions);
92+
logger.debug("Rewrote qualified expression [{}] to [{}]", resource, canonicalExpressions);
93+
} else {
94+
// un-qualified expression, i.e. flat-world
95+
List<String> canonicalExpressions = rewriteUnqualified(resource, originProject, linkedProjects);
96+
canonicalExpressionsMap.put(resource, canonicalExpressions);
97+
logger.debug("Rewrote unqualified expression [{}] to [{}]", resource, canonicalExpressions);
98+
}
99+
}
100+
return canonicalExpressionsMap;
101+
}
102+
103+
private static List<String> rewriteUnqualified(
104+
String indexExpression,
105+
@Nullable ProjectRoutingInfo origin,
106+
List<ProjectRoutingInfo> projects
107+
) {
108+
List<String> canonicalExpressions = new ArrayList<>();
109+
if (origin != null) {
110+
canonicalExpressions.add(indexExpression); // adding the original indexExpression for the _origin cluster.
111+
}
112+
for (ProjectRoutingInfo targetProject : projects) {
113+
canonicalExpressions.add(RemoteClusterAware.buildRemoteIndexName(targetProject.projectAlias(), indexExpression));
114+
}
115+
return canonicalExpressions;
116+
}
117+
118+
private static List<String> rewriteQualified(
119+
String requestedProjectAlias,
120+
String indexExpression,
121+
@Nullable ProjectRoutingInfo originProject,
122+
Set<String> allProjectAliases
123+
) {
124+
if (originProject != null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
125+
// handling case where we have a qualified expression like: _origin:indexName
126+
return List.of(indexExpression);
127+
}
128+
129+
if (originProject == null && ORIGIN_PROJECT_KEY.equals(requestedProjectAlias)) {
130+
// handling case where we have a qualified expression like: _origin:indexName but no _origin project is set
131+
throw new NoMatchingProjectException(requestedProjectAlias);
132+
}
133+
134+
try {
135+
if (originProject != null) {
136+
allProjectAliases.add(originProject.projectAlias());
137+
}
138+
List<String> resourcesMatchingAliases = new ArrayList<>();
139+
List<String> allProjectsMatchingAlias = ClusterNameExpressionResolver.resolveClusterNames(
140+
allProjectAliases,
141+
requestedProjectAlias
142+
);
143+
144+
if (allProjectsMatchingAlias.isEmpty()) {
145+
throw new NoMatchingProjectException(requestedProjectAlias);
146+
}
147+
148+
for (String project : allProjectsMatchingAlias) {
149+
if (originProject != null && project.equals(originProject.projectAlias())) {
150+
resourcesMatchingAliases.add(indexExpression);
151+
} else {
152+
resourcesMatchingAliases.add(RemoteClusterAware.buildRemoteIndexName(project, indexExpression));
153+
}
154+
}
155+
156+
return resourcesMatchingAliases;
157+
} catch (NoSuchRemoteClusterException ex) {
158+
logger.debug(ex.getMessage(), ex);
159+
throw new NoMatchingProjectException(requestedProjectAlias);
160+
}
161+
}
162+
163+
private static void maybeThrowOnUnsupportedResource(String resource) {
164+
// TODO To be handled in future PR.
165+
if (resource.startsWith(EXCLUSION)) {
166+
throw new IllegalArgumentException("Exclusions are not currently supported but was found in the expression [" + resource + "]");
167+
}
168+
if (resource.startsWith(DATE_MATH)) {
169+
throw new IllegalArgumentException("Date math are not currently supported but was found in the expression [" + resource + "]");
170+
}
171+
if (IndexNameExpressionResolver.hasSelectorSuffix(resource)) {
172+
throw new IllegalArgumentException("Selectors are not currently supported but was found in the expression [" + resource + "]");
173+
174+
}
175+
}
176+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.search.crossproject;
11+
12+
import org.elasticsearch.ResourceNotFoundException;
13+
14+
/**
15+
* An exception that a project is missing
16+
*/
17+
public final class NoMatchingProjectException extends ResourceNotFoundException {
18+
19+
public NoMatchingProjectException(String projectName) {
20+
super("No such project: [" + projectName + "]");
21+
}
22+
23+
}

0 commit comments

Comments
 (0)