diff --git a/server/src/main/java/org/elasticsearch/rest/action/RestActions.java b/server/src/main/java/org/elasticsearch/rest/action/RestActions.java index 69cf5bbb1b89d..544d2bd9cf394 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/RestActions.java +++ b/server/src/main/java/org/elasticsearch/rest/action/RestActions.java @@ -12,6 +12,7 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.ShardOperationFailedException; +import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.broadcast.BaseBroadcastResponse; import org.elasticsearch.action.support.nodes.BaseNodeResponse; import org.elasticsearch.action.support.nodes.BaseNodesResponse; @@ -47,6 +48,7 @@ public class RestActions { public static final ParseField SKIPPED_FIELD = new ParseField("skipped"); public static final ParseField FAILED_FIELD = new ParseField("failed"); public static final ParseField FAILURES_FIELD = new ParseField("failures"); + public static final ParseField PROJECT_ROUTING = new ParseField("project_routing"); public static long parseVersion(RestRequest request) { if (request.hasParam("version")) { @@ -260,10 +262,14 @@ public RestResponse buildResponse(NodesResponse response, XContentBuilder builde } + public static QueryBuilder getQueryContent(XContentParser parser) { + return getQueryContent(parser, null); + } + /** * Parses a top level query including the query element that wraps it */ - public static QueryBuilder getQueryContent(XContentParser parser) { + public static QueryBuilder getQueryContent(XContentParser parser, SearchRequest searchRequest) { try { QueryBuilder queryBuilder = null; XContentParser.Token first = parser.nextToken(); @@ -281,6 +287,9 @@ public static QueryBuilder getQueryContent(XContentParser parser) { String currentName = parser.currentName(); if ("query".equals(currentName)) { queryBuilder = parseTopLevelQuery(parser); + } else if (PROJECT_ROUTING.match(currentName, parser.getDeprecationHandler()) && searchRequest != null) { + parser.nextToken(); + searchRequest.setProjectRouting(parser.text()); } else { throw new ParsingException(parser.getTokenLocation(), "request does not support [" + parser.currentName() + "]"); } diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestCountAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestCountAction.java index 864ea8f05e21b..dbeaa87c49ac8 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestCountAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestCountAction.java @@ -13,6 +13,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; import org.elasticsearch.common.Table; @@ -25,24 +26,31 @@ import org.elasticsearch.rest.action.RestActions; import org.elasticsearch.rest.action.RestResponseListener; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import java.io.IOException; import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; @ServerlessScope(Scope.PUBLIC) public class RestCountAction extends AbstractCatAction { - private final Settings settings; + private final CrossProjectModeDecider crossProjectModeDecider; public RestCountAction(Settings settings) { - this.settings = settings; + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override public List routes() { - return List.of(new Route(GET, "/_cat/count"), new Route(GET, "/_cat/count/{index}")); + return List.of( + new Route(GET, "/_cat/count"), + new Route(POST, "/_cat/count"), + new Route(GET, "/_cat/count/{index}"), + new Route(POST, "/_cat/count/{index}") + ); } @Override @@ -58,24 +66,25 @@ protected void documentation(StringBuilder sb) { @Override public RestChannelConsumer doCatRequest(final RestRequest request, final NodeClient client) { - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { - // accept but drop project_routing param until fully supported - request.param("project_routing"); - } - String[] indices = Strings.splitStringByCommaToArray(request.param("index")); SearchRequest countRequest = new SearchRequest(indices); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().size(0).trackTotalHits(true); countRequest.source(searchSourceBuilder); + if (crossProjectModeDecider.crossProjectEnabled() && countRequest.allowsCrossProject()) { + countRequest.indicesOptions( + IndicesOptions.builder().crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true)).build() + ); + } try { request.withContentOrSourceParamParserOrNull(parser -> { if (parser == null) { QueryBuilder queryBuilder = RestActions.urlParamsToQueryBuilder(request); if (queryBuilder != null) { + // since there is no request body, no need to pass in countRequest to handle project_routing param searchSourceBuilder.query(queryBuilder); } } else { - searchSourceBuilder.query(RestActions.getQueryContent(parser)); + searchSourceBuilder.query(RestActions.getQueryContent(parser, countRequest)); } }); } catch (IOException e) { diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestCountAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestCountAction.java index a763578ab39c0..0b65ccbf27f2b 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestCountAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestCountAction.java @@ -24,6 +24,7 @@ import org.elasticsearch.rest.action.RestActions; import org.elasticsearch.rest.action.RestBuilderListener; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; @@ -37,10 +38,10 @@ @ServerlessScope(Scope.PUBLIC) public class RestCountAction extends BaseRestHandler { - private Settings settings; + private final CrossProjectModeDecider crossProjectModeDecider; public RestCountAction(Settings settings) { - this.settings = settings; + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override @@ -60,23 +61,26 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { - // accept but drop project_routing param until fully supported - request.param("project_routing"); + SearchRequest countRequest = new SearchRequest(Strings.splitStringByCommaToArray(request.param("index"))); + IndicesOptions indicesOptions = IndicesOptions.fromRequest(request, countRequest.indicesOptions()); + if (crossProjectModeDecider.crossProjectEnabled() && countRequest.allowsCrossProject()) { + indicesOptions = IndicesOptions.builder(indicesOptions) + .crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true)) + .build(); } + countRequest.indicesOptions(indicesOptions); - SearchRequest countRequest = new SearchRequest(Strings.splitStringByCommaToArray(request.param("index"))); - countRequest.indicesOptions(IndicesOptions.fromRequest(request, countRequest.indicesOptions())); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().size(0).trackTotalHits(true); countRequest.source(searchSourceBuilder); request.withContentOrSourceParamParserOrNull(parser -> { if (parser == null) { QueryBuilder queryBuilder = RestActions.urlParamsToQueryBuilder(request); if (queryBuilder != null) { + // since there is no request body, no need to pass in countRequest to handle project_routing param searchSourceBuilder.query(queryBuilder); } } else { - searchSourceBuilder.query(RestActions.getQueryContent(parser)); + searchSourceBuilder.query(RestActions.getQueryContent(parser, countRequest)); } }); countRequest.routing(request.param("routing")); diff --git a/server/src/test/java/org/elasticsearch/rest/action/RestActionsTests.java b/server/src/test/java/org/elasticsearch/rest/action/RestActionsTests.java index fb9d5df7056fa..52d31c0c6c70c 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/RestActionsTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/RestActionsTests.java @@ -10,6 +10,7 @@ package org.elasticsearch.rest.action; import org.elasticsearch.action.ShardOperationFailedException; +import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.ParsingException; @@ -42,6 +43,8 @@ import static java.util.Collections.emptyList; import static org.elasticsearch.index.query.QueryStringQueryBuilder.DEFAULT_OPERATOR; import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; public class RestActionsTests extends ESTestCase { @@ -233,6 +236,51 @@ public void testUrlParamsToQueryBuilderError() { ); } + public void testParseWithProjectRouting() throws IOException { + QueryBuilder query = new MatchQueryBuilder("foo", "bar"); + String requestBody1 = """ + { + "query": _QUERY_, + "project_routing": "_alias:_origin" + } + """; + String requestBody2 = """ + { + "project_routing": "_csp:aws AND (_region:us* OR _region:eu-west-1)", + "query": _QUERY_ + } + """; + + { + String requestBody = randomFrom(requestBody1, requestBody2).replaceFirst("_QUERY_", query.toString()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, requestBody)) { + // if no SearchRequest passed in, an error should be thrown that project_routing is not supported for that endpoint + ParsingException e = expectThrows(ParsingException.class, () -> RestActions.getQueryContent(parser)); + assertEquals(e.getMessage(), "request does not support [project_routing]"); + } + } + { + SearchRequest searchRequest = new SearchRequest("index"); + String requestBody = requestBody1.replaceFirst("_QUERY_", query.toString()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, requestBody)) { + assertNull(searchRequest.getProjectRouting()); + QueryBuilder actual = RestActions.getQueryContent(parser, searchRequest); + assertEquals(query, actual); + assertEquals(searchRequest.getProjectRouting(), "_alias:_origin"); + } + } + { + String requestBody = requestBody2.replaceFirst("_QUERY_", query.toString()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, requestBody)) { + SearchRequest searchRequest = new SearchRequest("index"); + assertNull(searchRequest.getProjectRouting()); + QueryBuilder actual = RestActions.getQueryContent(parser, searchRequest); + assertEquals(query, actual); + assertEquals(searchRequest.getProjectRouting(), "_csp:aws AND (_region:us* OR _region:eu-west-1)"); + } + } + } + private static ShardSearchFailure createShardFailureParsingException(String nodeId, int shardId, String clusterAlias) { String index = "index"; ParsingException ex = new ParsingException(0, 0, "error", new IllegalArgumentException("some bad argument"));