Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -57,13 +57,20 @@ public Set<String> supportedCapabilities() {
protected BaseRestHandler.RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
String[] indices = Strings.splitStringByCommaToArray(request.param("name"));
String modeParam = request.param("mode");
if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) {
final boolean crossProjectEnabled = settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false);
if (crossProjectEnabled) {
// accept but drop project_routing param until fully supported
request.param("project_routing");
}
IndicesOptions indicesOptions = IndicesOptions.fromRequest(request, ResolveIndexAction.Request.DEFAULT_INDICES_OPTIONS);
if (crossProjectEnabled) {
indicesOptions = IndicesOptions.builder(indicesOptions)
.crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true))
.build();
}
ResolveIndexAction.Request resolveRequest = new ResolveIndexAction.Request(
indices,
IndicesOptions.fromRequest(request, ResolveIndexAction.Request.DEFAULT_INDICES_OPTIONS),
indicesOptions,
modeParam == null
? null
: Arrays.stream(modeParam.split(","))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
// this might be set by old clients
request.param("min_compatible_shard_node");

if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) {
final boolean crossProjectEnabled = settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false);
if (crossProjectEnabled) {
// accept but drop project_routing param until fully supported
request.param("project_routing");
}
Expand All @@ -128,7 +129,15 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
*/
IntConsumer setSize = size -> searchRequest.source().size(size);
request.withContentOrSourceParamParserOrNull(
parser -> parseSearchRequest(searchRequest, request, parser, clusterSupportsFeature, setSize, searchUsageHolder)
parser -> parseSearchRequest(
searchRequest,
request,
parser,
clusterSupportsFeature,
setSize,
searchUsageHolder,
crossProjectEnabled
)
);

