Change skip_unavailable default value to true (#105792)

In order to improve the experience of cross-cluster search, we are changing
the default value of the remote cluster `skip_unavailable` setting from `false` to `true`.

This setting causes any cross-cluster _search (or _async_search) to entirely fail when
any remote cluster with `skip_unavailable=false` is either unavailable (connection to it fails)
or if the search on it fails on all shards.

Setting `skip_unavailable=true` allows partial results from other clusters to be
returned. In that case, the search response cluster metadata will show a `skipped`
status, so the user can see that no data came in from that cluster. Kibana also
now leverages this metadata in the cross-cluster search responses to allow users
to see how many clusters returned data and drill down into which clusters did not
(including failure messages).

Currently, the user/admin has to specifically set the value to `true` in the configs, like so:

```
cluster:
    remote:
        remote1:
            seeds: 10.10.10.10:9300
            skip_unavailable: true
```

even though that is probably what search admins want in the vast majority of cases.

Setting `skip_unavailable=false` should be a conscious (and probably rare) choice
by an Elasticsearch admin that a particular cluster's results are so essential to a
search (or visualization in dashboard or Discover panel) that no results at all should
be shown if it cannot return any results.
This commit is contained in:
Michael Peterson 2024-04-29 15:53:47 -04:00 committed by GitHub
parent 32deb7fa46
commit a451511e3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 258 additions and 111 deletions

View file

@ -0,0 +1,18 @@
pr: 105792
summary: "Change `skip_unavailable` remote cluster setting default value to true"
area: Search
type: breaking
issues: []
breaking:
title: "Change `skip_unavailable` remote cluster setting default value to true"
area: Cluster and node setting
details: The default value of the `skip_unavailable` setting is now set to true.
All existing and future remote clusters that do not define this setting will use the new default.
This setting only affects cross-cluster searches using the _search or _async_search API.
impact: Unavailable remote clusters in a cross-cluster search will no longer cause the search to fail unless
skip_unavailable is configured to be `false` in elasticsearch.yml or via the `_cluster/settings` API.
Unavailable clusters with `skip_unavailable`=`true` (either explicitly or by using the new default) are marked
as SKIPPED in the search response metadata section and do not fail the entire search. If users want to ensure that a
search returns a failure when a particular remote cluster is not available, `skip_unavailable` must be now be
set explicitly.
notable: false

View file

@ -147,7 +147,7 @@ cluster with cluster alias `leader`.
"num_nodes_connected" : 1, <1> "num_nodes_connected" : 1, <1>
"max_connections_per_cluster" : 3, "max_connections_per_cluster" : 3,
"initial_connect_timeout" : "30s", "initial_connect_timeout" : "30s",
"skip_unavailable" : false, "skip_unavailable" : true,
"mode" : "sniff" "mode" : "sniff"
} }
} }

View file

@ -86,7 +86,7 @@ cluster with the cluster alias `cluster_one`:
"num_nodes_connected" : 1, <1> "num_nodes_connected" : 1, <1>
"max_connections_per_cluster" : 3, "max_connections_per_cluster" : 3,
"initial_connect_timeout" : "30s", "initial_connect_timeout" : "30s",
"skip_unavailable" : false, <2> "skip_unavailable" : true, <2>
ifeval::["{trust-mechanism}"=="api-key"] ifeval::["{trust-mechanism}"=="api-key"]
"cluster_credentials": "::es_redacted::", <3> "cluster_credentials": "::es_redacted::", <3>
endif::[] endif::[]

View file

@ -28,9 +28,20 @@ mode are described separately.
Per cluster boolean setting that allows to skip specific clusters when no Per cluster boolean setting that allows to skip specific clusters when no
nodes belonging to them are available and they are the target of a remote nodes belonging to them are available and they are the target of a remote
cluster request. Default is `false`, meaning that all clusters are mandatory cluster request.
by default, but they can selectively be made optional by setting this setting
to `true`. IMPORTANT: In Elasticsearch 8.15, the default value for `skip_unavailable` was
changed from `false` to `true`. Before Elasticsearch 8.15, if you want a cluster
to be treated as optional for a {ccs}, then you need to set that configuration.
From Elasticsearch 8.15 forward, you need to set the configuration in order to
make a cluster required for the {ccs}. Once you upgrade the local ("querying")
cluster search coordinator node (the node you send CCS requests to) to 8.15 or later,
any remote clusters that do not have an explicit setting for `skip_unavailable` will
immediately change over to using the new default of true. This is true regardless of
whether you have upgraded the remote clusters to 8.15, as the `skip_unavailable`
search behavior is entirely determined by the setting on the local cluster where
you configure the remotes.
`cluster.remote.<cluster_alias>.transport.ping_schedule`:: `cluster.remote.<cluster_alias>.transport.ping_schedule`::

View file

