Skip to content

Commit b3a032c

Browse files
authored
Resolve/cluster allows querying for cluster info only (no index expression required) (#119898)
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 0b65bc1 commit b3a032c

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;
@@ -405,6 +406,52 @@ public void testClusterResolveWithIndices() throws IOException {
405406
}
406407
}
407408

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

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

620-
private Map<String, Object> setupThreeClusters(boolean useAlias) throws IOException {
725+
private Map<String, Object> setupThreeClusters(boolean useAlias) {
621726
String localAlias = randomAlphaOfLengthBetween(5, 25);
622727
String remoteAlias1 = randomAlphaOfLengthBetween(5, 25);
623728
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
@@ -160,6 +160,7 @@ static TransportVersion def(int id) {
160160
public static final TransportVersion REVERT_BYTE_SIZE_VALUE_ALWAYS_USES_BYTES_1 = def(8_826_00_0);
161161
public static final TransportVersion ESQL_SKIP_ES_INDEX_SERIALIZATION = def(8_827_00_0);
162162
public static final TransportVersion ADD_INDEX_BLOCK_TWO_PHASE = def(8_828_00_0);
163+
public static final TransportVersion RESOLVE_CLUSTER_NO_INDEX_EXPRESSION = def(8_829_00_0);
163164

164165
/*
165166
* 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)