return channel -> {
Expand Down Expand Up @@ -157,6 +166,17 @@ public static void parseSearchRequest(
parseSearchRequest(searchRequest, request, requestContentParser, clusterSupportsFeature, setSize, null);
}

public static void parseSearchRequest(
SearchRequest searchRequest,
RestRequest request,
@Nullable XContentParser requestContentParser,
Predicate<NodeFeature> clusterSupportsFeature,
IntConsumer setSize,
@Nullable SearchUsageHolder searchUsageHolder
) throws IOException {
parseSearchRequest(searchRequest, request, requestContentParser, clusterSupportsFeature, setSize, searchUsageHolder, false);
}

/**
* Parses the rest request on top of the SearchRequest, preserving values that are not overridden by the rest request.
*
Expand All @@ -167,14 +187,16 @@ public static void parseSearchRequest(
* @param clusterSupportsFeature used to check if certain features are available in this cluster
* @param setSize how the size url parameter is handled. {@code udpate_by_query} and regular search differ here.
* @param searchUsageHolder the holder of search usage stats
* @param crossProjectEnabled whether serverless.cross_project.enabled is set to true
Copy link
Contributor

@pawankartik-elastic pawankartik-elastic Oct 13, 2025

Choose a reason for hiding this comment

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

I just realised this: parseSearchRequest() can be called in 3 different scenarios:

  • For CPS-enabled endpoints and in a CPS context,
  • For CPS-enabled endpoints and in a non-CPS context, and,
  • For CPS-disabled endpoints.

Therefore, simply setting this param to false will not help us accurately distinguish between the 2nd and the 3rd options. There are other areas in Elasticsearch where we need to represent such scenarios, and we resorted to using an Optional<Boolean>. IIUC, we're using it for skip_unavailable too (see RemoteClusterService). I actually have a PR where we modified this method to do the same: #135614 (reverted and is pending Breaking Change approval due to some other reasons). Can you accommodate those changes here?

Wdyt @piergm?

Copy link
Member Author

Choose a reason for hiding this comment

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

Overall I'd prefer if you could make the necessary change with a follow-up. I updated it here because otherwise the serverless IT breaks without it. The search team is more qualified for further refinements. Does that work for you?

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough! Proceed.

*/
public static void parseSearchRequest(
SearchRequest searchRequest,
RestRequest request,
@Nullable XContentParser requestContentParser,
Predicate<NodeFeature> clusterSupportsFeature,
IntConsumer setSize,
@Nullable SearchUsageHolder searchUsageHolder
@Nullable SearchUsageHolder searchUsageHolder,
boolean crossProjectEnabled
) throws IOException {
if (searchRequest.source() == null) {
searchRequest.source(new SearchSourceBuilder());
Expand Down Expand Up @@ -222,7 +244,13 @@ public static void parseSearchRequest(
}
searchRequest.routing(request.param("routing"));
searchRequest.preference(request.param("preference"));
searchRequest.indicesOptions(IndicesOptions.fromRequest(request, searchRequest.indicesOptions()));
IndicesOptions indicesOptions = IndicesOptions.fromRequest(request, searchRequest.indicesOptions());
if (crossProjectEnabled) {
indicesOptions = IndicesOptions.builder(indicesOptions)
.crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true))
.build();
}
searchRequest.indicesOptions(indicesOptions);

validateSearchRequest(request, searchRequest);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,10 @@ public static ElasticsearchException validate(
}

public static IndicesOptions indicesOptionsForCrossProjectFanout(IndicesOptions indicesOptions) {
// TODO set resolveCrossProject=false here once we have an IndicesOptions flag for that
return IndicesOptions.builder(indicesOptions)
.concreteTargetOptions(new IndicesOptions.ConcreteTargetOptions(true))
.wildcardOptions(IndicesOptions.WildcardOptions.builder(indicesOptions.wildcardOptions()).allowEmptyExpressions(true).build())
.crossProjectModeOptions(IndicesOptions.CrossProjectModeOptions.DEFAULT)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.Booleans;

/**
* Utility class to determine whether Cross-Project Search (CPS) applies to an inbound request.
Expand All @@ -35,6 +34,7 @@
*/
public class CrossProjectModeDecider {
private static final String CROSS_PROJECT_ENABLED_SETTING_KEY = "serverless.cross_project.enabled";
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(CrossProjectModeDecider.class);
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove the logger as is not used?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep it's a debug leftover 🤦
Thanks for catching it. Removed in d85edf3

private final boolean crossProjectEnabled;

public CrossProjectModeDecider(Settings settings) {
Expand All @@ -49,8 +49,7 @@ public boolean resolvesCrossProject(IndicesRequest.Replaceable request) {
if (crossProjectEnabled == false) {
return false;
}
// TODO this needs to be based on the IndicesOptions flag instead, once available
final boolean indicesOptionsResolveCrossProject = Booleans.parseBoolean(System.getProperty("cps.resolve_cross_project", "false"));
return request.allowsCrossProject() && indicesOptionsResolveCrossProject;
// TODO: The following check can be an method on the request itself
return request.allowsCrossProject() && request.indicesOptions().resolveCrossProjectIndexExpression();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.rest.action.admin.indices;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.ActionType;
import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction;
import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.cluster.project.TestProjectResolvers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.rest.FakeRestChannel;
import org.elasticsearch.test.rest.FakeRestRequest;
import org.elasticsearch.threadpool.ThreadPool;

import java.util.List;

import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.mock;

public class RestResolveIndexActionTests extends ESTestCase {

public void testAddResolveCrossProjectBasedOnSettingValue() throws Exception {
final boolean cpsEnabled = randomBoolean();
final Settings settings = Settings.builder().put("serverless.cross_project.enabled", cpsEnabled).build();
final var action = new RestResolveIndexAction(settings);
final var request = new FakeRestRequest.Builder(xContentRegistry()).withMethod(RestRequest.Method.GET)
.withPath("/_resolve/index/foo")
.build();

final NodeClient nodeClient = new NodeClient(settings, mock(ThreadPool.class), TestProjectResolvers.DEFAULT_PROJECT_ONLY) {
@SuppressWarnings("unchecked")
@Override
public <Request extends ActionRequest, Response extends ActionResponse> void doExecute(
ActionType<Response> action,
Request request,
ActionListener<Response> listener
) {
final var resolveIndexRequest = asInstanceOf(ResolveIndexAction.Request.class, request);
assertThat(resolveIndexRequest.indicesOptions().resolveCrossProjectIndexExpression(), equalTo(cpsEnabled));
listener.onResponse((Response) new ResolveIndexAction.Response(List.of(), List.of(), List.of()));
}
};

final var restChannel = new FakeRestChannel(request, true, 1);
action.handleRequest(request, restChannel, nodeClient);
assertThat(restChannel.responses().get(), equalTo(1));
}
}