@ -1178,7 +1178,13 @@ gathered from all 3 clusters and the total shard count on each cluster is listed
By default, a {ccs} fails if a remote cluster in the request is unavailable By default, a {ccs} fails if a remote cluster in the request is unavailable
or returns an error where the search on all shards failed. Use the or returns an error where the search on all shards failed. Use the
`skip_unavailable` cluster setting to mark a specific remote cluster as `skip_unavailable` cluster setting to mark a specific remote cluster as
optional for {ccs}. either optional or required for {ccs}.
IMPORTANT: In Elasticsearch 8.15, the default value for `skip_unavailable` was
changed from `false` to `true`. Before Elasticsearch 8.15, if you want a cluster
to be treated as optional for a {ccs}, then you need to set that configuration.
From Elasticsearch 8.15 forward, you need to set the configuration in order to
make a cluster required for the {ccs}.
If `skip_unavailable` is `true`, a {ccs}: If `skip_unavailable` is `true`, a {ccs}:
@ -1196,25 +1202,33 @@ parameter and the related `search.default_allow_partial_results` cluster setting
when searching the remote cluster. This means searches on the remote cluster may when searching the remote cluster. This means searches on the remote cluster may
return partial results. return partial results.
The following <<cluster-update-settings,cluster update settings>> You can modify the `skip_unavailable` setting by editing the `cluster.remote.<cluster_alias>`
API request changes `skip_unavailable` setting to `true` for `cluster_two`. settings in the elasticsearch.yml config file. For example:
[source,console] ```
-------------------------------- cluster:
PUT _cluster/settings remote:
{ cluster_one:
"persistent": { seeds: 35.238.149.1:9300
"cluster.remote.cluster_two.skip_unavailable": true skip_unavailable: false
} cluster_two:
} seeds: 35.238.149.2:9300
-------------------------------- skip_unavailable: true
// TEST[continued] ```
If `cluster_two` is disconnected or unavailable during a {ccs}, {es} won't Or you can set the cluster.remote settings via the
include matching documents from that cluster in the final results. If at <<cluster-update-settings,cluster update settings>> API as shown
least one shard provides results, those results will be used and the <<ccs-remote-cluster-setup, here>>.
search will return partial data. (If doing {ccs} using async search,
the `is_partial` field will be set to `true` to indicate partial results.) When a remote cluster configured with `skip_unavailable: true` (such as
`cluster_two` above) is disconnected or unavailable during a {ccs}, {es} won't
include matching documents from that cluster in the final results and the
search will be considered successful (HTTP status 200 OK).
If at least one shard from a cluster provides search results, those results will
be used and the search will return partial data. This is true regardless of
the `skip_unavailable` setting of the remote cluster. (If doing {ccs} using async
search, the `is_partial` field will be set to `true` to indicate partial results.)
[discrete] [discrete]
[[ccs-network-delays]] [[ccs-network-delays]]

View file

