Skip to content

Commit

Permalink
Resolve/cluster allows querying for cluster info only (no index expre…
Browse files Browse the repository at this point in the history
…ssion required) (#119898) (#120650)

Resolve/cluster allows querying for cluster-info-only (no index expression required)

This enhancement provides users with the ability to query the _resolve/cluster API endpoint without specifying
an index expression to match against. This allows users to quickly test what remote clusters are configured on
a cluster and whether they are available for querying.

The new endpoint takes no index expression:

```
GET _resolve/cluster
```

and returns the same information as before except for the "matching_indices" field. Example response:

```
{
  "remote1": {
    "connected": false,
    "skip_unavailable": true
  },
  "remote2": {
    "connected": true,
    "skip_unavailable": false,
    "version": {
      "number": "8.17.0",
      "build_flavor": "default",
      "minimum_wire_compatibility_version": "7.17.0",
      "minimum_index_compatibility_version": "7.0.0"
    }
  }
}
```

For backwards compatibility, this new endpoint works with clusters from older versions by querying with the index expression `dummy*` on those older clusters and ignoring the matching_indices value in the response they return.
  • Loading branch information
quux00 authored Jan 22, 2025
1 parent dc73837 commit 36f5a55
Show file tree
Hide file tree
Showing 14 changed files with 560 additions and 90 deletions.
6 changes: 6 additions & 0 deletions docs/changelog/119898.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 119898
summary: Resolve/cluster allows querying for cluster info only (no index expression
required)
area: CCS
type: enhancement
issues: []
37 changes: 29 additions & 8 deletions docs/reference/indices/resolve-cluster.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ For the most up-to-date API details, refer to {api-es}/group/endpoint-indices[In
--

Resolves the specified index expressions to return information about
each cluster, including the local cluster, if included.
each cluster, including the local "querying" cluster, if included. If no index expression
is provided, this endpoint will return information about all the remote
clusters that are configured on the querying cluster.

This endpoint is useful before doing a <<modules-cross-cluster-search,{ccs}>> in
order to determine which remote clusters should be included in a search.
Expand All @@ -20,14 +22,13 @@ You use the same index expression with this endpoint as you would for cross-clus
search. Index and <<exclude-problematic-clusters,cluster exclusions>> are also supported
with this endpoint.

For each cluster in the index expression, information is returned about:
For each cluster in scope, information is returned about:

1. whether the querying ("local") cluster is currently connected to each remote cluster
in the index expression scope
1. whether the querying ("local") cluster is currently connected to it
2. whether each remote cluster is configured with `skip_unavailable` as `true` or `false`
3. whether there are any indices, aliases or data streams on that cluster that match
the index expression
4. whether the search is likely to have errors returned when you do the {ccs} (including any
the index expression (if one provided)
4. whether the search is likely to have errors returned when you do a {ccs} (including any
authorization errors if your user does not have permission to query a remote cluster or
the indices on that cluster)
5. (in some cases) cluster version information, including the Elasticsearch server version
Expand All @@ -41,6 +42,11 @@ Once the proper security permissions are obtained, then you can rely on the `con
in the response to determine whether the remote cluster is available and ready for querying.
====

NOTE: When querying older clusters that do not support the _resolve/cluster endpoint
without an index expression, the local cluster will send the index expression `dummy*`
to those remote clusters, so if an errors occur, you may see a reference to that index
expression even though you didn't request it. If it causes a problem, you can instead
include an index expression like `*:*` to this endpoint to bypass the issue.

////
[source,console]
Expand Down Expand Up @@ -71,14 +77,22 @@ PUT _cluster/settings
// TEST[s/35.238.149.\d+:930\d+/\${transport_host}/]
////

[source,console]
----
GET /_resolve/cluster
----
// TEST[continued]

Returns information about all remote clusters configured on the local cluster.

[source,console]
----
GET /_resolve/cluster/my-index-*,cluster*:my-index-*
----
// TEST[continued]

This will return information about the local cluster and all remotely configured
clusters that start with the alias `cluster*`. Each cluster will return information
Returns information about the local cluster and all remote clusters that
start with the alias `cluster*`. Each cluster will return information
about whether it has any indices, aliases or data streams that match `my-index-*`.

[[resolve-cluster-api-request]]
Expand Down Expand Up @@ -126,6 +140,13 @@ ignored when frozen. Defaults to `false`.
+
deprecated:[7.16.0]

[TIP]
====
The index options above are only allowed when specifying an index expression.
You will get an error if you specify index options to the _resolve/cluster API
that takes no index expression.
====


[discrete]
[[usecases-for-resolve-cluster]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.elasticsearch.action.admin.indices.resolve.ResolveClusterActionResponse;
import org.elasticsearch.action.admin.indices.resolve.ResolveClusterInfo;
import org.elasticsearch.action.admin.indices.resolve.TransportResolveClusterAction;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.Strings;
Expand Down Expand Up @@ -406,6 +407,52 @@ public void testClusterResolveWithIndices() throws IOException {
}
}

// corresponds to the GET _resolve/cluster endpoint with no index expression specified
public void testClusterResolveWithNoIndexExpression() throws IOException {
Map<String, Object> testClusterInfo = setupThreeClusters(false);
boolean skipUnavailable1 = (Boolean) testClusterInfo.get("remote1.skip_unavailable");
boolean skipUnavailable2 = true;

{
String[] noIndexSpecified = new String[0];
boolean clusterInfoOnly = true;
boolean runningOnQueryingCluster = true;
ResolveClusterActionRequest request = new ResolveClusterActionRequest(
noIndexSpecified,
IndicesOptions.DEFAULT,
clusterInfoOnly,
runningOnQueryingCluster
);

ActionFuture<ResolveClusterActionResponse> future = client(LOCAL_CLUSTER).admin()
.indices()
.execute(TransportResolveClusterAction.TYPE, request);
ResolveClusterActionResponse response = future.actionGet(30, TimeUnit.SECONDS);
assertNotNull(response);

Map<String, ResolveClusterInfo> clusterInfo = response.getResolveClusterInfo();
assertEquals(2, clusterInfo.size());

// only remote clusters should be present (not local)
Set<String> expectedClusterNames = Set.of(REMOTE_CLUSTER_1, REMOTE_CLUSTER_2);
assertThat(clusterInfo.keySet(), equalTo(expectedClusterNames));

ResolveClusterInfo remote1 = clusterInfo.get(REMOTE_CLUSTER_1);
assertThat(remote1.isConnected(), equalTo(true));
assertThat(remote1.getSkipUnavailable(), equalTo(skipUnavailable1));
assertThat(remote1.getMatchingIndices(), equalTo(null)); // should not be set
assertNotNull(remote1.getBuild().version());
assertNull(remote1.getError());

ResolveClusterInfo remote2 = clusterInfo.get(REMOTE_CLUSTER_2);
assertThat(remote2.isConnected(), equalTo(true));
assertThat(remote2.getSkipUnavailable(), equalTo(skipUnavailable2));
assertThat(remote2.getMatchingIndices(), equalTo(null)); // should not be set
assertNotNull(remote2.getBuild().version());
assertNull(remote2.getError());
}
}

public void testClusterResolveWithMatchingAliases() throws IOException {
Map<String, Object> testClusterInfo = setupThreeClusters(true);
String localAlias = (String) testClusterInfo.get("local.alias");
Expand Down Expand Up @@ -523,6 +570,24 @@ public void testClusterResolveWithMatchingAliases() throws IOException {
}
}

public void testClusterResolveWithNoMatchingClustersReturnsEmptyResult() throws Exception {
setupThreeClusters(false);
{
String[] indexExpressions = new String[] { "no_matching_cluster*:foo" };
ResolveClusterActionRequest request = new ResolveClusterActionRequest(indexExpressions);

ActionFuture<ResolveClusterActionResponse> future = client(LOCAL_CLUSTER).admin()
.indices()
.execute(TransportResolveClusterAction.TYPE, request);
ResolveClusterActionResponse response = future.actionGet(10, TimeUnit.SECONDS);
assertNotNull(response);

Map<String, ResolveClusterInfo> clusterInfo = response.getResolveClusterInfo();
assertEquals(0, clusterInfo.size());
assertThat(Strings.toString(response), equalTo("{}"));
}
}

public void testClusterResolveDisconnectedAndErrorScenarios() throws Exception {
Map<String, Object> testClusterInfo = setupThreeClusters(false);
String localIndex = (String) testClusterInfo.get("local.index");
Expand Down Expand Up @@ -616,9 +681,49 @@ public void testClusterResolveDisconnectedAndErrorScenarios() throws Exception {
assertNotNull(local.getBuild().version());
assertNull(local.getError());
}

// cluster1 was stopped/disconnected, so it should return a connected:false response when querying with no index expression,
// corresponding to GET _resolve/cluster endpoint
{
String[] noIndexSpecified = new String[0];
boolean clusterInfoOnly = true;
boolean runningOnQueryingCluster = true;
ResolveClusterActionRequest request = new ResolveClusterActionRequest(
noIndexSpecified,
IndicesOptions.DEFAULT,
clusterInfoOnly,
runningOnQueryingCluster
);

ActionFuture<ResolveClusterActionResponse> future = client(LOCAL_CLUSTER).admin()
.indices()
.execute(TransportResolveClusterAction.TYPE, request);
ResolveClusterActionResponse response = future.actionGet(30, TimeUnit.SECONDS);
assertNotNull(response);

Map<String, ResolveClusterInfo> clusterInfo = response.getResolveClusterInfo();
assertEquals(2, clusterInfo.size());
// local cluster is not present when querying without an index expression
Set<String> expectedClusterNames = Set.of(REMOTE_CLUSTER_1, REMOTE_CLUSTER_2);
assertThat(clusterInfo.keySet(), equalTo(expectedClusterNames));

ResolveClusterInfo remote1 = clusterInfo.get(REMOTE_CLUSTER_1);
assertThat(remote1.isConnected(), equalTo(false));
assertThat(remote1.getSkipUnavailable(), equalTo(skipUnavailable1));
assertNull(remote1.getMatchingIndices());
assertNull(remote1.getBuild());
assertNull(remote1.getError());

ResolveClusterInfo remote2 = clusterInfo.get(REMOTE_CLUSTER_2);
assertThat(remote2.isConnected(), equalTo(true));
assertThat(remote2.getSkipUnavailable(), equalTo(skipUnavailable2));
assertNull(remote2.getMatchingIndices()); // not present when no index expression specified
assertNotNull(remote2.getBuild().version());
assertNull(remote2.getError());
}
}

private Map<String, Object> setupThreeClusters(boolean useAlias) throws IOException {
private Map<String, Object> setupThreeClusters(boolean useAlias) {
String localAlias = randomAlphaOfLengthBetween(5, 25);
String remoteAlias1 = randomAlphaOfLengthBetween(5, 25);
String remoteAlias2 = randomAlphaOfLengthBetween(5, 25);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ static TransportVersion def(int id) {
public static final TransportVersion REVERT_BYTE_SIZE_VALUE_ALWAYS_USES_BYTES_1 = def(8_826_00_0);
public static final TransportVersion ESQL_SKIP_ES_INDEX_SERIALIZATION = def(8_827_00_0);
public static final TransportVersion ADD_INDEX_BLOCK_TWO_PHASE = def(8_828_00_0);
public static final TransportVersion RESOLVE_CLUSTER_NO_INDEX_EXPRESSION = def(8_829_00_0);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.ValidateActions;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
Expand Down Expand Up @@ -53,15 +52,25 @@ public class ResolveClusterActionRequest extends ActionRequest implements Indice
private boolean localIndicesRequested = false;
private IndicesOptions indicesOptions;

// true if the user did not provide any index expression - they only want cluster level info, not index matching
private final boolean clusterInfoOnly;
// Whether this request is being processed on the primary ("local") cluster being queried or on a remote.
// This is needed when clusterInfoOnly=true since we need to know whether to list out all possible remotes
// on a node. (We don't want cross-cluster chaining on remotes that might be configured with their own remotes.)
private final boolean isQueryingCluster;

public ResolveClusterActionRequest(String[] names) {
this(names, DEFAULT_INDICES_OPTIONS);
this(names, DEFAULT_INDICES_OPTIONS, false, true);
assert names != null && names.length > 0 : "One or more index expressions must be included with this constructor";
}

@SuppressWarnings("this-escape")
public ResolveClusterActionRequest(String[] names, IndicesOptions indicesOptions) {
public ResolveClusterActionRequest(String[] names, IndicesOptions indicesOptions, boolean clusterInfoOnly, boolean queryingCluster) {
this.names = names;
this.localIndicesRequested = localIndicesPresent(names);
this.indicesOptions = indicesOptions;
this.clusterInfoOnly = clusterInfoOnly;
this.isQueryingCluster = queryingCluster;
}

@SuppressWarnings("this-escape")
Expand All @@ -73,6 +82,13 @@ public ResolveClusterActionRequest(StreamInput in) throws IOException {
this.names = in.readStringArray();
this.indicesOptions = IndicesOptions.readIndicesOptions(in);
this.localIndicesRequested = localIndicesPresent(names);
if (in.getTransportVersion().onOrAfter(TransportVersions.RESOLVE_CLUSTER_NO_INDEX_EXPRESSION)) {
this.clusterInfoOnly = in.readBoolean();
this.isQueryingCluster = in.readBoolean();
} else {
this.clusterInfoOnly = false;
this.isQueryingCluster = false;
}
}

@Override
Expand All @@ -83,9 +99,13 @@ public void writeTo(StreamOutput out) throws IOException {
}
out.writeStringArray(names);
indicesOptions.writeIndicesOptions(out);
if (out.getTransportVersion().onOrAfter(TransportVersions.RESOLVE_CLUSTER_NO_INDEX_EXPRESSION)) {
out.writeBoolean(clusterInfoOnly);
out.writeBoolean(isQueryingCluster);
}
}

private String createVersionErrorMessage(TransportVersion versionFound) {
static String createVersionErrorMessage(TransportVersion versionFound) {
return Strings.format(
"%s %s but was %s",
TRANSPORT_VERSION_ERROR_MESSAGE_PREFIX,
Expand All @@ -96,11 +116,7 @@ private String createVersionErrorMessage(TransportVersion versionFound) {

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (names == null || names.length == 0) {
validationException = ValidateActions.addValidationError("no index expressions specified", validationException);
}
return validationException;
return null;
}

@Override
Expand All @@ -123,6 +139,14 @@ public String[] indices() {
return names;
}

public boolean clusterInfoOnly() {
return clusterInfoOnly;
}

public boolean queryingCluster() {
return isQueryingCluster;
}

public boolean isLocalIndicesRequested() {
return localIndicesRequested;
}
Expand Down Expand Up @@ -160,7 +184,11 @@ public Task createTask(long id, String type, String action, TaskId parentTaskId,
return new CancellableTask(id, type, action, "", parentTaskId, headers) {
@Override
public String getDescription() {
return "resolve/cluster for " + Arrays.toString(indices());
if (indices().length == 0) {
return "resolve/cluster";
} else {
return "resolve/cluster for " + Arrays.toString(indices());
}
}
};
}
Expand All @@ -173,4 +201,18 @@ boolean localIndicesPresent(String[] indices) {
}
return false;
}

@Override
public String toString() {
return "ResolveClusterActionRequest{"
+ "indices="
+ Arrays.toString(names)
+ ", localIndicesRequested="
+ localIndicesRequested
+ ", clusterInfoOnly="
+ clusterInfoOnly
+ ", queryingCluster="
+ isQueryingCluster
+ '}';
}
}
Loading

0 comments on commit 36f5a55

Please sign in to comment.