Skip to content

Commit 36f5a55

Browse files
authored
Resolve/cluster allows querying for cluster info only (no index expression 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.
1 parent dc73837 commit 36f5a55

File tree

14 files changed

+560
-90
lines changed

14 files changed

+560
-90
lines changed

docs/changelog/119898.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 119898
2+
summary: Resolve/cluster allows querying for cluster info only (no index expression
3+
required)
4+
area: CCS
5+
type: enhancement
6+
issues: []

docs/reference/indices/resolve-cluster.asciidoc

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ For the most up-to-date API details, refer to {api-es}/group/endpoint-indices[In
1111
--
1212

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

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

23-
For each cluster in the index expression, information is returned about:
25+
For each cluster in scope, information is returned about:
2426

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

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

4551
////
4652
[source,console]
@@ -71,14 +77,22 @@ PUT _cluster/settings
7177
// TEST[s/35.238.149.\d+:930\d+/\${transport_host}/]
7278
////
7379

80+
[source,console]
81+
----
82+
GET /_resolve/cluster
83+
----
84+
// TEST[continued]
85+
86+
Returns information about all remote clusters configured on the local cluster.
87+
7488
[source,console]
7589
----
7690
GET /_resolve/cluster/my-index-*,cluster*:my-index-*
7791
----
7892
// TEST[continued]
7993

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

8498
[[resolve-cluster-api-request]]
@@ -126,6 +140,13 @@ ignored when frozen. Defaults to `false`.
126140
+
127141
deprecated:[7.16.0]
128142

143+
[TIP]
144+
====
145+
The index options above are only allowed when specifying an index expression.
146+
You will get an error if you specify index options to the _resolve/cluster API
147+
that takes no index expression.
148+
====
149+
129150

130151
[discrete]
131152
[[usecases-for-resolve-cluster]]

server/src/internalClusterTest/java/org/elasticsearch/indices/cluster/ResolveClusterIT.java

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.action.admin.indices.resolve.ResolveClusterActionResponse;
1717
import org.elasticsearch.action.admin.indices.resolve.ResolveClusterInfo;
1818
import org.elasticsearch.action.admin.indices.resolve.TransportResolveClusterAction;
19+
import org.elasticsearch.action.support.IndicesOptions;
1920
import org.elasticsearch.client.internal.Client;
2021
import org.elasticsearch.cluster.metadata.IndexMetadata;
2122
import org.elasticsearch.common.Strings;
@@ -406,6 +407,52 @@ public void testClusterResolveWithIndices() throws IOException {
406407
}
407408
}
408409

410+
// corresponds to the GET _resolve/cluster endpoint with no index expression specified
411+
public void testClusterResolveWithNoIndexExpression() throws IOException {
412+
Map<String, Object> testClusterInfo = setupThreeClusters(false);
413+
boolean skipUnavailable1 = (Boolean) testClusterInfo.get("remote1.skip_unavailable");
414+
boolean skipUnavailable2 = true;
415+
416+
{
417+
String[] noIndexSpecified = new String[0];
418+
boolean clusterInfoOnly = true;
419+
boolean runningOnQueryingCluster = true;
420+
ResolveClusterActionRequest request = new ResolveClusterActionRequest(
421+
noIndexSpecified,
422+
IndicesOptions.DEFAULT,
423+
clusterInfoOnly,
424+
runningOnQueryingCluster
425+
);
426+
427+
ActionFuture<ResolveClusterActionResponse> future = client(LOCAL_CLUSTER).admin()
428+
.indices()
429+
.execute(TransportResolveClusterAction.TYPE, request);
430+
ResolveClusterActionResponse response = future.actionGet(30, TimeUnit.SECONDS);
431+
assertNotNull(response);
432+
433+
Map<String, ResolveClusterInfo> clusterInfo = response.getResolveClusterInfo();
434+
assertEquals(2, clusterInfo.size());
435+
436+
// only remote clusters should be present (not local)
437+
Set<String> expectedClusterNames = Set.of(REMOTE_CLUSTER_1, REMOTE_CLUSTER_2);
438+
assertThat(clusterInfo.keySet(), equalTo(expectedClusterNames));
439+
440+
ResolveClusterInfo remote1 = clusterInfo.get(REMOTE_CLUSTER_1);
441+
assertThat(remote1.isConnected(), equalTo(true));
442+
assertThat(remote1.getSkipUnavailable(), equalTo(skipUnavailable1));
443+
assertThat(remote1.getMatchingIndices(), equalTo(null)); // should not be set
444+
assertNotNull(remote1.getBuild().version());
445+
assertNull(remote1.getError());
446+
447+
ResolveClusterInfo remote2 = clusterInfo.get(REMOTE_CLUSTER_2);
448+
assertThat(remote2.isConnected(), equalTo(true));
449+
assertThat(remote2.getSkipUnavailable(), equalTo(skipUnavailable2));
450+
assertThat(remote2.getMatchingIndices(), equalTo(null)); // should not be set
451+
assertNotNull(remote2.getBuild().version());
452+
assertNull(remote2.getError());
453+
}
454+
}
455+
409456
public void testClusterResolveWithMatchingAliases() throws IOException {
410457
Map<String, Object> testClusterInfo = setupThreeClusters(true);
411458
String localAlias = (String) testClusterInfo.get("local.alias");
@@ -523,6 +570,24 @@ public void testClusterResolveWithMatchingAliases() throws IOException {
523570
}
524571
}
525572

573+
public void testClusterResolveWithNoMatchingClustersReturnsEmptyResult() throws Exception {
574+
setupThreeClusters(false);
575+
{
576+
String[] indexExpressions = new String[] { "no_matching_cluster*:foo" };
577+
ResolveClusterActionRequest request = new ResolveClusterActionRequest(indexExpressions);
578+
579+
ActionFuture<ResolveClusterActionResponse> future = client(LOCAL_CLUSTER).admin()
580+
.indices()
581+
.execute(TransportResolveClusterAction.TYPE, request);
582+
ResolveClusterActionResponse response = future.actionGet(10, TimeUnit.SECONDS);
583+
assertNotNull(response);
584+
585+
Map<String, ResolveClusterInfo> clusterInfo = response.getResolveClusterInfo();
586+
assertEquals(0, clusterInfo.size());
587+
assertThat(Strings.toString(response), equalTo("{}"));
588+
}
589+
}
590+
526591
public void testClusterResolveDisconnectedAndErrorScenarios() throws Exception {
527592
Map<String, Object> testClusterInfo = setupThreeClusters(false);
528593
String localIndex = (String) testClusterInfo.get("local.index");
@@ -616,9 +681,49 @@ public void testClusterResolveDisconnectedAndErrorScenarios() throws Exception {
616681
assertNotNull(local.getBuild().version());
617682
assertNull(local.getError());
618683
}
684+
685+
// cluster1 was stopped/disconnected, so it should return a connected:false response when querying with no index expression,
686+
// corresponding to GET _resolve/cluster endpoint
687+
{
688+
String[] noIndexSpecified = new String[0];
689+
boolean clusterInfoOnly = true;
690+
boolean runningOnQueryingCluster = true;
691+
ResolveClusterActionRequest request = new ResolveClusterActionRequest(
692+
noIndexSpecified,
693+
IndicesOptions.DEFAULT,
694+
clusterInfoOnly,
695+
runningOnQueryingCluster
696+
);
697+
698+
ActionFuture<ResolveClusterActionResponse> future = client(LOCAL_CLUSTER).admin()
699+
.indices()
700+
.execute(TransportResolveClusterAction.TYPE, request);
701+
ResolveClusterActionResponse response = future.actionGet(30, TimeUnit.SECONDS);
702+
assertNotNull(response);
703+
704+
Map<String, ResolveClusterInfo> clusterInfo = response.getResolveClusterInfo();
705+
assertEquals(2, clusterInfo.size());
706+
// local cluster is not present when querying without an index expression
707+
Set<String> expectedClusterNames = Set.of(REMOTE_CLUSTER_1, REMOTE_CLUSTER_2);
708+
assertThat(clusterInfo.keySet(), equalTo(expectedClusterNames));
709+
710+
ResolveClusterInfo remote1 = clusterInfo.get(REMOTE_CLUSTER_1);
711+
assertThat(remote1.isConnected(), equalTo(false));
712+
assertThat(remote1.getSkipUnavailable(), equalTo(skipUnavailable1));
713+
assertNull(remote1.getMatchingIndices());
714+
assertNull(remote1.getBuild());
715+
assertNull(remote1.getError());
716+
717+
ResolveClusterInfo remote2 = clusterInfo.get(REMOTE_CLUSTER_2);
718+
assertThat(remote2.isConnected(), equalTo(true));
719+
assertThat(remote2.getSkipUnavailable(), equalTo(skipUnavailable2));
720+
assertNull(remote2.getMatchingIndices()); // not present when no index expression specified
721+
assertNotNull(remote2.getBuild().version());
722+
assertNull(remote2.getError());
723+
}
619724
}
620725

621-
private Map<String, Object> setupThreeClusters(boolean useAlias) throws IOException {
726+
private Map<String, Object> setupThreeClusters(boolean useAlias) {
622727
String localAlias = randomAlphaOfLengthBetween(5, 25);
623728
String remoteAlias1 = randomAlphaOfLengthBetween(5, 25);
624729
String remoteAlias2 = randomAlphaOfLengthBetween(5, 25);

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ static TransportVersion def(int id) {
166166
public static final TransportVersion REVERT_BYTE_SIZE_VALUE_ALWAYS_USES_BYTES_1 = def(8_826_00_0);
167167
public static final TransportVersion ESQL_SKIP_ES_INDEX_SERIALIZATION = def(8_827_00_0);
168168
public static final TransportVersion ADD_INDEX_BLOCK_TWO_PHASE = def(8_828_00_0);
169+
public static final TransportVersion RESOLVE_CLUSTER_NO_INDEX_EXPRESSION = def(8_829_00_0);
169170

170171
/*
171172
* STOP! READ THIS FIRST! No, really,

server/src/main/java/org/elasticsearch/action/admin/indices/resolve/ResolveClusterActionRequest.java

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.elasticsearch.action.ActionRequest;
1515
import org.elasticsearch.action.ActionRequestValidationException;
1616
import org.elasticsearch.action.IndicesRequest;
17-
import org.elasticsearch.action.ValidateActions;
1817
import org.elasticsearch.action.support.IndicesOptions;
1918
import org.elasticsearch.common.Strings;
2019
import org.elasticsearch.common.io.stream.StreamInput;
@@ -53,15 +52,25 @@ public class ResolveClusterActionRequest extends ActionRequest implements Indice
5352
private boolean localIndicesRequested = false;
5453
private IndicesOptions indicesOptions;
5554

55+
// true if the user did not provide any index expression - they only want cluster level info, not index matching
56+
private final boolean clusterInfoOnly;
57+
// Whether this request is being processed on the primary ("local") cluster being queried or on a remote.
58+
// This is needed when clusterInfoOnly=true since we need to know whether to list out all possible remotes
59+
// on a node. (We don't want cross-cluster chaining on remotes that might be configured with their own remotes.)
60+
private final boolean isQueryingCluster;
61+
5662
public ResolveClusterActionRequest(String[] names) {
57-
this(names, DEFAULT_INDICES_OPTIONS);
63+
this(names, DEFAULT_INDICES_OPTIONS, false, true);
64+
assert names != null && names.length > 0 : "One or more index expressions must be included with this constructor";
5865
}
5966

6067
@SuppressWarnings("this-escape")
61-
public ResolveClusterActionRequest(String[] names, IndicesOptions indicesOptions) {
68+
public ResolveClusterActionRequest(String[] names, IndicesOptions indicesOptions, boolean clusterInfoOnly, boolean queryingCluster) {
6269
this.names = names;
6370
this.localIndicesRequested = localIndicesPresent(names);
6471
this.indicesOptions = indicesOptions;
72+
this.clusterInfoOnly = clusterInfoOnly;
73+
this.isQueryingCluster = queryingCluster;
6574
}
6675

6776
@SuppressWarnings("this-escape")
@@ -73,6 +82,13 @@ public ResolveClusterActionRequest(StreamInput in) throws IOException {
7382
this.names = in.readStringArray();
7483
this.indicesOptions = IndicesOptions.readIndicesOptions(in);
7584
this.localIndicesRequested = localIndicesPresent(names);
85+
if (in.getTransportVersion().onOrAfter(TransportVersions.RESOLVE_CLUSTER_NO_INDEX_EXPRESSION)) {
86+
this.clusterInfoOnly = in.readBoolean();
87+
this.isQueryingCluster = in.readBoolean();
88+
} else {
89+
this.clusterInfoOnly = false;
90+
this.isQueryingCluster = false;
91+
}
7692
}
7793

7894
@Override
@@ -83,9 +99,13 @@ public void writeTo(StreamOutput out) throws IOException {
8399
}
84100
out.writeStringArray(names);
85101
indicesOptions.writeIndicesOptions(out);
102+
if (out.getTransportVersion().onOrAfter(TransportVersions.RESOLVE_CLUSTER_NO_INDEX_EXPRESSION)) {
103+
out.writeBoolean(clusterInfoOnly);
104+
out.writeBoolean(isQueryingCluster);
105+
}
86106
}
87107

88-
private String createVersionErrorMessage(TransportVersion versionFound) {
108+
static String createVersionErrorMessage(TransportVersion versionFound) {
89109
return Strings.format(
90110
"%s %s but was %s",
91111
TRANSPORT_VERSION_ERROR_MESSAGE_PREFIX,
@@ -96,11 +116,7 @@ private String createVersionErrorMessage(TransportVersion versionFound) {
96116

97117
@Override
98118
public ActionRequestValidationException validate() {
99-
ActionRequestValidationException validationException = null;
100-
if (names == null || names.length == 0) {
101-
validationException = ValidateActions.addValidationError("no index expressions specified", validationException);
102-
}
103-
return validationException;
119+
return null;
104120
}
105121

106122
@Override
@@ -123,6 +139,14 @@ public String[] indices() {
123139
return names;
124140
}
125141

142+
public boolean clusterInfoOnly() {
143+
return clusterInfoOnly;
144+
}
145+
146+
public boolean queryingCluster() {
147+
return isQueryingCluster;
148+
}
149+
126150
public boolean isLocalIndicesRequested() {
127151
return localIndicesRequested;
128152
}
@@ -160,7 +184,11 @@ public Task createTask(long id, String type, String action, TaskId parentTaskId,
160184
return new CancellableTask(id, type, action, "", parentTaskId, headers) {
161185
@Override
162186
public String getDescription() {
163-
return "resolve/cluster for " + Arrays.toString(indices());
187+
if (indices().length == 0) {
188+
return "resolve/cluster";
189+
} else {
190+
return "resolve/cluster for " + Arrays.toString(indices());
191+
}
164192
}
165193
};
166194
}
@@ -173,4 +201,18 @@ boolean localIndicesPresent(String[] indices) {
173201
}
174202
return false;
175203
}
204+
205+
@Override
206+
public String toString() {
207+
return "ResolveClusterActionRequest{"
208+
+ "indices="
209+
+ Arrays.toString(names)
210+
+ ", localIndicesRequested="
211+
+ localIndicesRequested
212+
+ ", clusterInfoOnly="
213+
+ clusterInfoOnly
214+
+ ", queryingCluster="
215+
+ isQueryingCluster
216+
+ '}';
217+
}
176218
}

0 commit comments

Comments
 (0)