@ -19,6 +19,7 @@ import org.elasticsearch.test.AbstractMultiClustersTestCase;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -38,6 +39,11 @@ public class CrossClusterReindexIT extends AbstractMultiClustersTestCase {
return List.of(REMOTE_CLUSTER); return List.of(REMOTE_CLUSTER);
} }
@Override
protected Map<String, Boolean> skipUnavailableForRemoteClusters() {
return Map.of(REMOTE_CLUSTER, false);
}
@Override @Override
protected Collection<Class<? extends Plugin>> nodePlugins(String clusterAlias) { protected Collection<Class<? extends Plugin>> nodePlugins(String clusterAlias) {
return List.of(ReindexPlugin.class); return List.of(ReindexPlugin.class);

View file

@ -101,6 +101,7 @@ public class CcsCommonYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
.setting("node.roles", "[data,ingest,master,remote_cluster_client]") .setting("node.roles", "[data,ingest,master,remote_cluster_client]")
.setting("cluster.remote.remote_cluster.seeds", () -> "\"" + remoteCluster.getTransportEndpoint(0) + "\"") .setting("cluster.remote.remote_cluster.seeds", () -> "\"" + remoteCluster.getTransportEndpoint(0) + "\"")
.setting("cluster.remote.connections_per_cluster", "1") .setting("cluster.remote.connections_per_cluster", "1")
.setting("cluster.remote.remote_cluster.skip_unavailable", "false")
.apply(commonClusterConfig) .apply(commonClusterConfig)
.build(); .build();

View file

@ -246,6 +246,7 @@ public class RcsCcsCommonYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
private static void configureRemoteCluster() throws IOException { private static void configureRemoteCluster() throws IOException {
final Settings.Builder builder = Settings.builder(); final Settings.Builder builder = Settings.builder();
builder.put("cluster.remote." + REMOTE_CLUSTER_NAME + ".skip_unavailable", "false");
if (randomBoolean()) { if (randomBoolean()) {
builder.put("cluster.remote." + REMOTE_CLUSTER_NAME + ".mode", "proxy") builder.put("cluster.remote." + REMOTE_CLUSTER_NAME + ".mode", "proxy")
.put("cluster.remote." + REMOTE_CLUSTER_NAME + ".proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0)); .put("cluster.remote." + REMOTE_CLUSTER_NAME + ".proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0));

View file

@ -48,6 +48,7 @@ BuildParams.bwcVersions.withWireCompatible(ccsSupportedVersion) { bwcVersion, ba
setting 'cluster.remote.connections_per_cluster', '1' setting 'cluster.remote.connections_per_cluster', '1'
setting 'cluster.remote.my_remote_cluster.seeds', setting 'cluster.remote.my_remote_cluster.seeds',
{ "\"${remoteCluster.get().getAllTransportPortURI().get(0)}\"" } { "\"${remoteCluster.get().getAllTransportPortURI().get(0)}\"" }
setting 'cluster.remote.my_remote_cluster.skip_unavailable', 'false'
} }
tasks.register("${baseName}#remote-cluster", RestIntegTestTask) { tasks.register("${baseName}#remote-cluster", RestIntegTestTask) {

View file

@ -249,7 +249,7 @@
persistent: persistent:
cluster.remote.test_remote_cluster.seeds: $remote_ip cluster.remote.test_remote_cluster.seeds: $remote_ip
- match: {persistent: {cluster.remote.test_remote_cluster.seeds: $remote_ip}} - match: {persistent.cluster\.remote\.test_remote_cluster\.seeds: $remote_ip}
- do: - do:
search: search:

View file

@ -113,19 +113,6 @@
- do: - do:
cluster.remote_info: {} cluster.remote_info: {}
- is_false: remote1.skip_unavailable
- do:
cluster.put_settings:
body:
persistent:
cluster.remote.remote1.skip_unavailable: true
- is_true: persistent.cluster.remote.remote1.skip_unavailable
- do:
cluster.remote_info: {}
- is_true: remote1.skip_unavailable - is_true: remote1.skip_unavailable
- do: - do:
@ -141,6 +128,19 @@
- is_false: remote1.skip_unavailable - is_false: remote1.skip_unavailable
- do:
cluster.put_settings:
body:
persistent:
cluster.remote.remote1.skip_unavailable: true
- is_true: persistent.cluster.remote.remote1.skip_unavailable
- do:
cluster.remote_info: {}
- is_true: remote1.skip_unavailable
- do: - do:
cluster.put_settings: cluster.put_settings:
body: body:
@ -152,7 +152,7 @@
- do: - do:
cluster.remote_info: {} cluster.remote_info: {}
- is_false: remote1.skip_unavailable - is_true: remote1.skip_unavailable
- do: - do:
cluster.put_settings: cluster.put_settings:

View file

@ -87,7 +87,7 @@ public final class RemoteClusterService extends RemoteClusterAware implements Cl
public static final Setting.AffixSetting<Boolean> REMOTE_CLUSTER_SKIP_UNAVAILABLE = Setting.affixKeySetting( public static final Setting.AffixSetting<Boolean> REMOTE_CLUSTER_SKIP_UNAVAILABLE = Setting.affixKeySetting(
"cluster.remote.", "cluster.remote.",
"skip_unavailable", "skip_unavailable",
(ns, key) -> boolSetting(key, false, new RemoteConnectionEnabled<>(ns, key), Setting.Property.Dynamic, Setting.Property.NodeScope) (ns, key) -> boolSetting(key, true, new RemoteConnectionEnabled<>(ns, key), Setting.Property.Dynamic, Setting.Property.NodeScope)
); );
public static final Setting.AffixSetting<TimeValue> REMOTE_CLUSTER_PING_SCHEDULE = Setting.affixKeySetting( public static final Setting.AffixSetting<TimeValue> REMOTE_CLUSTER_PING_SCHEDULE = Setting.affixKeySetting(

View file

@ -469,7 +469,8 @@ public class TransportSearchActionTests extends ESTestCase {
int numClusters, int numClusters,
DiscoveryNode[] nodes, DiscoveryNode[] nodes,
Map<String, OriginalIndices> remoteIndices, Map<String, OriginalIndices> remoteIndices,
Settings.Builder settingsBuilder Settings.Builder settingsBuilder,
boolean skipUnavailable
) { ) {
MockTransportService[] mockTransportServices = new MockTransportService[numClusters]; MockTransportService[] mockTransportServices = new MockTransportService[numClusters];
for (int i = 0; i < numClusters; i++) { for (int i = 0; i < numClusters; i++) {
@ -486,6 +487,7 @@ public class TransportSearchActionTests extends ESTestCase {
knownNodes.add(remoteSeedNode); knownNodes.add(remoteSeedNode);
nodes[i] = remoteSeedNode; nodes[i] = remoteSeedNode;
settingsBuilder.put("cluster.remote.remote" + i + ".seeds", remoteSeedNode.getAddress().toString()); settingsBuilder.put("cluster.remote.remote" + i + ".seeds", remoteSeedNode.getAddress().toString());
settingsBuilder.put("cluster.remote.remote" + i + ".skip_unavailable", Boolean.toString(skipUnavailable));
remoteIndices.put("remote" + i, new OriginalIndices(new String[] { "index" }, IndicesOptions.lenientExpandOpen())); remoteIndices.put("remote" + i, new OriginalIndices(new String[] { "index" }, IndicesOptions.lenientExpandOpen()));
} }
return mockTransportServices; return mockTransportServices;
@ -496,7 +498,8 @@ public class TransportSearchActionTests extends ESTestCase {
DiscoveryNode[] nodes = new DiscoveryNode[numClusters]; DiscoveryNode[] nodes = new DiscoveryNode[numClusters];
Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>(); Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>();
Settings.Builder builder = Settings.builder(); Settings.Builder builder = Settings.builder();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder); boolean skipUnavailable = randomBoolean();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder, skipUnavailable);
Settings settings = builder.build(); Settings settings = builder.build();
boolean local = randomBoolean(); boolean local = randomBoolean();
OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null; OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null;
@ -566,7 +569,8 @@ public class TransportSearchActionTests extends ESTestCase {
DiscoveryNode[] nodes = new DiscoveryNode[numClusters]; DiscoveryNode[] nodes = new DiscoveryNode[numClusters];
Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>(); Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>();
Settings.Builder builder = Settings.builder(); Settings.Builder builder = Settings.builder();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder); boolean skipUnavailable = randomBoolean();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder, skipUnavailable);
Settings settings = builder.build(); Settings settings = builder.build();
boolean local = randomBoolean(); boolean local = randomBoolean();
OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null; OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null;
@ -709,7 +713,8 @@ public class TransportSearchActionTests extends ESTestCase {
DiscoveryNode[] nodes = new DiscoveryNode[numClusters]; DiscoveryNode[] nodes = new DiscoveryNode[numClusters];
Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>(); Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>();
Settings.Builder builder = Settings.builder(); Settings.Builder builder = Settings.builder();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder); boolean skipUnavailable = randomBoolean();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder, skipUnavailable);
Settings settings = builder.build(); Settings settings = builder.build();
boolean local = randomBoolean(); boolean local = randomBoolean();
OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null; OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null;
@ -734,10 +739,13 @@ public class TransportSearchActionTests extends ESTestCase {
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
SetOnce<Tuple<SearchRequest, ActionListener<SearchResponse>>> setOnce = new SetOnce<>(); SetOnce<Tuple<SearchRequest, ActionListener<SearchResponse>>> setOnce = new SetOnce<>();
AtomicReference<Exception> failure = new AtomicReference<>(); AtomicReference<Exception> failure = new AtomicReference<>();
LatchedActionListener<SearchResponse> listener = new LatchedActionListener<>( LatchedActionListener<SearchResponse> listener = new LatchedActionListener<>(ActionListener.wrap(r -> {
ActionListener.wrap(r -> fail("no response expected"), failure::set), if (skipUnavailable) {
latch assertThat(r.getClusters().getClusterStateCount(SearchResponse.Cluster.Status.SKIPPED), equalTo(numClusters));
); } else {
fail("no response expected"); // failure should be returned, not SearchResponse
}
}, failure::set), latch);
TaskId parentTaskId = new TaskId("n", 1); TaskId parentTaskId = new TaskId("n", 1);
SearchTask task = new SearchTask(2, "search", "search", () -> "desc", parentTaskId, Collections.emptyMap()); SearchTask task = new SearchTask(2, "search", "search", () -> "desc", parentTaskId, Collections.emptyMap());
@ -763,11 +771,15 @@ public class TransportSearchActionTests extends ESTestCase {
resolveWithEmptySearchResponse(tuple); resolveWithEmptySearchResponse(tuple);
} }
awaitLatch(latch, 5, TimeUnit.SECONDS); awaitLatch(latch, 5, TimeUnit.SECONDS);
if (skipUnavailable) {
assertNull(failure.get());
} else {
assertNotNull(failure.get()); assertNotNull(failure.get());
assertThat(failure.get(), instanceOf(RemoteTransportException.class)); assertThat(failure.get(), instanceOf(RemoteTransportException.class));
RemoteTransportException remoteTransportException = (RemoteTransportException) failure.get(); RemoteTransportException remoteTransportException = (RemoteTransportException) failure.get();
assertEquals(RestStatus.NOT_FOUND, remoteTransportException.status()); assertEquals(RestStatus.NOT_FOUND, remoteTransportException.status());
} }
}
} finally { } finally {
for (MockTransportService mockTransportService : mockTransportServices) { for (MockTransportService mockTransportService : mockTransportServices) {
@ -781,7 +793,7 @@ public class TransportSearchActionTests extends ESTestCase {
DiscoveryNode[] nodes = new DiscoveryNode[numClusters]; DiscoveryNode[] nodes = new DiscoveryNode[numClusters];
Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>(); Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>();
Settings.Builder builder = Settings.builder(); Settings.Builder builder = Settings.builder();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder); MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder, false);
Settings settings = builder.build(); Settings settings = builder.build();
boolean local = randomBoolean(); boolean local = randomBoolean();
OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null; OriginalIndices localIndices = local ? new OriginalIndices(new String[] { "index" }, SearchRequest.DEFAULT_INDICES_OPTIONS) : null;
@ -1035,7 +1047,7 @@ public class TransportSearchActionTests extends ESTestCase {
DiscoveryNode[] nodes = new DiscoveryNode[numClusters]; DiscoveryNode[] nodes = new DiscoveryNode[numClusters];
Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>(); Map<String, OriginalIndices> remoteIndicesByCluster = new HashMap<>();
Settings.Builder builder = Settings.builder(); Settings.Builder builder = Settings.builder();
MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder); MockTransportService[] mockTransportServices = startTransport(numClusters, nodes, remoteIndicesByCluster, builder, false);
Settings settings = builder.build(); Settings settings = builder.build();
try ( try (
MockTransportService service = MockTransportService.createNewService( MockTransportService service = MockTransportService.createNewService(

View file

@ -145,6 +145,7 @@ public class RemoteClusterClientTests extends ESTestCase {
Settings localSettings = Settings.builder() Settings localSettings = Settings.builder()
.put(onlyRole(DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE)) .put(onlyRole(DiscoveryNodeRole.REMOTE_CLUSTER_CLIENT_ROLE))
.put("cluster.remote.test.seeds", remoteNode.getAddress().getAddress() + ":" + remoteNode.getAddress().getPort()) .put("cluster.remote.test.seeds", remoteNode.getAddress().getAddress() + ":" + remoteNode.getAddress().getPort())
.put("cluster.remote.test.skip_unavailable", "false") // ensureConnected is only true for skip_unavailable=false
.build(); .build();
try ( try (
MockTransportService service = MockTransportService.createNewService( MockTransportService service = MockTransportService.createNewService(

View file

@ -1282,7 +1282,7 @@ public class RemoteClusterServiceTests extends ESTestCase {
service.start(); service.start();
service.acceptIncomingRequests(); service.acceptIncomingRequests();
assertFalse(service.getRemoteClusterService().isSkipUnavailable("cluster1")); assertTrue(service.getRemoteClusterService().isSkipUnavailable("cluster1"));
if (randomBoolean()) { if (randomBoolean()) {
updateSkipUnavailable(service.getRemoteClusterService(), "cluster1", false); updateSkipUnavailable(service.getRemoteClusterService(), "cluster1", false);

View file

@ -68,7 +68,7 @@ public class RemoteClusterSettingsTests extends ESTestCase {
public void testSkipUnavailableDefault() { public void testSkipUnavailableDefault() {
final String alias = randomAlphaOfLength(8); final String alias = randomAlphaOfLength(8);
assertFalse(REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace(alias).get(Settings.EMPTY)); assertTrue(REMOTE_CLUSTER_SKIP_UNAVAILABLE.getConcreteSettingForNamespace(alias).get(Settings.EMPTY));
} }
public void testSeedsDefault() { public void testSeedsDefault() {

View file

@ -320,14 +320,16 @@ public class RemoteClusterSecurityApiKeyRestIT extends AbstractRemoteClusterSecu
) )
); );
// Check that authentication fails if we use a non-existent cross cluster access API key // Check that authentication fails if we use a non-existent cross cluster access API key (when skip_unavailable=false)
updateClusterSettings( updateClusterSettings(
randomBoolean() randomBoolean()
? Settings.builder() ? Settings.builder()
.put("cluster.remote.invalid_remote.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.invalid_remote.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.put("cluster.remote.invalid_remote.skip_unavailable", "false")
.build() .build()
: Settings.builder() : Settings.builder()
.put("cluster.remote.invalid_remote.mode", "proxy") .put("cluster.remote.invalid_remote.mode", "proxy")
.put("cluster.remote.invalid_remote.skip_unavailable", "false")
.put("cluster.remote.invalid_remote.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.invalid_remote.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.build() .build()
); );

View file

@ -499,12 +499,18 @@ public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTe
configureRemoteCluster(); configureRemoteCluster();
populateData(); populateData();
final boolean skipUnavailable = randomBoolean();
// avoids getting 404 errors // avoids getting 404 errors
updateClusterSettings( updateClusterSettings(
randomBoolean() randomBoolean()
? Settings.builder().put("cluster.remote.invalid_remote.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0)).build() ? Settings.builder()
.put("cluster.remote.invalid_remote.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.put("cluster.remote.invalid_remote.skip_unavailable", Boolean.toString(skipUnavailable))
.build()
: Settings.builder() : Settings.builder()
.put("cluster.remote.invalid_remote.mode", "proxy") .put("cluster.remote.invalid_remote.mode", "proxy")
.put("cluster.remote.invalid_remote.skip_unavailable", Boolean.toString(skipUnavailable))
.put("cluster.remote.invalid_remote.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.invalid_remote.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.build() .build()
); );
@ -520,8 +526,14 @@ public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTe
var q2 = "FROM invalid_remote:employees | SORT emp_id DESC | LIMIT 10"; var q2 = "FROM invalid_remote:employees | SORT emp_id DESC | LIMIT 10";
performRequestWithRemoteSearchUser(esqlRequest(q2)); performRequestWithRemoteSearchUser(esqlRequest(q2));
}); });
if (skipUnavailable == false) {
assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(401)); assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(401));
assertThat(error.getMessage(), containsString("unable to find apikey")); assertThat(error.getMessage(), containsString("unable to find apikey"));
} else {
assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(500));
assertThat(error.getMessage(), containsString("Unable to connect to [invalid_remote]"));
}
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View file

@ -47,6 +47,7 @@ import org.elasticsearch.test.rest.ObjectPath;
import org.elasticsearch.test.transport.MockTransportService; import org.elasticsearch.test.transport.MockTransportService;
import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.TestThreadPool;
import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.ConnectTransportException;
import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.RemoteClusterService;
import org.elasticsearch.transport.RemoteConnectionInfo; import org.elasticsearch.transport.RemoteConnectionInfo;
import org.elasticsearch.xpack.ccr.action.repositories.ClearCcrRestoreSessionAction; import org.elasticsearch.xpack.ccr.action.repositories.ClearCcrRestoreSessionAction;
@ -82,6 +83,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase { public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase {
@ -176,7 +178,9 @@ public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase
} }
// Simulate QC behaviours by directly connecting to the FC using a transport service // Simulate QC behaviours by directly connecting to the FC using a transport service
try (MockTransportService service = startTransport("node", threadPool, (String) crossClusterApiKeyMap.get("encoded"))) { final String apiKey = (String) crossClusterApiKeyMap.get("encoded");
final boolean skipUnavailable = randomBoolean();
try (MockTransportService service = startTransport("node", threadPool, apiKey, skipUnavailable)) {
final RemoteClusterService remoteClusterService = service.getRemoteClusterService(); final RemoteClusterService remoteClusterService = service.getRemoteClusterService();
final List<RemoteConnectionInfo> remoteConnectionInfos = remoteClusterService.getRemoteConnectionInfos().toList(); final List<RemoteConnectionInfo> remoteConnectionInfos = remoteClusterService.getRemoteConnectionInfos().toList();
assertThat(remoteConnectionInfos, hasSize(1)); assertThat(remoteConnectionInfos, hasSize(1));
@ -328,22 +332,28 @@ public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase
final Response createApiKeyResponse = adminClient().performRequest(createApiKeyRequest); final Response createApiKeyResponse = adminClient().performRequest(createApiKeyRequest);
assertOK(createApiKeyResponse); assertOK(createApiKeyResponse);
final Map<String, Object> apiKeyMap = responseAsMap(createApiKeyResponse); final Map<String, Object> apiKeyMap = responseAsMap(createApiKeyResponse);
try (MockTransportService service = startTransport("node", threadPool, (String) apiKeyMap.get("encoded"))) { final String apiKey = (String) apiKeyMap.get("encoded");
final boolean skipUnavailable = randomBoolean();
try (MockTransportService service = startTransport("node", threadPool, apiKey, skipUnavailable)) {
final RemoteClusterService remoteClusterService = service.getRemoteClusterService(); final RemoteClusterService remoteClusterService = service.getRemoteClusterService();
final var remoteClusterClient = remoteClusterService.getRemoteClusterClient( final var remoteClusterClient = remoteClusterService.getRemoteClusterClient(
"my_remote_cluster", "my_remote_cluster",
EsExecutors.DIRECT_EXECUTOR_SERVICE, EsExecutors.DIRECT_EXECUTOR_SERVICE,
RemoteClusterService.DisconnectedStrategy.RECONNECT_UNLESS_SKIP_UNAVAILABLE RemoteClusterService.DisconnectedStrategy.RECONNECT_UNLESS_SKIP_UNAVAILABLE
); );
final Exception e = expectThrows(
final ElasticsearchSecurityException e = expectThrows( Exception.class,
ElasticsearchSecurityException.class,
() -> executeRemote( () -> executeRemote(
remoteClusterClient, remoteClusterClient,
RemoteClusterNodesAction.REMOTE_TYPE, RemoteClusterNodesAction.REMOTE_TYPE,
RemoteClusterNodesAction.Request.REMOTE_CLUSTER_SERVER_NODES RemoteClusterNodesAction.Request.REMOTE_CLUSTER_SERVER_NODES
) )
); );
if (skipUnavailable) {
assertThat(e, instanceOf(ConnectTransportException.class));
assertThat(e.getMessage(), containsString("Unable to connect to [my_remote_cluster]"));
} else {
assertThat(e, instanceOf(ElasticsearchSecurityException.class));
assertThat( assertThat(
e.getMessage(), e.getMessage(),
containsString( containsString(
@ -352,6 +362,7 @@ public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase
); );
} }
} }
}
public void testUpdateCrossClusterApiKey() throws Exception { public void testUpdateCrossClusterApiKey() throws Exception {
final Map<String, Object> crossClusterApiKeyMap = createCrossClusterAccessApiKey(adminClient(), """ final Map<String, Object> crossClusterApiKeyMap = createCrossClusterAccessApiKey(adminClient(), """
@ -392,12 +403,14 @@ public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase
final FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices("index").fields("name"); final FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices("index").fields("name");
// Perform cross-cluster requests // Perform cross-cluster requests
boolean skipUnavailable = randomBoolean();
try ( try (
MockTransportService service = startTransport( MockTransportService service = startTransport(
"node", "node",
threadPool, threadPool,
(String) crossClusterApiKeyMap.get("encoded"), (String) crossClusterApiKeyMap.get("encoded"),
Map.of(TransportFieldCapabilitiesAction.NAME, crossClusterAccessSubjectInfo) Map.of(TransportFieldCapabilitiesAction.NAME, crossClusterAccessSubjectInfo),
skipUnavailable
) )
) { ) {
final RemoteClusterService remoteClusterService = service.getRemoteClusterService(); final RemoteClusterService remoteClusterService = service.getRemoteClusterService();
@ -508,7 +521,8 @@ public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase
"node", "node",
threadPool, threadPool,
(String) crossClusterApiKeyMap.get("encoded"), (String) crossClusterApiKeyMap.get("encoded"),
Map.of(TransportGetAction.TYPE.name() + "[s]", buildCrossClusterAccessSubjectInfo(indexA)) Map.of(TransportGetAction.TYPE.name() + "[s]", buildCrossClusterAccessSubjectInfo(indexA)),
randomBoolean()
) )
) { ) {
final RemoteClusterService remoteClusterService = service.getRemoteClusterService(); final RemoteClusterService remoteClusterService = service.getRemoteClusterService();
@ -552,15 +566,21 @@ public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase
); );
} }
private static MockTransportService startTransport(final String nodeName, final ThreadPool threadPool, String encodedApiKey) { private static MockTransportService startTransport(
return startTransport(nodeName, threadPool, encodedApiKey, Map.of()); final String nodeName,
final ThreadPool threadPool,
String encodedApiKey,
boolean skipUnavailable
) {
return startTransport(nodeName, threadPool, encodedApiKey, Map.of(), skipUnavailable);
} }
private static MockTransportService startTransport( private static MockTransportService startTransport(
final String nodeName, final String nodeName,
final ThreadPool threadPool, final ThreadPool threadPool,
String encodedApiKey, String encodedApiKey,
Map<String, CrossClusterAccessSubjectInfo> subjectInfoLookup Map<String, CrossClusterAccessSubjectInfo> subjectInfoLookup,
boolean skipUnavailable
) { ) {
final String remoteClusterServerEndpoint = testCluster.getRemoteClusterServerEndpoint(0); final String remoteClusterServerEndpoint = testCluster.getRemoteClusterServerEndpoint(0);
@ -573,9 +593,11 @@ public class RemoteClusterSecurityFcActionAuthorizationIT extends ESRestTestCase
builder.setSecureSettings(secureSettings); builder.setSecureSettings(secureSettings);
if (randomBoolean()) { if (randomBoolean()) {
builder.put("cluster.remote.my_remote_cluster.mode", "sniff") builder.put("cluster.remote.my_remote_cluster.mode", "sniff")
.put("cluster.remote.my_remote_cluster.skip_unavailable", Boolean.toString(skipUnavailable))
.put("cluster.remote.my_remote_cluster.seeds", remoteClusterServerEndpoint); .put("cluster.remote.my_remote_cluster.seeds", remoteClusterServerEndpoint);
} else { } else {
builder.put("cluster.remote.my_remote_cluster.mode", "proxy") builder.put("cluster.remote.my_remote_cluster.mode", "proxy")
.put("cluster.remote.my_remote_cluster.skip_unavailable", Boolean.toString(skipUnavailable))
.put("cluster.remote.my_remote_cluster.proxy_address", remoteClusterServerEndpoint); .put("cluster.remote.my_remote_cluster.proxy_address", remoteClusterServerEndpoint);
} }

View file

@ -105,9 +105,11 @@ public class RemoteClusterSecurityLicensingAndFeatureUsageRestIT extends Abstrac
final Settings.Builder builder = Settings.builder(); final Settings.Builder builder = Settings.builder();
if (isProxyMode) { if (isProxyMode) {
builder.put("cluster.remote.my_remote_cluster.mode", "proxy") builder.put("cluster.remote.my_remote_cluster.mode", "proxy")
.put("cluster.remote.my_remote_cluster.skip_unavailable", "false")
.put("cluster.remote.my_remote_cluster.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0)); .put("cluster.remote.my_remote_cluster.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0));
} else { } else {
builder.put("cluster.remote.my_remote_cluster.mode", "sniff") builder.put("cluster.remote.my_remote_cluster.mode", "sniff")
.put("cluster.remote.my_remote_cluster.skip_unavailable", "false")
.putList("cluster.remote.my_remote_cluster.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0)); .putList("cluster.remote.my_remote_cluster.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0));
} }
updateClusterSettings(builder.build()); updateClusterSettings(builder.build());

View file

@ -7,6 +7,7 @@
package org.elasticsearch.xpack.remotecluster; package org.elasticsearch.xpack.remotecluster;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Request; import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RequestOptions;
@ -331,35 +332,65 @@ public class RemoteClusterSecurityRestIT extends AbstractRemoteClusterSecurityTe
) )
); );
// Check that authentication fails if we use a non-existent API key // Check that authentication fails if we use a non-existent API key (when skip_unavailable=false)
boolean skipUnavailable = randomBoolean();
updateClusterSettings( updateClusterSettings(
randomBoolean() randomBoolean()
? Settings.builder() ? Settings.builder()
.put("cluster.remote.invalid_remote.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.invalid_remote.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.put("cluster.remote.invalid_remote.skip_unavailable", Boolean.toString(skipUnavailable))
.build() .build()
: Settings.builder() : Settings.builder()
.put("cluster.remote.invalid_remote.mode", "proxy") .put("cluster.remote.invalid_remote.mode", "proxy")
.put("cluster.remote.invalid_remote.skip_unavailable", Boolean.toString(skipUnavailable))
.put("cluster.remote.invalid_remote.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.invalid_remote.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.build() .build()
); );
if (skipUnavailable) {
/*
when skip_unavailable=true, response should be something like:
{"took":1,"timed_out":false,"num_reduce_phases":0,"_shards":{"total":0,"successful":0,"skipped":0,"failed":0},
"_clusters":{"total":1,"successful":0,"skipped":1,"running":0,"partial":0,"failed":0,
"details":{"invalid_remote":{"status":"skipped","indices":"index1","timed_out":false,
"failures":[{"shard":-1,"index":null,"reason":{"type":"connect_transport_exception",
"reason":"Unable to connect to [invalid_remote]"}}]}}},
"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
*/
Response invalidRemoteResponse = performRequestWithRemoteSearchUser(new Request("GET", "/invalid_remote:index1/_search"));
assertThat(invalidRemoteResponse.getStatusLine().getStatusCode(), equalTo(200));
String responseJson = EntityUtils.toString(invalidRemoteResponse.getEntity());
assertThat(responseJson, containsString("\"status\":\"skipped\""));
assertThat(responseJson, containsString("connect_transport_exception"));
} else {
final ResponseException exception4 = expectThrows( final ResponseException exception4 = expectThrows(
ResponseException.class, ResponseException.class,
() -> performRequestWithRemoteSearchUser(new Request("GET", "/invalid_remote:index1/_search")) () -> performRequestWithRemoteSearchUser(new Request("GET", "/invalid_remote:index1/_search"))
); );
assertThat(exception4.getResponse().getStatusLine().getStatusCode(), equalTo(401)); assertThat(exception4.getResponse().getStatusLine().getStatusCode(), equalTo(401));
assertThat(exception4.getMessage(), containsString("unable to find apikey")); assertThat(exception4.getMessage(), containsString("unable to find apikey"));
}
// check that REST API key is not supported by cross cluster access // check that REST API key is not supported by cross cluster access (when skip_unavailable=false)
skipUnavailable = randomBoolean();
updateClusterSettings( updateClusterSettings(
randomBoolean() randomBoolean()
? Settings.builder() ? Settings.builder()
.put("cluster.remote.wrong_api_key_type.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.wrong_api_key_type.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.put("cluster.remote.wrong_api_key_type.skip_unavailable", Boolean.toString(skipUnavailable))
.build() .build()
: Settings.builder() : Settings.builder()
.put("cluster.remote.wrong_api_key_type.mode", "proxy") .put("cluster.remote.wrong_api_key_type.mode", "proxy")
.put("cluster.remote.wrong_api_key_type.skip_unavailable", Boolean.toString(skipUnavailable))
.put("cluster.remote.wrong_api_key_type.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.wrong_api_key_type.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.build() .build()
); );
if (skipUnavailable) {
Response invalidRemoteResponse = performRequestWithRemoteSearchUser(new Request("GET", "/wrong_api_key_type:*/_search"));
assertThat(invalidRemoteResponse.getStatusLine().getStatusCode(), equalTo(200));
String responseJson = EntityUtils.toString(invalidRemoteResponse.getEntity());
assertThat(responseJson, containsString("\"status\":\"skipped\""));
assertThat(responseJson, containsString("connect_transport_exception"));
} else {
final ResponseException exception5 = expectThrows( final ResponseException exception5 = expectThrows(
ResponseException.class, ResponseException.class,
() -> performRequestWithRemoteSearchUser(new Request("GET", "/wrong_api_key_type:*/_search")) () -> performRequestWithRemoteSearchUser(new Request("GET", "/wrong_api_key_type:*/_search"))
@ -373,18 +404,29 @@ public class RemoteClusterSecurityRestIT extends AbstractRemoteClusterSecurityTe
+ "] has type [rest]" + "] has type [rest]"
) )
); );
}
// Check invalid cross-cluster API key length is rejected // Check invalid cross-cluster API key length is rejected (and gets security error when skip_unavailable=false)
skipUnavailable = randomBoolean();
updateClusterSettings( updateClusterSettings(
randomBoolean() randomBoolean()
? Settings.builder() ? Settings.builder()
.put("cluster.remote.invalid_secret_length.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.invalid_secret_length.seeds", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.put("cluster.remote.invalid_secret_length.skip_unavailable", Boolean.toString(skipUnavailable))
.build() .build()
: Settings.builder() : Settings.builder()
.put("cluster.remote.invalid_secret_length.mode", "proxy") .put("cluster.remote.invalid_secret_length.mode", "proxy")
.put("cluster.remote.invalid_secret_length.skip_unavailable", Boolean.toString(skipUnavailable))
.put("cluster.remote.invalid_secret_length.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0)) .put("cluster.remote.invalid_secret_length.proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0))
.build() .build()
); );
if (skipUnavailable) {
Response invalidRemoteResponse = performRequestWithRemoteSearchUser(new Request("GET", "/invalid_secret_length:*/_search"));
assertThat(invalidRemoteResponse.getStatusLine().getStatusCode(), equalTo(200));
String responseJson = EntityUtils.toString(invalidRemoteResponse.getEntity());
assertThat(responseJson, containsString("\"status\":\"skipped\""));
assertThat(responseJson, containsString("connect_transport_exception"));
} else {
final ResponseException exception6 = expectThrows( final ResponseException exception6 = expectThrows(
ResponseException.class, ResponseException.class,
() -> performRequestWithRemoteSearchUser(new Request("GET", "/invalid_secret_length:*/_search")) () -> performRequestWithRemoteSearchUser(new Request("GET", "/invalid_secret_length:*/_search"))
@ -393,6 +435,7 @@ public class RemoteClusterSecurityRestIT extends AbstractRemoteClusterSecurityTe
assertThat(exception6.getMessage(), containsString("invalid cross-cluster API key value")); assertThat(exception6.getMessage(), containsString("invalid cross-cluster API key value"));
} }
} }
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void testNodesInfo() throws IOException { public void testNodesInfo() throws IOException {

View file

@ -45,6 +45,7 @@ def queryingCluster = testClusters.register('querying-cluster') {
setting 'cluster.remote.connections_per_cluster', "1" setting 'cluster.remote.connections_per_cluster', "1"
user username: "test_user", password: "x-pack-test-password" user username: "test_user", password: "x-pack-test-password"
setting 'cluster.remote.my_remote_cluster.skip_unavailable', 'false'
if (proxyMode) { if (proxyMode) {
setting 'cluster.remote.my_remote_cluster.mode', 'proxy' setting 'cluster.remote.my_remote_cluster.mode', 'proxy'
setting 'cluster.remote.my_remote_cluster.proxy_address', { setting 'cluster.remote.my_remote_cluster.proxy_address', {